diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7584eb8075..1d1a6eec16 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,6 +2,7 @@ "name": "Immich - Backend, Frontend and ML", "service": "immich-server", "runServices": [ + "immich-init", "immich-server", "redis", "database", @@ -26,7 +27,59 @@ "vitest.explorer", "ms-playwright.playwright", "ms-azuretools.vscode-docker" - ] + ], + "settings": { + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "Immich API Server (Nest)", + "type": "shell", + "command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true, + "clear": false, + "group": "Devcontainer tasks", + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + }, + "problemMatcher": [] + }, + { + "label": "Immich Web Server (Vite)", + "type": "shell", + "command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0", + "isBackground": true, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "dedicated", + "showReuseMessage": true, + "clear": false, + "group": "Devcontainer tasks", + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + }, + "problemMatcher": [] + }, + { + "label": "Build Immich CLI", + "type": "shell", + "command": "pnpm --filter cli build:dev" + } + ] + } + } } }, "features": { @@ -56,8 +109,8 @@ } }, "overrideCommand": true, - "workspaceFolder": "/workspaces/immich", - "remoteUser": "node", + "workspaceFolder": "/usr/src/app", + "remoteUser": "root", "userEnvProbe": "loginInteractiveShell", "remoteEnv": { // The location where your uploaded files are stored diff --git a/.devcontainer/mobile/container-compose-overrides.yml b/.devcontainer/mobile/container-compose-overrides.yml index 99e41cbece..3d9e1b00b6 100644 --- a/.devcontainer/mobile/container-compose-overrides.yml +++ b/.devcontainer/mobile/container-compose-overrides.yml @@ -1,23 +1,17 @@ services: + immich-app-base: + image: busybox immich-server: + extends: + service: immich-app-base + profiles: !reset [] + image: immich-server-dev:latest build: target: dev-container-mobile environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ - volumes: !override # bind mount host to /workspaces/immich - - ..:/workspaces/immich + volumes: - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage - /etc/localtime:/etc/localtime:ro immich-web: env_file: !reset [] diff --git a/.devcontainer/mobile/devcontainer.json b/.devcontainer/mobile/devcontainer.json index 140a2ecac3..0be9b72969 100644 --- a/.devcontainer/mobile/devcontainer.json +++ b/.devcontainer/mobile/devcontainer.json @@ -2,6 +2,7 @@ "name": "Immich - Mobile", "service": "immich-server", "runServices": [ + "immich-init", "immich-server", "redis", "database", @@ -35,7 +36,7 @@ }, "forwardPorts": [], "overrideCommand": true, - "workspaceFolder": "/workspaces/immich", + "workspaceFolder": "/usr/src/app", "remoteUser": "node", "userEnvProbe": "loginInteractiveShell", "remoteEnv": { diff --git a/.devcontainer/server/container-common.sh b/.devcontainer/server/container-common.sh index 3aa72379c3..fa3e60f211 100755 --- a/.devcontainer/server/container-common.sh +++ b/.devcontainer/server/container-common.sh @@ -2,11 +2,6 @@ export IMMICH_PORT="${DEV_SERVER_PORT:-2283}" export DEV_PORT="${DEV_PORT:-3000}" -# search for immich directory inside workspace. -# /workspaces/immich is the bind mount, but other directories can be mounted if runing -# Devcontainer: Clone [repository|pull request] in container volumne -WORKSPACES_DIR="/workspaces" -IMMICH_DIR="$WORKSPACES_DIR/immich" IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log" log() { @@ -30,52 +25,8 @@ run_cmd() { return "${PIPESTATUS[0]}" } -# Find directories excluding /workspaces/immich -mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*") - -if [ ${#other_dirs[@]} -gt 1 ]; then - log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR." - exit 1 -elif [ ${#other_dirs[@]} -eq 1 ]; then - export IMMICH_WORKSPACE="${other_dirs[0]}" -else - export IMMICH_WORKSPACE="$IMMICH_DIR" -fi +export IMMICH_WORKSPACE="/usr/src/app" log "Found immich workspace in $IMMICH_WORKSPACE" log "" -fix_permissions() { - - log "Fixing permissions for ${IMMICH_WORKSPACE}" - - # Change ownership for directories that exist - for dir in "${IMMICH_WORKSPACE}/.vscode" \ - "${IMMICH_WORKSPACE}/server/upload" \ - "${IMMICH_WORKSPACE}/.pnpm-store" \ - "${IMMICH_WORKSPACE}/.github/node_modules" \ - "${IMMICH_WORKSPACE}/cli/node_modules" \ - "${IMMICH_WORKSPACE}/e2e/node_modules" \ - "${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \ - "${IMMICH_WORKSPACE}/server/node_modules" \ - "${IMMICH_WORKSPACE}/server/dist" \ - "${IMMICH_WORKSPACE}/web/node_modules" \ - "${IMMICH_WORKSPACE}/web/dist"; do - if [ -d "$dir" ]; then - run_cmd sudo chown node -R "$dir" - fi - done - - log "" -} - -install_dependencies() { - - log "Installing dependencies" - ( - cd "${IMMICH_WORKSPACE}" || exit 1 - 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 cc2b0c907b..5c312efd07 100644 --- a/.devcontainer/server/container-compose-overrides.yml +++ b/.devcontainer/server/container-compose-overrides.yml @@ -1,26 +1,21 @@ services: + immich-app-base: + image: busybox immich-server: + extends: + service: immich-app-base + profiles: !reset [] + image: immich-server-dev:latest build: target: dev-container-server env_file: !reset [] hostname: immich-dev environment: - IMMICH_SERVER_URL=http://127.0.0.1:2283/ - volumes: !override - - ..:/workspaces/immich + volumes: - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - /etc/localtime:/etc/localtime:ro - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage + - pnpm_store_server:/buildcache/pnpm-store - ../plugins:/build/corePlugin immich-web: env_file: !reset [] diff --git a/.devcontainer/server/container-start.sh b/.devcontainer/server/container-start.sh deleted file mode 100755 index 0edd38172e..0000000000 --- a/.devcontainer/server/container-start.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# shellcheck source=common.sh -# shellcheck disable=SC1091 -source /immich-devcontainer/container-common.sh - -log "Setting up Immich dev container..." -fix_permissions - -log "Setup complete, please wait while backend and frontend services automatically start" -log -log "If necessary, the services may be manually started using" -log -log "$ /immich-devcontainer/container-start-backend.sh" -log "$ /immich-devcontainer/container-start-frontend.sh" -log -log "From different terminal windows, as these scripts automatically restart the server" -log "on error, and will continuously run in a loop" diff --git a/.github/.nvmrc b/.github/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0bd3b30814..2d1fdafa30 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,6 +26,7 @@ The `/api/something` endpoint is now `/api/something-else` ## Checklist: +- [ ] I have carefully read CONTRIBUTING.md - [ ] I have performed a self-review of my own code - [ ] I have made corresponding changes to the documentation if applicable - [ ] I have no unrelated changes in the PR. diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 10dc88088f..44645c1e1b 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -30,18 +30,6 @@ on: required: true IOS_CERTIFICATE_PASSWORD: required: true - IOS_PROVISIONING_PROFILE: - required: true - IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: - required: true - IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: - required: true - IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: - required: true FASTLANE_TEAM_ID: required: true pull_request: @@ -63,14 +51,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -91,12 +79,12 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false @@ -108,14 +96,14 @@ jobs: working-directory: ./mobile run: printf "%s" $KEY_JKS | base64 -d > android/key.jks - - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: distribution: 'zulu' java-version: '17' - name: Restore Gradle Cache id: cache-gradle-restore - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | ~/.gradle/caches @@ -165,14 +153,14 @@ jobs: fi - name: Publish Android Artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 if: github.ref == 'refs/heads/main' with: path: | @@ -190,11 +178,14 @@ jobs: contents: read # Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload) if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }} - runs-on: macos-latest + runs-on: macos-15 steps: + - name: Select Xcode 26 + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false @@ -240,35 +231,14 @@ jobs: mkdir -p ~/.appstoreconnect/private_keys echo "$API_KEY_CONTENT" | base64 --decode > ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8 - - name: Import Certificate and Provisioning Profiles + - name: Import Certificate env: IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} - IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} - IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION }} - ENVIRONMENT: ${{ inputs.environment || 'development' }} working-directory: ./mobile/ios run: | # Decode certificate echo "$IOS_CERTIFICATE_P12" | base64 --decode > certificate.p12 - # Decode provisioning profiles based on environment - if [[ "$ENVIRONMENT" == "development" ]]; then - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE" | base64 --decode > profile_dev.mobileprovision - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_dev_share.mobileprovision - echo "$IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_dev_widget.mobileprovision - ls -lh profile_dev*.mobileprovision - else - echo "$IOS_PROVISIONING_PROFILE" | base64 --decode > profile.mobileprovision - echo "$IOS_PROVISIONING_PROFILE_SHARE_EXTENSION" | base64 --decode > profile_share.mobileprovision - echo "$IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION" | base64 --decode > profile_widget.mobileprovision - ls -lh profile*.mobileprovision - fi - - name: Create keychain and import certificate env: KEYCHAIN_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} @@ -299,6 +269,8 @@ jobs: ENVIRONMENT: ${{ inputs.environment || 'development' }} BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }} GITHUB_REF: ${{ github.ref }} + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 + FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 working-directory: ./mobile/ios run: | # Only upload to TestFlight on main branch @@ -319,7 +291,7 @@ jobs: security delete-keychain build.keychain || true - name: Upload IPA artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: ios-release-ipa path: mobile/ios/Runner.ipa diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index a75770ec49..3de4676622 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -19,13 +19,13 @@ jobs: actions: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check out code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml new file mode 100644 index 0000000000..2aaf73ef22 --- /dev/null +++ b/.github/workflows/check-openapi.yml @@ -0,0 +1,32 @@ +name: Check OpenAPI +on: + workflow_dispatch: + pull_request: + paths: + - 'open-api/**' + - '.github/workflows/check-openapi.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + check-openapi: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Check for breaking API changes + # sha is pinning to a commit instead of a tag since the action does not tag versions + uses: oasdiff/oasdiff-action/breaking@ccb863950ce437a50f8f1a40d2a1112117e06ce4 + with: + base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json + revision: open-api/immich-openapi-specs.json + fail-on: ERR diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 8bf8da30d7..a2c763a0f6 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -24,18 +24,19 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + id-token: write + packages: write defaults: run: working-directory: ./cli - steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -44,7 +45,7 @@ jobs: uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './cli/.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -57,10 +58,8 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm build - - run: pnpm publish --no-git-checks + - run: pnpm publish --provenance --no-git-checks if: ${{ github.event_name == 'release' }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} docker: name: Docker @@ -72,13 +71,13 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -87,10 +86,10 @@ jobs: uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 if: ${{ !github.event.pull_request.head.repo.fork }} with: registry: ghcr.io @@ -116,7 +115,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index 24630bbb87..1b18c0c5e1 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,7 +35,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:237cdae7783609c96f18037a513d38088713cf4a2e493a3aa136d0c45490749a + image: ghcr.io/immich-app/mdq:main@sha256:4f9860d04c88f7f87861f8ee84bfeedaec15ed7ca5ca87bc7db44b036f81645f outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/close-llm-pr.yml b/.github/workflows/close-llm-pr.yml new file mode 100644 index 0000000000..511d5c7f55 --- /dev/null +++ b/.github/workflows/close-llm-pr.yml @@ -0,0 +1,38 @@ +name: Close LLM-generated PRs + +on: + pull_request_target: + types: [labeled] + +permissions: {} + +jobs: + comment_and_close: + runs-on: ubuntu-latest + if: ${{ github.event.label.name == 'llm-generated' }} + permissions: + pull-requests: write + steps: + - name: Comment and close + env: + GH_TOKEN: ${{ github.token }} + NODE_ID: ${{ github.event.pull_request.node_id }} + run: | + gh api graphql \ + -f prId="$NODE_ID" \ + -f body="Thank you for your interest in contributing to Immich! Unfortunately this PR looks like it was generated using an LLM. As noted in our [CONTRIBUTING.md](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md#use-of-generative-ai), we request that you don't use LLMs to generate PRs as those are not a good use of maintainer time." \ + -f query=' + mutation CommentAndClosePR($prId: ID!, $body: String!) { + addComment(input: { + subjectId: $prId, + body: $body + }) { + __typename + } + + closePullRequest(input: { + pullRequestId: $prId + }) { + __typename + } + }' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 20a5e23c0c..67e0b4b972 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,20 +44,20 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout repository - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -70,7 +70,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -83,6 +83,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e27f1ebdf9..1636076491 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,14 +23,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -60,7 +60,7 @@ jobs: suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -90,7 +90,7 @@ jobs: suffix: [''] steps: - name: Login to GitHub Container Registry - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -131,8 +131,8 @@ jobs: - device: rocm suffixes: '-rocm' platforms: linux/amd64 - runner-mapping: '{"linux/amd64": "mich"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0 + runner-mapping: '{"linux/amd64": "pokedex-giant"}' + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 permissions: contents: read actions: read @@ -155,7 +155,7 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1 permissions: contents: read actions: read diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 680cd0318c..28828f22c6 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -21,14 +21,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -54,22 +54,23 @@ jobs: steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} + fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './docs/.nvmrc' cache: 'pnpm' @@ -85,7 +86,7 @@ jobs: run: pnpm build - name: Upload build output - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: docs-build-output path: docs/build/ diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 3a0e918812..babda72c33 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -20,7 +20,7 @@ jobs: artifact: ${{ steps.get-artifact.outputs.result }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -119,19 +119,19 @@ jobs: if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 + uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 - name: Load parameters id: parameters @@ -192,16 +192,13 @@ jobs: ' >> $GITHUB_OUTPUT - name: Publish to Cloudflare Pages - # TODO: Action is deprecated - uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ steps.docs-output.outputs.projectName }} - workingDirectory: 'docs' - directory: 'build' - branch: ${{ steps.parameters.outputs.name }} - wranglerVersion: '3' + working-directory: docs + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PROJECT_NAME: ${{ steps.docs-output.outputs.projectName }} + BRANCH_NAME: ${{ steps.parameters.outputs.name }} + run: mise run //docs:deploy - name: Deploy Docs Release Domain if: ${{ steps.parameters.outputs.event == 'release' }} diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 643c35b1af..05842889cc 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -17,19 +17,19 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 + uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2 - name: Destroy Docs Subdomain env: diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml index f77ca48b41..1daa279cd2 100644 --- a/.github/workflows/fix-format.yml +++ b/.github/workflows/fix-format.yml @@ -22,7 +22,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: 'Checkout' - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ github.event.pull_request.head.ref }} token: ${{ steps.generate-token.outputs.token }} @@ -32,14 +32,14 @@ jobs: uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' cache-dependency-path: '**/pnpm-lock.yaml' - name: Fix formatting - run: pnpm --recursive install && pnpm run --recursive --parallel fix:format + run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix - name: Commit and push uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4 diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 0544de3dad..e04b32d74f 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 263426e548..24f3f8faf1 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index b6e2eb1ac6..a1d31a61ea 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -56,20 +56,20 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true ref: main - name: Install uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' @@ -109,12 +109,6 @@ jobs: APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} IOS_CERTIFICATE_P12: ${{ secrets.IOS_CERTIFICATE_P12 }} IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} - IOS_PROVISIONING_PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }} - IOS_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_PROVISIONING_PROFILE_WIDGET_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_SHARE_EXTENSION }} - IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION: ${{ secrets.IOS_DEVELOPMENT_PROVISIONING_PROFILE_WIDGET_EXTENSION }} FASTLANE_TEAM_ID: ${{ secrets.FASTLANE_TEAM_ID }} with: @@ -136,13 +130,13 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: false - name: Download APK - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: release-apk-signed github-token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml index 8760b67fc0..dc6f0eff0a 100644 --- a/.github/workflows/preview-label.yaml +++ b/.github/workflows/preview-label.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} @@ -32,7 +32,7 @@ jobs: pull-requests: write steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 4a06957203..93e18a4fcc 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -23,20 +23,20 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: true ref: main - name: Install uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' @@ -159,7 +159,7 @@ jobs: - name: Create PR id: create-pr - uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ steps.generate-token.outputs.token }} commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb64cd37cf..30e9c1c7ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,7 @@ jobs: private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ steps.generate-token.outputs.token }} persist-credentials: false @@ -74,7 +74,7 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Download APK - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: release-apk-signed github-token: ${{ steps.generate-token.outputs.token }} @@ -88,6 +88,7 @@ jobs: draft: true files: | docker/docker-compose.yml + docker/docker-compose.rootless.yml docker/example.env docker/hwaccel.ml.yml docker/hwaccel.transcoding.yml diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 9c70922df1..1bcdec4747 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -12,17 +12,19 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + id-token: write + packages: write defaults: run: working-directory: ./open-api/typescript-sdk steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -31,7 +33,7 @@ jobs: uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './open-api/typescript-sdk/.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -42,6 +44,4 @@ jobs: - name: Build run: pnpm build - name: Publish - run: pnpm publish --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: pnpm publish --provenance --no-git-checks diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 2b72ceb40a..d100dd281f 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -20,14 +20,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -49,13 +49,13 @@ jobs: working-directory: ./mobile steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -69,6 +69,14 @@ jobs: - name: Install dependencies run: dart pub get + - name: Install dependencies for UI package + run: dart pub get + working-directory: ./mobile/packages/ui + + - name: Install dependencies for UI Showcase + run: dart pub get + working-directory: ./mobile/packages/ui/showcase + - name: Install DCM uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5d196a084..1cad2b0023 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,14 +17,14 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -63,13 +63,13 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -77,7 +77,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' @@ -108,20 +108,20 @@ jobs: working-directory: ./cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './cli/.nvmrc' cache: 'pnpm' @@ -155,20 +155,20 @@ jobs: working-directory: ./cli steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './cli/.nvmrc' cache: 'pnpm' @@ -197,20 +197,20 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './web/.nvmrc' cache: 'pnpm' @@ -241,20 +241,20 @@ jobs: working-directory: ./web steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './web/.nvmrc' cache: 'pnpm' @@ -279,28 +279,28 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './web/.nvmrc' cache: 'pnpm' cache-dependency-path: '**/pnpm-lock.yaml' - name: Install dependencies - run: pnpm --filter=immich-web install --frozen-lockfile + run: pnpm --filter=immich-i18n install --frozen-lockfile - name: Format - run: pnpm --filter=immich-web format:i18n + run: pnpm --filter=immich-i18n format:fix - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files @@ -327,20 +327,20 @@ jobs: working-directory: ./e2e steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './e2e/.nvmrc' cache: 'pnpm' @@ -373,13 +373,13 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false submodules: 'recursive' @@ -387,7 +387,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' @@ -412,13 +412,13 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false submodules: 'recursive' @@ -426,7 +426,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './e2e/.nvmrc' cache: 'pnpm' @@ -446,12 +446,29 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile if: ${{ !cancelled() }} - - name: Docker build - run: docker compose build + - name: Start Docker Compose + run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} - name: Run e2e tests (api & cli) + env: + VITEST_DISABLE_DOCKER_SETUP: true run: pnpm test if: ${{ !cancelled() }} + - name: Run e2e tests (maintenance) + env: + VITEST_DISABLE_DOCKER_SETUP: true + run: pnpm test:maintenance + if: ${{ !cancelled() }} + - name: Capture Docker logs + if: always() + run: docker compose logs --no-color > docker-compose-logs.txt + working-directory: ./e2e + - name: Archive Docker logs + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: always() + with: + name: e2e-server-docker-logs-${{ matrix.runner }} + path: e2e/docker-compose-logs.txt e2e-tests-web: name: End-to-End Tests (Web) needs: pre-job @@ -467,13 +484,13 @@ jobs: runner: [ubuntu-latest, ubuntu-24.04-arm] steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false submodules: 'recursive' @@ -481,7 +498,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './e2e/.nvmrc' cache: 'pnpm' @@ -494,22 +511,54 @@ jobs: run: pnpm install --frozen-lockfile if: ${{ !cancelled() }} - name: Install Playwright Browsers - run: npx playwright install chromium --only-shell + run: pnpm exec playwright install chromium --only-shell if: ${{ !cancelled() }} - name: Docker build - run: docker compose build + run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} - name: Run e2e tests (web) env: - CI: true - run: npx playwright test + PLAYWRIGHT_DISABLE_WEBSERVER: true + run: pnpm test:web if: ${{ !cancelled() }} - - name: Archive test results - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + - name: Archive e2e test (web) results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: success() || failure() with: name: e2e-web-test-results-${{ matrix.runner }} path: e2e/playwright-report/ + - name: Run ui tests (web) + env: + PLAYWRIGHT_DISABLE_WEBSERVER: true + run: pnpm test:web:ui + if: ${{ !cancelled() }} + - name: Archive ui test (web) results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: success() || failure() + with: + name: e2e-ui-test-results-${{ matrix.runner }} + path: e2e/playwright-report/ + - name: Run maintenance tests + env: + PLAYWRIGHT_DISABLE_WEBSERVER: true + run: pnpm test:web:maintenance + if: ${{ !cancelled() }} + - name: Archive maintenance tests (web) results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: success() || failure() + with: + name: e2e-maintenance-isolated-test-results-${{ matrix.runner }} + path: e2e/playwright-report/ + - name: Capture Docker logs + if: always() + run: docker compose logs --no-color > docker-compose-logs.txt + working-directory: ./e2e + - name: Archive Docker logs + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: always() + with: + name: e2e-web-docker-logs-${{ matrix.runner }} + path: e2e/docker-compose-logs.txt success-check-e2e: name: End-to-End Tests Success needs: [e2e-tests-server-cli, e2e-tests-web] @@ -529,12 +578,12 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -561,31 +610,28 @@ jobs: working-directory: ./machine-learning steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Install uv - uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) - # with: - # python-version: 3.11 - # cache: 'uv' + uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0 + with: + python-version: 3.11 - name: Install dependencies run: | uv sync --extra cpu - name: Lint with ruff run: | uv run ruff check --output-format=github immich_ml - - name: Check black formatting + - name: Format with ruff run: | - uv run black --check immich_ml + uv run ruff format --check immich_ml - name: Run mypy type checking run: | uv run mypy --strict immich_ml/ @@ -604,20 +650,20 @@ jobs: working-directory: ./.github steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './.github/.nvmrc' cache: 'pnpm' @@ -634,12 +680,12 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} @@ -655,20 +701,20 @@ jobs: contents: read steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' @@ -717,20 +763,20 @@ jobs: working-directory: ./server steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Checkout code - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false token: ${{ steps.token.outputs.token }} - name: Setup pnpm uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: './server/.nvmrc' cache: 'pnpm' diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index e37497b9bb..6e997ad76a 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -24,19 +24,19 @@ jobs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@08bac802a312fc89808e0dd589271ca0974087b5 # pre-job-action-v2.0.0 + uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2 with: github-token: ${{ steps.token.outputs.token }} filters: | i18n: - - modified: 'i18n/!(en)**\.json' + - modified: 'i18n/!(en|package)**\.json' skip-force-logic: 'true' enforce-lock: @@ -47,7 +47,7 @@ jobs: if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} steps: - id: token - uses: immich-app/devtools/actions/create-workflow-token@da177fa133657503ddb7503f8ba53dccefec5da1 # create-workflow-token-action-v1.0.0 + uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1 with: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs index 0e76dabe66..6dbed0bb6c 100644 --- a/.pnpmfile.cjs +++ b/.pnpmfile.cjs @@ -4,12 +4,18 @@ module.exports = { if (!pkg.name) { return pkg; } + // make exiftool-vendored.pl a regular dependency since Docker prod + // images build with --no-optional to reduce image size if (pkg.name === "exiftool-vendored") { - if (pkg.optionalDependencies["exiftool-vendored.pl"]) { - // make exiftool-vendored.pl a regular dependency - pkg.dependencies["exiftool-vendored.pl"] = - pkg.optionalDependencies["exiftool-vendored.pl"]; - delete pkg.optionalDependencies["exiftool-vendored.pl"]; + const binaryPackage = + process.platform === "win32" + ? "exiftool-vendored.exe" + : "exiftool-vendored.pl"; + + if (pkg.optionalDependencies[binaryPackage]) { + pkg.dependencies[binaryPackage] = + pkg.optionalDependencies[binaryPackage]; + delete pkg.optionalDependencies[binaryPackage]; } } return pkg; diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 478a46b4bd..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Fix Permissions, Install Dependencies", - "type": "shell", - "command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": true, - "clear": false, - "group": "Devcontainer tasks", - "close": true - }, - "runOptions": { - "runOn": "default" - }, - "problemMatcher": [] - }, - { - "label": "Immich API Server (Nest)", - "dependsOn": ["Fix Permissions, Install Dependencies"], - "type": "shell", - "command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": true, - "clear": false, - "group": "Devcontainer tasks", - "close": true - }, - "runOptions": { - "runOn": "default" - }, - "problemMatcher": [] - }, - { - "label": "Immich Web Server (Vite)", - "dependsOn": ["Fix Permissions, Install Dependencies"], - "type": "shell", - "command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": true, - "clear": false, - "group": "Devcontainer tasks", - "close": true - }, - "runOptions": { - "runOn": "default" - }, - "problemMatcher": [] - }, - { - "label": "Immich Server and Web", - "dependsOn": ["Immich Web Server (Vite)", "Immich API Server (Nest)"], - "runOptions": { - "runOn": "folderOpen" - }, - "problemMatcher": [] - }, - { - "label": "Build Immich CLI", - "type": "shell", - "command": "pnpm --filter cli build:dev" - } - ] -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..1695403cb4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Immich + +We appreciate every contribution, and we're happy about every new contributor. So please feel invited to help make Immich a better product! + +## Getting started + +To get you started quickly we have detailed guides for the dev setup on our [website](https://docs.immich.app/developer/setup). If you prefer, you can also use [Devcontainers](https://docs.immich.app/developer/devcontainers). +There are also additional resources about Immich's architecture, database migrations, the use of OpenAPI, and more in our [developer documentation](https://docs.immich.app/developer/architecture). + +## General + +Please try to keep pull requests as focused as possible. A PR should do exactly one thing and not bleed into other, unrelated areas. The smaller a PR, the fewer changes are likely needed, and the quicker it will likely be merged. For larger/more impactful PRs, please reach out to us first to discuss your plans. The best way to do this is through our [Discord](https://discord.immich.app). We have a dedicated `#contributing` channel there. Additionally, please fill out the entire template when opening a PR. + +## Finding work + +If you are looking for something to work on, there are discussions and issues with a `good-first-issue` label on them. These are always a good starting point. If none of them sound interesting or fit your skill set, feel free to reach out on our Discord. We're happy to help you find something to work on! + +## Use of generative AI + +We ask you not to open PRs generated with an LLM. We find that code generated like this tends to need a large amount of back-and-forth, which is a very inefficient use of our time. If we want LLM-generated code, it's much faster for us to use an LLM ourselves than to go through an intermediary via a pull request. + +## Feature freezes + +From time to time, we put a feature freeze on parts of the codebase. For us, this means we won't accept most PRs that make changes in that area. Exempted from this are simple bug fixes that require only minor changes. We will close feature PRs that target a feature-frozen area, even if that feature is highly requested and you put a lot of work into it. Please keep that in mind, and if you're ever uncertain if a PR would be accepted, reach out to us first (e.g., in the aforementioned `#contributing` channel). We hate to throw away work. Currently, we have feature freezes on: + +- Sharing/Asset ownership +- (External) libraries + +## Non-code contributions + +If you want to contribute to Immich but you don't feel comfortable programming in our tech stack, there are other ways you can help the team. + +### Translations + +All our translations are done through [Weblate](https://hosted.weblate.org/projects/immich). These rely entirely on the community; if you speak a language that isn't fully translated yet, submitting translations there is greatly appreciated! + +### Datasets + +Help us improve our [Immich Datasets](https://datasets.immich.app) by submitting photos and videos taken from a variety of devices, including smartphones, DSLRs, and action cameras, as well as photos with unique features, such as panoramas, burst photos, and photo spheres. These datasets will be publically available for anyone to use, do not submit private/sensitive photos. + +### Community support + +If you like helping others, answering Q&A discussions here on GitHub and replying to people on our Discord is also always appreciated. diff --git a/Makefile b/Makefile index 2fc1c5d801..4d76913d8f 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ attach-server: docker exec -it docker_immich-server_1 sh renovate: - LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset + LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset # Directories that need to be created for volumes or build output VOLUME_DIRS = \ diff --git a/cli/.nvmrc b/cli/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/cli/package.json b/cli/package.json index 38b46a9a05..849957ae36 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.104", + "version": "2.5.6", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -13,30 +13,30 @@ "cli" ], "devDependencies": { - "@eslint/js": "^9.8.0", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@eslint/js": "^10.0.0", + "@immich/sdk": "workspace:*", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.10.3", + "@types/node": "^24.10.13", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.0.0", "mock-fs": "^5.2.0", "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", "vite": "^7.0.0", - "vite-tsconfig-paths": "^5.0.0", + "vite-tsconfig-paths": "^6.0.0", "vitest": "^3.0.0", "vitest-fetch-mock": "^0.4.0", "yaml": "^2.3.1" @@ -45,8 +45,8 @@ "build": "vite build", "build:dev": "vite build --sourcemap true", "lint": "eslint \"src/**/*.ts\" --max-warnings 0", - "lint:fix": "npm run lint -- --fix", - "prepack": "npm run build", + "lint:fix": "pnpm run lint --fix", + "prepack": "pnpm run build", "test": "vitest", "test:cov": "vitest --coverage", "format": "prettier --check .", @@ -69,6 +69,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" } } diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 7dce135985..ea57eeb74b 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -7,7 +7,15 @@ import { describe, expect, it, MockedFunction, vi } from 'vitest'; import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; -import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset'; +import { + checkForDuplicates, + deleteFiles, + findSidecar, + getAlbumName, + startWatch, + uploadFiles, + UploadOptionsDto, +} from 'src/commands/asset'; vi.mock('@immich/sdk'); @@ -309,3 +317,85 @@ describe('startWatch', () => { await fs.promises.rm(testFolder, { recursive: true, force: true }); }); }); + +describe('findSidecar', () => { + let testDir: string; + let testFilePath: string; + + beforeEach(() => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-sidecar-')); + testFilePath = path.join(testDir, 'test.jpg'); + fs.writeFileSync(testFilePath, 'test'); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it('should find sidecar file with photo.xmp naming convention', () => { + const sidecarPath = path.join(testDir, 'test.xmp'); + fs.writeFileSync(sidecarPath, 'xmp data'); + + const result = findSidecar(testFilePath); + expect(result).toBe(sidecarPath); + }); + + it('should find sidecar file with photo.ext.xmp naming convention', () => { + const sidecarPath = path.join(testDir, 'test.jpg.xmp'); + fs.writeFileSync(sidecarPath, 'xmp data'); + + const result = findSidecar(testFilePath); + expect(result).toBe(sidecarPath); + }); + + it('should prefer photo.ext.xmp over photo.xmp when both exist', () => { + const sidecarPath1 = path.join(testDir, 'test.xmp'); + const sidecarPath2 = path.join(testDir, 'test.jpg.xmp'); + fs.writeFileSync(sidecarPath1, 'xmp data 1'); + fs.writeFileSync(sidecarPath2, 'xmp data 2'); + + const result = findSidecar(testFilePath); + // Should return the first one found (photo.xmp) based on the order in the code + expect(result).toBe(sidecarPath1); + }); + + it('should return undefined when no sidecar file exists', () => { + const result = findSidecar(testFilePath); + expect(result).toBeUndefined(); + }); +}); + +describe('deleteFiles', () => { + let testDir: string; + let testFilePath: string; + + beforeEach(() => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-delete-')); + testFilePath = path.join(testDir, 'test.jpg'); + fs.writeFileSync(testFilePath, 'test'); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it('should delete asset and sidecar file when main file is deleted', async () => { + const sidecarPath = path.join(testDir, 'test.xmp'); + fs.writeFileSync(sidecarPath, 'xmp data'); + + await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: true, concurrency: 1 }); + + expect(fs.existsSync(testFilePath)).toBe(false); + expect(fs.existsSync(sidecarPath)).toBe(false); + }); + + it('should not delete sidecar file when delete option is false', async () => { + const sidecarPath = path.join(testDir, 'test.xmp'); + fs.writeFileSync(sidecarPath, 'xmp data'); + + await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: false, concurrency: 1 }); + + expect(fs.existsSync(testFilePath)).toBe(true); + expect(fs.existsSync(sidecarPath)).toBe(true); + }); +}); diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index ff7b609eef..7d4b09b69d 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -4,6 +4,7 @@ import { AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + Permission, addAssetsToAlbum, checkBulkUpload, createAlbum, @@ -16,17 +17,15 @@ import { Matcher, watch as watchFs } from 'chokidar'; import { MultiBar, Presets, SingleBar } from 'cli-progress'; import { chunk } from 'lodash-es'; import micromatch from 'micromatch'; -import { Stats, createReadStream } from 'node:fs'; +import { Stats, createReadStream, existsSync } from 'node:fs'; import { stat, unlink } from 'node:fs/promises'; import path, { basename } from 'node:path'; import { Queue } from 'src/queue'; -import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils'; +import { BaseOptions, Batcher, authenticate, crawl, requirePermissions, s, sha1 } from 'src/utils'; const UPLOAD_WATCH_BATCH_SIZE = 100; const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000; -const s = (count: number) => (count === 1 ? '' : 's'); - // TODO figure out why `id` is missing type AssetBulkUploadCheckResults = Array; type Asset = { id: string; filepath: string }; @@ -136,6 +135,7 @@ export const startWatch = async ( export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { await authenticate(baseOptions); + await requirePermissions([Permission.AssetUpload]); const scanFiles = await scan(paths, options); @@ -180,18 +180,49 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas } let multiBar: MultiBar | undefined; + let totalSize = 0; + const statsMap = new Map(); + + // Calculate total size first + for (const filepath of files) { + const stats = await stat(filepath); + statsMap.set(filepath, stats); + totalSize += stats.size; + } if (progress) { multiBar = new MultiBar( - { format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + { + format: '{message} | {bar} | {percentage}% | ETA: {eta_formatted} | {value}/{total}', + formatValue: (v: number, options, type) => { + // Don't format percentage + if (type === 'percentage') { + return v.toString(); + } + return byteSize(v).toString(); + }, + etaBuffer: 100, // Increase samples for ETA calculation + }, Presets.shades_classic, ); + + // Ensure we restore cursor on interrupt + process.on('SIGINT', () => { + if (multiBar) { + multiBar.stop(); + } + process.exit(0); + }); } else { - console.log(`Received ${files.length} files, hashing...`); + console.log(`Received ${files.length} files (${byteSize(totalSize)}), hashing...`); } - const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' }); - const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' }); + const hashProgressBar = multiBar?.create(totalSize, 0, { + message: 'Hashing files ', + }); + const checkProgressBar = multiBar?.create(totalSize, 0, { + message: 'Checking for duplicates', + }); const newFiles: string[] = []; const duplicates: Asset[] = []; @@ -211,7 +242,13 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas } } - checkProgressBar?.increment(assets.length); + // Update progress based on total size of processed files + let processedSize = 0; + for (const asset of assets) { + const stats = statsMap.get(asset.id); + processedSize += stats?.size || 0; + } + checkProgressBar?.increment(processedSize); }, { concurrency, retry: 3 }, ); @@ -221,6 +258,10 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const queue = new Queue( async (filepath: string): Promise => { + const stats = statsMap.get(filepath); + if (!stats) { + throw new Error(`Stats not found for ${filepath}`); + } const dto = { id: filepath, checksum: await sha1(filepath) }; results.push(dto); @@ -231,7 +272,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas void checkBulkUploadQueue.push(batch); } - hashProgressBar?.increment(); + hashProgressBar?.increment(stats.size); return results; }, { concurrency, retry: 3 }, @@ -362,23 +403,6 @@ export const uploadFiles = async ( const uploadFile = async (input: string, stats: Stats): Promise => { const { baseUrl, headers } = defaults; - const assetPath = path.parse(input); - const noExtension = path.join(assetPath.dir, assetPath.name); - - const sidecarsFiles = await Promise.all( - // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp - [`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => { - try { - const stats = await stat(sidecarPath); - return new UploadFile(sidecarPath, stats.size); - } catch { - return false; - } - }), - ); - - const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false); - const formData = new FormData(); formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, '')); formData.append('deviceId', 'CLI'); @@ -388,8 +412,15 @@ const uploadFile = async (input: string, stats: Stats): Promise => { +export const findSidecar = (filepath: string): string | undefined => { + const assetPath = path.parse(filepath); + const noExtension = path.join(assetPath.dir, assetPath.name); + + // XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp + for (const sidecarPath of [`${noExtension}.xmp`, `${filepath}.xmp`]) { + if (existsSync(sidecarPath)) { + return sidecarPath; + } + } +}; + +export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise => { let fileCount = 0; if (options.delete) { fileCount += uploaded.length; @@ -433,7 +476,15 @@ const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: Uplo const chunkDelete = async (files: Asset[]) => { for (const assetBatch of chunk(files, options.concurrency)) { - await Promise.all(assetBatch.map((input: Asset) => unlink(input.filepath))); + await Promise.all( + assetBatch.map(async (input: Asset) => { + await unlink(input.filepath); + const sidecarPath = findSidecar(input.filepath); + if (sidecarPath) { + await unlink(sidecarPath); + } + }), + ); deletionProgress.update(assetBatch.length); } }; diff --git a/cli/src/commands/auth.ts b/cli/src/commands/auth.ts index f0011c6a24..1e1efa97b4 100644 --- a/cli/src/commands/auth.ts +++ b/cli/src/commands/auth.ts @@ -1,7 +1,15 @@ -import { getMyUser } from '@immich/sdk'; +import { getMyUser, Permission } from '@immich/sdk'; import { existsSync } from 'node:fs'; import { mkdir, unlink } from 'node:fs/promises'; -import { BaseOptions, connect, getAuthFilePath, logError, withError, writeAuthFile } from 'src/utils'; +import { + BaseOptions, + connect, + getAuthFilePath, + logError, + requirePermissions, + withError, + writeAuthFile, +} from 'src/utils'; export const login = async (url: string, key: string, options: BaseOptions) => { console.log(`Logging in to ${url}`); @@ -9,6 +17,7 @@ export const login = async (url: string, key: string, options: BaseOptions) => { const { configDirectory: configDir } = options; await connect(url, key); + await requirePermissions([Permission.UserRead]); const [error, user] = await withError(getMyUser()); if (error) { diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts index bea49231c9..9a5098e628 100644 --- a/cli/src/commands/server-info.ts +++ b/cli/src/commands/server-info.ts @@ -1,8 +1,9 @@ -import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes } from '@immich/sdk'; -import { BaseOptions, authenticate } from 'src/utils'; +import { getAssetStatistics, getMyUser, getServerVersion, getSupportedMediaTypes, Permission } from '@immich/sdk'; +import { authenticate, BaseOptions, requirePermissions } from 'src/utils'; export const serverInfo = async (options: BaseOptions) => { const { url } = await authenticate(options); + await requirePermissions([Permission.ServerAbout, Permission.AssetStatistics, Permission.UserRead]); const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([ getServerVersion(), diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 9ef20b3679..38bd119459 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -1,4 +1,4 @@ -import { getMyUser, init, isHttpError } from '@immich/sdk'; +import { ApiKeyResponseDto, getMyApiKey, getMyUser, init, isHttpError, Permission } from '@immich/sdk'; import { convertPathToPattern, glob } from 'fast-glob'; import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; @@ -34,6 +34,36 @@ export const authenticate = async (options: BaseOptions): Promise => { return auth; }; +export const s = (count: number) => (count === 1 ? '' : 's'); + +let _apiKey: ApiKeyResponseDto; +export const requirePermissions = async (permissions: Permission[]) => { + if (!_apiKey) { + _apiKey = await getMyApiKey(); + } + + if (_apiKey.permissions.includes(Permission.All)) { + return; + } + + const missing: Permission[] = []; + + for (const permission of permissions) { + if (!_apiKey.permissions.includes(permission)) { + missing.push(permission); + } + } + + if (missing.length > 0) { + const combined = missing.map((permission) => `"${permission}"`).join(', '); + console.log( + `Missing required permission${s(missing.length)}: ${combined}. +Please make sure your API key has the correct permissions.`, + ); + process.exit(1); + } +}; + export const connect = async (url: string, key: string) => { const wellKnownUrl = new URL('.well-known/immich', url); try { diff --git a/deployment/mise.toml b/deployment/mise.toml index 53b683a7d3..d77ec84125 100644 --- a/deployment/mise.toml +++ b/deployment/mise.toml @@ -1,6 +1,6 @@ [tools] -terragrunt = "0.93.10" -opentofu = "1.10.7" +terragrunt = "0.98.0" +opentofu = "1.11.4" [tasks."tg:fmt"] run = "terragrunt hclfmt" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 4c74d1d640..8c46d3c51f 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -14,33 +14,65 @@ name: immich-dev services: + immich-app-base: + profiles: ['_base'] + tmpfs: + - /tmp + volumes: + - ..:/usr/src/app + - pnpm_cache:/buildcache/pnpm_cache + - server_node_modules:/usr/src/app/server/node_modules + - web_node_modules:/usr/src/app/web/node_modules + - github_node_modules:/usr/src/app/.github/node_modules + - cli_node_modules:/usr/src/app/cli/node_modules + - docs_node_modules:/usr/src/app/docs/node_modules + - e2e_node_modules:/usr/src/app/e2e/node_modules + - sdk_node_modules:/usr/src/app/open-api/typescript-sdk/node_modules + - app_node_modules:/usr/src/app/node_modules + - sveltekit:/usr/src/app/web/.svelte-kit + - coverage:/usr/src/app/web/coverage + + immich-init: + extends: + service: immich-app-base + profiles: !reset [] + container_name: immich_init + image: immich-server-dev:latest + build: + context: ../ + dockerfile: server/Dockerfile.dev + target: dev + command: + - | + pnpm install + touch /tmp/init-complete + exec tail -f /dev/null + volumes: + - pnpm_store_server:/buildcache/pnpm-store + restart: 'no' + healthcheck: + test: ['CMD', 'test', '-f', '/tmp/init-complete'] + interval: 2s + timeout: 3s + retries: 300 + start_period: 300s + immich-server: + extends: + service: immich-app-base + profiles: !reset [] container_name: immich_server command: ['immich-dev'] image: immich-server-dev:latest - # extends: - # file: hwaccel.transcoding.yml - # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding build: context: ../ dockerfile: server/Dockerfile.dev target: dev restart: unless-stopped volumes: - - ..:/usr/src/app - ${UPLOAD_LOCATION}/photos:/data - /etc/localtime:/etc/localtime:ro - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage + - pnpm_store_server:/buildcache/pnpm-store - ../plugins:/build/corePlugin env_file: - .env @@ -63,6 +95,8 @@ services: - 9231:9231 - 2283:2283 depends_on: + immich-init: + condition: service_healthy redis: condition: service_started database: @@ -71,6 +105,9 @@ services: disable: false immich-web: + extends: + service: immich-app-base + profiles: !reset [] container_name: immich_web image: immich-web-dev:latest build: @@ -84,20 +121,11 @@ services: - 3000:3000 - 24678:24678 volumes: - - ..:/usr/src/app - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage + - pnpm_store_web:/buildcache/pnpm-store restart: unless-stopped depends_on: + immich-init: + condition: service_healthy immich-server: condition: service_started @@ -116,7 +144,7 @@ services: - 3003:3003 volumes: - ../machine-learning/immich_ml:/usr/src/immich_ml - - model-cache:/cache + - model_cache:/cache env_file: - .env depends_on: @@ -127,7 +155,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 @@ -146,6 +174,8 @@ services: ports: - 5432:5432 shm_size: 128mb + healthcheck: + disable: false # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics # immich-prometheus: # container_name: immich_prometheus @@ -154,7 +184,7 @@ services: # image: prom/prometheus # volumes: # - ./prometheus.yml:/etc/prometheus/prometheus.yml - # - prometheus-data:/prometheus + # - prometheus_data:/prometheus # first login uses admin/admin # add data source for http://immich-prometheus:9090 to get started @@ -165,20 +195,22 @@ services: # - 3000:3000 # image: grafana/grafana:10.3.3-ubuntu # volumes: - # - grafana-data:/var/lib/grafana + # - grafana_data:/var/lib/grafana volumes: - model-cache: - prometheus-data: - grafana-data: - pnpm-store: - server-node_modules: - web-node_modules: - github-node_modules: - cli-node_modules: - docs-node_modules: - e2e-node_modules: - sdk-node_modules: - app-node_modules: + model_cache: + prometheus_data: + grafana_data: + pnpm_cache: + pnpm_store_server: + pnpm_store_web: + server_node_modules: + web_node_modules: + github_node_modules: + cli_node_modules: + docs_node_modules: + e2e_node_modules: + sdk_node_modules: + app_node_modules: sveltekit: coverage: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 21178d8d76..4d9e7efbe9 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -56,7 +56,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 restart: always @@ -77,13 +77,15 @@ services: - 5432:5432 shm_size: 128mb restart: always + healthcheck: + disable: false # set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics immich-prometheus: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09 + image: prom/prometheus@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -95,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.3.0-ubuntu@sha256:cee936306135e1925ab21dffa16f8a411535d16ab086bef2309339a8e74d62df + image: grafana/grafana:12.3.2-ubuntu@sha256:6cca4b429a1dc0d37d401dee54825c12d40056c3c6f3f56e3f0d6318ce77749b volumes: - grafana-data:/var/lib/grafana diff --git a/docker/docker-compose.rootless.yml b/docker/docker-compose.rootless.yml new file mode 100644 index 0000000000..f6eb38a429 --- /dev/null +++ b/docker/docker-compose.rootless.yml @@ -0,0 +1,100 @@ +# +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose +# +# Make sure to use the docker-compose.yml of the current release: +# +# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +# +# The compose file on main may not be compatible with the latest release. + +name: immich + +services: + immich-server: + container_name: immich_server + image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} + # extends: + # file: hwaccel.transcoding.yml + # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file + - ${UPLOAD_LOCATION}:/data + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ports: + - '2283:2283' + depends_on: + - redis + - database + restart: always + healthcheck: + disable: false + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + - ./ml-model-cache:/cache + - ./ml-dotcache:/.cache + - ./ml-config:/.config + env_file: + - .env + restart: always + healthcheck: + disable: false + + redis: + container_name: immich_redis + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + volumes: + - ./redis:/data + healthcheck: + test: redis-cli ping || exit 1 + restart: always + + database: + container_name: immich_postgres + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23 + user: '1000:1000' + security_opt: + - no-new-privileges:true + cap_drop: + - NET_RAW + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs + # DB_STORAGE_TYPE: 'HDD' + 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 + healthcheck: + disable: false + +volumes: + model-cache: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f5dfb1233f..3d92655453 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,7 +49,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e healthcheck: test: redis-cli ping || exit 1 restart: always @@ -69,6 +69,8 @@ services: - ${DB_DATA_LOCATION}:/var/lib/postgresql/data shm_size: 128mb restart: always + healthcheck: + disable: false volumes: model-cache: diff --git a/docs/.nvmrc b/docs/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 9dcfcac48b..7b7a265ddf 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -22,7 +22,7 @@ For organizations seeking to resell Immich, we have established the following gu - Do not misrepresent your reseller site or services as being officially affiliated with or endorsed by Immich or our development team. -- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase licenses directly from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work. +- For small resellers who wish to contribute financially to Immich's development, we recommend directing your customers to purchase product keys directly from us rather than attempting to broker revenue-sharing arrangements. We ask that you refrain from misrepresenting reseller activities as directly supporting our development work. When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app @@ -402,6 +402,9 @@ To decrease Redis logs, you can add the following line to the `redis:` section o ### How can I run Immich as a non-root user? You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. + +[Example docker-compose.yml file](https://github.com/immich-app/immich/blob/main/docker/docker-compose.rootless.yml) + You may need to add mount points or docker volumes for the following internal container paths: - `immich-machine-learning:/.config` diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 2ca965624f..ae605f8462 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -2,6 +2,8 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import { mdiAlertCircle, mdiCheckCircle } from '@mdi/js'; +import Icon from '@mdi/react'; A [3-2-1 backup strategy](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) is recommended to protect your data. You should keep copies of your uploaded photos/videos as well as the Immich database for a comprehensive backup solution. This page provides an overview on how to backup the database and the location of user-uploaded pictures and videos. A template bash script that can be run as a cron job is provided [here](/guides/template-backup-script.md) @@ -11,54 +13,135 @@ The instructions on this page show you how to prepare your Immich instance to be ## Database +Immich stores [file paths](https://github.com/immich-app/immich/discussions/3299) and user metadata in the database. It does not scan the library folder, so database backups are essential. + +### Automatic Database Backups + +Immich automatically creates database backups for disaster-recovery purposes. These backups are stored in `UPLOAD_LOCATION/backups` and can be managed through the web interface. + +You can adjust the backup schedule and retention settings in **Administration > Settings > Backup** (default: keep last 14 backups, create daily at 2:00 AM). + :::caution -Immich saves [file paths in the database](https://github.com/immich-app/immich/discussions/3299), it does not scan the library folder to update the database so backups are crucial. +Database backups do **not** contain photos or videos — only metadata. They must be used together with a copy of the files in `UPLOAD_LOCATION` as outlined below. ::: +#### Creating a Backup + +You can trigger a database backup manually: + +1. Go to **Administration > Job Queues** +2. Click **Create job** in the top right +3. Select **Create Database Backup** and click **Confirm** + +The backup will appear in `UPLOAD_LOCATION/backups` and counts toward your retention limit. + +### Restoring a Database Backup + +Immich provides two ways to restore a database backup: through the web interface or via the command line. The web interface is the recommended method for most users. + +#### Restore from Settings {#restore-from-settings} + +If you have an existing Immich installation: + + + +1. Go to **Administration > Maintenance** +2. Expand the **Restore database backup** section +3. You'll see a list of available backups with their version and creation date +4. Click **Restore** next to the backup you want to restore +5. Confirm the restore operation + :::info -Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database. +Restoring a backup will wipe the current database and replace it with the backup. A restore point is automatically created before the operation begins, allowing rollback if the restore fails. ::: -:::caution -It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored. +#### Restore from Onboarding {#restore-from-onboarding} + +If you're setting up Immich on a fresh installation and want to restore from an existing backup: + +1. Download and populate `.env` and `docker-compose.yml` as per the [installation instructions](/install/docker-compose). +2. Move the previous's instance data directories containing `backups`, `encoded-video`, `library`, `profile`, `thumbs` and `upload` into the new `UPLOAD_LOCATION` +3. **(For external libraries)** If you used external library feature in your previous instance, make sure that the mount settings in your new `docker-compose.yml` reflect the same structure. You may need to move files accordingly. + +:::info Example + +Assuming your previous `UPLOAD_LOCATION` was `UPLOAD_LOCATION=/my-broken-instance/media` and your new one is `UPLOAD_LOCATION=/a-brand-new-instance/data`, you will need to perform the following file moves: + +``` +/my-broken-instance/media/backups -> /a-brand-new-instance/data/backups +/my-broken-instance/media/encoded-video -> /a-brand-new-instance/data/encoded-video +/my-broken-instance/media/library -> /a-brand-new-instance/data/library +/my-broken-instance/media/profile -> /a-brand-new-instance/data/profile +/my-broken-instance/media/thumbs -> /a-brand-new-instance/data/thumbs +/my-broken-instance/media/upload -> /a-brand-new-instance/data/upload +``` + ::: -### Automatic Database Dumps +4. Start the Immich services with `docker compose up -d` + + + +5. On the welcome screen, click **Restore from backup** +6. Immich will enter maintenance mode and display integrity checks for your storage folders +7. Review the folder status to ensure your library files are accessible +8. Click **Next** to proceed to backup selection +9. Select a backup from the list or upload a backup file (`.sql.gz`) +10. Click **Restore** to begin the restoration process + +:::tip +Before restoring, ensure your `UPLOAD_LOCATION` folders contain the same files that existed when the backup was created. The integrity check will show you which folders are readable/writable and how many files they contain. +::: + +### Uploading a Backup File {#uploading-backup} + +You can upload a database backup file directly: + +1. In the **Restore database backup** section, click **Select from computer** +2. Choose a `.sql.gz` file +3. The uploaded backup will appear in the list with an `uploaded-` prefix +4. Click **Restore** to restore from the uploaded file + +### Backup Version Compatibility {#backup-compatibility} + +When viewing backups, Immich displays compatibility indicators based on the current version and the information from the filename: + +- Backup version matches current Immich version +- Backup was created with a different Immich version +- Could not determine backup version :::warning -The automatic database dumps can be used to restore the database in the event of damage to the Postgres database files. -There is no monitoring for these dumps and you will not be notified if they are unsuccessful. +Restoring a backup from a different Immich version may require database migrations. The restore process will attempt to run migrations automatically, but you should ensure you're restoring to a compatible version when possible. ::: -:::caution -The database dumps do **NOT** contain any pictures or videos, only metadata. They are only usable with a copy of the other files in `UPLOAD_LOCATION` as outlined below. -::: +### Restore Process {#restore-process} -For disaster-recovery purposes, Immich will automatically create database dumps. The dumps are stored in `UPLOAD_LOCATION/backups`. -Please be sure to make your own, independent backup of the database together with the asset folders as noted below. -You can adjust the schedule and amount of kept database dumps in the [admin settings](http://my.immich.app/admin/system-settings?isOpen=backup). -By default, Immich will keep the last 14 database dumps and create a new dump every day at 2:00 AM. +During restoration, Immich will: -#### Trigger Dump +1. Create a backup of the current database (restore point) +2. Restore the selected backup +3. Run database migrations if needed +4. Perform a health check to verify the restore succeeded -You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/queues). -Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm". -A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder. -This dumps will count towards the last `X` dumps that will be kept based on your settings. +If the restore fails (e.g., corrupted backup or missing admin user), Immich will automatically roll back to the restore point. -#### Restoring +### Restore via Command Line {#restore-cli} -We hope to make restoring simpler in future versions, for now you can find the database dumps in the `UPLOAD_LOCATION/backups` folder on your host. -Then please follow the steps in the following section for restoring the database. - -### Manual Backup and Restore +For advanced users or automated recovery scenarios, you can restore a database backup using the command line. ```bash title='Backup' # Replace with the database username - usually postgres unless you have changed it. -docker exec -t immich_postgres pg_dumpall --clean --if-exists --username= | gzip > "/path/to/backup/dump.sql.gz" +# Replace with the database name - usually immich unless you have changed it. +docker exec -t immich_postgres pg_dump --clean --if-exists --dbname= --username= | gzip > "/path/to/backup/dump.sql.gz" ``` ```bash title='Restore' @@ -71,9 +154,10 @@ docker start immich_postgres # Start Postgres server sleep 10 # Wait for Postgres server to start up # Check the database user if you deviated from the default # Replace with the database username - usually postgres unless you have changed it. +# Replace with the database name - usually immich unless you have changed it. gunzip --stdout "/path/to/backup/dump.sql.gz" \ | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ -| docker exec -i immich_postgres psql --dbname=postgres --username= # Restore Backup +| docker exec -i immich_postgres psql --dbname= --username= --single-transaction --set ON_ERROR_STOP=on # Restore Backup docker compose up -d # Start remainder of Immich apps ``` @@ -82,7 +166,8 @@ docker compose up -d # Start remainder of Immich apps ```powershell title='Backup' # Replace with the database username - usually postgres unless you have changed it. -[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=)) +# Replace with the database name - usually immich unless you have changed it. +[System.IO.File]::WriteAllLines("C:\absolute\path\to\backup\dump.sql", (docker exec -t immich_postgres pg_dump --clean --if-exists --dbname= --username=)) ``` ```powershell title='Restore' @@ -97,8 +182,9 @@ sleep 10 # Wait for Postgres server to docker exec -it immich_postgres bash # Enter the Docker shell and run the following command # If your backup ends in `.gz`, replace `cat` with `gunzip --stdout` # Replace with the database username - usually postgres unless you have changed it. +# Replace with the database name - usually immich unless you have changed it. -cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username= +cat "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname= --username= --single-transaction --set ON_ERROR_STOP=on exit # Exit the Docker shell docker compose up -d # Start remainder of Immich apps ``` @@ -106,10 +192,20 @@ docker compose up -d # Start remainder of Immich ap -Note that for the database restore to proceed properly, it requires a completely fresh install (i.e. the Immich server has never run since creating the Docker containers). If the Immich app has run, Postgres conflicts may be encountered upon database restoration (relation already exists, violated foreign key constraints, multiple primary keys, etc.), in which case you need to delete the `DB_DATA_LOCATION` folder to reset the database. +:::warning +The backup and restore process changed in v2.5.0, if you have a backup created with an older version of Immich, use the documentation version selector to find manual restore instructions for your backup. +::: + +:::note +For the database restore to proceed properly, it requires a completely fresh install (i.e., the Immich server has never run since creating the Docker containers). If the Immich app has run, you may encounter Postgres conflicts (relation already exists, violated foreign key constraints, etc.). In this case, delete the `DB_DATA_LOCATION` folder to reset the database. +::: :::tip -Some deployment methods make it difficult to start the database without also starting the server. In these cases, you may set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Be sure to remove this variable and restart the services after the database is restored. +Some deployment methods make it difficult to start the database without also starting the server. In these cases, set the environment variable `DB_SKIP_MIGRATIONS=true` before starting the services. This prevents the server from running migrations that interfere with the restore process. Remove this variable and restart services after the database is restored. +::: + +:::tip +The provided restore process ensures your database is never in a broken state by committing all changes in one transaction. This may be undesirable behaviour in some circumstances, you can disable it by removing `--single-transaction --set ON_ERROR_STOP=on` from the command. ::: ## Filesystem @@ -157,17 +253,14 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele - **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/`. - +- **Database Dump Backups:** + - Automatic database backups created by Immich for disaster recovery. + - Stored in `UPLOAD_LOCATION/backups/`. - **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`. - :::danger - A backup of this folder does not constitute a backup of your database! - Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. - ::: - @@ -203,16 +296,14 @@ When you turn off the storage template engine, it will leave the assets in `UPLO - Files uploaded through mobile apps. - Temporarily located in `UPLOAD_LOCATION/upload/`. - Transferred to `UPLOAD_LOCATION/library/` upon successful upload. +- **Database Dump Backups:** + - Automatic database backups created by Immich for disaster recovery. + - Stored in `UPLOAD_LOCATION/backups/`. - **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`. - :::danger - A backup of this folder does not constitute a backup of your database! - Follow the instructions listed [here](/administration/backup-and-restore#database) to learn how to perform a proper backup. - ::: - diff --git a/docs/docs/administration/img/admin-jobs.webp b/docs/docs/administration/img/admin-jobs.webp index 2867e18adc..c9863d163a 100644 Binary files a/docs/docs/administration/img/admin-jobs.webp and b/docs/docs/administration/img/admin-jobs.webp differ diff --git a/docs/docs/administration/img/admin-nightly-tasks.webp b/docs/docs/administration/img/admin-nightly-tasks.webp index b3d8f13cb6..e95aa56a7b 100644 Binary files a/docs/docs/administration/img/admin-nightly-tasks.webp and b/docs/docs/administration/img/admin-nightly-tasks.webp differ diff --git a/docs/docs/administration/img/customize-delete-user.webp b/docs/docs/administration/img/customize-delete-user.webp deleted file mode 100644 index 6f171b4bc2..0000000000 Binary files a/docs/docs/administration/img/customize-delete-user.webp and /dev/null differ diff --git a/docs/docs/administration/img/immediately-remove-user.webp b/docs/docs/administration/img/immediately-remove-user.webp index 8addeff14c..0960548f1d 100644 Binary files a/docs/docs/administration/img/immediately-remove-user.webp and b/docs/docs/administration/img/immediately-remove-user.webp differ diff --git a/docs/docs/administration/img/restore-from-onboarding.webp b/docs/docs/administration/img/restore-from-onboarding.webp new file mode 100644 index 0000000000..d09454ef19 Binary files /dev/null and b/docs/docs/administration/img/restore-from-onboarding.webp differ diff --git a/docs/docs/administration/img/restore-from-settings.webp b/docs/docs/administration/img/restore-from-settings.webp new file mode 100644 index 0000000000..f205e7ec6d Binary files /dev/null and b/docs/docs/administration/img/restore-from-settings.webp differ diff --git a/docs/docs/administration/img/server-stats.webp b/docs/docs/administration/img/server-stats.webp index 3048c38b66..33ffa1353b 100644 Binary files a/docs/docs/administration/img/server-stats.webp and b/docs/docs/administration/img/server-stats.webp differ diff --git a/docs/docs/administration/img/user-edit-menu.webp b/docs/docs/administration/img/user-edit-menu.webp new file mode 100644 index 0000000000..5dd7edd298 Binary files /dev/null and b/docs/docs/administration/img/user-edit-menu.webp differ diff --git a/docs/docs/administration/img/user-notifications-settings.webp b/docs/docs/administration/img/user-notifications-settings.webp index 301dce7c6b..964556e928 100644 Binary files a/docs/docs/administration/img/user-notifications-settings.webp and b/docs/docs/administration/img/user-notifications-settings.webp differ diff --git a/docs/docs/administration/img/user-notifications-templates.webp b/docs/docs/administration/img/user-notifications-templates.webp index a40bf82414..5c5f68ac5e 100644 Binary files a/docs/docs/administration/img/user-notifications-templates.webp and b/docs/docs/administration/img/user-notifications-templates.webp differ diff --git a/docs/docs/administration/img/user-quota-size.webp b/docs/docs/administration/img/user-quota-size.webp index 2989bba392..d35fca571b 100644 Binary files a/docs/docs/administration/img/user-quota-size.webp and b/docs/docs/administration/img/user-quota-size.webp differ diff --git a/docs/docs/administration/img/user-storage-label.webp b/docs/docs/administration/img/user-storage-label.webp index 5d54e43899..661dd2b23f 100644 Binary files a/docs/docs/administration/img/user-storage-label.webp and b/docs/docs/administration/img/user-storage-label.webp differ diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index 8ed3ba2694..74025f8ae8 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -50,7 +50,7 @@ When a new asset is uploaded it kicks off a series of jobs, which include metada 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](/features/libraries) scanning, Database Dump) are configured in their own sections in System Settings. diff --git a/docs/docs/administration/maintenance-mode.md b/docs/docs/administration/maintenance-mode.md index 300c27ca40..47848bef42 100644 --- a/docs/docs/administration/maintenance-mode.md +++ b/docs/docs/administration/maintenance-mode.md @@ -4,7 +4,7 @@ Maintenance mode is used to perform administrative tasks such as restoring backu You can enter maintenance mode by either: -- Selecting "enable maintenance mode" in system settings in administration. +- Selecting "Switch to maintenance mode" in `Maintenance` tab in administration. - Running the enable maintenance mode [administration command](./server-commands.md). ## Logging in during maintenance diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 47f4a96c6a..d0a9ce733e 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -56,11 +56,13 @@ Once you have a new OAuth client application configured, Immich can be configure | Setting | Type | Default | Description | | ---------------------------------------------------- | ------- | -------------------- | ----------------------------------------------------------------------------------- | | Enabled | boolean | false | Enable/disable OAuth | -| Issuer URL | URL | (required) | Required. Self-discovery URL for client (from previous step) | -| Client ID | string | (required) | Required. Client ID (from previous step) | -| Client Secret | string | (required) | Required. Client Secret (previous step) | -| 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) | +| `issuer_url` | URL | (required) | Required. Self-discovery URL for client (from previous step) | +| `client_id` | string | (required) | Required. Client ID (from previous step) | +| `client_secret` | string | (required) | Required. Client Secret (previous step) | +| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) | +| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) | +| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) | +| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up | | Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**š** | | Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**š** | | Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**š** | diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md index 2b7527623f..84681fdfa6 100644 --- a/docs/docs/administration/postgres-standalone.md +++ b/docs/docs/administration/postgres-standalone.md @@ -22,7 +22,7 @@ Immich is known to work with Postgres versions `>= 14, < 19`. VectorChord is known to work with pgvector versions `>= 0.7, < 0.9`. The Immich server will check the VectorChord version on startup to ensure compatibility, and refuse to start if a compatible version is not found. -The current accepted range for VectorChord is `>= 0.3, < 0.6`. +The current accepted range for VectorChord is `>= 0.3, < 2.0`. ::: ## Specifying the connection URL @@ -88,7 +88,7 @@ The easiest option is to have both extensions installed during the migration:
Migration steps (automatic) 1. Ensure you still have pgvecto.rs installed -2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`) +2. Install `pgvector` (`>= 0.7, < 0.9`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`) 3. [Install VectorChord][vchord-install] 4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed 5. Restart the Postgres database diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 8dd1674448..b53356139f 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -98,7 +98,6 @@ entryPoints: respondingTimeouts: readTimeout: 600s idleTimeout: 600s - writeTimeout: 600s ``` The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example. diff --git a/docs/docs/administration/user-management.mdx b/docs/docs/administration/user-management.mdx index b98ffe0d69..6d2b2f9062 100644 --- a/docs/docs/administration/user-management.mdx +++ b/docs/docs/administration/user-management.mdx @@ -31,7 +31,7 @@ Admin can send a welcome email if the Email option is set, you can learn here ho Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore. -In order to select a storage quota, click on the pencil icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default). +In order to select a storage quota, click on the edit user icon and enter the storage quota in GiB. You can choose an unlimited quota by leaving it empty (default). :::tip The system administrator can see the usage quota percentage of all users in Server Stats page. @@ -41,12 +41,12 @@ The system administrator can see the usage quota percentage of all users in Serv External libraries don't take up space from the storage quota. ::: - + ## Set Storage Label For User The admin can add a custom label for each user, so instead of `upload/{userId}/your-template` it will be `upload/{custom_user_label}/your-template`. -To apply a storage template, go to the Administration page -> click on the pencil button next to the user. +To apply a storage template, go to the `Administration > Users`, then click on the context menu button next to the user. :::note To apply the Storage Label to previously uploaded assets, run the Storage Migration Job. ::: @@ -55,25 +55,21 @@ To apply the Storage Label to previously uploaded assets, run the Storage Migrat ## Password Reset -To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in. + - +To reset a user's password, go to `Administration > Users`, then click on the context menu button next to the user, then click "Reset Password". The user's password will be reset to a random password and they have to change it next time they sign in. ## Delete a User -If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default. - - +If you need to remove a user from Immich, go to `Administration > Users`, then click on the context menu button next to the user. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default. ### Delete Delay -You can customize the time of the deletion of the users from the Administration -> Settings -> User Settings. +You can customize the time of the deletion of the users from `Administration -> Settings -> User Settings`. :::info user deletion job The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution. ::: - - ### Immediately Remove User You can choose to delete a user immediately by checking the box diff --git a/docs/docs/developer/open-api.md b/docs/docs/api.md similarity index 99% rename from docs/docs/developer/open-api.md rename to docs/docs/api.md index f627b2c459..edf58dc94d 100644 --- a/docs/docs/developer/open-api.md +++ b/docs/docs/api.md @@ -1,4 +1,4 @@ -# OpenAPI +# API Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generate API documentation. To view the published docs see [here](https://api.immich.app/). diff --git a/docs/docs/developer/architecture.mdx b/docs/docs/developer/architecture.mdx index 42d9c1b974..954d264d55 100644 --- a/docs/docs/developer/architecture.mdx +++ b/docs/docs/developer/architecture.mdx @@ -24,7 +24,7 @@ Immich has three main clients: 3. CLI - Command-line utility for bulk upload :::info -All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](./open-api.md). +All three clients use [OpenAPI](/api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](/api.md). ::: ### Mobile App @@ -71,7 +71,7 @@ An incoming HTTP request is mapped to a controller (`src/controllers`). Controll ### Domain Transfer Objects (DTOs) -The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client. +The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](/api.md) schemas and control the generated code used by each client. ### Background Jobs diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md index f50ec62d8a..4bd60262ad 100644 --- a/docs/docs/developer/devcontainers.md +++ b/docs/docs/developer/devcontainers.md @@ -44,7 +44,7 @@ While this guide focuses on VS Code, you have many options for Dev Container dev **Self-Hostable Options:** - [Coder](https://coder.com) - Enterprise-focused, requires Terraform knowledge, self-managed -- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise) +- [DevPod](https://devpod.sh) - Client-only tool with excellent devcontainer.json support, works with any provider (local, cloud, or on-premise). Check [quick-start guide](#quick-start-guide-for-devpod-with-docker) ::: ## Dev Container Services @@ -408,7 +408,27 @@ If you encounter issues: 1. Check container logs: View → Output → Select "Dev Containers" 2. Rebuild without cache: "Dev Containers: Rebuild Container Without Cache" 3. Review [common Docker issues](https://docs.docker.com/desktop/troubleshoot/) -4. Ask in [Discord](https://discord.immich.app) `#help-desk-support` channel +4. Ask in [Discord](https://discord.immich.app) `#contributing` channel + +### Quick-start guide for DevPod with docker + +You will need DevPod CLI (check [DevPod CLI installation guide](https://devpod.sh/docs/getting-started/install)) and Docker Desktop. + +```sh +# Step 1: Clone the Repository +git clone https://github.com/immich-app/immich.git +cd immich + +# Step 2: Prepare DevPod (if you haven't already) +devpod provider add docker +devpod provider use docker + +# Step 3: Build 'immich-server-dev' docker image first manually +docker build -f server/Dockerfile.dev -t immich-server-dev . + +# Step 4: Now you can start devcontainer +devpod up . +``` ## Mobile Development diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md index e68567bc8f..e5dc6cc1e5 100644 --- a/docs/docs/developer/pr-checklist.md +++ b/docs/docs/developer/pr-checklist.md @@ -53,7 +53,7 @@ You can use `dart fix --apply` and `dcm fix lib` to potentially correct some iss ## OpenAPI -The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/developer/open-api.md) for more details. +The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details. ## Database Migrations diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 23c1862c19..4bbf71dd89 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -4,6 +4,10 @@ sidebar_position: 2 # Setup +:::warning +Make sure to read the [`CONTRIBUTING.md`](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before you dive into the code. +::: + :::note If there's a feature you're planning to work on, just give us a heads up in [#contributing](https://discord.com/channels/979116623879368755/1071165397228855327) on [our Discord](https://discord.immich.app) so we can: @@ -33,7 +37,8 @@ All the services are packaged to run as with single Docker Compose command. 1. Clone the project repo. 2. Run `cp docker/example.env docker/.env`. 3. Edit `docker/.env` to provide values for the required variable `UPLOAD_LOCATION`. -4. From the root directory, run: +4. Install dependencies - `pnpm i` +5. From the root directory, run: ```bash title="Start development server" make dev # required Makefile installed on the system. @@ -85,10 +90,13 @@ To see local changes to `@immich/ui` in Immich, do the following: #### Setup -1. Setup Flutter toolchain using FVM. -2. Run `flutter pub get` to install the dependencies. -3. Run `make translation` to generate the translation file. -4. Run `fvm flutter run` to start the app. +1. [Install mise](https://mise.jdx.dev/installing-mise.html). +2. Change to the immich (root) directory and trust the mise config with `mise trust`. +3. Install tools with mise: `mise install`. +4. Change to the `mobile/` directory. +5. Run `flutter pub get` to install the dependencies. +6. Run `make translation` to generate the translation file. +7. Run `flutter run` to start the app. #### Translation diff --git a/docs/docs/features/automatic-backup.md b/docs/docs/features/automatic-backup.md deleted file mode 100644 index 30d132cef8..0000000000 --- a/docs/docs/features/automatic-backup.md +++ /dev/null @@ -1,42 +0,0 @@ -# Automatic Backup - -Immich supports uploading photos and videos from your mobile device to the server automatically. - ---- - -You can enable the settings by accessing the upload options from the upload page - - - - - -## Foreground backup - -If foreground backup is enabled: whenever the app is opened or resumed, it will check if any photos or videos in the selected album(s) have yet to be uploaded to the cloud (the remainder count). If there are any, they will be uploaded. - -## Background backup - -This feature is intended for everyday use. For initial bulk uploading, please use the foreground upload feature. For more information on why background upload is not working as expected, please refer to the [FAQ](/FAQ#why-does-foreground-backup-stop-when-i-navigate-away-from-the-app-shouldnt-it-transfer-the-job-to-background-backup). - -If background backup is enabled. The app will periodically check if there are any new photos or videos in the selected album(s) to be uploaded to the server. If there are, it will upload them to the cloud in the background. - -:::info Note - -#### General - -- The app must be in the background for the backup worker to start running. -- If you reopen the app and the first page you see is the backup page, the counts will not reflect the background uploaded result. You have to navigate out of the page and come back to see the updated counts. - -#### Android - -- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone. - -#### iOS - -- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh. - -
- -
- -::: diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md index 9a00cb50e1..03e96e5080 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -183,11 +183,13 @@ For example to get a list of files that would be uploaded for further processing: ```bash -immich upload --dry-run . | tail -n +6 | jq .newFiles[] +immich upload --dry-run --json-output . | tail -n +6 | jq .newFiles[] ``` ### Obtain the API Key -The API key can be obtained in the user setting panel on the web interface. +The API key can be obtained in the user setting panel on the web interface. You can also specify permissions for the key to limit its access. ![Obtain Api Key](./img/obtain-api-key.webp) + +![Specify permissions for the key](./img/obtain-api-key-2.webp) diff --git a/docs/docs/features/editing.mdx b/docs/docs/features/editing.mdx new file mode 100644 index 0000000000..5d51798e15 --- /dev/null +++ b/docs/docs/features/editing.mdx @@ -0,0 +1,19 @@ +# Editing + +Immich supports non-destructive editing of photos. This means that any edits you make to an asset do not modify the original file, but instead create a new version of the asset with the edits applied. You can always revert back to the original asset if needed. + +## Supported Edits + +Currently, Immich supports the following types of edits: + +- Cropping +- Rotation +- Mirroring + + + +## Download + +When you download an edited asset, Immich provides the edited version of the asset by default. However, you can choose to download the original version if needed. + + diff --git a/docs/docs/features/facial-recognition.md b/docs/docs/features/facial-recognition.md index 85712ef5f6..cb896ca19e 100644 --- a/docs/docs/features/facial-recognition.md +++ b/docs/docs/features/facial-recognition.md @@ -21,14 +21,14 @@ The asset detail view will also show the faces that are recognized in the asset. Additional actions you can do include: - Changing the feature photo of the person -- Setting a person's date of birth -- Merging two or more detected faces into one person - Hiding the faces of a person from the Explore page and detail view -- Assigning an unrecognized face to a person +- Setting a person's date of birth, so that the age of the person can be shown at the time the photo was taken +- Merging two or more detected people into one person +- Favoriting a person to pin them to the top of the list It can be found from the app bar when you access the detail view of a person. - + ## How Face Detection Works diff --git a/docs/docs/features/hardware-transcoding.md b/docs/docs/features/hardware-transcoding.md index d28cd97de0..e68f6f6983 100644 --- a/docs/docs/features/hardware-transcoding.md +++ b/docs/docs/features/hardware-transcoding.md @@ -71,6 +71,22 @@ For RKMPP to work: 5. (Optional) Enable hardware decoding for optimal performance. +
+immich.json + +If you use a [configuration file](/install/config-file.md), use the `accel` option to select the hardware (e.g. `qsv` for Intel or `nvenc` for Nvidia). Set `accelDecode` to `true` if you want hardware decoding. + +```json +{ + "ffmpeg": { + "accel": "qsv", + "accelDecode": true + } +} +``` + +
+ #### Single Compose File Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly. diff --git a/docs/docs/features/img/advanced-search-filters.webp b/docs/docs/features/img/advanced-search-filters.webp index 822d84faec..2d56ccad15 100644 Binary files a/docs/docs/features/img/advanced-search-filters.webp and b/docs/docs/features/img/advanced-search-filters.webp differ diff --git a/docs/docs/features/img/android-backup-options.webp b/docs/docs/features/img/android-backup-options.webp new file mode 100644 index 0000000000..aa5364d812 Binary files /dev/null and b/docs/docs/features/img/android-backup-options.webp differ diff --git a/docs/docs/features/img/background-foreground-backup.webp b/docs/docs/features/img/background-foreground-backup.webp deleted file mode 100644 index dddef137d2..0000000000 Binary files a/docs/docs/features/img/background-foreground-backup.webp and /dev/null differ diff --git a/docs/docs/features/img/backup-album-selection.webp b/docs/docs/features/img/backup-album-selection.webp new file mode 100644 index 0000000000..8c978c678e Binary files /dev/null and b/docs/docs/features/img/backup-album-selection.webp differ diff --git a/docs/docs/features/img/backup-album-sync.webp b/docs/docs/features/img/backup-album-sync.webp new file mode 100644 index 0000000000..1a05ef0584 Binary files /dev/null and b/docs/docs/features/img/backup-album-sync.webp differ diff --git a/docs/docs/features/img/backup-options.webp b/docs/docs/features/img/backup-options.webp new file mode 100644 index 0000000000..7fdccd27fb Binary files /dev/null and b/docs/docs/features/img/backup-options.webp differ diff --git a/docs/docs/features/img/enable-backup-button.webp b/docs/docs/features/img/enable-backup-button.webp new file mode 100644 index 0000000000..d3d4bb29e5 Binary files /dev/null and b/docs/docs/features/img/enable-backup-button.webp differ diff --git a/docs/docs/features/img/facial-recognition-1.webp b/docs/docs/features/img/facial-recognition-1.webp index dd96393b06..6d8f90f8e5 100644 Binary files a/docs/docs/features/img/facial-recognition-1.webp and b/docs/docs/features/img/facial-recognition-1.webp differ diff --git a/docs/docs/features/img/facial-recognition-2.webp b/docs/docs/features/img/facial-recognition-2.webp index 3c910fd315..363dd7e9bc 100644 Binary files a/docs/docs/features/img/facial-recognition-2.webp and b/docs/docs/features/img/facial-recognition-2.webp differ diff --git a/docs/docs/features/img/facial-recognition-3.webp b/docs/docs/features/img/facial-recognition-3.webp index fd0180ac66..c094617452 100644 Binary files a/docs/docs/features/img/facial-recognition-3.webp and b/docs/docs/features/img/facial-recognition-3.webp differ diff --git a/docs/docs/features/img/facial-recognition-4.webp b/docs/docs/features/img/facial-recognition-4.webp index 07dd378e9e..94c48320fd 100644 Binary files a/docs/docs/features/img/facial-recognition-4.webp and b/docs/docs/features/img/facial-recognition-4.webp differ diff --git a/docs/docs/features/img/folder-view-enable.webp b/docs/docs/features/img/folder-view-enable.webp index 784ecffc73..46477b120e 100644 Binary files a/docs/docs/features/img/folder-view-enable.webp and b/docs/docs/features/img/folder-view-enable.webp differ diff --git a/docs/docs/features/img/free-up-space.webp b/docs/docs/features/img/free-up-space.webp new file mode 100644 index 0000000000..603a088e99 Binary files /dev/null and b/docs/docs/features/img/free-up-space.webp differ diff --git a/docs/docs/features/img/gcast-enable.webp b/docs/docs/features/img/gcast-enable.webp index f128b82e25..a39c83dd84 100644 Binary files a/docs/docs/features/img/gcast-enable.webp and b/docs/docs/features/img/gcast-enable.webp differ diff --git a/docs/docs/features/img/library-custom-scan-interval.webp b/docs/docs/features/img/library-custom-scan-interval.webp index d9861ada97..a383c480dd 100644 Binary files a/docs/docs/features/img/library-custom-scan-interval.webp and b/docs/docs/features/img/library-custom-scan-interval.webp differ diff --git a/docs/docs/features/img/mobile-smart-search.webp b/docs/docs/features/img/mobile-smart-search.webp deleted file mode 100644 index e125fa5c62..0000000000 Binary files a/docs/docs/features/img/mobile-smart-search.webp and /dev/null differ diff --git a/docs/docs/features/img/mobile-upload-selected-photos.webp b/docs/docs/features/img/mobile-upload-selected-photos.webp index 3c69d0c459..fa032752cb 100644 Binary files a/docs/docs/features/img/mobile-upload-selected-photos.webp and b/docs/docs/features/img/mobile-upload-selected-photos.webp differ diff --git a/docs/docs/features/img/my-wife.webp b/docs/docs/features/img/my-wife.webp deleted file mode 100644 index cac17c1a37..0000000000 Binary files a/docs/docs/features/img/my-wife.webp and /dev/null differ diff --git a/docs/docs/features/img/obtain-api-key-2.webp b/docs/docs/features/img/obtain-api-key-2.webp new file mode 100644 index 0000000000..3f946f2ea8 Binary files /dev/null and b/docs/docs/features/img/obtain-api-key-2.webp differ diff --git a/docs/docs/features/img/obtain-api-key.webp b/docs/docs/features/img/obtain-api-key.webp index 5706d39524..daba7d8b4b 100644 Binary files a/docs/docs/features/img/obtain-api-key.webp and b/docs/docs/features/img/obtain-api-key.webp differ diff --git a/docs/docs/features/img/partner-sharing-1.webp b/docs/docs/features/img/partner-sharing-1.webp index 489cfa9a70..0c8e96be34 100644 Binary files a/docs/docs/features/img/partner-sharing-1.webp and b/docs/docs/features/img/partner-sharing-1.webp differ diff --git a/docs/docs/features/img/partner-sharing-2.webp b/docs/docs/features/img/partner-sharing-2.webp index d1d9b4df5f..394302d6b0 100644 Binary files a/docs/docs/features/img/partner-sharing-2.webp and b/docs/docs/features/img/partner-sharing-2.webp differ diff --git a/docs/docs/features/img/partner-sharing-3.webp b/docs/docs/features/img/partner-sharing-3.webp index 47bb89d072..86e1fcb986 100644 Binary files a/docs/docs/features/img/partner-sharing-3.webp and b/docs/docs/features/img/partner-sharing-3.webp differ diff --git a/docs/docs/features/img/partner-sharing-4.webp b/docs/docs/features/img/partner-sharing-4.webp index 4bdf9263e7..15e2204d39 100644 Binary files a/docs/docs/features/img/partner-sharing-4.webp and b/docs/docs/features/img/partner-sharing-4.webp differ diff --git a/docs/docs/features/img/partner-sharing-5.webp b/docs/docs/features/img/partner-sharing-5.webp index 80ab5da037..d648cc717a 100644 Binary files a/docs/docs/features/img/partner-sharing-5.webp and b/docs/docs/features/img/partner-sharing-5.webp differ diff --git a/docs/docs/features/img/partner-sharing-7.webp b/docs/docs/features/img/partner-sharing-7.webp index 7a71107f4e..0db1a1aeb5 100644 Binary files a/docs/docs/features/img/partner-sharing-7.webp and b/docs/docs/features/img/partner-sharing-7.webp differ diff --git a/docs/docs/features/img/public-shared-link-album.webp b/docs/docs/features/img/public-shared-link-album.webp index 1b68cb0869..6f54ef95f0 100644 Binary files a/docs/docs/features/img/public-shared-link-album.webp and b/docs/docs/features/img/public-shared-link-album.webp differ diff --git a/docs/docs/features/img/public-shared-link-form.webp b/docs/docs/features/img/public-shared-link-form.webp index 1f2a791691..b3ccc732ed 100644 Binary files a/docs/docs/features/img/public-shared-link-form.webp and b/docs/docs/features/img/public-shared-link-form.webp differ diff --git a/docs/docs/features/img/public-shared-link-individual.webp b/docs/docs/features/img/public-shared-link-individual.webp index 63ddb04668..c7463060f6 100644 Binary files a/docs/docs/features/img/public-shared-link-individual.webp and b/docs/docs/features/img/public-shared-link-individual.webp differ diff --git a/docs/docs/features/img/read-only-mode.webp b/docs/docs/features/img/read-only-mode.webp new file mode 100644 index 0000000000..cb1694f609 Binary files /dev/null and b/docs/docs/features/img/read-only-mode.webp differ diff --git a/docs/docs/features/img/reverse-geocoding-mobile1.webp b/docs/docs/features/img/reverse-geocoding-mobile1.webp index 8df3a0dd6e..6a16b4c433 100644 Binary files a/docs/docs/features/img/reverse-geocoding-mobile1.webp and b/docs/docs/features/img/reverse-geocoding-mobile1.webp differ diff --git a/docs/docs/features/img/reverse-geocoding-mobile2.webp b/docs/docs/features/img/reverse-geocoding-mobile2.webp index d0c4c3e39d..5c2a3c3364 100644 Binary files a/docs/docs/features/img/reverse-geocoding-mobile2.webp and b/docs/docs/features/img/reverse-geocoding-mobile2.webp differ diff --git a/docs/docs/features/img/reverse-geocoding-mobile3.webp b/docs/docs/features/img/reverse-geocoding-mobile3.webp index 542ac678ac..2bd78c778b 100644 Binary files a/docs/docs/features/img/reverse-geocoding-mobile3.webp and b/docs/docs/features/img/reverse-geocoding-mobile3.webp differ diff --git a/docs/docs/features/img/search-ex-1.webp b/docs/docs/features/img/search-ex-1.webp deleted file mode 100644 index f441fc4789..0000000000 Binary files a/docs/docs/features/img/search-ex-1.webp and /dev/null differ diff --git a/docs/docs/features/img/shared-album-mobile.webp b/docs/docs/features/img/shared-album-mobile.webp index 13c4ac24f9..26bf7793f9 100644 Binary files a/docs/docs/features/img/shared-album-mobile.webp and b/docs/docs/features/img/shared-album-mobile.webp differ diff --git a/docs/docs/features/img/shared-album-user-selection.webp b/docs/docs/features/img/shared-album-user-selection.webp index 5852233bd3..1e7e3203f9 100644 Binary files a/docs/docs/features/img/shared-album-user-selection.webp and b/docs/docs/features/img/shared-album-user-selection.webp differ diff --git a/docs/docs/features/img/shared-album.webp b/docs/docs/features/img/shared-album.webp index dcd03c6b75..506219e1ee 100644 Binary files a/docs/docs/features/img/shared-album.webp and b/docs/docs/features/img/shared-album.webp differ diff --git a/docs/docs/features/img/web-edit-download.webp b/docs/docs/features/img/web-edit-download.webp new file mode 100644 index 0000000000..07b0ebfcb5 Binary files /dev/null and b/docs/docs/features/img/web-edit-download.webp differ diff --git a/docs/docs/features/img/web-edit-interface.webp b/docs/docs/features/img/web-edit-interface.webp new file mode 100644 index 0000000000..d3b73a4607 Binary files /dev/null and b/docs/docs/features/img/web-edit-interface.webp differ diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 9f1cef0bc4..6e8246b06c 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -80,6 +80,10 @@ There is an automatic scan job that is scheduled to run once a day. Its schedule This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page. +### Deleting a Library + +When deleting an external library, all assets inside are immediately deleted along with the library. Note that while a library can take a long time to fully delete in the background, it is immediately removed from the library list. If the deletion process is interrupted (for example, due to server restart), it will be cleaned up in the next nightly cron job. The cleanup process can also be manually initiated by clicking the "Scan All Libraries" button in the library list. + ## Usage Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add: @@ -118,46 +122,35 @@ _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 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 -- Enter `/mnt/media/christmas-trip` then click Add -- Click on Save -- Click the drop-down menu on the newly created library -- Click on Scan -- Click the drop-down menu on the newly created library -- Click on Rename Library and rename it to "Christmas Trip" +- Click on your avatar in the upper right corner. +- Click on `Administration -> External Libraries`. +- Click on `Create Library`. +- Select which user owns the library, this **can not** be changed later +- You are now entering the library management page. +- Click on `Add` in the `Folders` section. +- Enter `/mnt/media/christmas-trip` then click Add. +- Click on `Edit` Library and rename it to "Christmas Trip". NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see. Next, we'll add an exclusion pattern to filter out raw files. -- Click the drop-down menu on the newly-created Christmas library -- Click on Manage -- Click on Scan Settings -- Click on Add Exclusion Pattern -- Enter `**/Raw/**` and click save. -- Click save -- Click the drop-down menu on the newly created library -- Click on Scan +- Click on `Add` in the `Exclusion Patterns` section. +- Enter `**/Raw/**` and click Add. +- Click on `Scan` The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library. -- Click on Create External Library. - -:::note -If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release. -::: - -- Click the drop-down menu on the newly created library -- Click Edit Import Paths -- Click on Add Path +- Go back to `Administration -> External Libraries`. +- Click on `Create Library`. +- Select which user owns the library, +- You are now entering the library management page. +- Click on `Add` in the `Folders` section. - Enter `/mnt/media/old-pics` then click Add -- Click on Add Path +- Click on `Add` in the `Folders` section. - Enter `/mnt/media/videos` then click Add -- Click Save -- Click on Scan +- Click on `Scan` +- Click on `Edit` Library and rename it to "Old videos and photos". Within seconds, the assets from the old-pics and videos folders should show up in the main timeline. diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index 685f23932c..bd4fe49e96 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -50,6 +50,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`. - The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached. - This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting). +- MIGraphX is a new backend for AMD cards, which compiles models at runtime. As such, the first few inferences will be slow. #### OpenVINO @@ -86,8 +87,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele ## Setup 1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`. -2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend. -3. Still in `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line. +2. In `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino, rknn] to the `image` section's tag at the end of the line. +3. Still in the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend. 4. Redeploy the `immich-machine-learning` container with these updated settings. ### Confirming Device Usage diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 8b9a204741..59a4844c46 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -20,14 +20,6 @@ Below are the SHA-256 fingerprints for the certificates signing the android appl ::: -:::info Beta Program -The beta release channel allows users to test upcoming changes before they are officially released. To join the channel use the links below. - -- Android: Invitation link from [web](https://play.google.com/store/apps/details?id=app.alextran.immich) or from [mobile](https://play.google.com/store/apps/details?id=app.alextran.immich) -- iOS: [TestFlight invitation link](https://testflight.apple.com/join/1vYsAa8P) - -::: - ## Login @@ -36,15 +28,11 @@ The beta release channel allows users to test upcoming changes before they are o -:::info -You can enable automatic backup on supported devices. For more information see [Automatic Backup](/features/automatic-backup.md). -::: - ## Sync only selected photos If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server. -First, you need to enable the Storage Indicator in your app's settings. Navigate to **Settings -> Photo Grid** and enable **"Show Storage indicator on asset tiles"**; this makes it easy to distinguish local-only assets and synced assets. +First, you need to enable the Storage Indicator in your app's settings. Navigate to **Settings -> Photo Grid** and enable **`Show Storage indicator on asset tiles`**; this makes it easy to distinguish local-only assets and synced assets. :::note @@ -55,19 +43,57 @@ This will enable a small cloud icon on the bottom right corner of the asset tile ::: -Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **Library -> On this device**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu. +Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **Library -> On this device**. To selectively upload photos from these albums, simply select the local-only photos and tap on the `Upload` button in the dynamic bottom menu. - +## Free Up Space + +**Free Up Space** allows you to remove local media files from your device that have already been successfully backed up to your Immich server (and are not in Immich trash). This helps reclaim storage on your mobile device without losing your memories. + +### How it works + + + +1. **Configuration:** + - **Cutoff date:** Free Up Space will only look for photos and videos **on or before** this date. Photos removed from the device don't show up in other (messaging) apps and have to be shared from Immich in order to send them. + - **Keep favorites:** This works the same way `Keep albums` does. By default, favorited assets are preserved on your device. + - **Keep albums:** Hold all photos and videos in the selected albums on your device, regardless of other settings. By default, `WhatsApp` [related albums](#external-app-dependencies) are selected to be kept on the device. Assets not already on the device will not be re-downloaded. + - **Keep on device:** You can choose to restrict removal to `Always keep` **All photos** or **All videos**, regardless of other settings. This setting can hamper freeing up space significantly — with 80 GB of videos and 40 GB photos, selecting `Always keep photos` retains thousands of photos on your device. + +2. **Scan & Review:** Before any files are removed, you are presented with a review screen to verify which items will be deleted and how much storage is reclamable. +3. **Deletion:** Confirmed items are moved to your device's native Trash/Recycle Bin. For large queues, Immich processes deletion in batches for stability (`2000` assets per batch on Android, `10000` per batch on iOS). + +:::info reclaim storage +To use the reclaimed space right away, you must empty the system/gallery trash manually outside of Immich. +::: + +Provided the server is healthy and [backed up](/administration/backup-and-restore.md), assets removed by Free Up Space can always be accessed in the Immich app. + +### iCloud Photos + +If you use **iCloud Photos** alongside Immich, it is vital to understand how deletion affects your data. After using **Free Up Space**, the photo will be stored **only** on your Immich server (and your phone's "Recently Deleted" folder for 30 days). + +Assets that are part of an **iCloud Shared Album** are automatically excluded from the cleanup scan because iCloud does not allow removing the items in Shared Album from the device. + +:::warning iCloud & Backups +If, in addition to Immich, you rely on iCloud as a secondary backup (as part of your [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup strategy), you should instead use `Optimize iPhone Storage` in [iCloud Photos](https://support.apple.com/en-us/105061). + +iCloud utilizes a two-way sync; this means deleting a photo, or using Free Up Space from your iPhone will **also delete it from iCloud** and all other devices (Mac, iPad) where you're signed in with the same Apple Account. See [Apple Support](https://support.apple.com/en-us/108922#iCloud_photo_library) for more info. +::: + +### External App Dependencies (WhatsApp, etc.) \{#external-app-dependencies\} + +Android applications like **WhatsApp** rely on local files to display media in chat history. + +If Immich backs up your WhatsApp folder and you run **Free Up Space**, the local copies of these images will be deleted. Consequently, **media in your WhatsApp chats will appear blurry or missing.** You will only be able to view these photos inside the Immich app; they will no longer be visible within the WhatsApp interface. + +**Recommendation:** If keeping chat history intact is important, exclude WhatsApp with `Keep albums` in Free Up Space and review the deletion list carefully. You have to enable [Album Sync](#album-sync) for WhatsApp to show up in the list. Alternatively, don't [back up](#backup) WhatsApp with Immich. + ## Album Sync You can sync or mirror an album from your phone to the Immich server on your account. For example, if you select Recents, Camera and Videos album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically. @@ -88,18 +114,19 @@ You can sync or mirror an album from your phone to the Immich server on your acc ### Synchronizing albums from the past -Albums can be synchronized to the server even if they did not exist on the server before. In order to apply this setting you have to: -Enter the cloud on the top right -> cog wheel on the top right -> select the sync option under Sync albums. + + +Albums can be synchronized to the server even if they did not exist on the server before. You can enable this feature at any time and use the **Reorganize into album** button to backfill existing uploads into their corresponding albums. :::info Sync albums delete/move photos If you delete/move photos in the local album on your device, it will not be reflected in the album on the server **even if** you click Sync albums 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 ignore list by -App settings -> Advanced -> Duplicate Assets -> Clear +## Read-only/kid Mode -:::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 ignore list again at the end of the synchronization. -::: +You can set the app to read-only mode to prevent accidental deletion of photos from your device, and only allow viewing photos on the timeline. + +To toggle this feature, long-press the profile icon or go to `Settings > Advanced > Read-only Mode`. + + diff --git a/docs/docs/features/mobile-backup.md b/docs/docs/features/mobile-backup.md new file mode 100644 index 0000000000..f3eb1a359c --- /dev/null +++ b/docs/docs/features/mobile-backup.md @@ -0,0 +1,85 @@ +--- +sidebar_position: 1 +--- + +# Mobile Backup + +## Overview + +Immich supports uploading photos and videos from your mobile device to the server automatically. + +When backup is enabled, Immich will upload new photos and videos from selected albums when you open or resume the app, as well as periodically in the background. + + + +## General Features + +### Backup albums selection + + + +You can select which albums on your mobile device to back up to the server. You can also exclude specific albums (by double-tapping on them) from being backed up. This is useful for iOS users since assets can belong to multiple albums. For example, you may want to back up all assets except those in the "Videos" album. + +### Deduplication + +When you first select albums for backup, Immich calculates a checksum for each file's content. This checksum identifies assets already on the server—whether uploaded via CLI, web interface, or another device. Files matching existing assets are skipped, preventing duplicate uploads and saving bandwidth. + +### Networking requirements + +By default, Immich will only upload photos and videos when connected to Wi-Fi. You can change this behavior in the backup settings page. + + + +### Backup album synchronization + + + +When enabled, Immich automatically creates albums on the server that mirror the albums on your mobile device. Photos and videos are organized into these server-side albums to match your device's album structure, making it easy to find and browse your content the same way you do on your phone. + +This is a one-way sync from your device to the server. You can enable this feature at any time and use the **Reorganize into album** button to backfill existing uploads into their corresponding albums. + +## Platform Specific Features + +### Android + + + +- It is a well-known problem that some Android models are very strict with battery optimization settings, which can cause a problem with the background worker. Please visit [Don't kill my app](https://dontkillmyapp.com/) for a guide on disabling this setting on your phone. +- You can allow the background task to run only when the device is charging. +- You can set the minimum delay from the time a photo is taken to when the background upload task will run. + +### iOS + +- You must enable **Background App Refresh** for the app to work in the background. You can enable it in the Settings app under General > Background App Refresh. + +
+ +
+ +- iOS automatically manages background tasks; the app cannot control when the background upload task will run. The more frequently you open the app, the more often background tasks will run. + +#### iCloud Backup + +Local albums containing assets from iCloud and marked for backup in Immich will be pulled from iCloud and temporarily stored in the app's cache folder. Once the hashing and uploading process is completed, the temporary files will be emptied. + +This process may consume additional data and storage space on your device, especially if you have a large number of iCloud photos and videos. Please ensure you have sufficient storage space and monitor your data usage if you are not connected to Wi-Fi. diff --git a/docs/docs/features/monitoring.md b/docs/docs/features/monitoring.md index f087a3306f..46063fded6 100644 --- a/docs/docs/features/monitoring.md +++ b/docs/docs/features/monitoring.md @@ -112,4 +112,40 @@ You can then make a new panel, specifying Prometheus as the data source for it. -- TODO: add images and more details here +## Structured Logging + +In addition to Prometheus metrics, Immich supports structured JSON logging which is ideal for log aggregation systems like Grafana Loki, ELK Stack, Datadog, Splunk, and others. + +### Configuration + +By default, Immich outputs human-readable console logs. To enable JSON logging, set the `IMMICH_LOG_FORMAT` environment variable: + +```bash +IMMICH_LOG_FORMAT=json +``` + +:::tip +The default is `IMMICH_LOG_FORMAT=console` for human-readable logs with colors during development. For production deployments using log aggregation, use `IMMICH_LOG_FORMAT=json`. +::: + +### JSON Log Format + +When enabled, logs are output in structured JSON format: + +```json +{"level":"log","pid":36,"timestamp":1766533331507,"message":"Initialized websocket server","context":"WebsocketRepository"} +{"level":"warn","pid":48,"timestamp":1766533331629,"message":"Unable to open /build/www/index.html, skipping SSR.","context":"ApiService"} +{"level":"error","pid":36,"timestamp":1766533331690,"message":"Failed to load plugin immich-core:","context":"Error"} +``` + +This format includes: + +- `level`: Log level (log, warn, error, etc.) +- `pid`: Process ID +- `timestamp`: Unix timestamp in milliseconds +- `message`: Log message +- `context`: Service or component that generated the log + +For more information on log formats, see [`IMMICH_LOG_FORMAT`](/install/environment-variables.md#general). + [prom-file]: https://github.com/immich-app/immich/releases/latest/download/prometheus.yml diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md index e8985b0c92..7360787127 100644 --- a/docs/docs/features/searching.md +++ b/docs/docs/features/searching.md @@ -11,45 +11,25 @@ Contextual CLIP search is powered by the [VectorChord](https://github.com/tensor In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app). -The filters smart search allows you to search by include: +You can search the following types of content: -- People -- Location - - Country - - State - - City -- Camera - - Make - - Model -- Date range -- File name or extension -- Media type - - Image (including live/motion photos) - - Video - - All -- Condition - - Not in any album - - Archived - - Favorited - - Rating - - - - -Some search examples: +| Type | Description | +| ----------------------------------- | ----------------------------------------------------- | +| People | Faces that are recognized in your photos/videos. | +| Contextual | Content of the photos and videos. | +| File name or extension | Full or partial file's name, or file's extension | +| Description | Description added to assets. | +| Optical Character Recognition (OCR) | Text in images | +| Locations | Cities, states, and countries from reverse geocoding. | +| Tags | Tags assigned or extracted from assets. | +| Camera | make, model and lens model | +| Time frame | Start and end date of a specific time bucket | +| Media type | Image or video or both | +| Display options | In Archive, in Favorites or Not in any album | +| Start rating | User-assigned start rating | - - - - - - - - - - ## Configuration Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available. diff --git a/docs/docs/features/sharing.md b/docs/docs/features/sharing.md index c19b4f48e1..a884884bee 100644 --- a/docs/docs/features/sharing.md +++ b/docs/docs/features/sharing.md @@ -33,7 +33,7 @@ You can create a public link to share a group of photos or videos, or an album, The public shared link is generated with a random URL, which acts as as a secret to avoid the link being guessed by unwanted parties, for instance. ``` -https://immich.yourdomain.com/share/JUckRMxlgpo7F9BpyqGk_cZEwDzaU_U5LU5_oNZp1ETIBa9dpQ0b5ghNm_22QVJfn3k +https://my.immich.app/share/JUckRMxlgpo7F9BpyqGk_cZEwDzaU_U5LU5_oNZp1ETIBa9dpQ0b5ghNm_22QVJfn3k ``` ### Creating a public share link diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md index 16f1ab0b6b..4c4ac6039a 100644 --- a/docs/docs/features/supported-formats.md +++ b/docs/docs/features/supported-formats.md @@ -38,6 +38,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a | `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | | | `MP4` | `.mp4` `.insv` | :white_check_mark: | | | `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | +| `MXF` | `.mxf` | :white_check_mark: | | | `QUICKTIME` | `.mov` | :white_check_mark: | | | `WEBM` | `.webm` | :white_check_mark: | | | `WMV` | `.wmv` | :white_check_mark: | | diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 3f366bb0d4..a1c8092732 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -30,26 +30,17 @@ In the Immich web UI: - click the **Administration** link in the upper right corner. -- Select the **External Libraries** tab - - -- Click the **Create Library** button - +- Select the **External Libraries** tab and click the **Create Library** button + - In the dialog, select which user should own the new library -- Click the three-dots menu and select **Edit Import Paths** - +- You are now entering the library management page. + -- Click Add path - - -- Enter **/home/user/photos1** as the path and click Add - - -- Save the new path - +- Click `Add` in the Folder section to specify a path for scanning and enter **/home/user/photos1** as the path and click Add + - Click the three-dots menu and select **Scan New Library Files** @@ -64,4 +55,3 @@ In the Immich web UI: - You should see non-zero Active jobs for Library, Generate Thumbnails, and Extract Metadata. - diff --git a/docs/docs/guides/img/administration-link.webp b/docs/docs/guides/img/administration-link.webp index 22bc4e4c87..dc0b6cd63a 100644 Binary files a/docs/docs/guides/img/administration-link.webp and b/docs/docs/guides/img/administration-link.webp differ diff --git a/docs/docs/guides/img/create-external-library.webp b/docs/docs/guides/img/create-external-library.webp index 595d699829..90c38af077 100644 Binary files a/docs/docs/guides/img/create-external-library.webp and b/docs/docs/guides/img/create-external-library.webp differ diff --git a/docs/docs/guides/img/edit-import-path.webp b/docs/docs/guides/img/edit-import-path.webp new file mode 100644 index 0000000000..c07ae7b7fc Binary files /dev/null and b/docs/docs/guides/img/edit-import-path.webp differ diff --git a/docs/docs/guides/img/external-libraries.webp b/docs/docs/guides/img/external-libraries.webp deleted file mode 100644 index b257ac3def..0000000000 Binary files a/docs/docs/guides/img/external-libraries.webp and /dev/null differ diff --git a/docs/docs/guides/img/job-status.webp b/docs/docs/guides/img/job-status.webp deleted file mode 100644 index 2ec8709859..0000000000 Binary files a/docs/docs/guides/img/job-status.webp and /dev/null differ diff --git a/docs/docs/guides/img/jobs-tab.webp b/docs/docs/guides/img/jobs-tab.webp index b8f45494b9..4cd5ec5026 100644 Binary files a/docs/docs/guides/img/jobs-tab.webp and b/docs/docs/guides/img/jobs-tab.webp differ diff --git a/docs/docs/guides/img/library-management-page.webp b/docs/docs/guides/img/library-management-page.webp new file mode 100644 index 0000000000..dc81ece2d7 Binary files /dev/null and b/docs/docs/guides/img/library-management-page.webp differ diff --git a/docs/docs/guides/img/library-owner.webp b/docs/docs/guides/img/library-owner.webp index f92342f205..9a3ccb7778 100644 Binary files a/docs/docs/guides/img/library-owner.webp and b/docs/docs/guides/img/library-owner.webp differ diff --git a/docs/docs/guides/img/scan-new-library-files.webp b/docs/docs/guides/img/scan-new-library-files.webp index 815cc594cd..f5ef481db8 100644 Binary files a/docs/docs/guides/img/scan-new-library-files.webp and b/docs/docs/guides/img/scan-new-library-files.webp differ diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index a6aaae149b..bf815521ef 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -8,7 +8,8 @@ A config file can be provided as an alternative to the UI configuration. ### Step 1 - Create a new config file -In JSON format, create a new config file (e.g. `immich.json`) and put it in a location that can be accessed by Immich. +In JSON format, create a new config file (e.g. `immich.json`) and put it in a location mounted in the container that can be accessed by Immich. +YAML-formatted config files are also supported. The default configuration looks like this:
@@ -251,6 +252,15 @@ So you can just grab it from there, paste it into a file and you're pretty much In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config. For more information, refer to the [Environment Variables](/install/environment-variables.md) section. -:::tip -YAML-formatted config files are also supported. -::: +:::info Docker Compose +In your `.env` file, the variables `UPLOAD_LOCATION` and `DB_DATA_LOCATION` concern the location on the host. +However, the variable `IMMICH_CONFIG_FILE` concerns the location inside the container, and informs the `immich-server` container that a configuration file is present. + +It is recommended to reuse this variable in your `docker-compose.yml`: + +```yaml +volumes: + - ./configuration.yml:${IMMICH_CONFIG_FILE} +``` + +:: diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 76784b285a..07b37f0e41 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -17,11 +17,11 @@ If this does not work, try running `docker compose up -d --force-recreate`. ## Docker Compose -| Variable | Description | Default | Containers | -| :----------------- | :------------------------------ | :-------: | :----------------------- | -| `IMMICH_VERSION` | Image tags | `release` | server, machine learning | -| `UPLOAD_LOCATION` | Host path for uploads | | server | -| `DB_DATA_LOCATION` | Host path for Postgres database | | database | +| Variable | Description | Default | Containers | +| :----------------- | :------------------------------ | :-----: | :----------------------- | +| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning | +| `UPLOAD_LOCATION` | Host path for uploads | | server | +| `DB_DATA_LOCATION` | Host path for Postgres database | | database | :::tip These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. @@ -34,6 +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_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media location inside the container âš ī¸**You probably shouldn't set this**\*2âš ī¸ | `/data` | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | @@ -43,6 +44,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 2e3fef07d6..ee5db45c9a 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -17,12 +17,17 @@ Hardware and software requirements for Immich: - Immich runs well in a virtualized environment when running in a full virtual machine. The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users. If you have issues, we recommend that you switch to a supported VM deployment. -- **RAM**: Minimum 4GB, recommended 6GB. +- **RAM**: Minimum 6GB, recommended 8GB. - **CPU**: Minimum 2 cores, recommended 4 cores. - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average. -:::tip +:::note RAM requirements +For a smooth experience, especially during asset upload, Immich requires at least 6GB of RAM. +For systems with only 4GB of RAM, Immich can be run with machine learning features disabled. +::: + +:::tip Postgres setup Good performance and a stable connection to the Postgres database is critical to a smooth Immich experience. The Postgres database files are typically between 1-3 GB in size. For this reason, the Postgres database (`DB_DATA_LOCATION`) should ideally use local SSD storage, and never a network share of any kind. diff --git a/docs/docs/install/synology.md b/docs/docs/install/synology.md index 3e5b780db2..b86561dbbf 100644 --- a/docs/docs/install/synology.md +++ b/docs/docs/install/synology.md @@ -8,8 +8,6 @@ sidebar_position: 85 This is a community contribution and not officially supported by the Immich team, but included here for convenience. Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/). - -**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).** ::: Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager. diff --git a/docs/docs/install/upgrading.md b/docs/docs/install/upgrading.md index bf788cb680..12e5c9c342 100644 --- a/docs/docs/install/upgrading.md +++ b/docs/docs/install/upgrading.md @@ -26,6 +26,16 @@ docker image prune [breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created [releases]: https://github.com/immich-app/immich/releases +## Versioning Policy + +Immich follows [semantic versioning][semver], which tags releases in the format `..`. We intend for breaking changes to be limited to major version releases. +You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`. + +Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich. +Switching back to an earlier version, even within the same minor release tag, is not supported. + +[semver]: https://semver.org/ + ## Migrating to VectorChord :::info diff --git a/docs/docs/overview/quick-start.mdx b/docs/docs/overview/quick-start.mdx index d80a194ad2..521d0a232c 100644 --- a/docs/docs/overview/quick-start.mdx +++ b/docs/docs/overview/quick-start.mdx @@ -10,7 +10,7 @@ to install and use it. ## Requirements -- A system with at least 4GB of RAM and 2 CPU cores. +- A system with at least 6GB of RAM and 2 CPU cores. - [Docker](https://docs.docker.com/engine/install/) > For a more detailed list of requirements, see the [requirements page](/install/requirements). @@ -63,9 +63,9 @@ The backup time differs depending on how many photos are on your mobile device. take quite a while. To quickly get going, you can selectively upload few photos first, by following this [guide](/features/mobile-app#sync-only-selected-photos). -You can select the **Jobs** tab to see Immich processing your photos. +You can select the **Job Queues** tab to see Immich processing your photos. - + --- @@ -90,4 +90,4 @@ You may want to [upload photos from your own archive](/features/command-line-int You may want to incorporate a pre-existing archive of photos from an [External Library](/features/libraries); there's a [guide](/guides/external-library) for that. -You may want your mobile device to [back photos up to your server automatically](/features/automatic-backup). +You may want your mobile device to [back photos up to your server automatically](/features/mobile-backup). diff --git a/docs/docs/partials/_mobile-app-backup.md b/docs/docs/partials/_mobile-app-backup.md index 67c43e83b7..777a989334 100644 --- a/docs/docs/partials/_mobile-app-backup.md +++ b/docs/docs/partials/_mobile-app-backup.md @@ -6,4 +6,8 @@ -3. Scroll down to the bottom and press "**Start Backup**" to start the backup process. This will upload all the assets in the selected albums. +3. Scroll down to the bottom and press "**Enable Backup**" to start the backup process. This will upload all the assets in the selected albums. + +:::info +You can read more about backup options [here](/features/mobile-backup.md). +::: diff --git a/docs/docs/partials/_user-create.md b/docs/docs/partials/_user-create.md index 5c5e1fd6f9..8856b8f2e9 100644 --- a/docs/docs/partials/_user-create.md +++ b/docs/docs/partials/_user-create.md @@ -2,6 +2,6 @@ If you have friends or family members who want to use the application as well, y -In the Administration panel, you can click on the **Create user** button, and you'll be presented with the following dialog: +On the **Administration > Users** page, you can click on the **Create user** button, and you'll be presented with the following dialog: - + diff --git a/docs/docs/partials/img/admin-registration-form.webp b/docs/docs/partials/img/admin-registration-form.webp index eac5da94d0..5300a888f8 100644 Binary files a/docs/docs/partials/img/admin-registration-form.webp and b/docs/docs/partials/img/admin-registration-form.webp differ diff --git a/docs/docs/partials/img/album-selection.webp b/docs/docs/partials/img/album-selection.webp index fc7faf2150..8c81350e0c 100644 Binary files a/docs/docs/partials/img/album-selection.webp and b/docs/docs/partials/img/album-selection.webp differ diff --git a/docs/docs/partials/img/create-new-user-dialog.webp b/docs/docs/partials/img/create-new-user-dialog.webp index 47d50f8b04..058abc698d 100644 Binary files a/docs/docs/partials/img/create-new-user-dialog.webp and b/docs/docs/partials/img/create-new-user-dialog.webp differ diff --git a/docs/docs/partials/img/create-new-user.webp b/docs/docs/partials/img/create-new-user.webp index e3cdb796a3..c4497aa3dc 100644 Binary files a/docs/docs/partials/img/create-new-user.webp and b/docs/docs/partials/img/create-new-user.webp differ diff --git a/docs/docs/partials/img/enable-storage-template.webp b/docs/docs/partials/img/enable-storage-template.webp index 809bf09adf..d27ed59379 100644 Binary files a/docs/docs/partials/img/enable-storage-template.webp and b/docs/docs/partials/img/enable-storage-template.webp differ diff --git a/docs/docs/partials/img/sign-in-phone.webp b/docs/docs/partials/img/sign-in-phone.webp index 2af8163af3..45265bed39 100644 Binary files a/docs/docs/partials/img/sign-in-phone.webp and b/docs/docs/partials/img/sign-in-phone.webp differ diff --git a/docs/docs/partials/img/storage-template-migration-job.webp b/docs/docs/partials/img/storage-template-migration-job.webp index 7d4c62cfbe..b6d07300f7 100644 Binary files a/docs/docs/partials/img/storage-template-migration-job.webp and b/docs/docs/partials/img/storage-template-migration-job.webp differ diff --git a/docs/docs/partials/img/storage-template.webp b/docs/docs/partials/img/storage-template.webp index e2f9401a70..07cf05dfed 100644 Binary files a/docs/docs/partials/img/storage-template.webp and b/docs/docs/partials/img/storage-template.webp differ diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 70e0189a00..00a120b8b6 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -6,7 +6,7 @@ const prism = require('prism-react-renderer'); /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Immich', - tagline: 'High performance self-hosted photo and video backup solution directly from your mobile phone', + tagline: 'Self-hosted photo and video management solution', url: 'https://docs.immich.app', baseUrl: '/', onBrokenLinks: 'throw', @@ -26,6 +26,12 @@ const config = { locales: ['en'], }, + // Mermaid diagrams + markdown: { + mermaid: true, + }, + themes: ['@docusaurus/theme-mermaid'], + plugins: [ async function myPlugin(context, options) { return { @@ -70,6 +76,10 @@ const config = { autoCollapseCategories: false, }, }, + tableOfContents: { + minHeadingLevel: 2, + maxHeadingLevel: 4, + }, navbar: { logo: { alt: 'Immich Logo', @@ -83,35 +93,15 @@ const config = { position: 'right', }, { - to: '/overview/quick-start', + href: 'https://immich.app/', position: 'right', - label: 'Docs', - }, - { - href: 'https://immich.app/roadmap', - position: 'right', - label: 'Roadmap', - }, - { - href: 'https://api.immich.app/', - position: 'right', - label: 'API', - }, - { - href: 'https://immich.store', - position: 'right', - label: 'Merch', + label: 'Home', }, { href: 'https://github.com/immich-app/immich', label: 'GitHub', position: 'right', }, - { - href: 'https://discord.immich.app', - label: 'Discord', - position: 'right', - }, { type: 'html', position: 'right', @@ -124,19 +114,78 @@ const config = { style: 'light', links: [ { - title: 'Overview', + title: 'Download', items: [ { - label: 'Quick start', - to: '/overview/quick-start', + label: 'Android', + href: 'https://get.immich.app/android', }, { - label: 'Installation', - to: '/install/requirements', + label: 'iOS', + href: 'https://get.immich.app/ios', }, { - label: 'Contributing', - to: '/overview/support-the-project', + label: 'Server', + href: 'https://immich.app/download', + }, + ], + }, + { + title: 'Company', + items: [ + { + label: 'FUTO', + href: 'https://futo.tech/', + }, + { + label: 'Purchase', + href: 'https://buy.immich.app/', + }, + { + label: 'Merch', + href: 'https://immich.store/', + }, + ], + }, + { + title: 'Sites', + items: [ + { + label: 'Home', + href: 'https://immich.app', + }, + { + label: 'My Immich', + href: 'https://my.immich.app/', + }, + { + label: 'Awesome Immich', + href: 'https://awesome.immich.app/', + }, + { + label: 'Immich API', + href: 'https://api.immich.app/', + }, + { + label: 'Immich Data', + href: 'https://data.immich.app/', + }, + { + label: 'Immich Datasets', + href: 'https://datasets.immich.app/', + }, + ], + }, + { + title: 'Miscellaneous', + items: [ + { + label: 'Roadmap', + href: 'https://immich.app/roadmap', + }, + { + label: 'Cursed Knowledge', + href: 'https://immich.app/cursed-knowledge', }, { label: 'Privacy Policy', @@ -145,24 +194,7 @@ const config = { ], }, { - title: 'Documentation', - items: [ - { - label: 'Roadmap', - href: 'https://immich.app/roadmap', - }, - { - label: 'API', - href: 'https://api.immich.app/', - }, - { - label: 'Cursed Knowledge', - href: 'https://immich.app/cursed-knowledge', - }, - ], - }, - { - title: 'Links', + title: 'Social', items: [ { label: 'GitHub', diff --git a/docs/mise.toml b/docs/mise.toml index 4ffb7d5cce..32fcac5578 100644 --- a/docs/mise.toml +++ b/docs/mise.toml @@ -23,3 +23,9 @@ run = "prettier --check ." [tasks."format-fix"] env._.path = "./node_modules/.bin" run = "prettier --write ." + +[tasks.deploy] +run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}" + +[tools] +wrangler = "4.66.0" diff --git a/docs/package.json b/docs/package.json index d37b256a3f..8c270f013b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,7 +8,7 @@ "format:fix": "prettier --write .", "start": "docusaurus start --port 3005", "copy:openapi": "jq -c < ../open-api/immich-openapi-specs.json > ./static/openapi.json || exit 0", - "build": "npm run copy:openapi && docusaurus build", + "build": "pnpm run copy:openapi && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", @@ -20,6 +20,7 @@ "@docusaurus/core": "~3.9.0", "@docusaurus/preset-classic": "~3.9.0", "@docusaurus/theme-common": "~3.9.0", + "@docusaurus/theme-mermaid": "~3.9.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -57,6 +58,6 @@ "node": ">=20" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" } } diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 7f8c6d5761..665bc8fd55 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -8,19 +8,19 @@ @tailwind utilities; @font-face { - font-family: 'Overpass'; - src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations'); - font-weight: 1 999; + font-family: 'GoogleSans'; + src: url('/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations'); + font-weight: 410 900; font-style: normal; ascent-override: 106.25%; size-adjust: 106.25%; } @font-face { - font-family: 'Overpass Mono'; - src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); - font-weight: 1 999; - font-style: normal; + font-family: 'GoogleSansCode'; + src: url('/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations'); + font-weight: 1 900; + font-style: monospace; ascent-override: 106.25%; size-adjust: 106.25%; } @@ -37,7 +37,8 @@ img { /* You can override the default Infima variables here. */ :root { - font-family: 'Overpass', sans-serif; + font-family: 'GoogleSans', sans-serif; + letter-spacing: 0.1px; --ifm-color-primary: #4250af; --ifm-color-primary-dark: #4250af; --ifm-color-primary-darker: #4250af; @@ -48,6 +49,16 @@ img { --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: 'GoogleSans', sans-serif; + letter-spacing: 0.1px; +} + /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { --ifm-color-primary: #adcbfa; @@ -58,7 +69,13 @@ img { --ifm-color-primary-lighter: #e9f1fe; --ifm-color-primary-lightest: #ffffff; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); - --ifm-background-color: #000000; + --ifm-navbar-background-color: #0c0c0c; + --ifm-footer-background-color: #0c0c0c; +} + +[data-theme='dark'] body, +[data-theme='dark'] .main-wrapper { + background-color: #070707; } div[class^='announcementBar_'] { @@ -71,15 +88,22 @@ div[class^='announcementBar_'] { padding: 10px 10px 10px 16px; border-radius: 24px; margin-right: 16px; + font-weight: 500; } .menu__list-item-collapsible { margin-right: 16px; border-radius: 24px; + font-weight: 500; } .menu__link--active { - font-weight: 500; + font-weight: 600; +} + +.table-of-contents__link { + font-size: 14px; + font-weight: 450; } /* workaround for version switcher PR 15894 */ @@ -88,13 +112,14 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) { } code { - font-weight: 600; + font-weight: 500; + font-family: 'GoogleSansCode'; } .buy-button { padding: 8px 14px; border: 1px solid transparent; - font-family: 'Overpass', sans-serif; + font-family: 'GoogleSans', sans-serif; font-weight: 500; cursor: pointer; box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4); diff --git a/docs/src/pages/errors.md b/docs/src/pages/errors.md index fed72f21c7..6189fcaae1 100644 --- a/docs/src/pages/errors.md +++ b/docs/src/pages/errors.md @@ -32,3 +32,7 @@ If you would like to migrate from one media location to another, simply successf 4. Start up Immich After version `1.136.0`, Immich can detect when a media location has moved and will automatically update the database paths to keep them in sync. + +## Schema drift + +Schema drift is when the database schema is out of sync with the code. This could be the result of manual database tinkering, issues during a database restore, or something else. Schema drift can lead to data corruption, application bugs, and other unpredictable behavior. Please reconcile the differences as soon as possible. Specifically, missing `CONSTRAINT`s can result in duplicate assets being uploaded, since the server relies on a checksum `CONSTRAINT` to prevent duplicates. diff --git a/docs/static/_redirects b/docs/static/_redirects index ce4b670246..218bb71d69 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -23,6 +23,7 @@ /features/storage-template /administration/storage-template 307 /features/user-management /administration/user-management 307 /developer/contributing /developer/pr-checklist 307 +/developer/open-api /api 307 /guides/machine-learning /guides/remote-machine-learning 307 /administration/password-login /administration/system-settings 307 /features/search /features/searching 307 @@ -34,3 +35,4 @@ /overview/introduction /overview/quick-start 307 /overview/welcome /overview/quick-start 307 /docs/* /:splat 307 +/features/automatic-backup /features/mobile-backup 307 diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 34d8e6e59f..564eeafa94 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,32 +1,20 @@ [ { - "label": "v2.4.0", - "url": "https://docs.v2.4.0.archive.immich.app" + "label": "v2.5.6", + "url": "https://docs.v2.5.6.archive.immich.app" + }, + { + "label": "v2.4.1", + "url": "https://docs.v2.4.1.archive.immich.app" }, { "label": "v2.3.1", "url": "https://docs.v2.3.1.archive.immich.app" }, - { - "label": "v2.3.0", - "url": "https://docs.v2.3.0.archive.immich.app" - }, { "label": "v2.2.3", "url": "https://docs.v2.2.3.archive.immich.app" }, - { - "label": "v2.2.2", - "url": "https://docs.v2.2.2.archive.immich.app" - }, - { - "label": "v2.2.1", - "url": "https://docs.v2.2.1.archive.immich.app" - }, - { - "label": "v2.2.0", - "url": "https://docs.v2.2.0.archive.immich.app" - }, { "label": "v2.1.0", "url": "https://docs.v2.1.0.archive.immich.app" @@ -35,18 +23,10 @@ "label": "v2.0.1", "url": "https://docs.v2.0.1.archive.immich.app" }, - { - "label": "v2.0.0", - "url": "https://docs.v2.0.0.archive.immich.app" - }, { "label": "v1.144.1", "url": "https://docs.v1.144.1.archive.immich.app" }, - { - "label": "v1.144.0", - "url": "https://docs.v1.144.0.archive.immich.app" - }, { "label": "v1.143.1", "url": "https://docs.v1.143.1.archive.immich.app" diff --git a/docs/static/fonts/GoogleSans/GoogleSans.ttf b/docs/static/fonts/GoogleSans/GoogleSans.ttf new file mode 100644 index 0000000000..5d9102f856 Binary files /dev/null and b/docs/static/fonts/GoogleSans/GoogleSans.ttf differ diff --git a/docs/static/fonts/GoogleSansCode/GoogleSansCode.ttf b/docs/static/fonts/GoogleSansCode/GoogleSansCode.ttf new file mode 100644 index 0000000000..b68d037edf Binary files /dev/null and b/docs/static/fonts/GoogleSansCode/GoogleSansCode.ttf differ diff --git a/docs/static/fonts/overpass/Overpass-Italic.ttf b/docs/static/fonts/overpass/Overpass-Italic.ttf deleted file mode 100644 index 281dd742bb..0000000000 Binary files a/docs/static/fonts/overpass/Overpass-Italic.ttf and /dev/null differ diff --git a/docs/static/fonts/overpass/Overpass.ttf b/docs/static/fonts/overpass/Overpass.ttf deleted file mode 100644 index 1cf730a5ad..0000000000 Binary files a/docs/static/fonts/overpass/Overpass.ttf and /dev/null differ diff --git a/docs/static/fonts/overpass/OverpassMono.ttf b/docs/static/fonts/overpass/OverpassMono.ttf deleted file mode 100644 index 71ef818b33..0000000000 Binary files a/docs/static/fonts/overpass/OverpassMono.ttf and /dev/null differ diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index 5ed28c737d..9a654487cc 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -17,9 +17,9 @@ module.exports = { // Dark Theme 'immich-dark-primary': '#adcbfa', - 'immich-dark-bg': '#070a14', + 'immich-dark-bg': '#000000', 'immich-dark-fg': '#e5e7eb', - 'immich-dark-gray': '#212121', + 'immich-dark-gray': '#111111', }, }, }, diff --git a/e2e-auth-server/Dockerfile b/e2e-auth-server/Dockerfile new file mode 100644 index 0000000000..aa7527c483 --- /dev/null +++ b/e2e-auth-server/Dockerfile @@ -0,0 +1,6 @@ +FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 +RUN corepack enable +ADD package.json *.ts ./ +RUN pnpm install +EXPOSE 2286 +CMD ["pnpm", "run", "start"] diff --git a/e2e/src/setup/auth-server.ts b/e2e-auth-server/auth-server.ts similarity index 96% rename from e2e/src/setup/auth-server.ts rename to e2e-auth-server/auth-server.ts index 489bda2ee4..a190ecd023 100644 --- a/e2e/src/setup/auth-server.ts +++ b/e2e-auth-server/auth-server.ts @@ -125,7 +125,7 @@ const setup = async () => { ], }); - const onStart = () => console.log(`[auth-server] http://${host}:${port}/.well-known/openid-configuration`); + const onStart = () => console.log(`[e2e-auth-server] http://${host}:${port}/.well-known/openid-configuration`); const app = oidc.listen(port, host, onStart); return () => app.close(); }; diff --git a/e2e-auth-server/package.json b/e2e-auth-server/package.json new file mode 100644 index 0000000000..73ede1b7c4 --- /dev/null +++ b/e2e-auth-server/package.json @@ -0,0 +1,15 @@ +{ + "name": "@immich/e2e-auth-server", + "version": "0.1.0", + "type": "module", + "main": "auth-server.ts", + "scripts": { + "start": "tsx startup.ts" + }, + "devDependencies": { + "jose": "^5.6.3", + "@types/oidc-provider": "^9.0.0", + "oidc-provider": "^9.0.0", + "tsx": "^4.20.6" + } +} diff --git a/e2e-auth-server/startup.ts b/e2e-auth-server/startup.ts new file mode 100644 index 0000000000..442cf6dfc2 --- /dev/null +++ b/e2e-auth-server/startup.ts @@ -0,0 +1,8 @@ +import setup from './auth-server' + +const teardown = await setup() +process.on('exit', () => { + teardown() + console.log('[e2e-auth-server] stopped') + process.exit(0) +}) diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/e2e/docker-compose.dev.yml b/e2e/docker-compose.dev.yml index cd1d3d4982..b301ef8441 100644 --- a/e2e/docker-compose.dev.yml +++ b/e2e/docker-compose.dev.yml @@ -1,86 +1,77 @@ name: immich-e2e services: + immich-app-base: + extends: + file: ../docker/docker-compose.dev.yml + service: immich-app-base + + immich-init: + extends: + file: ../docker/docker-compose.dev.yml + service: immich-init + container_name: immich-e2e-init + immich-server: + extends: + file: ../docker/docker-compose.dev.yml + service: immich-server container_name: immich-e2e-server - command: ['immich-dev'] - image: immich-server-dev:latest - build: - context: ../ - dockerfile: server/Dockerfile.dev - target: dev + ports: !reset [] + env_file: !reset [] environment: - - DB_HOSTNAME=database - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - DB_DATABASE_NAME=immich - - IMMICH_MACHINE_LEARNING_ENABLED=false - - IMMICH_TELEMETRY_INCLUDE=all - - IMMICH_ENV=testing - - IMMICH_PORT=2285 - - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true + DB_HOSTNAME: database + DB_USERNAME: postgres + DB_PASSWORD: postgres + DB_DATABASE_NAME: immich + IMMICH_MACHINE_LEARNING_ENABLED: 'false' + IMMICH_TELEMETRY_INCLUDE: all + IMMICH_ENV: testing + IMMICH_PORT: '2285' + IMMICH_IGNORE_MOUNT_CHECK_ERRORS: 'true' volumes: - ./test-assets:/test-assets - - ..:/usr/src/app - - ${UPLOAD_LOCATION}/photos:/data - - /etc/localtime:/etc/localtime:ro - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage - - ../plugins:/build/corePlugin depends_on: + immich-init: + condition: service_healthy redis: condition: service_started database: condition: service_healthy immich-web: + extends: + file: ../docker/docker-compose.dev.yml + service: immich-web container_name: immich-e2e-web - image: immich-web-dev:latest - build: - context: ../ - dockerfile: server/Dockerfile.dev - target: dev - command: ['immich-web'] - ports: + ports: !override - 2285:3000 environment: - - IMMICH_SERVER_URL=http://immich-server:2285/ - volumes: - - ..:/usr/src/app - - pnpm-store:/usr/src/app/.pnpm-store - - server-node_modules:/usr/src/app/server/node_modules - - web-node_modules:/usr/src/app/web/node_modules - - github-node_modules:/usr/src/app/.github/node_modules - - cli-node_modules:/usr/src/app/cli/node_modules - - docs-node_modules:/usr/src/app/docs/node_modules - - e2e-node_modules:/usr/src/app/e2e/node_modules - - sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules - - app-node_modules:/usr/src/app/node_modules - - sveltekit:/usr/src/app/web/.svelte-kit - - coverage:/usr/src/app/web/coverage + IMMICH_SERVER_URL: http://immich-server:2285/ + depends_on: + immich-init: + condition: service_healthy restart: unless-stopped redis: - image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb + extends: + file: ../docker/docker-compose.dev.yml + service: redis + container_name: immich-e2e-redis database: - image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338 + extends: + file: ../docker/docker-compose.dev.yml + service: database + container_name: immich-e2e-postgres command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf + env_file: !reset [] + ports: !override + - 5435:5432 environment: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: immich - ports: - - 5435:5432 healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres -d immich'] interval: 1s @@ -89,17 +80,19 @@ services: start_period: 10s volumes: - model-cache: - prometheus-data: - grafana-data: - pnpm-store: - server-node_modules: - web-node_modules: - github-node_modules: - cli-node_modules: - docs-node_modules: - e2e-node_modules: - sdk-node_modules: - app-node_modules: + model_cache: + prometheus_data: + grafana_data: + pnpm_cache: + pnpm_store_server: + pnpm_store_web: + server_node_modules: + web_node_modules: + github_node_modules: + cli_node_modules: + docs_node_modules: + e2e_node_modules: + sdk_node_modules: + app_node_modules: sveltekit: coverage: diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 867a367d54..8ae5762a1b 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -1,6 +1,13 @@ name: immich-e2e services: + e2e-auth-server: + container_name: immich-e2e-auth-server + build: + context: ../e2e-auth-server + ports: + - 2286:2286 + immich-server: container_name: immich-e2e-server image: immich-server:latest @@ -16,19 +23,17 @@ services: - BUILD_SOURCE_REF=e2e - BUILD_SOURCE_COMMIT=e2eeeeeeeeeeeeeeeeee environment: - - DB_HOSTNAME=database - - DB_USERNAME=postgres - - DB_PASSWORD=postgres - - DB_DATABASE_NAME=immich - - IMMICH_MACHINE_LEARNING_ENABLED=false - - IMMICH_TELEMETRY_INCLUDE=all - - IMMICH_ENV=testing - - IMMICH_PORT=2285 - - IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true + DB_HOSTNAME: database + DB_USERNAME: postgres + DB_PASSWORD: postgres + DB_DATABASE_NAME: immich + IMMICH_MACHINE_LEARNING_ENABLED: 'false' + IMMICH_TELEMETRY_INCLUDE: all + IMMICH_ENV: testing + IMMICH_PORT: '2285' + IMMICH_IGNORE_MOUNT_CHECK_ERRORS: 'true' volumes: - ./test-assets:/test-assets - extra_hosts: - - 'auth-server:host-gateway' depends_on: redis: condition: service_started @@ -38,10 +43,14 @@ services: - 2285:2285 redis: - image: redis:6.2-alpine@sha256:37e002448575b32a599109664107e374c8709546905c372a34d64919043b9ceb + container_name: immich-e2e-redis + image: docker.io/valkey/valkey:9@sha256:930b41430fb727f533c5982fe509b6f04233e26d0f7354e04de4b0d5c706e44e + healthcheck: + test: redis-cli ping || exit 1 database: - image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:6f3e9d2c2177af16c2988ff71425d79d89ca630ec2f9c8db03209ab716542338 + container_name: immich-e2e-postgres + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23 command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf environment: POSTGRES_PASSWORD: postgres @@ -49,6 +58,7 @@ services: POSTGRES_DB: immich ports: - 5435:5432 + shm_size: 128mb healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres -d immich'] interval: 1s diff --git a/e2e/package.json b/e2e/package.json index b7ccd8e1e1..ac1ae081b3 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,46 +1,49 @@ { "name": "immich-e2e", - "version": "2.4.0", + "version": "2.5.6", "description": "", "main": "index.js", "type": "module", "scripts": { "test": "vitest --run", "test:watch": "vitest", - "test:web": "npx playwright test", - "start:web": "npx playwright test --ui", + "test:maintenance": "vitest --run --config vitest.maintenance.config.ts", + "test:web": "pnpm exec playwright test --project=web", + "test:web:maintenance": "pnpm exec playwright test --project=maintenance", + "test:web:ui": "pnpm exec playwright test --project=ui", + "start:web": "pnpm exec playwright test --ui --project=web", + "start:web:maintenance": "pnpm exec playwright test --ui --project=maintenance", + "start:web:ui": "pnpm exec playwright test --ui --project=ui", "format": "prettier --check .", "format:fix": "prettier --write .", "lint": "eslint \"src/**/*.ts\" --max-warnings 0", - "lint:fix": "npm run lint -- --fix", + "lint:fix": "pnpm run lint --fix", "check": "tsc --noEmit" }, "keywords": [], "author": "", "license": "GNU Affero General Public License version 3", "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.1.0", - "@immich/cli": "file:../cli", - "@immich/sdk": "file:../open-api/typescript-sdk", + "@immich/cli": "workspace:*", + "@immich/e2e-auth-server": "workspace:*", + "@immich/sdk": "workspace:*", "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.10.3", - "@types/oidc-provider": "^9.0.0", + "@types/node": "^24.10.13", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", "dotenv": "^17.2.3", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "exiftool-vendored": "^34.0.0", - "globals": "^16.0.0", - "jose": "^5.6.3", + "eslint-plugin-unicorn": "^63.0.0", + "exiftool-vendored": "^35.0.0", + "globals": "^17.0.0", "luxon": "^3.4.4", - "oidc-provider": "^9.0.0", "pg": "^8.11.3", "pngjs": "^7.0.0", "prettier": "^3.7.4", @@ -54,6 +57,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 4ae542bacf..040546b7bb 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -3,18 +3,19 @@ import dotenv from 'dotenv'; import { cpus } from 'node:os'; import { resolve } from 'node:path'; -dotenv.config({ path: resolve(import.meta.dirname, '.env') }); +dotenv.config({ quiet: true, path: resolve(import.meta.dirname, '.env') }); export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1'; export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1'; export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`; -export const playwriteSlowMo = parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0'); +export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0'); export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER; process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'; const config: PlaywrightTestConfig = { - testDir: './src/web/specs', + testDir: './src/specs/server', + testMatch: /.*\.e2e-spec\.ts/, fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 4 : 0, @@ -28,54 +29,28 @@ const config: PlaywrightTestConfig = { }, }, - testMatch: /.*\.e2e-spec\.ts/, - workers: process.env.CI ? 4 : Math.round(cpus().length * 0.75), projects: [ { - name: 'chromium', + name: 'web', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.e2e-spec\.ts/, + testDir: './src/specs/web', workers: 1, }, { - name: 'parallel tests', + name: 'ui', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.parallel-e2e-spec\.ts/, + testDir: './src/ui/specs', fullyParallel: true, workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1), }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + { + name: 'maintenance', + use: { ...devices['Desktop Chrome'] }, + testDir: './src/specs/maintenance/web', + workers: 1, + }, ], /* Run your local dev server before starting the tests */ diff --git a/e2e/src/setup/docker-compose.ts b/e2e/src/docker-compose.ts similarity index 100% rename from e2e/src/setup/docker-compose.ts rename to e2e/src/docker-compose.ts diff --git a/e2e/src/generators.ts b/e2e/src/generators.ts index c87427ceab..5e4895d708 100644 --- a/e2e/src/generators.ts +++ b/e2e/src/generators.ts @@ -26,6 +26,5 @@ export const makeRandomImage = () => { if (!value) { throw new Error('Ran out of random asset data'); } - return value; }; diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 9585484355..3d7971d6f0 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -43,10 +43,10 @@ export const errorDto = { message: 'Invalid share key', correlationId: expect.any(String), }, - invalidSharePassword: { + passwordRequired: { error: 'Unauthorized', statusCode: 401, - message: 'Invalid password', + message: 'Password required', correlationId: expect.any(String), }, badRequest: (message: any = null) => ({ diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..2b0f6ae61a --- /dev/null +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -0,0 +1,350 @@ +import { LoginResponseDto, ManualJobName } from '@immich/sdk'; +import { errorDto } from 'src/responses'; +import { app, utils } from 'src/utils'; +import request from 'supertest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +describe('/admin/database-backups', () => { + let cookie: string | undefined; + let admin: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup(); + await utils.resetBackups(admin.accessToken); + }); + + describe('GET /', async () => { + it('should succeed and be empty', async () => { + const { status, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + backups: [], + }); + }); + + it('should contain a created backup', async () => { + await utils.createJob(admin.accessToken, { + name: ManualJobName.BackupDatabase, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'backupDatabase'); + + await expect + .poll( + async () => { + const { status, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toEqual( + expect.objectContaining({ + backups: [ + expect.objectContaining({ + filename: expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/), + filesize: expect.any(Number), + }), + ], + }), + ); + }); + }); + + describe('DELETE /', async () => { + it('should delete backup', async () => { + const filename = await utils.createBackup(admin.accessToken); + + const { status } = await request(app) + .delete(`/admin/database-backups`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ backups: [filename] }); + + expect(status).toBe(200); + + const { status: listStatus, body } = await request(app) + .get('/admin/database-backups') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(listStatus).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + backups: [], + }), + ); + }); + }); + + // => action: restore database flow + + describe.sequential('POST /start-restore', () => { + afterAll(async () => { + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' }); + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + + admin = await utils.adminSetup(); + }); + + it.sequential('should not work when the server is configured', async () => { + const { status, body } = await request(app).post('/admin/database-backups/start-restore').send(); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('The server already has an admin')); + }); + + it.sequential('should enter maintenance mode in "database restore mode"', async () => { + await utils.resetDatabase(); // reset database before running this test + + const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send(); + + expect(status).toBe(201); + + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status2).toBe(200); + expect(body).toEqual({ + active: true, + action: 'select_database_restore', + }); + }); + }); + + // => action: restore database + + describe.sequential('POST /backups/restore', () => { + beforeAll(async () => { + await utils.disconnectDatabase(); + }); + + afterAll(async () => { + await utils.connectDatabase(); + }); + + it.sequential('should restore a backup', { timeout: 60_000 }, async () => { + let filename = await utils.createBackup(admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + filename = 'immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz'; + + const { status } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: filename, + }); + + expect(status).toBe(201); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status2).toBe(200); + expect(body).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + }), + ); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 60_000, + }, + ) + .toBeFalsy(); + }); + + it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => { + await utils.prepareTestBackup('corrupted'); + + const { status, headers } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: 'development-corrupted.sql.gz', + }); + + expect(status).toBe(201); + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: 'Something went wrong, see logs!', + }), + ); + + const { status: status2, body: body2 } = await request(app) + .get('/admin/maintenance/status') + .set('cookie', cookie!) + .send({ token: 'token' }); + expect(status2).toBe(200); + expect(body2).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: expect.stringContaining('IM CORRUPTED'), + }), + ); + + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ + action: 'end', + }); + + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + }); + + it.sequential('rollback to restore point if backup is missing admin', { timeout: 60_000 }, async () => { + await utils.prepareTestBackup('empty'); + + const { status, headers } = await request(app) + .post('/admin/maintenance') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + action: 'restore_database', + restoreBackupFilename: 'development-empty.sql.gz', + }); + + expect(status).toBe(201); + cookie = headers['set-cookie'][0].split(';')[0]; + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); + return body.maintenanceMode; + }, + { + interval: 500, + timeout: 10_000, + }, + ) + .toBeTruthy(); + + await expect + .poll( + async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + return body; + }, + { + interval: 500, + timeout: 30_000, + }, + ) + .toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: 'Something went wrong, see logs!', + }), + ); + + const { status: status2, body: body2 } = await request(app) + .get('/admin/maintenance/status') + .set('cookie', cookie!) + .send({ token: 'token' }); + expect(status2).toBe(200); + expect(body2).toEqual( + expect.objectContaining({ + active: true, + action: 'restore_database', + error: expect.stringContaining('Server health check failed, no admin exists.'), + }), + ); + + await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ + action: 'end', + }); + + await utils.poll( + () => request(app).get('/server/config'), + ({ status, body }) => status === 200 && !body.maintenanceMode, + ); + }); + }); +}); diff --git a/e2e/src/api/specs/maintenance.e2e-spec.ts b/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts similarity index 81% rename from e2e/src/api/specs/maintenance.e2e-spec.ts rename to e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts index b6c7540bc5..8e4e154328 100644 --- a/e2e/src/api/specs/maintenance.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts @@ -14,6 +14,7 @@ describe('/admin/maintenance', () => { await utils.resetDatabase(); admin = await utils.adminSetup(); nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetBackups(admin.accessToken); }); // => outside of maintenance mode @@ -26,6 +27,17 @@ describe('/admin/maintenance', () => { }); }); + describe('GET /status', async () => { + it('to always indicate we are not in maintenance mode', async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + expect(body).toEqual({ + active: false, + action: 'end', + }); + }); + }); + describe('POST /login', async () => { it('should not work out of maintenance mode', async () => { const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' }); @@ -39,6 +51,7 @@ describe('/admin/maintenance', () => { describe.sequential('POST /', () => { it('should require authentication', async () => { const { status, body } = await request(app).post('/admin/maintenance').send({ + active: false, action: 'end', }); expect(status).toBe(401); @@ -69,6 +82,7 @@ describe('/admin/maintenance', () => { .send({ action: 'start', }); + expect(status).toBe(201); cookie = headers['set-cookie'][0].split(';')[0]; @@ -79,12 +93,13 @@ describe('/admin/maintenance', () => { await expect .poll( async () => { - const { body } = await request(app).get('/server/config'); + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); return body.maintenanceMode; }, { - interval: 5e2, - timeout: 1e4, + interval: 500, + timeout: 10_000, }, ) .toBeTruthy(); @@ -102,6 +117,17 @@ describe('/admin/maintenance', () => { }); }); + describe('GET /status', async () => { + it('to indicate we are in maintenance mode', async () => { + const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' }); + expect(status).toBe(200); + expect(body).toEqual({ + active: true, + action: 'start', + }); + }); + }); + describe('POST /login', async () => { it('should fail without cookie or token in body', async () => { const { status, body } = await request(app).post('/admin/maintenance/login').send({}); @@ -158,12 +184,13 @@ describe('/admin/maintenance', () => { await expect .poll( async () => { - const { body } = await request(app).get('/server/config'); + const { status, body } = await request(app).get('/server/config'); + expect(status).toBe(200); return body.maintenanceMode; }, { - interval: 5e2, - timeout: 1e4, + interval: 500, + timeout: 10_000, }, ) .toBeFalsy(); diff --git a/e2e/src/specs/maintenance/web/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/web/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..d101215ceb --- /dev/null +++ b/e2e/src/specs/maintenance/web/database-backups.e2e-spec.ts @@ -0,0 +1,105 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe.configure({ mode: 'serial' }); + +test.describe('Database Backups', () => { + let admin: LoginResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test('restore a backup from settings', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + const filename = await utils.createBackup(admin.accessToken); + await utils.setAuthCookies(context, admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await page.waitForURL('/admin/maintenance**', { timeout: 60_000 }); + }); + + test('handle backup restore failure', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + await utils.prepareTestBackup('corrupted'); + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await expect(page.getByText('IM CORRUPTED')).toBeVisible({ timeout: 60_000 }); + await page.getByRole('button', { name: 'End maintenance mode' }).click(); + await page.waitForURL('/admin/maintenance**'); + }); + + test('rollback to restore point if backup is missing admin', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + await utils.prepareTestBackup('empty'); + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/admin/maintenance?isOpen=backups'); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await expect(page.getByText('Server health check failed, no admin exists.')).toBeVisible({ timeout: 60_000 }); + await page.getByRole('button', { name: 'End maintenance mode' }).click(); + await page.waitForURL('/admin/maintenance**'); + }); + + test('restore a backup from onboarding', async ({ context, page }) => { + test.setTimeout(60_000); + + await utils.resetBackups(admin.accessToken); + const filename = await utils.createBackup(admin.accessToken); + await utils.setAuthCookies(context, admin.accessToken); + + // work-around until test is running on released version + await utils.move( + `/data/backups/${filename}`, + '/data/backups/immich-db-backup-20260114T184016-v2.5.0-pg14.19.sql.gz', + ); + + await utils.resetDatabase(); + + await page.goto('/'); + await page.getByRole('button', { name: 'Restore from backup' }).click(); + + try { + await page.waitForURL('/maintenance**'); + } catch { + // when chained with the rest of the tests + // this navigation may fail..? not sure why... + await page.goto('/maintenance'); + await page.waitForURL('/maintenance**'); + } + + await page.getByRole('button', { name: 'Next' }).click(); + await page.getByRole('button', { name: 'Restore', exact: true }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click(); + + await page.waitForURL('/maintenance?**'); + await page.waitForURL('/photos', { timeout: 60_000 }); + }); +}); diff --git a/e2e/src/web/specs/maintenance.e2e-spec.ts b/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts similarity index 88% rename from e2e/src/web/specs/maintenance.e2e-spec.ts rename to e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts index 534c05f783..8b1631f0bf 100644 --- a/e2e/src/web/specs/maintenance.e2e-spec.ts +++ b/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts @@ -16,12 +16,12 @@ test.describe('Maintenance', () => { test('enter and exit maintenance mode', async ({ context, page }) => { await utils.setAuthCookies(context, admin.accessToken); - await page.goto('/admin/system-settings?isOpen=maintenance'); - await page.getByRole('button', { name: 'Start maintenance mode' }).click(); + await page.goto('/admin/maintenance'); + await page.getByRole('button', { name: 'Switch to maintenance mode' }).click(); await expect(page.getByText('Temporarily Unavailable')).toBeVisible({ timeout: 10_000 }); await page.getByRole('button', { name: 'End maintenance mode' }).click(); - await page.waitForURL('**/admin/system-settings*', { timeout: 10_000 }); + await page.waitForURL('**/admin/maintenance*', { timeout: 10_000 }); }); test('maintenance shows no options to users until they authenticate', async ({ page }) => { diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/specs/server/api/activity.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/activity.e2e-spec.ts rename to e2e/src/specs/server/api/activity.e2e-spec.ts diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/album.e2e-spec.ts rename to e2e/src/specs/server/api/album.e2e-spec.ts diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/specs/server/api/api-key.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/api-key.e2e-spec.ts rename to e2e/src/specs/server/api/api-key.e2e-spec.ts diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts similarity index 99% rename from e2e/src/api/specs/asset.e2e-spec.ts rename to e2e/src/specs/server/api/asset.e2e-spec.ts index ab3252c40b..11e825a7cd 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -253,7 +253,8 @@ describe('/asset', () => { expect(status).toBe(200); expect(body.id).toEqual(facesAsset.id); - expect(body.people).toMatchObject(expectedFaces); + const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name)); + expect(sortedPeople).toMatchObject(expectedFaces); }); }); @@ -473,6 +474,7 @@ describe('/asset', () => { id: user1Assets[0].id, exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', + timeZone: 'UTC-7', }), }); expect(status).toEqual(200); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/specs/server/api/download.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/download.e2e-spec.ts rename to e2e/src/specs/server/api/download.e2e-spec.ts diff --git a/e2e/src/api/specs/jobs.e2e-spec.ts b/e2e/src/specs/server/api/jobs.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/jobs.e2e-spec.ts rename to e2e/src/specs/server/api/jobs.e2e-spec.ts diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/library.e2e-spec.ts rename to e2e/src/specs/server/api/library.e2e-spec.ts diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/map.e2e-spec.ts rename to e2e/src/specs/server/api/map.e2e-spec.ts diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/specs/server/api/memory.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/memory.e2e-spec.ts rename to e2e/src/specs/server/api/memory.e2e-spec.ts diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts similarity index 99% rename from e2e/src/api/specs/oauth.e2e-spec.ts rename to e2e/src/specs/server/api/oauth.e2e-spec.ts index 58fc43a2d5..cbd68c003a 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -1,3 +1,4 @@ +import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server'; import { LoginResponseDto, SystemConfigOAuthDto, @@ -8,13 +9,12 @@ import { } from '@immich/sdk'; import { createHash, randomBytes } from 'node:crypto'; import { errorDto } from 'src/responses'; -import { OAuthClient, OAuthUser } from 'src/setup/auth-server'; import { app, asBearerAuth, baseUrl, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; const authServer = { - internal: 'http://auth-server:2286', + internal: 'http://e2e-auth-server:2286', external: 'http://127.0.0.1:2286', }; diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/specs/server/api/partner.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/partner.e2e-spec.ts rename to e2e/src/specs/server/api/partner.e2e-spec.ts diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/specs/server/api/person.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/person.e2e-spec.ts rename to e2e/src/specs/server/api/person.e2e-spec.ts diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/search.e2e-spec.ts rename to e2e/src/specs/server/api/search.e2e-spec.ts diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/server.e2e-spec.ts rename to e2e/src/specs/server/api/server.e2e-spec.ts diff --git a/e2e/src/api/specs/session.e2e-spec.ts b/e2e/src/specs/server/api/session.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/session.e2e-spec.ts rename to e2e/src/specs/server/api/session.e2e-spec.ts diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts similarity index 94% rename from e2e/src/api/specs/shared-link.e2e-spec.ts rename to e2e/src/specs/server/api/shared-link.e2e-spec.ts index f25a54786a..80232beb75 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -20,7 +20,6 @@ describe('/shared-links', () => { let user1: LoginResponseDto; let user2: LoginResponseDto; let album: AlbumResponseDto; - let metadataAlbum: AlbumResponseDto; let deletedAlbum: AlbumResponseDto; let linkWithDeletedAlbum: SharedLinkResponseDto; let linkWithPassword: SharedLinkResponseDto; @@ -41,18 +40,9 @@ describe('/shared-links', () => { [asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]); - [album, deletedAlbum, metadataAlbum] = await Promise.all([ + [album, deletedAlbum] = await Promise.all([ createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }), createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }), - createAlbum( - { - createAlbumDto: { - albumName: 'metadata album', - assetIds: [asset1.id], - }, - }, - { headers: asBearerAuth(user1.accessToken) }, - ), ]); [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = @@ -75,14 +65,14 @@ describe('/shared-links', () => { password: 'foo', }), utils.createSharedLink(user1.accessToken, { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, + type: SharedLinkType.Individual, + assetIds: [asset1.id], showMetadata: true, - slug: 'metadata-album', + slug: 'metadata-slug', }), utils.createSharedLink(user1.accessToken, { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, + type: SharedLinkType.Individual, + assetIds: [asset1.id], showMetadata: false, }), ]); @@ -95,9 +85,7 @@ describe('/shared-links', () => { const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); - expect(resp.text).toContain( - ``, - ); + expect(resp.text).toContain(``); }); it('should have correct asset count in meta tag for empty album', async () => { @@ -144,9 +132,7 @@ describe('/shared-links', () => { const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); - expect(resp.text).toContain( - ``, - ); + expect(resp.text).toContain(``); }); }); @@ -253,7 +239,7 @@ describe('/shared-links', () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key }); expect(status).toBe(401); - expect(body).toEqual(errorDto.invalidSharePassword); + expect(body).toEqual(errorDto.passwordRequired); }); it('should get data for correct password protected link', async () => { @@ -271,12 +257,12 @@ describe('/shared-links', () => { ); }); - it('should return metadata for album shared link', async () => { + it('should return metadata for individual shared link', async () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key }); expect(status).toBe(200); - expect(body.assets).toHaveLength(0); - expect(body.album).toBeDefined(); + expect(body.assets).toHaveLength(1); + expect(body.album).not.toBeDefined(); }); it('should not return metadata for album shared link without metadata', async () => { @@ -284,7 +270,7 @@ describe('/shared-links', () => { expect(status).toBe(200); expect(body.assets).toHaveLength(1); - expect(body.album).toBeDefined(); + expect(body.album).not.toBeDefined(); const asset = body.assets[0]; expect(asset).not.toHaveProperty('exifInfo'); diff --git a/e2e/src/api/specs/stack.e2e-spec.ts b/e2e/src/specs/server/api/stack.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/stack.e2e-spec.ts rename to e2e/src/specs/server/api/stack.e2e-spec.ts diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/specs/server/api/system-config.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/system-config.e2e-spec.ts rename to e2e/src/specs/server/api/system-config.e2e-spec.ts diff --git a/e2e/src/api/specs/system-metadata.e2e-spec.ts b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/system-metadata.e2e-spec.ts rename to e2e/src/specs/server/api/system-metadata.e2e-spec.ts diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/tag.e2e-spec.ts rename to e2e/src/specs/server/api/tag.e2e-spec.ts diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/specs/server/api/trash.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/trash.e2e-spec.ts rename to e2e/src/specs/server/api/trash.e2e-spec.ts diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/user-admin.e2e-spec.ts rename to e2e/src/specs/server/api/user-admin.e2e-spec.ts diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts similarity index 100% rename from e2e/src/api/specs/user.e2e-spec.ts rename to e2e/src/specs/server/api/user.e2e-spec.ts diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/specs/server/cli/login.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/login.e2e-spec.ts rename to e2e/src/specs/server/cli/login.e2e-spec.ts diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/specs/server/cli/server-info.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/server-info.e2e-spec.ts rename to e2e/src/specs/server/cli/server-info.e2e-spec.ts diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/specs/server/cli/upload.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/upload.e2e-spec.ts rename to e2e/src/specs/server/cli/upload.e2e-spec.ts diff --git a/e2e/src/cli/specs/version.e2e-spec.ts b/e2e/src/specs/server/cli/version.e2e-spec.ts similarity index 100% rename from e2e/src/cli/specs/version.e2e-spec.ts rename to e2e/src/specs/server/cli/version.e2e-spec.ts diff --git a/e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts b/e2e/src/specs/server/immich-admin/immich-admin.e2e-spec.ts similarity index 100% rename from e2e/src/immich-admin/specs/immich-admin.e2e-spec.ts rename to e2e/src/specs/server/immich-admin/immich-admin.e2e-spec.ts diff --git a/e2e/src/web/specs/album.e2e-spec.ts b/e2e/src/specs/web/album.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/album.e2e-spec.ts rename to e2e/src/specs/web/album.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/detail-panel.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/navbar.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/navbar.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/slideshow.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/slideshow.e2e-spec.ts diff --git a/e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts b/e2e/src/specs/web/asset-viewer/stack.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/asset-viewer/stack.e2e-spec.ts rename to e2e/src/specs/web/asset-viewer/stack.e2e-spec.ts diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/specs/web/auth.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/auth.e2e-spec.ts rename to e2e/src/specs/web/auth.e2e-spec.ts diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts similarity index 97% rename from e2e/src/web/specs/photo-viewer.e2e-spec.ts rename to e2e/src/specs/web/photo-viewer.e2e-spec.ts index c8a9b42b2a..3f9bb4237a 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -3,7 +3,7 @@ import { Page, expect, test } from '@playwright/test'; import { utils } from 'src/utils'; function imageLocator(page: Page) { - return page.getByAltText('Image taken on').locator('visible=true'); + return page.getByAltText('Image taken').locator('visible=true'); } test.describe('Photo Viewer', () => { let admin: LoginResponseDto; diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/specs/web/shared-link.e2e-spec.ts similarity index 97% rename from e2e/src/web/specs/shared-link.e2e-spec.ts rename to e2e/src/specs/web/shared-link.e2e-spec.ts index 017bc0fcb2..f6d1ec98d4 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/specs/web/shared-link.e2e-spec.ts @@ -45,8 +45,7 @@ test.describe('Shared Links', () => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.locator(`[data-asset-id="${asset.id}"]`).hover(); - await page.waitForSelector('[data-group] svg'); - await page.getByRole('checkbox').click(); + await page.waitForSelector(`[data-asset-id="${asset.id}"] [role="checkbox"]`); await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]); }); diff --git a/e2e/src/web/specs/user-admin.e2e-spec.ts b/e2e/src/specs/web/user-admin.e2e-spec.ts similarity index 96% rename from e2e/src/web/specs/user-admin.e2e-spec.ts rename to e2e/src/specs/web/user-admin.e2e-spec.ts index 7a2cd77177..67a537ba9d 100644 --- a/e2e/src/web/specs/user-admin.e2e-spec.ts +++ b/e2e/src/specs/web/user-admin.e2e-spec.ts @@ -56,7 +56,7 @@ test.describe('User Administration', () => { await expect(page.getByLabel('Admin User')).not.toBeChecked(); await page.getByLabel('Admin User').click(); await expect(page.getByLabel('Admin User')).toBeChecked(); - await page.getByRole('button', { name: 'Confirm' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); await expect .poll(async () => { @@ -85,7 +85,7 @@ test.describe('User Administration', () => { await expect(page.getByLabel('Admin User')).toBeChecked(); await page.getByLabel('Admin User').click(); await expect(page.getByLabel('Admin User')).not.toBeChecked(); - await page.getByRole('button', { name: 'Confirm' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); await expect .poll(async () => { diff --git a/e2e/src/web/specs/websocket.e2e-spec.ts b/e2e/src/specs/web/websocket.e2e-spec.ts similarity index 100% rename from e2e/src/web/specs/websocket.e2e-spec.ts rename to e2e/src/specs/web/websocket.e2e-spec.ts diff --git a/e2e/src/ui/generators/memory.ts b/e2e/src/ui/generators/memory.ts new file mode 100644 index 0000000000..c17b4aa476 --- /dev/null +++ b/e2e/src/ui/generators/memory.ts @@ -0,0 +1,2 @@ +export { generateMemoriesFromTimeline, generateMemory } from './memory/model-objects'; +export type { MemoryConfig, MemoryYearConfig } from './memory/model-objects'; diff --git a/e2e/src/ui/generators/memory/model-objects.ts b/e2e/src/ui/generators/memory/model-objects.ts new file mode 100644 index 0000000000..f81b2f8896 --- /dev/null +++ b/e2e/src/ui/generators/memory/model-objects.ts @@ -0,0 +1,84 @@ +import { faker } from '@faker-js/faker'; +import { MemoryType, type MemoryResponseDto, type OnThisDayDto } from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { toAssetResponseDto } from 'src/ui/generators/timeline/rest-response'; +import type { MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config'; +import { SeededRandom, selectRandomMultiple } from 'src/ui/generators/timeline/utils'; + +export type MemoryConfig = { + id?: string; + ownerId: string; + year: number; + memoryAt: string; + isSaved?: boolean; +}; + +export type MemoryYearConfig = { + year: number; + assetCount: number; +}; + +export function generateMemory(config: MemoryConfig, assets: MockTimelineAsset[]): MemoryResponseDto { + const now = new Date().toISOString(); + const memoryId = config.id ?? faker.string.uuid(); + + return { + id: memoryId, + assets: assets.map((asset) => toAssetResponseDto(asset)), + data: { year: config.year } as OnThisDayDto, + memoryAt: config.memoryAt, + createdAt: now, + updatedAt: now, + isSaved: config.isSaved ?? false, + ownerId: config.ownerId, + type: MemoryType.OnThisDay, + }; +} + +export function generateMemoriesFromTimeline( + timelineAssets: MockTimelineAsset[], + ownerId: string, + memoryConfigs: MemoryYearConfig[], + seed: number = 42, +): MemoryResponseDto[] { + const rng = new SeededRandom(seed); + const memories: MemoryResponseDto[] = []; + const usedAssetIds = new Set(); + + for (const config of memoryConfigs) { + const yearAssets = timelineAssets.filter((asset) => { + const assetYear = DateTime.fromISO(asset.fileCreatedAt).year; + return assetYear === config.year && !usedAssetIds.has(asset.id); + }); + + if (yearAssets.length === 0) { + continue; + } + + const countToSelect = Math.min(config.assetCount, yearAssets.length); + const selectedAssets = selectRandomMultiple(yearAssets, countToSelect, rng); + + for (const asset of selectedAssets) { + usedAssetIds.add(asset.id); + } + + selectedAssets.sort( + (a, b) => DateTime.fromISO(b.fileCreatedAt).diff(DateTime.fromISO(a.fileCreatedAt)).milliseconds, + ); + + const memoryAt = DateTime.now().set({ year: config.year }).toISO()!; + + memories.push( + generateMemory( + { + ownerId, + year: config.year, + memoryAt, + }, + selectedAssets, + ), + ); + } + + return memories; +} diff --git a/e2e/src/generators/timeline.ts b/e2e/src/ui/generators/timeline.ts similarity index 100% rename from e2e/src/generators/timeline.ts rename to e2e/src/ui/generators/timeline.ts diff --git a/e2e/src/generators/timeline/distribution-patterns.ts b/e2e/src/ui/generators/timeline/distribution-patterns.ts similarity index 98% rename from e2e/src/generators/timeline/distribution-patterns.ts rename to e2e/src/ui/generators/timeline/distribution-patterns.ts index ae621fd9c5..b6f3aab6de 100644 --- a/e2e/src/generators/timeline/distribution-patterns.ts +++ b/e2e/src/ui/generators/timeline/distribution-patterns.ts @@ -1,5 +1,5 @@ -import { generateConsecutiveDays, generateDayAssets } from 'src/generators/timeline/model-objects'; -import { SeededRandom, selectRandomDays } from 'src/generators/timeline/utils'; +import { generateConsecutiveDays, generateDayAssets } from 'src/ui/generators/timeline/model-objects'; +import { SeededRandom, selectRandomDays } from 'src/ui/generators/timeline/utils'; import type { MockTimelineAsset } from './timeline-config'; import { GENERATION_CONSTANTS } from './timeline-config'; diff --git a/e2e/src/generators/timeline/images.ts b/e2e/src/ui/generators/timeline/images.ts similarity index 98% rename from e2e/src/generators/timeline/images.ts rename to e2e/src/ui/generators/timeline/images.ts index 69ec576714..9330cf137d 100644 --- a/e2e/src/generators/timeline/images.ts +++ b/e2e/src/ui/generators/timeline/images.ts @@ -1,5 +1,5 @@ import sharp from 'sharp'; -import { SeededRandom } from 'src/generators/timeline/utils'; +import { SeededRandom } from 'src/ui/generators/timeline/utils'; export const randomThumbnail = async (seed: string, ratio: number) => { const height = 235; diff --git a/e2e/src/generators/timeline/model-objects.ts b/e2e/src/ui/generators/timeline/model-objects.ts similarity index 99% rename from e2e/src/generators/timeline/model-objects.ts rename to e2e/src/ui/generators/timeline/model-objects.ts index f06596fd1a..e300de1161 100644 --- a/e2e/src/generators/timeline/model-objects.ts +++ b/e2e/src/ui/generators/timeline/model-objects.ts @@ -6,7 +6,7 @@ import { faker } from '@faker-js/faker'; import { AssetVisibility } from '@immich/sdk'; import { DateTime } from 'luxon'; import { writeFileSync } from 'node:fs'; -import { SeededRandom } from 'src/generators/timeline/utils'; +import { SeededRandom } from 'src/ui/generators/timeline/utils'; import type { DayPattern, MonthDistribution } from './distribution-patterns'; import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns'; import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config'; diff --git a/e2e/src/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts similarity index 98% rename from e2e/src/generators/timeline/rest-response.ts rename to e2e/src/ui/generators/timeline/rest-response.ts index 6fcfe52fc2..0c4bd06dc3 100644 --- a/e2e/src/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -15,7 +15,7 @@ import { } from '@immich/sdk'; import { DateTime } from 'luxon'; import { signupDto } from 'src/fixtures'; -import { parseTimeBucketKey } from 'src/generators/timeline/utils'; +import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils'; import type { MockTimelineAsset, MockTimelineData } from './timeline-config'; /** @@ -346,6 +346,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons duplicateId: null, resized: true, checksum: asset.checksum, + width: exifInfo.exifImageWidth ?? 1, + height: exifInfo.exifImageHeight ?? 1, + isEdited: false, }; } diff --git a/e2e/src/generators/timeline/timeline-config.ts b/e2e/src/ui/generators/timeline/timeline-config.ts similarity index 98% rename from e2e/src/generators/timeline/timeline-config.ts rename to e2e/src/ui/generators/timeline/timeline-config.ts index 8dbe8399b1..992480eef9 100644 --- a/e2e/src/generators/timeline/timeline-config.ts +++ b/e2e/src/ui/generators/timeline/timeline-config.ts @@ -1,5 +1,5 @@ import type { AssetVisibility } from '@immich/sdk'; -import { DayPattern, MonthDistribution } from 'src/generators/timeline/distribution-patterns'; +import { DayPattern, MonthDistribution } from 'src/ui/generators/timeline/distribution-patterns'; // Constants for generation parameters export const GENERATION_CONSTANTS = { diff --git a/e2e/src/generators/timeline/utils.ts b/e2e/src/ui/generators/timeline/utils.ts similarity index 98% rename from e2e/src/generators/timeline/utils.ts rename to e2e/src/ui/generators/timeline/utils.ts index 686a8223ef..283f56c6f0 100644 --- a/e2e/src/generators/timeline/utils.ts +++ b/e2e/src/ui/generators/timeline/utils.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon'; -import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/generators/timeline/timeline-config'; +import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config'; /** * Linear Congruential Generator for deterministic pseudo-random numbers diff --git a/e2e/src/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts similarity index 100% rename from e2e/src/mock-network/base-network.ts rename to e2e/src/ui/mock-network/base-network.ts diff --git a/e2e/src/ui/mock-network/memory-network.ts b/e2e/src/ui/mock-network/memory-network.ts new file mode 100644 index 0000000000..9a3a9e6555 --- /dev/null +++ b/e2e/src/ui/mock-network/memory-network.ts @@ -0,0 +1,65 @@ +import type { MemoryResponseDto } from '@immich/sdk'; +import { BrowserContext } from '@playwright/test'; + +export type MemoryChanges = { + memoryDeletions: string[]; + assetRemovals: Map; +}; + +export const setupMemoryMockApiRoutes = async ( + context: BrowserContext, + memories: MemoryResponseDto[], + changes: MemoryChanges, +) => { + await context.route('**/api/memories*', async (route, request) => { + const url = new URL(request.url()); + const pathname = url.pathname; + + if (pathname === '/api/memories' && request.method() === 'GET') { + const activeMemories = memories + .filter((memory) => !changes.memoryDeletions.includes(memory.id)) + .map((memory) => { + const removedAssets = changes.assetRemovals.get(memory.id) ?? []; + return { + ...memory, + assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)), + }; + }) + .filter((memory) => memory.assets.length > 0); + + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: activeMemories, + }); + } + + const memoryMatch = pathname.match(/\/api\/memories\/([^/]+)$/); + if (memoryMatch && request.method() === 'GET') { + const memoryId = memoryMatch[1]; + const memory = memories.find((m) => m.id === memoryId); + + if (!memory || changes.memoryDeletions.includes(memoryId)) { + return route.fulfill({ status: 404 }); + } + + const removedAssets = changes.assetRemovals.get(memoryId) ?? []; + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: { + ...memory, + assets: memory.assets.filter((asset) => !removedAssets.includes(asset.id)), + }, + }); + } + + if (/\/api\/memories\/([^/]+)$/.test(pathname) && request.method() === 'DELETE') { + const memoryId = pathname.split('/').pop()!; + changes.memoryDeletions.push(memoryId); + return route.fulfill({ status: 204 }); + } + + await route.fallback(); + }); +}; diff --git a/e2e/src/mock-network/timeline-network.ts b/e2e/src/ui/mock-network/timeline-network.ts similarity index 74% rename from e2e/src/mock-network/timeline-network.ts rename to e2e/src/ui/mock-network/timeline-network.ts index 59bce71dd8..b20a812eb1 100644 --- a/e2e/src/mock-network/timeline-network.ts +++ b/e2e/src/ui/mock-network/timeline-network.ts @@ -1,3 +1,4 @@ +import { AssetResponseDto } from '@immich/sdk'; import { BrowserContext, Page, Request, Route } from '@playwright/test'; import { basename } from 'node:path'; import { @@ -9,8 +10,8 @@ import { randomPreview, randomThumbnail, TimelineData, -} from 'src/generators/timeline'; -import { sleep } from 'src/web/specs/timeline/utils'; +} from 'src/ui/generators/timeline'; +import { sleep } from 'src/ui/specs/timeline/utils'; export class TimelineTestContext { slowBucket = false; @@ -63,15 +64,33 @@ export const setupTimelineMockApiRoutes = async ( }); await context.route('**/api/assets/*', async (route, request) => { - const url = new URL(request.url()); - const pathname = url.pathname; - const assetId = basename(pathname); - const asset = getAsset(timelineRestData, assetId); - return route.fulfill({ - status: 200, - contentType: 'application/json', - json: asset, - }); + if (request.method() === 'GET') { + const url = new URL(request.url()); + const pathname = url.pathname; + const assetId = basename(pathname); + let asset = getAsset(timelineRestData, assetId); + if (changes.assetDeletions.includes(asset!.id)) { + asset = { + ...asset, + isTrashed: true, + } as AssetResponseDto; + } + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: asset, + }); + } + await route.fallback(); + }); + + await context.route('**/api/assets', async (route, request) => { + if (request.method() === 'DELETE') { + return route.fulfill({ + status: 204, + }); + } + await route.fallback(); }); await context.route('**/api/assets/*/ocr', async (route) => { @@ -117,17 +136,28 @@ export const setupTimelineMockApiRoutes = async ( }); await context.route('**/api/albums/**', async (route, request) => { - const pattern = /\/api\/albums\/(?[^/?]+)/; - const match = request.url().match(pattern); - if (!match) { - return route.continue(); + const albumsMatch = request.url().match(/\/api\/albums\/(?[^/?]+)/); + if (albumsMatch) { + const album = getAlbum(timelineRestData, testContext.adminId, albumsMatch.groups?.albumId, changes); + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: album, + }); } - const album = getAlbum(timelineRestData, testContext.adminId, match.groups?.albumId, changes); - return route.fulfill({ - status: 200, - contentType: 'application/json', - json: album, - }); + return route.fallback(); + }); + + await context.route('**/api/albums**', async (route, request) => { + const allAlbums = request.url().match(/\/api\/albums\?assetId=(?[^&]+)/); + if (allAlbums) { + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: [], + }); + } + return route.fallback(); }); }; diff --git a/e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts b/e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts new file mode 100644 index 0000000000..082ff1f7a1 --- /dev/null +++ b/e2e/src/ui/specs/asset-viewer/asset-viewer.e2e-spec.ts @@ -0,0 +1,269 @@ +import { faker } from '@faker-js/faker'; +import { expect, test } from '@playwright/test'; +import { + Changes, + createDefaultTimelineConfig, + generateTimelineData, + SeededRandom, + selectRandom, + TimelineAssetConfig, + TimelineData, +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; +import { utils } from 'src/utils'; +import { assetViewerUtils } from '../timeline/utils'; + +test.describe.configure({ mode: 'parallel' }); +test.describe('asset-viewer', () => { + const rng = new SeededRandom(529); + let adminUserId: string; + let timelineRestData: TimelineData; + const assets: TimelineAssetConfig[] = []; + const yearMonths: string[] = []; + const testContext = new TimelineTestContext(); + const changes: Changes = { + albumAdditions: [], + assetDeletions: [], + assetArchivals: [], + assetFavorites: [], + }; + + test.beforeAll(async () => { + utils.initSdk(); + adminUserId = faker.string.uuid(); + testContext.adminId = adminUserId; + timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId }); + for (const timeBucket of timelineRestData.buckets.values()) { + assets.push(...timeBucket); + } + for (const yearMonth of timelineRestData.buckets.keys()) { + const [year, month] = yearMonth.split('-'); + yearMonths.push(`${year}-${Number(month)}`); + } + }); + + test.beforeEach(async ({ context }) => { + await setupBaseMockApiRoutes(context, adminUserId); + await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext); + }); + + test.afterEach(() => { + testContext.slowBucket = false; + changes.albumAdditions = []; + changes.assetDeletions = []; + changes.assetArchivals = []; + changes.assetFavorites = []; + }); + + test.describe('/photos/:id', () => { + test('Navigate to next asset via button', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); + + await page.getByLabel('View next asset').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`); + }); + + test('Navigate to previous asset via button', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); + + await page.getByLabel('View previous asset').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`); + }); + + test('Navigate to next asset via keyboard (ArrowRight)', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); + + await page.keyboard.press('ArrowRight'); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`); + }); + + test('Navigate to previous asset via keyboard (ArrowLeft)', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); + + await page.keyboard.press('ArrowLeft'); + await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`); + }); + + test('Navigate forward 5 times via button', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + + for (let i = 1; i <= 5; i++) { + await page.getByLabel('View next asset').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + i]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + i].id}`); + } + }); + + test('Navigate backward 5 times via button', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + + for (let i = 1; i <= 5; i++) { + await page.getByLabel('View previous asset').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index - i]); + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - i].id}`); + } + }); + + test('Navigate forward then backward via keyboard', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + + // Navigate forward 3 times + for (let i = 1; i <= 3; i++) { + await page.keyboard.press('ArrowRight'); + await assetViewerUtils.waitForViewerLoad(page, assets[index + i]); + } + + // Navigate backward 3 times to return to original + for (let i = 2; i >= 0; i--) { + await page.keyboard.press('ArrowLeft'); + await assetViewerUtils.waitForViewerLoad(page, assets[index + i]); + } + + // Verify we're back at the original asset + await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`); + }); + + test('Verify no next button on last asset', async ({ page }) => { + const lastAsset = assets.at(-1)!; + await page.goto(`/photos/${lastAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, lastAsset); + + // Verify next button doesn't exist + await expect(page.getByLabel('View next asset')).toHaveCount(0); + }); + + test('Verify no previous button on first asset', async ({ page }) => { + const firstAsset = assets[0]; + await page.goto(`/photos/${firstAsset.id}`); + await assetViewerUtils.waitForViewerLoad(page, firstAsset); + + // Verify previous button doesn't exist + await expect(page.getByLabel('View previous asset')).toHaveCount(0); + }); + + test('Delete photo advances to next', async ({ page }) => { + const asset = selectRandom(assets, rng); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + const index = assets.indexOf(asset); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + }); + test('Delete photo advances to next (2x)', async ({ page }) => { + const asset = selectRandom(assets, rng); + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + const index = assets.indexOf(asset); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 2]); + }); + test('Delete last photo advances to prev', async ({ page }) => { + const asset = assets.at(-1)!; + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + const index = assets.indexOf(asset); + await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]); + }); + test('Delete last photo advances to prev (2x)', async ({ page }) => { + const asset = assets.at(-1)!; + await page.goto(`/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + const index = assets.indexOf(asset); + await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index - 2]); + }); + }); + test.describe('/trash/photos/:id', () => { + test('Delete trashed photo advances to next', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id); + changes.assetDeletions.push(...deletedAssets); + await page.goto(`/trash/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + }); + test('Delete trashed photo advances to next 2x', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id); + changes.assetDeletions.push(...deletedAssets); + await page.goto(`/trash/photos/${asset.id}`); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 2]); + }); + test('Delete trashed photo advances to prev', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id); + changes.assetDeletions.push(...deletedAssets); + await page.goto(`/trash/photos/${assets[index + 9].id}`); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 9]); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 8]); + }); + test('Delete trashed photo advances to prev 2x', async ({ page }) => { + const asset = selectRandom(assets, rng); + const index = assets.indexOf(asset); + const deletedAssets = assets.slice(index - 10, index + 10).map((asset) => asset.id); + changes.assetDeletions.push(...deletedAssets); + await page.goto(`/trash/photos/${assets[index + 9].id}`); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 9]); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 8]); + await page.getByLabel('Delete').click(); + // confirm dialog + await page.getByRole('button').getByText('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[index + 7]); + }); + }); +}); diff --git a/e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts b/e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts new file mode 100644 index 0000000000..dbc21ad1c9 --- /dev/null +++ b/e2e/src/ui/specs/memory/memory-viewer.e2e-spec.ts @@ -0,0 +1,289 @@ +import { faker } from '@faker-js/faker'; +import type { MemoryResponseDto } from '@immich/sdk'; +import { test } from '@playwright/test'; +import { generateMemoriesFromTimeline } from 'src/ui/generators/memory'; +import { + Changes, + createDefaultTimelineConfig, + generateTimelineData, + TimelineAssetConfig, + TimelineData, +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/ui/mock-network/memory-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; +import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from './utils'; + +test.describe.configure({ mode: 'parallel' }); + +test.describe('Memory Viewer - Gallery Asset Viewer Navigation', () => { + let adminUserId: string; + let timelineRestData: TimelineData; + let memories: MemoryResponseDto[]; + const assets: TimelineAssetConfig[] = []; + const testContext = new TimelineTestContext(); + const changes: Changes = { + albumAdditions: [], + assetDeletions: [], + assetArchivals: [], + assetFavorites: [], + }; + const memoryChanges: MemoryChanges = { + memoryDeletions: [], + assetRemovals: new Map(), + }; + + test.beforeAll(async () => { + adminUserId = faker.string.uuid(); + testContext.adminId = adminUserId; + + timelineRestData = generateTimelineData({ + ...createDefaultTimelineConfig(), + ownerId: adminUserId, + }); + + for (const timeBucket of timelineRestData.buckets.values()) { + assets.push(...timeBucket); + } + + memories = generateMemoriesFromTimeline( + assets, + adminUserId, + [ + { year: 2024, assetCount: 3 }, + { year: 2023, assetCount: 2 }, + { year: 2022, assetCount: 4 }, + ], + 42, + ); + }); + + test.beforeEach(async ({ context }) => { + await setupBaseMockApiRoutes(context, adminUserId); + await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext); + await setupMemoryMockApiRoutes(context, memories, memoryChanges); + }); + + test.afterEach(() => { + testContext.slowBucket = false; + changes.albumAdditions = []; + changes.assetDeletions = []; + changes.assetArchivals = []; + changes.assetFavorites = []; + memoryChanges.memoryDeletions = []; + memoryChanges.assetRemovals.clear(); + }); + + test.describe('Asset viewer navigation from gallery', () => { + test('shows both prev/next buttons for middle asset within a memory', async ({ page }) => { + const firstMemory = memories[0]; + const middleAsset = firstMemory.assets[1]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, middleAsset.id); + await memoryGalleryUtils.clickThumbnail(page, middleAsset.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, middleAsset); + + await memoryAssetViewerUtils.expectPreviousButtonVisible(page); + await memoryAssetViewerUtils.expectNextButtonVisible(page); + }); + + test('shows next button when at last asset of first memory (next memory exists)', async ({ page }) => { + const firstMemory = memories[0]; + const lastAssetOfFirstMemory = firstMemory.assets.at(-1)!; + + await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirstMemory.id); + await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirstMemory.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirstMemory); + + await memoryAssetViewerUtils.expectNextButtonVisible(page); + await memoryAssetViewerUtils.expectPreviousButtonVisible(page); + }); + + test('shows prev button when at first asset of last memory (prev memory exists)', async ({ page }) => { + const lastMemory = memories.at(-1)!; + const firstAssetOfLastMemory = lastMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfLastMemory.id); + await memoryGalleryUtils.clickThumbnail(page, firstAssetOfLastMemory.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfLastMemory); + + await memoryAssetViewerUtils.expectPreviousButtonVisible(page); + await memoryAssetViewerUtils.expectNextButtonVisible(page); + }); + + test('can navigate from last asset of memory to first asset of next memory', async ({ page }) => { + const firstMemory = memories[0]; + const secondMemory = memories[1]; + const lastAssetOfFirst = firstMemory.assets.at(-1)!; + const firstAssetOfSecond = secondMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id); + await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst); + + await memoryAssetViewerUtils.clickNextButton(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond); + + await memoryAssetViewerUtils.expectCurrentAssetId(page, firstAssetOfSecond.id); + }); + + test('can navigate from first asset of memory to last asset of previous memory', async ({ page }) => { + const firstMemory = memories[0]; + const secondMemory = memories[1]; + const lastAssetOfFirst = firstMemory.assets.at(-1)!; + const firstAssetOfSecond = secondMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id); + await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond); + + await memoryAssetViewerUtils.clickPreviousButton(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst); + }); + + test('hides prev button at very first asset (first memory, first asset, no prev memory)', async ({ page }) => { + const firstMemory = memories[0]; + const veryFirstAsset = firstMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, veryFirstAsset.id); + await memoryGalleryUtils.clickThumbnail(page, veryFirstAsset.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, veryFirstAsset); + + await memoryAssetViewerUtils.expectPreviousButtonNotVisible(page); + await memoryAssetViewerUtils.expectNextButtonVisible(page); + }); + + test('hides next button at very last asset (last memory, last asset, no next memory)', async ({ page }) => { + const lastMemory = memories.at(-1)!; + const veryLastAsset = lastMemory.assets.at(-1)!; + + await memoryViewerUtils.openMemoryPageWithAsset(page, veryLastAsset.id); + await memoryGalleryUtils.clickThumbnail(page, veryLastAsset.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, veryLastAsset); + + await memoryAssetViewerUtils.expectNextButtonNotVisible(page); + await memoryAssetViewerUtils.expectPreviousButtonVisible(page); + }); + }); + + test.describe('Keyboard navigation', () => { + test('ArrowLeft navigates to previous asset across memory boundary', async ({ page }) => { + const firstMemory = memories[0]; + const secondMemory = memories[1]; + const lastAssetOfFirst = firstMemory.assets.at(-1)!; + const firstAssetOfSecond = secondMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, firstAssetOfSecond.id); + await memoryGalleryUtils.clickThumbnail(page, firstAssetOfSecond.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond); + + await page.keyboard.press('ArrowLeft'); + await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst); + }); + + test('ArrowRight navigates to next asset across memory boundary', async ({ page }) => { + const firstMemory = memories[0]; + const secondMemory = memories[1]; + const lastAssetOfFirst = firstMemory.assets.at(-1)!; + const firstAssetOfSecond = secondMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, lastAssetOfFirst.id); + await memoryGalleryUtils.clickThumbnail(page, lastAssetOfFirst.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, lastAssetOfFirst); + + await page.keyboard.press('ArrowRight'); + await memoryAssetViewerUtils.waitForAssetLoad(page, firstAssetOfSecond); + }); + }); +}); + +test.describe('Memory Viewer - Single Asset Memory Edge Cases', () => { + let adminUserId: string; + let timelineRestData: TimelineData; + let memories: MemoryResponseDto[]; + const assets: TimelineAssetConfig[] = []; + const testContext = new TimelineTestContext(); + const changes: Changes = { + albumAdditions: [], + assetDeletions: [], + assetArchivals: [], + assetFavorites: [], + }; + const memoryChanges: MemoryChanges = { + memoryDeletions: [], + assetRemovals: new Map(), + }; + + test.beforeAll(async () => { + adminUserId = faker.string.uuid(); + testContext.adminId = adminUserId; + + timelineRestData = generateTimelineData({ + ...createDefaultTimelineConfig(), + ownerId: adminUserId, + }); + + for (const timeBucket of timelineRestData.buckets.values()) { + assets.push(...timeBucket); + } + + memories = generateMemoriesFromTimeline( + assets, + adminUserId, + [ + { year: 2024, assetCount: 2 }, + { year: 2023, assetCount: 1 }, + { year: 2022, assetCount: 2 }, + ], + 123, + ); + }); + + test.beforeEach(async ({ context }) => { + await setupBaseMockApiRoutes(context, adminUserId); + await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext); + await setupMemoryMockApiRoutes(context, memories, memoryChanges); + }); + + test.afterEach(() => { + testContext.slowBucket = false; + changes.albumAdditions = []; + changes.assetDeletions = []; + changes.assetArchivals = []; + changes.assetFavorites = []; + memoryChanges.memoryDeletions = []; + memoryChanges.assetRemovals.clear(); + }); + + test('single asset memory shows both prev/next when surrounded by other memories', async ({ page }) => { + const singleAssetMemory = memories[1]; + const singleAsset = singleAssetMemory.assets[0]; + + await memoryViewerUtils.openMemoryPageWithAsset(page, singleAsset.id); + await memoryGalleryUtils.clickThumbnail(page, singleAsset.id); + + await memoryAssetViewerUtils.waitForViewerOpen(page); + await memoryAssetViewerUtils.waitForAssetLoad(page, singleAsset); + + await memoryAssetViewerUtils.expectPreviousButtonVisible(page); + await memoryAssetViewerUtils.expectNextButtonVisible(page); + }); +}); diff --git a/e2e/src/ui/specs/memory/utils.ts b/e2e/src/ui/specs/memory/utils.ts new file mode 100644 index 0000000000..cf99033e7e --- /dev/null +++ b/e2e/src/ui/specs/memory/utils.ts @@ -0,0 +1,123 @@ +import type { AssetResponseDto } from '@immich/sdk'; +import { expect, Page } from '@playwright/test'; + +function getAssetIdFromUrl(url: URL): string | null { + const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/); + if (pathMatch) { + return pathMatch[1]; + } + return url.searchParams.get('id'); +} + +export const memoryViewerUtils = { + locator(page: Page) { + return page.locator('#memory-viewer'); + }, + + async waitForMemoryLoad(page: Page) { + await expect(this.locator(page)).toBeVisible(); + await expect(page.locator('#memory-viewer img').first()).toBeVisible(); + }, + + async openMemoryPage(page: Page) { + await page.goto('/memory'); + await this.waitForMemoryLoad(page); + }, + + async openMemoryPageWithAsset(page: Page, assetId: string) { + await page.goto(`/memory?id=${assetId}`); + await this.waitForMemoryLoad(page); + }, +}; + +export const memoryGalleryUtils = { + locator(page: Page) { + return page.locator('#gallery-memory'); + }, + + thumbnailWithAssetId(page: Page, assetId: string) { + return page.locator(`#gallery-memory [data-thumbnail-focus-container][data-asset="${assetId}"]`); + }, + + async scrollToGallery(page: Page) { + const showGalleryButton = page.getByLabel('Show gallery'); + if (await showGalleryButton.isVisible()) { + await showGalleryButton.click(); + } + await expect(this.locator(page)).toBeInViewport(); + }, + + async clickThumbnail(page: Page, assetId: string) { + await this.scrollToGallery(page); + await this.thumbnailWithAssetId(page, assetId).click(); + }, + + async getAllThumbnails(page: Page) { + await this.scrollToGallery(page); + return page.locator('#gallery-memory [data-thumbnail-focus-container]'); + }, +}; + +export const memoryAssetViewerUtils = { + locator(page: Page) { + return page.locator('#immich-asset-viewer'); + }, + + async waitForViewerOpen(page: Page) { + await expect(this.locator(page)).toBeVisible(); + }, + + async waitForAssetLoad(page: Page, asset: AssetResponseDto) { + const viewer = this.locator(page); + const imgLocator = viewer.locator(`img[draggable="false"][src*="/api/assets/${asset.id}/thumbnail?size=preview"]`); + const videoLocator = viewer.locator(`video[poster*="/api/assets/${asset.id}/thumbnail?size=preview"]`); + + await imgLocator.or(videoLocator).waitFor({ timeout: 10_000 }); + }, + + nextButton(page: Page) { + return page.getByLabel('View next asset'); + }, + + previousButton(page: Page) { + return page.getByLabel('View previous asset'); + }, + + async expectNextButtonVisible(page: Page) { + await expect(this.nextButton(page)).toBeVisible(); + }, + + async expectNextButtonNotVisible(page: Page) { + await expect(this.nextButton(page)).toHaveCount(0); + }, + + async expectPreviousButtonVisible(page: Page) { + await expect(this.previousButton(page)).toBeVisible(); + }, + + async expectPreviousButtonNotVisible(page: Page) { + await expect(this.previousButton(page)).toHaveCount(0); + }, + + async clickNextButton(page: Page) { + await this.nextButton(page).click(); + }, + + async clickPreviousButton(page: Page) { + await this.previousButton(page).click(); + }, + + async closeViewer(page: Page) { + await page.keyboard.press('Escape'); + await expect(this.locator(page)).not.toBeVisible(); + }, + + getCurrentAssetId(page: Page): string | null { + const url = new URL(page.url()); + return getAssetIdFromUrl(url); + }, + + async expectCurrentAssetId(page: Page, expectedAssetId: string) { + await expect.poll(() => this.getCurrentAssetId(page)).toBe(expectedAssetId); + }, +}; diff --git a/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts new file mode 100644 index 0000000000..c3721b1c54 --- /dev/null +++ b/e2e/src/ui/specs/search/search-gallery.e2e-spec.ts @@ -0,0 +1,116 @@ +import { faker } from '@faker-js/faker'; +import { expect, test } from '@playwright/test'; +import { + Changes, + createDefaultTimelineConfig, + generateTimelineData, + TimelineAssetConfig, + TimelineData, +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network'; +import { assetViewerUtils } from '../timeline/utils'; + +const buildSearchUrl = (assetId: string) => { + const searchQuery = encodeURIComponent(JSON.stringify({ originalFileName: 'test' })); + return `/search/photos/${assetId}?query=${searchQuery}`; +}; + +test.describe.configure({ mode: 'parallel' }); +test.describe('search gallery-viewer', () => { + let adminUserId: string; + let timelineRestData: TimelineData; + const assets: TimelineAssetConfig[] = []; + const testContext = new TimelineTestContext(); + const changes: Changes = { + albumAdditions: [], + assetDeletions: [], + assetArchivals: [], + assetFavorites: [], + }; + + test.beforeAll(async () => { + adminUserId = faker.string.uuid(); + testContext.adminId = adminUserId; + timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId }); + for (const timeBucket of timelineRestData.buckets.values()) { + assets.push(...timeBucket); + } + }); + + test.beforeEach(async ({ context }) => { + await setupBaseMockApiRoutes(context, adminUserId); + await setupTimelineMockApiRoutes(context, timelineRestData, changes, testContext); + + await context.route('**/api/search/metadata', async (route, request) => { + if (request.method() === 'POST') { + const searchAssets = assets.slice(0, 5).filter((asset) => !changes.assetDeletions.includes(asset.id)); + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: { + albums: { total: 0, count: 0, items: [], facets: [] }, + assets: { + total: searchAssets.length, + count: searchAssets.length, + items: searchAssets, + facets: [], + nextPage: null, + }, + }, + }); + } + await route.fallback(); + }); + }); + + test.afterEach(() => { + testContext.slowBucket = false; + changes.albumAdditions = []; + changes.assetDeletions = []; + changes.assetArchivals = []; + changes.assetFavorites = []; + }); + + test.describe('/search/photos/:id', () => { + test('Deleting a photo advances to the next photo', async ({ page }) => { + const asset = assets[0]; + await page.goto(buildSearchUrl(asset.id)); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[1]); + }); + + test('Deleting two photos in a row advances to the next photo each time', async ({ page }) => { + const asset = assets[0]; + await page.goto(buildSearchUrl(asset.id)); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[1]); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[2]); + }); + + test('Navigating backward then deleting advances to the next photo', async ({ page }) => { + const asset = assets[1]; + await page.goto(buildSearchUrl(asset.id)); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('View previous asset').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[0]); + await page.getByLabel('View next asset').click(); + await assetViewerUtils.waitForViewerLoad(page, asset); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[2]); + }); + + test('Deleting the last photo advances to the previous photo', async ({ page }) => { + const lastAsset = assets[4]; + await page.goto(buildSearchUrl(lastAsset.id)); + await assetViewerUtils.waitForViewerLoad(page, lastAsset); + await expect(page.getByLabel('View next asset')).toHaveCount(0); + await page.getByLabel('Delete').click(); + await assetViewerUtils.waitForViewerLoad(page, assets[3]); + await expect(page.getByLabel('View previous asset')).toBeVisible(); + }); + }); +}); diff --git a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts similarity index 98% rename from e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts rename to e2e/src/ui/specs/timeline/timeline.e2e-spec.ts index 6314688abb..6a7ce82672 100644 --- a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts +++ b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts @@ -12,19 +12,15 @@ import { selectRandomMultiple, TimelineAssetConfig, TimelineData, -} from 'src/generators/timeline'; -import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; -import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; -import { utils } from 'src/utils'; +} from 'src/ui/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network'; import { - assetViewerUtils, - cancelAllPollers, - padYearMonth, - pageUtils, - poll, - thumbnailUtils, - timelineUtils, -} from 'src/web/specs/timeline/utils'; + pageRoutePromise, + setupTimelineMockApiRoutes, + TimelineTestContext, +} from 'src/ui/mock-network/timeline-network'; +import { utils } from 'src/utils'; +import { assetViewerUtils, padYearMonth, pageUtils, poll, thumbnailUtils, timelineUtils } from './utils'; test.describe.configure({ mode: 'parallel' }); test.describe('Timeline', () => { @@ -64,7 +60,6 @@ test.describe('Timeline', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; @@ -443,7 +438,7 @@ test.describe('Timeline', () => { const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); - await thumbnailUtils.expectSelectedReadonly(page, asset.id); + await thumbnailUtils.expectSelectedDisabled(page, asset.id); }); test('Add photos to album', async ({ page }) => { const album = timelineRestData.album; @@ -452,7 +447,7 @@ test.describe('Timeline', () => { const asset = getAsset(timelineRestData, album.assetIds[0])!; await pageUtils.goToAsset(page, asset.fileCreatedAt); await thumbnailUtils.expectInViewport(page, asset.id); - await thumbnailUtils.expectSelectedReadonly(page, asset.id); + await thumbnailUtils.expectSelectedDisabled(page, asset.id); await pageUtils.selectDay(page, 'Tue, Feb 27, 2024'); const put = pageRoutePromise(page, `**/api/albums/${album.id}/assets`, async (route, request) => { const requestJson = request.postDataJSON(); @@ -463,7 +458,7 @@ test.describe('Timeline', () => { }); changes.albumAdditions.push(...requestJson.ids); }); - await page.getByText('Done').click(); + await page.getByText('Add assets').click(); await expect(put).resolves.toEqual({ ids: [ 'c077ea7b-cfa1-45e4-8554-f86c00ee5658', diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts similarity index 88% rename from e2e/src/web/specs/timeline/utils.ts rename to e2e/src/ui/specs/timeline/utils.ts index 0b49f02941..d3e4e5f7ec 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -1,6 +1,6 @@ import { BrowserContext, expect, Page } from '@playwright/test'; import { DateTime } from 'luxon'; -import { TimelineAssetConfig } from 'src/generators/timeline'; +import { TimelineAssetConfig } from 'src/ui/generators/timeline'; export const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -23,13 +23,6 @@ export async function throttlePage(context: BrowserContext, page: Page) { await session.send('Emulation.setCPUThrottlingRate', { rate: 10 }); } -let activePollsAbortController = new AbortController(); - -export const cancelAllPollers = () => { - activePollsAbortController.abort(); - activePollsAbortController = new AbortController(); -}; - export const poll = async ( page: Page, query: () => Promise, @@ -37,21 +30,14 @@ export const poll = async ( ) => { let result; const timeout = Date.now() + 10_000; - const signal = activePollsAbortController.signal; const terminate = callback || ((result: Awaited | undefined) => !!result); while (!terminate(result) && Date.now() < timeout) { - if (signal.aborted) { - return; - } try { result = await query(); } catch { // ignore } - if (signal.aborted) { - return; - } if (page.isClosed()) { return; } @@ -79,7 +65,7 @@ export const thumbnailUtils = { return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`); }, selectedAsset(page: Page) { - return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])'); + return page.locator('[data-thumbnail-focus-container][data-selected]'); }, async clickAssetId(page: Page, assetId: string) { await thumbnailUtils.withAssetId(page, assetId).click(); @@ -116,12 +102,9 @@ export const thumbnailUtils = { async expectThumbnailIsNotArchive(page: Page, assetId: string) { await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0); }, - async expectSelectedReadonly(page: Page, assetId: string) { - // todo - need a data attribute for selected + async expectSelectedDisabled(page: Page, assetId: string) { await expect( - page.locator( - `[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`, - ), + page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected][data-disabled]`), ).toBeVisible(); }, async expectTimelineHasOnScreenAssets(page: Page) { @@ -181,8 +164,12 @@ export const assetViewerUtils = { }, async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) { await page - .locator(`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}"]`) - .or(page.locator(`video[poster="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}"]`)) + .locator( + `img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`, + ) + .or( + page.locator(`video[poster="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`), + ) .waitFor(); }, async expectActiveAssetToBe(page: Page, assetId: string) { diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 15bb112cd8..7307f87854 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -6,7 +6,9 @@ import { CheckExistingAssetsDto, CreateAlbumDto, CreateLibraryDto, + JobCreateDto, MaintenanceAction, + ManualJobName, MetadataSearchDto, Permission, PersonCreateDto, @@ -21,6 +23,7 @@ import { checkExistingAssets, createAlbum, createApiKey, + createJob, createLibrary, createPartner, createPerson, @@ -28,10 +31,12 @@ import { createStack, createUserAdmin, deleteAssets, + deleteDatabaseBackup, getAssetInfo, getConfig, getConfigDefaults, getQueuesLegacy, + listDatabaseBackups, login, runQueueCommandLegacy, scanLibrary, @@ -52,11 +57,15 @@ import { import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'; +import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { dirname, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; import { setTimeout as setAsyncTimeout } from 'node:timers/promises'; import { promisify } from 'node:util'; +import { createGzip } from 'node:zlib'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; import { loginDto, signupDto } from 'src/fixtures'; @@ -84,8 +93,9 @@ export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer $ export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const immichCli = (args: string[]) => executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise; -export const immichAdmin = (args: string[]) => - executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); +export const dockerExec = (args: string[]) => + executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]); +export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]); export const specialCharStrings = ["'", '"', ',', '{', '}', '*']; export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -149,12 +159,26 @@ const onEvent = ({ event, id }: { event: EventType; id: string }) => { }; export const utils = { + connectDatabase: async () => { + if (!client) { + client = new pg.Client(dbUrl); + client.on('end', () => (client = null)); + client.on('error', () => (client = null)); + await client.connect(); + } + + return client; + }, + + disconnectDatabase: async () => { + if (client) { + await client.end(); + } + }, + resetDatabase: async (tables?: string[]) => { try { - if (!client) { - client = new pg.Client(dbUrl); - await client.connect(); - } + client = await utils.connectDatabase(); tables = tables || [ // TODO e2e test for deleting a stack, since it is quite complex @@ -481,6 +505,9 @@ export const utils = { tagAssets: (accessToken: string, tagId: string, assetIds: string[]) => tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }), + createJob: async (accessToken: string, jobCreateDto: JobCreateDto) => + createJob({ jobCreateDto }, { headers: asBearerAuth(accessToken) }), + queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) => runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }), @@ -559,6 +586,45 @@ export const utils = { mkdirSync(`${testAssetDir}/temp`, { recursive: true }); }, + async move(source: string, dest: string) { + return executeCommand('docker', ['exec', 'immich-e2e-server', 'mv', source, dest]).promise; + }, + + createBackup: async (accessToken: string) => { + await utils.createJob(accessToken, { + name: ManualJobName.BackupDatabase, + }); + + return utils.poll( + () => request(app).get('/admin/database-backups').set('Authorization', `Bearer ${accessToken}`), + ({ status, body }) => status === 200 && body.backups.length === 1, + ({ body }) => body.backups[0].filename, + ); + }, + + resetBackups: async (accessToken: string) => { + const { backups } = await listDatabaseBackups({ headers: asBearerAuth(accessToken) }); + + const backupFiles = backups.map((b) => b.filename); + await deleteDatabaseBackup( + { databaseBackupDeleteDto: { backups: backupFiles } }, + { headers: asBearerAuth(accessToken) }, + ); + }, + + prepareTestBackup: async (generate: 'empty' | 'corrupted') => { + const dir = await mkdtemp(join(tmpdir(), 'test-')); + const fn = join(dir, 'file'); + + const sql = Readable.from(generate === 'corrupted' ? 'IM CORRUPTED;' : 'SELECT 1;'); + const gzip = createGzip(); + const writeStream = createWriteStream(fn); + await pipeline(sql, gzip, writeStream); + + await executeCommand('docker', ['cp', fn, `immich-e2e-server:/data/backups/development-${generate}.sql.gz`]) + .promise; + }, + resetAdminConfig: async (accessToken: string) => { const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); @@ -601,6 +667,25 @@ export const utils = { await utils.waitForQueueFinish(accessToken, 'sidecar'); await utils.waitForQueueFinish(accessToken, 'metadataExtraction'); }, + + async poll(cb: () => Promise, validate: (value: T) => boolean, map?: (value: T) => any) { + let timeout = 0; + while (true) { + try { + const data = await cb(); + if (validate(data)) { + return map ? map(data) : data; + } + timeout++; + if (timeout >= 10) { + throw 'Could not clean up test.'; + } + await new Promise((resolve) => setTimeout(resolve, 5e2)); + } catch { + // no-op + } + } + }, }; utils.initSdk(); diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 1c7388b6ec..bfad377089 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -15,7 +15,6 @@ "incremental": true, "skipLibCheck": true, "esModuleInterop": true, - "rootDirs": ["src"], "baseUrl": "./" }, "include": ["src/**/*.ts"], diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 9c80f25ace..1312bf9b75 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -1,16 +1,21 @@ import { defineConfig } from 'vitest/config'; -// skip `docker compose up` if `make e2e` was already run -const globalSetup: string[] = ['src/setup/auth-server.ts']; -try { - await fetch('http://127.0.0.1:2285/api/server-info/ping'); -} catch { - globalSetup.push('src/setup/docker-compose.ts'); +const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true'; + +// skip `docker compose up` if `make e2e` was already run or if VITEST_DISABLE_DOCKER_SETUP is set +const globalSetup: string[] = []; +if (!skipDockerSetup) { + try { + await fetch('http://127.0.0.1:2285/api/server/ping'); + } catch { + globalSetup.push('src/docker-compose.ts'); + } } export default defineConfig({ test: { - include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'], + retry: process.env.CI ? 4 : 0, + include: ['src/specs/server/**/*.e2e-spec.ts'], globalSetup, testTimeout: 15_000, pool: 'threads', diff --git a/e2e/vitest.maintenance.config.ts b/e2e/vitest.maintenance.config.ts new file mode 100644 index 0000000000..6bb6721a6d --- /dev/null +++ b/e2e/vitest.maintenance.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; + +const skipDockerSetup = process.env.VITEST_DISABLE_DOCKER_SETUP === 'true'; + +// skip `docker compose up` if `make e2e` was already run or if VITEST_DISABLE_DOCKER_SETUP is set +const globalSetup: string[] = []; +if (!skipDockerSetup) { + try { + await fetch('http://127.0.0.1:2285/api/server/ping'); + } catch { + globalSetup.push('src/docker-compose.ts'); + } +} + +export default defineConfig({ + test: { + retry: process.env.CI ? 4 : 0, + include: ['src/specs/maintenance/server/**/*.e2e-spec.ts'], + globalSetup, + testTimeout: 15_000, + pool: 'threads', + poolOptions: { + threads: { + singleThread: true, + }, + }, + }, +}); diff --git a/i18n/.prettierrc b/i18n/.prettierrc new file mode 100644 index 0000000000..30581eb7d1 --- /dev/null +++ b/i18n/.prettierrc @@ -0,0 +1,5 @@ +{ + "jsonRecursiveSort": true, + "jsonSortOrder": "{\"/.*/\": \"lexical\"}", + "plugins": ["prettier-plugin-sort-json"] +} diff --git a/i18n/ar.json b/i18n/ar.json index 9ec02a31e3..a1c29402c2 100644 --- a/i18n/ar.json +++ b/i18n/ar.json @@ -5,8 +5,10 @@ "acknowledge": "ØŖŲØ¯ØąŲƒ Ø°Ų„Ųƒ", "action": "ØšŲ…Ų„ŲŠØŠ", "action_common_update": "ØĒØ­Ø¯ŲŠØĢ", + "action_description": "Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† Ø§Ų„ŲØšØ§Ų„ŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØŗØĒŲ†ŲØ° ØšŲ„Ų‰ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØĒ؊ ØĒŲ… ØĒØĩ؁؊ØĒŲ‡Ø§", "actions": "ØšŲ…Ų„ŲŠØ§ØĒ", "active": "Ų†Ø´Øˇ", + "active_count": "ŲØšØ§Ų„: {count}", "activity": "Ų†Ø´Ø§Øˇ", "activity_changed": "Ø§Ų„Ų†Ø´Ø§Øˇ {enabled, select, true {Ų…ŲŲŲ’ØšŲ„} other {Ų…ØšØˇŲ‘Ų„}}", "add": "ØĨØļØ§ŲØŠ", @@ -14,9 +16,14 @@ "add_a_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", "add_a_name": "ØĨØļØ§ŲØŠ ØĨØŗŲ…", "add_a_title": "ØĨØļØ§ŲØŠ ØšŲ†ŲˆØ§Ų†", + "add_action": "اØļ؁ ŲØšØ§Ų„ŲŠØŠ", + "add_action_description": "اØļØēØˇ Ų„ØĨØļØ§ŲØŠ ŲØšØ§Ų„ŲŠØŠ Ų„ØĒŲ†ŲŲŠØ°Ų‡Ø§", + "add_assets": "اØļ؁ اØĩŲˆŲ„", "add_birthday": "ØŖØļ؁ ØĒØ§ØąŲŠØŽ Ø§Ų„Ų…ŲŠŲ„Ø§Ø¯", "add_endpoint": "اØļ؁ Ų†Ų‚ØˇØŠ Ų†Ų‡Ø§ŲŠØŠ", "add_exclusion_pattern": "ØĨØļØ§ŲØŠ Ų†Ų…Øˇ ØĨØŗØĒØĢŲ†Ø§ØĄ", + "add_filter": "اØļ؁ ØĒØĩŲŲŠØŠ", + "add_filter_description": "اØļØēØˇ Ų„Ø§ØļØ§ŲØŠ Ø´ØąØˇ ØĒØĩŲŲŠØŠ", "add_location": "ØĨØļØ§ŲØŠ Ų…ŲˆŲ‚Øš", "add_more_users": "ØĨØļØ§ŲØŠ Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØĸØŽØąŲŠŲ†", "add_partner": "ØŖØļ؁ Ø´ØąŲŠŲƒŲ‹Ø§", @@ -31,10 +38,11 @@ "add_to_album_toggle": "ØĒØ¨Ø¯ŲŠŲ„ Ø§Ų„ØĒØ­Ø¯ŲŠØ¯ Ų„Ų€{album}", "add_to_albums": "ØĨØļØ§ŲØŠ Ø§Ų„Ų‰ Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", "add_to_albums_count": "ØĨØļØ§ŲŲ‡ ØĨŲ„Ų‰ Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ ({count})", - "add_to_bottom_bar": "اØļ؁ Ø§Ų„Ų‰", + "add_to_bottom_bar": "اØļØ§ŲŲ‡ Ø§Ų„Ų‰", "add_to_shared_album": "ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ ØŖŲ„Ø¨ŲˆŲ… Ų…Ø´Ø§ØąŲƒ", "add_upload_to_stack": "اØļ؁ ØąŲØš Ø§Ų„Ų‰ Ø­Ø˛Ų…ØŠ", "add_url": "ØĨØļØ§ŲØŠ ØąØ§Ø¨Øˇ", + "add_workflow_step": "اØļ؁ ØŽØˇŲˆØŠ ØŗŲŠØą ØšŲ…Ų„", "added_to_archive": "ØŖŲØļ؊؁ØĒ Ų„Ų„ØŖØąØ´ŲŠŲ", "added_to_favorites": "ØŖŲØļ؊؁ØĒ ؄؄؅؁ØļŲ„Ø§ØĒ", "added_to_favorites_count": "ØĒŲ… ØĨØļØ§ŲØŠ {count, number} ØĨŲ„Ų‰ Ø§Ų„Ų…ŲØļŲ„Ø§ØĒ", @@ -52,20 +60,20 @@ "backup_keep_last_amount": "Ų…Ų‚Ø¯Ø§Øą Ø§Ų„ØĒŲØąŲŠØēاØĒ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ Ų„Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų‡Ø§", "backup_onboarding_1_description": "Ų†ØŗØŽØŠ ØŽØ§ØąØŦ Ø§Ų„Ų…ŲˆŲ‚Øš ؁؊ Ų…ŲˆŲ‚Øš ØĸØŽØą.", "backup_onboarding_2_description": "Ų†ØŗØŽ Ų…Ø­Ų„ŲŠØŠ ØšŲ„Ų‰ ØŖØŦŲ‡Ø˛ØŠ Ų…ØŽØĒŲ„ŲØŠ. ŲŠØ´Ų…Ų„ Ø°Ų„Ųƒ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØąØĻŲŠØŗŲŠØŠ ŲˆŲ†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ Ų…Ø­Ų„ŲŠØŠ Ų…Ų†Ų‡Ø§.", - "backup_onboarding_3_description": "ØĨØŦŲ…Ø§Ų„ŲŠ Ų†ØŗØŽ Ø¨ŲŠØ§Ų†Ø§ØĒŲƒØŒ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØŖØĩŲ„ŲŠØŠ. ŲŠØ´Ų…Ų„ Ø°Ų„Ųƒ Ų†ØŗØŽØŠŲ‹ ŲˆØ§Ø­Ø¯ØŠŲ‹ ØŽØ§ØąØŦ Ø§Ų„Ų…ŲˆŲ‚Øš ŲˆŲ†ØŗØŽØĒŲŠŲ† Ų…Ø­Ų„ŲŠØĒŲŠŲ†.", + "backup_onboarding_3_description": "ØĨØŦŲ…Ø§Ų„ŲŠ Ų†ŲØŗØŽ Ø¨ŲŠØ§Ų†Ø§ØĒŲƒØŒ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„ØŖØĩŲ„ŲŠØŠ. ŲŠØ´Ų…Ų„ Ø°Ų„Ųƒ Ų†ØŗØŽØŠŲ‹ ŲˆØ§Ø­Ø¯ØŠŲ‹ ØŽØ§ØąØŦ Ø§Ų„Ų…ŲˆŲ‚Øš ŲˆŲ†ØŗØŽØĒŲŠŲ† Ų…Ø­Ų„ŲŠØĒŲŠŲ†.", "backup_onboarding_description": "ŲŠŲŲ†ØĩØ­ باØĒباؚ Ø§ØŗØĒØąØ§ØĒ؊ØŦŲŠØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ 3-2-1 Ų„Ø­Ų…Ø§ŲŠØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ؃. احØĒŲØ¸ Ø¨Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠØŠ Ų…Ų† ØĩŲˆØąŲƒ/ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ؃ Ø§Ų„Ų…Ø­Ų…Ų‘Ų„ØŠØŒ Ø¨Ø§Ų„ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ Ų‚Ø§ØšØ¯ØŠ Ø¨ŲŠØ§Ų†Ø§ØĒ Immich، Ų„ØļŲ…Ø§Ų† Ø­Ų„ Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠ Ø´Ø§Ų…Ų„.", "backup_onboarding_footer": "Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø­ŲˆŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų€ Immich، ŲŠØąØŦŲ‰ Ø§Ų„ØąØŦŲˆØš ØĨŲ„Ų‰ Ø§Ų„ØĒØšŲ„ŲŠŲ…Ø§ØĒ .", "backup_onboarding_parts_title": "؊ØĒØļŲ…Ų† Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ 3-2-1 Ų…Ø§ ŲŠŲ„ŲŠ:", "backup_onboarding_title": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ", "backup_settings": "ØĨؚداداØĒ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", "backup_settings_description": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ ØĒŲØąŲŠØē Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ.", - "cleared_jobs": "ØĒŲ… ØĨØŽŲ„Ø§ØĄ Ų…Ų‡Ø§Ų…: {job}", + "cleared_jobs": "ØĒŲ… ØĨØŽŲ„Ø§ØĄ Ų…Ų‡Ø§Ų… Ų„: {job}", "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_password_reset": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨؚاد؊ ØĒØšŲŠŲŠŲ† ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ų„ {user}؟", "confirm_user_pin_code_reset": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØ§ŲƒØ¯ Ų…Ų† اؚاد؊ ØļØ¨Øˇ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ ب {user}؟", "copy_config_to_clipboard_description": "Ø§Ų†ØŗØŽ اؚداداØĒ Ø§Ų„Ų†Ø¸Ø§Ų… Ø§Ų„Ø­Ø§Ų„ŲŠØŠ بØĒŲ†ØŗŲŠŲ‚ JSON Ø§Ų„Ų‰ Ø§Ų„Ø­Ø§ŲØ¸ØŠ", "create_job": "ØĨŲ†Ø´Ø§ØĄ ŲˆØ¸ŲŠŲØŠ", @@ -96,6 +104,8 @@ "image_preview_description": "ØĩŲˆØąØŠ Ų…ØĒŲˆØŗØˇØŠ Ø§Ų„Ø­ØŦŲ… Ų…Øš Ø¨ŲŠØ§Ų†Ø§ØĒ ؈ØĩŲŲŠØŠ Ų…ØŦØąØ¯ØŠØŒ ØĒŲØŗØĒØŽØ¯Ų… ØšŲ†Ø¯ ØšØąØļ ØŖØĩŲ„ ŲˆØ§Ø­Ø¯ ŲˆŲ„Ų„ØĒØšŲ„Ų… Ø§Ų„ØĸŲ„ŲŠ", "image_preview_quality_description": "ØŦŲˆØ¯ØŠ Ø§Ų„Ų…ØšØ§ŲŠŲ†ØŠ Ų…Ų† 1 ØĨŲ„Ų‰ 100. ŲƒŲ„Ų…Ø§ ŲƒØ§Ų†ØĒ Ø§Ų„Ų‚ŲŠŲ…ØŠ ØŖØšŲ„Ų‰ ŲƒØ§Ų† Ø°Ų„Ųƒ ØŖŲØļŲ„ØŒ ŲˆŲ„ŲƒŲ†Ų‡Ø§ ØĒŲ†ØĒØŦ Ų…Ų„ŲØ§ØĒ ØŖŲƒØ¨Øą ŲˆŲ‚Ø¯ ØĒŲ‚Ų„Ų„ Ų…Ų† Ø§ØŗØĒØŦاب؊ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚. Ų‚Ø¯ ŲŠØ¤ØĢØą ØļØ¨Øˇ Ų‚ŲŠŲ…ØŠ Ų…Ų†ØŽŲØļØŠ ØšŲ„Ų‰ ØŦŲˆØ¯ØŠ Ø§Ų„ØĒØšŲ„Ų… Ø§Ų„ØĸŲ„ŲŠ.", "image_preview_title": "ØĨؚداداØĒ Ø§Ų„Ų…ØšØ§ŲŠŲ†ØŠ", + "image_progressive": "Ų…ØĒØ¯ØąØŦ", + "image_progressive_description": "ØĒØąŲ…ŲŠØ˛ ØĩŲˆØą JPEG ØĒØ¯ØąŲŠØŦŲŠØ§Ų‹ Ų„ØšØąØļŲ‡Ø§ Ø¨Ø´ŲƒŲ„ ØĒØ¯ØąŲŠØŦ؊. Ų‡Ø°Ø§ Ų„Ø§ ŲŠØ¤ØĢØą ØšŲ„Ų‰ ØĩŲˆØą WebP.", "image_quality": "Ø§Ų„ØŦŲˆØ¯ØŠ", "image_resolution": "Ø§Ų„Ø¯Ų‚ØŠ", "image_resolution_description": "ŲŠŲ…ŲƒŲ† Ų„Ų„Ø¯Ų‚ØŠ Ø§Ų„ØšØ§Ų„ŲŠØŠ Ø§Ų„Ø­ŲØ§Ø¸ ØšŲ„Ų‰ Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĒŲØ§ØĩŲŠŲ„ ŲˆŲ„ŲƒŲ†Ų‡Ø§ ØĒØŗØĒØēØąŲ‚ ŲˆŲ‚ØĒŲ‹Ø§ ØŖØˇŲˆŲ„ Ų„Ų„ØĒØąŲ…ŲŠØ˛ØŒ ؈ØĒØ­ØĒ؈؊ ØšŲ„Ų‰ ØŖØ­ØŦØ§Ų… Ų…Ų„ŲØ§ØĒ ØŖŲƒØ¨Øą ŲˆŲŠŲ…ŲƒŲ† ØŖŲ† ØĒŲ‚Ų„Ų„ Ų…Ų† Ø§ØŗØĒØŦاب؊ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚.", @@ -112,6 +122,7 @@ "job_settings_description": "ØĨØ¯Ø§ØąØŠ ØĒØ˛Ø§Ų…Ų† Ø§Ų„ŲˆØ¸Ø§ØĻ؁", "jobs_delayed": "{jobCount, plural, other {# Ų…Ø¤ØŦŲ„ØŠ}}", "jobs_failed": "{jobCount, plural, other {# ŲØ´Ų„ØĒ}}", + "jobs_over_time": "Ø§Ų„ŲˆØ¸Ø§ØĻ؁ Ø¨Ų…ØąŲˆØą Ø§Ų„ŲˆŲ‚ØĒ", "library_created": "ØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ų…ŲƒØĒب؊: {library}", "library_deleted": "ØĒŲ… Ø­Ø°Ų Ø§Ų„Ų…ŲƒØĒب؊", "library_details": "ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„Ų…ŲƒØĒب؊", @@ -179,10 +190,21 @@ "machine_learning_smart_search_enabled": "ØĒŲØšŲŠŲ„ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„Ø°ŲƒŲŠ", "machine_learning_smart_search_enabled_description": "ØĨذا ØĒŲ… ØĒØšØˇŲŠŲ„Ų‡ØŒ ؁؄؆ ؊ØĒŲ… ØĒØąŲ…ŲŠØ˛ Ø§Ų„ØĩŲˆØą Ų„Ų„Ø¨Ø­ØĢ Ø§Ų„Ø°ŲƒŲŠ.", "machine_learning_url_description": "ØšŲ†ŲˆØ§Ų† URL Ų„ØŽØ§Ø¯Ų… Ø§Ų„ØĒØšŲ„Ų… Ø§Ų„ØĸŲ„ŲŠ. ØĨذا ØĒŲ… ØĒŲˆŲŲŠØą ØŖŲƒØĢØą Ų…Ų† ØšŲ†ŲˆØ§Ų† URL ŲˆØ§Ø­Ø¯ØŒ ØŗŲŠØĒŲ… Ų…Ø­Ø§ŲˆŲ„ØŠ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨ŲƒŲ„ ØŽØ§Ø¯Ų… ØšŲ„Ų‰ حد؊ Ø­ØĒŲ‰ ŲŠØŗØĒØŦŲŠØ¨ ØŖØ­Ø¯Ų‡Ų… Ø¨Ų†ØŦاح، Ø¨Ø¯ØĄŲ‹Ø§ Ų…Ų† Ø§Ų„ØŖŲˆŲ„ ØĨŲ„Ų‰ Ø§Ų„ØŖØŽŲŠØą. ØŗŲŠØĒŲ… ØĒØŦØ§Ų‡Ų„ Ø§Ų„ØŽŲˆØ§Ø¯Ų… Ø§Ų„ØĒ؊ Ų„Ø§ ØĒØŗØĒØŦŲŠØ¨ Ų…Ø¤Ų‚ØĒŲ‹Ø§ Ø­ØĒŲ‰ ØĒØšŲˆØ¯ Ų„Ų„ØšŲ…Ų„.", + "maintenance_delete_backup": "Ø­Ø°Ų Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", + "maintenance_delete_backup_description": "Ų‡Ø°Ø§ Ø§Ų„Ų…Ų„Ų ØŗŲŠØĒŲ… Ø­Ø°ŲŲ‡ Ø¨Ø´ŲƒŲ„ Ų„Ø§ ØąØŦØšŲ‡ ŲŲŠŲ‡.", + "maintenance_delete_error": "ŲØ´Ų„ Ø­Ø°Ų Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ.", + "maintenance_restore_backup": "Ø§ØŗØĒؚاد؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", + "maintenance_restore_backup_description": "ØŗŲŠØĒŲ… Ų…ØŗØ­ Ø¨ŲŠØ§Ų†Ø§ØĒ Immich ŲˆØ§ØŗØĒؚادØĒŲ‡Ø§ Ų…Ų† Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ø§Ų„Ų…ØŽØĒØ§Øą. ØŗŲŠØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ų†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ Ų‚Ø¨Ų„ Ø§Ų„Ų…ØĒابؚ؊.", + "maintenance_restore_backup_different_version": "Ų‡Ø°Ø§ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ ØĒŲ… Ø§Ų†Ø´Ø§ØĻŲ‡ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… اØĩØ¯Ø§Øą Ų…ØŽØĒ؄؁ Ų…Ų† Immich!", + "maintenance_restore_backup_unknown_version": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† اØĩØ¯Ø§Øą Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ.", + "maintenance_restore_database_backup": "Ø§ØŗØĒؚاد؊ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "maintenance_restore_database_backup_description": "Ø§ØŗØĒؚاد؊ Ø­Ø§Ų„ØŠ Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… ؅؄؁ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "maintenance_settings": "ØĩŲŠØ§Ų†ØŠ", "maintenance_settings_description": "ØļØš Immich ؁؊ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ.", - "maintenance_start": "Ø§Ø¨Ø¯ØŖ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ", + "maintenance_start": "Ø§Ų„ØĒØ­Ø˛ŲŠŲ„ Ø§Ų„Ų‰ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ", "maintenance_start_error": "ŲØ´Ų„ Ø§Ų„Ø¨Ø¯ØĄ ؁؊ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ.", + "maintenance_upload_backup": "ØąŲØš ؅؄؁ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "maintenance_upload_backup_error": "Ų„Ų… ؊ØĒŲ… ØąŲØš Ø§Ų„ØŽØ˛Ų† Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ, Ų‡Ų„ Ø§Ų„Ų…Ų„Ų بØĩ؊ØēØŠ .sql/.sql.gz?", "manage_concurrency": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĒØ˛Ø§Ų…Ų†", "manage_concurrency_description": "Ø§Ų†ØĒŲ‚Ų„ Ø§Ų„Ų‰ ØĩŲØ­ØŠ Ø§Ų„Ø§ØšŲ…Ø§Ų„ Ų„Ø§Ø¯Ø§ØąØŠ ØĒØ˛Ø§Ų…Ų† Ø§Ų„Ų…Ų‡Ø§Ų…", "manage_log_settings": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„ØŗØŦŲ„Ø§ØĒ", @@ -250,7 +272,7 @@ "oauth_auto_register": "Ø§Ų„ØĒØŗØŦŲŠŲ„ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊", "oauth_auto_register_description": "Ø§Ų„ØĒØŗØŦŲŠŲ„ Ø§Ų„ØĒŲ„Ų‚Ø§ØĻ؊ Ų„Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† Ø§Ų„ØŦدد بؚد ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… OAuth", "oauth_button_text": "Ų†Øĩ Ø§Ų„Ø˛Øą", - "oauth_client_secret_description": "Ų…ØˇŲ„ŲˆØ¨ اذاPKCE(؅؁ØĒاح Ø§Ų„Ø§ØĢباØĒ Ų„ØĒØ¨Ø§Ø¯Ų„ Ø§Ų„ŲƒŲˆØ¯) Ų„Ų… ؊ØĒŲ… ØĒŲˆŲŲŠØąŲ‡ Ų…Ų† Ų…Ø˛ŲˆØ¯ OAuth", + "oauth_client_secret_description": "Ų…ØˇŲ„ŲˆØ¨ Ų„Ų„ØšŲ…ŲŠŲ„ Ø§Ų„ØŗØąŲŠØŒ Ø§Ųˆ اذا PKCE(؅؁ØĒاح Ø§Ų„Ø§ØĢباØĒ Ų„ØĒØ¨Ø§Ø¯Ų„ Ø§Ų„ŲƒŲˆØ¯) Ų„ŲŠØŗ Ų…Ø¯ØšŲˆŲ… Ų…Ų† Ø§Ų„ØšŲ…ŲŠŲ„ Ø§Ų„ØšØ§Ų….", "oauth_enable_description": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… OAuth", "oauth_mobile_redirect_uri": "ØšŲ†ŲˆØ§Ų† URI Ų„ØĨؚاد؊ Ø§Ų„ØĒ؈ØŦŲŠŲ‡ ØšŲ„Ų‰ Ø§Ų„Ų‡Ø§ØĒ؁", "oauth_mobile_redirect_uri_override": "ØĒØŦØ§ŲˆØ˛ ØšŲ†ŲˆØ§Ų† URI Ų„ØĨؚاد؊ Ø§Ų„ØĒ؈ØŦŲŠŲ‡ ØšŲ„Ų‰ Ø§Ų„Ų‡Ø§ØĒ؁", @@ -274,10 +296,14 @@ "password_settings_description": "ØĨØ¯Ø§ØąØŠ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø¨ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "paths_validated_successfully": "ØĒŲ… Ø§Ų„ØĒØ­Ų‚Ų‚ Ų…Ų† Øĩح؊ ŲƒØ§ŲØŠ Ø§Ų„Ų…ØŗØ§ØąØ§ØĒ Ø¨Ų†ØŦاح", "person_cleanup_job": "ØĒŲ†Ø¸ŲŠŲ Ø§Ų„Ø´ØŽØĩ", + "queue_details": "ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„ØˇØ§Ø¨ŲˆØą", + "queues": "ØˇŲˆØ§Ø¨ŲŠØą Ø§Ų„ŲˆØ¸Ø§ØĻ؁", + "queues_page_description": "ØĩŲØ­ØŠ ØˇŲˆØ§Ø¨ŲŠØą ŲˆØ¸Ø§ØĻ؁ Ø§Ų„Ų…Ø¯ŲŠØą", "quota_size_gib": "Ø­ØŦŲ… Ø§Ų„Ø­ØĩØŠ (ØŦ؊ØŦØ§Ø¨Ø§ŲŠØĒ)", "refreshing_all_libraries": "ØĒØ­Ø¯ŲŠØĢ ŲƒØ§ŲØŠ Ø§Ų„Ų…ŲƒØĒباØĒ", "registration": "ØĒØŗØŦŲŠŲ„ Ø§Ų„Ų…Ø¯ŲŠØą", "registration_description": "Ø¨Ų…Ø§ ØŖŲ†Ųƒ ØŖŲˆŲ„ Ų…ØŗØĒØŽØ¯Ų… ؁؊ Ø§Ų„Ų†Ø¸Ø§Ų…ØŒ ØŗŲŠØĒŲ… ØĒØšŲŠŲŠŲ†Ųƒ ŲƒŲ…ØŗØ¤ŲˆŲ„ ŲˆØŗØĒŲƒŲˆŲ† Ų…ØŗØ¤ŲˆŲ„Ų‹Ø§ ØšŲ† Ø§Ų„Ų…Ų‡Ø§Ų… Ø§Ų„ØĨØ¯Ø§ØąŲŠØŠØŒ ŲˆØŗŲŠØĒŲ… ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØĨØļØ§ŲŲŠŲŠŲ† Ø¨ŲˆØ§ØŗØˇØĒ؃.", + "remove_failed_jobs": "Ø§Ø˛Ø§Ų„ØŠ Ø§Ų„ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„ØĒ؊ ŲØ´Ų„ØĒ", "require_password_change_on_login": "Ø§Ų„ØˇŲ„Ø¨ Ų…Ų† Ø§Ų„Ų…ØŗØĒØŽØ¯Ų… ØĒØēŲŠŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą ØšŲ†Ø¯ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ Ø§Ų„ØŖŲˆŲ„", "reset_settings_to_default": "ØĨؚاد؊ ØļØ¨Øˇ Ø§Ų„ØĨؚداداØĒ ØĨŲ„Ų‰ Ø§Ų„ŲˆØļØš Ø§Ų„Ø§ŲØĒØąØ§Øļ؊", "reset_settings_to_recent_saved": "ØĨؚاد؊ ØļØ¨Øˇ Ø§Ų„ØĨؚداداØĒ ØĨŲ„Ų‰ Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…Ø­ŲŲˆØ¸ØŠ Ų…Ø¤ØŽØąŲ‹Ø§", @@ -357,7 +383,7 @@ "transcoding_hardware_acceleration": "Ø§Ų„ØĒØŗØąŲŠØš Ø§Ų„ØšØĒØ§Ø¯ŲŠ", "transcoding_hardware_acceleration_description": "ØĒØŦØąŲŠØ¨ŲŠ: ØĒØąŲ…ŲŠØ˛ Ø§ØŗØąØš Ų„ŲƒŲ† Ų‚Ø¯ ŲŠŲ‚Ų„Ų„ Ų…Ų† Ø§Ų„ØŦŲˆØ¯ØŠ Ų…Øš Ų…ØšØ¯Ų„ بØĒ Ø§Ų‚Ų„", "transcoding_hardware_decoding": "؁؃ ØĒØ´ŲŲŠØą Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ", - "transcoding_hardware_decoding_setting_description": "ŲŠŲ†ØˇØ¨Ų‚ Ø°Ų„Ųƒ ŲŲ‚Øˇ ØšŲ„Ų‰ NVENC، QSV، ؈ RKMPP. ŲŠŲ…ŲƒŲ† Ø§Ų„ØĒØŗØąŲŠØš Ų…Ų† ØˇØąŲ Ų„ØˇØąŲ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† ØĒØŗØąŲŠØš Ø§Ų„ØĒØąŲ…ŲŠØ˛ ŲŲ‚Øˇ. Ų‚Ø¯ Ų„Ø§ ŲŠØšŲ…Ų„ ØšŲ„Ų‰ ØŦŲ…ŲŠØš Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ.", + "transcoding_hardware_decoding_setting_description": "ŲŠŲŲ…ŲƒŲ‘Ų† Ų…Ų† ØĒØŗØąŲŠØš Ų…Ų† Ø§Ų„Ø¨Ø¯Ø§ŲŠØŠ ØĨŲ„Ų‰ Ø§Ų„Ų†Ų‡Ø§ŲŠØŠ Ø¨Ø¯Ų„Ø§Ų‹ Ų…Ų† ØĒØŗØąŲŠØš ØšŲ…Ų„ŲŠØŠ Ø§Ų„ØĒØ´ŲŲŠØą ŲŲ‚Øˇ. Ų‚Ø¯ Ų„Ø§ ŲŠØšŲ…Ų„ Ų…Øš ØŦŲ…ŲŠØš Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ.", "transcoding_max_b_frames": "ØŖŲ‚ØĩŲ‰ ؚدد Ų…Ų† Ø§Ų„ØĨØˇØ§ØąØ§ØĒ B", "transcoding_max_b_frames_description": "Ø§Ų„Ų‚ŲŠŲ… Ø§Ų„ØŖØšŲ„Ų‰ ØĒØšØ˛Ø˛ ŲƒŲØ§ØĄØŠ Ø§Ų„ØļØēØˇØŒ ŲˆŲ„ŲƒŲ†Ų‡Ø§ ØĒØ¨ØˇØĻ ØšŲ…Ų„ŲŠØŠ Ø§Ų„ØĒØąŲ…ŲŠØ˛. Ų‚Ø¯ Ų„Ø§ ØĒŲƒŲˆŲ† Ų…ØĒŲˆØ§ŲŲ‚ØŠ Ų…Øš Ø§Ų„ØĒØŗØąŲŠØš Ø§Ų„ØšØĒØ§Ø¯ŲŠ ØšŲ„Ų‰ Ø§Ų„ØŖØŦŲ‡Ø˛ØŠ Ø§Ų„Ų‚Ø¯ŲŠŲ…ØŠ. Ų‚ŲŠŲ…ØŠ 0 ØĒØšØˇŲ„ ØĨØˇØ§ØąØ§ØĒ B، Ø¨ŲŠŲ†Ų…Ø§ ØĒØļØ¨Øˇ Ø§Ų„Ų‚ŲŠŲ…ØŠ -1 Ų‡Ø°Ø§ Ø§Ų„Ų‚ŲŠŲ…ØŠ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§.", "transcoding_max_bitrate": "Ø§Ų„Ø­Ø¯ Ø§Ų„ØŖŲ‚ØĩŲ‰ Ų„Ų…ØšØ¯Ų„ Ø§Ų„Ø¨ØĒ", @@ -425,6 +451,9 @@ "admin_password": "ŲƒŲ„Ų…ØŠ ØŗØą Ø§Ų„Ų…Ø´ØąŲ", "administration": "Ø§Ų„ØĨØ¯Ø§ØąØŠ", "advanced": "Ų…ØĒŲ‚Ø¯Ų…", + "advanced_settings_clear_image_cache": "Ų…ØŗØ­ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„Ų„ØĩŲˆØą", + "advanced_settings_clear_image_cache_error": "ŲØ´Ų„ Ų…ØŗØ­ Ø°Ø§ŲƒØąØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø§Ų„Ų…Ø¤Ų‚ØĒ Ų„Ų„ØĩŲˆØą", + "advanced_settings_clear_image_cache_success": "ØĒŲ… Ø§Ų„Ų…ØŗØ­ Ø¨Ų†ØŦاح {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Ø§ØŗØĒØŽØ¯Ų… Ų‡Ø°Ø§ Ø§Ų„ØŽŲŠØ§Øą Ų„ØĒØĩŲŲŠØŠ Ø§Ų„ŲˆØŗØ§ØĻØˇ اØĢŲ†Ø§ØĄ Ø§Ų„Ų…Ø˛Ø§Ų…Ų†Ų‡ Ø¨Ų†Ø§ØĄ ØšŲ„Ų‰ Ų…ØšØ§ŲŠŲŠØą Ø¨Ø¯ŲŠŲ„ØŠ. ØŦØąØ¨ Ų‡Ø°Ø§ Ø§Ų„ØŽŲŠØ§Øą ŲŲ‚Øˇ ŲƒØ§Ų† Ų„Ø¯ŲŠŲƒ Ų…Ø´Ø§ŲƒŲ„ Ų…Øš Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø¨Ø§Ų„ŲƒØ´Ų ØšŲ† ØŦŲ…ŲŠØš Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ.", "advanced_settings_enable_alternate_media_filter_title": "[ØĒØŦØąŲŠØ¨ŲŠ] Ø§ØŗØĒØŽØ¯Ų… ØŦŲ‡Ø§Ø˛ ØĒØĩŲŲŠØŠ Ų…Ø˛Ø§Ų…Ų†Ų‡ Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ Ø¨Ø¯ŲŠŲ„", "advanced_settings_log_level_title": "Ų…ØŗØĒŲˆŲ‰ Ø§Ų„ØŗØŦŲ„: {level}", @@ -461,10 +490,12 @@ "album_remove_user": "Ų‡Ų„ ØĒØąØēب ؁؊ ØĨØ˛Ø§Ų„ØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ØŸ", "album_remove_user_confirmation": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØĨØ˛Ø§Ų„ØŠ {user}؟", "album_search_not_found": "Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦاد Ø§Ų„Ø¨ŲˆŲ… Ų…ØˇØ§Ø¨Ų‚ Ų„Ø¨Ø­ØĢ؃", + "album_selected": "ا؎ØĒŲŠØą Ø§Ų„Ø¨ŲˆŲ…", "album_share_no_users": "ŲŠØ¨Ø¯Ųˆ ØŖŲ†Ųƒ Ų‚Ų…ØĒ Ø¨Ų…Ø´Ø§ØąŲƒØŠ Ų‡Ø°Ø§ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ų…Øš ØŦŲ…ŲŠØš Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØŖŲˆ Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ Ų…ØŗØĒØŽØ¯Ų… Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…ØšŲ‡.", "album_summary": "Ų…Ų„ØŽØĩ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "album_updated": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "album_updated_setting_description": "ØĒŲ„Ų‚ŲŠ ØĨØ´ØšØ§ØąŲ‹Ø§ ØšØ¨Øą Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ ØšŲ†Ø¯Ų…Ø§ ŲŠØ­ØĒ؈؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ø§Ų„Ų…Ø´ØĒØąŲƒ ØšŲ„Ų‰ Ų…Ø­ØĒŲˆŲŠØ§ØĒ ØŦØ¯ŲŠØ¯ØŠ", + "album_upload_assets": "ØąŲØš Ø§Ų„Ø§ØĩŲˆŲ„ Ų…Ų† ØŦŲ‡Ø§Ø˛ Ø§Ų„ŲƒŲˆŲ…Ø¨ŲŠŲˆØĒØą Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ ؈ اØļØ§ŲØĒŲ‡Ø§ Ø§Ų„Ų‰ Ø§Ų„Ø¨ŲˆŲ…", "album_user_left": "ØĒŲ… ØĒØąŲƒ {album}", "album_user_removed": "ØĒŲ… ØĨØ˛Ø§Ų„ØŠ {user}", "album_viewer_appbar_delete_confirm": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ ØŖŲ†Ųƒ ØĒØąŲŠØ¯ Ø­Ø°Ų Ų‡Ø°Ø§ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… Ų…Ų† Ø­ØŗØ§Ø¨ŲƒØŸ", @@ -482,9 +513,11 @@ "albums_default_sort_order_description": "ØĒØąØĒŲŠØ¨ ŲØąØ˛ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØŖŲˆŲ„ŲŠ ØšŲ†Ø¯ ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ ØŦØ¯ŲŠØ¯ØŠ.", "albums_feature_description": "Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„ØĒ؊ ŲŠŲ…ŲƒŲ† Ų…Ø´Ø§ØąŲƒØĒŲ‡Ø§ Ų…Øš Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØĸØŽØąŲŠŲ†.", "albums_on_device_count": "ؚدد Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛ ({count})", + "albums_selected": "{count, plural, one {# Ø§Ų„Ø¨ŲˆŲ… Ų…ØŽØĒØ§Øą} other {# Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ Ų…ØŽØĒØ§ØąØŠ}}", "all": "Ø§Ų„ŲƒŲ„", "all_albums": "ØŦŲ…ŲŠØš Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ", "all_people": "ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", + "all_photos": "ØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą", "all_videos": "ØŦŲ…ŲŠØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ", "allow_dark_mode": "Ø§Ų„ØŗŲ…Ø§Ø­ Ø¨Ø§Ų„ŲˆØļØš Ø§Ų„Ų…ØšØĒŲ…", "allow_edits": "ØĨØŗŲ…Ø­ Ø¨Ø§Ų„ØĒØšØ¯ŲŠŲ„", @@ -492,6 +525,9 @@ "allow_public_user_to_upload": "Ø§Ų„ØŗŲ…Ø§Ø­ Ų„Ų„Ų…ØŗØĒØŽØ¯Ų… Ø§Ų„ØšØ§Ų… Ø¨Ø§Ų„ØąŲØš", "allowed": "Ų…ØŗŲ…ŲˆØ­", "alt_text_qr_code": "ØĩŲˆØąØŠ ØąŲ…Ø˛ Ø§Ų„Ø§ØŗØĒØŦاب؊ Ø§Ų„ØŗØąŲŠØšØŠ (QR)", + "always_keep": "داØĻŲ…Ø§ Ø­Ø§ŲØ¸ ØšŲ„Ų‰", + "always_keep_photos_hint": "ØŗŲŠØ­ØĒŲØ¸ ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ بØŦŲ…ŲŠØš Ø§Ų„ØĩŲˆØą ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛.", + "always_keep_videos_hint": "ØŗŲŠØ­ØĒŲØ¸ ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ بØŦŲ…ŲŠØš Ø§Ų„ŲØ¯ŲŠŲˆØ§ØĒ ØšŲ„Ų‰ Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛.", "anti_clockwise": "ØšŲƒØŗ اØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", "api_key": "؅؁ØĒاح API", "api_key_description": "ØŗŲŠØĒŲ… ØšØąØļ Ų‡Ø°Ų‡ Ø§Ų„Ų‚ŲŠŲ…ØŠ Ų…ØąØŠ ŲˆØ§Ø­Ø¯ØŠ ŲŲ‚Øˇ. ŲŠØąØŦŲ‰ Ø§Ų„ØĒØŖŲƒØ¯ Ų…Ų† Ų†ØŗØŽŲ‡Ø§ Ų‚Ø¨Ų„ ØĨØēŲ„Ø§Ų‚ Ø§Ų„Ų†Ø§ŲØ°ØŠ.", @@ -518,10 +554,12 @@ "archived_count": "{count, plural, other {Ø§Ų„ØŖØąØ´ŲŠŲ #}}", "are_these_the_same_person": "Ų‡Ų„ Ų‡Ø¤Ų„Ø§ØĄ Ų‡Ų… Ų†ŲØŗ Ø§Ų„Ø´ØŽØĩ؟", "are_you_sure_to_do_this": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØŖŲ†Ųƒ ØĒØąŲŠØ¯ ØŖŲ† ØĒŲØšŲ„ Ų‡Ø°Ø§ØŸ", + "array_field_not_fully_supported": "Ø­Ų‚ŲˆŲ„ Ø§Ų„Ų…ØĩŲŲˆŲØŠ ØĒØĒØˇŲ„Ø¨ ØĒØšØ¯ŲŠŲ„ ŲŠØ¯ŲˆŲŠ Ų„JSON", "asset_action_delete_err_read_only": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø­Ø°Ų Ø§Ų„ØŖØĩŲˆŲ„ ذاØĒ Ų„Ų„Ų‚ØąØ§ØĄØŠ ŲŲ‚ØˇØŒ ŲˆØŗŲˆŲ ؊ØĒŲ… Ø§Ų„ØĒØŽØˇŲŠ", "asset_action_share_err_offline": "Ų„Ø§ ŲŠŲ…ŲƒŲ† ØŦŲ„Ø¨ Ø§Ų„ØŖØĩŲˆŲ„ ØēŲŠØą Ø§Ų„Ų…ØĒØĩŲ„ØŠ Ø¨Ø§Ų„ØĨŲ†ØĒØąŲ†ØĒ، ŲˆØŗŲˆŲ ؊ØĒŲ… Ø§Ų„ØĒØŽØˇŲŠ", "asset_added_to_album": "ØĒŲ…ØĒ ØĨØļØ§ŲØĒŲ‡ ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "asset_adding_to_album": "ØŦØ§ØąŲ Ø§Ų„ØĨØļØ§ŲØŠ ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…â€Ļ", + "asset_created": "Ø§Ų†Ø´ØĻ اØĩŲ„", "asset_description_updated": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ؈Øĩ؁ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰", "asset_filename_is_offline": "Ø§Ų„ØŖØĩŲ„ {filename} ØēŲŠØą Ų…ØĒØĩŲ„", "asset_has_unassigned_faces": "ŲŠØ­ØĒ؈؊ Ø§Ų„ØŖØĩŲ„ ØšŲ„Ų‰ ؈ØŦŲˆŲ‡ ØēŲŠØą Ų…ØŽØĩØĩØŠ", @@ -534,6 +572,9 @@ "asset_list_layout_sub_title": "ØĒØĩŲ…ŲŠŲ…", "asset_list_settings_subtitle": "ØĨؚداداØĒ ØĒØŽØˇŲŠØˇ Ø´Ø¨ŲƒØŠ Ø§Ų„ØĩŲˆØą", "asset_list_settings_title": "Ø´Ø¨ŲƒØŠ Ø§Ų„ØĩŲˆØą", + "asset_not_found_on_device_android": "Ø§Ų„Ø§ØĩŲ„ Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦØ§Ø¯Ų‡ ؁؊ Ø§Ų„ØŦŲ‡Ø§Ø˛", + "asset_not_found_on_device_ios": "Ø§Ų„ØŖØĩŲ„ Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦØ§Ø¯Ų‡ ؁؊ Ø§Ų„ØŦŲ‡Ø§Ø˛. اذا ØĒØŗØĒØŽØ¯Ų… ØŽØ¯Ų…ØŠ iCloud, ŲØ§Ų„ØŖØĩŲ„ Ų‚Ø¯ Ų„Ø§ ؊ØĒŲ… Ø§Ų„ŲˆØĩŲˆŲ„ Ų„Ų‡ Ø¨ØŗØ¨Ø¨ ؅؄؁ Ų…ØĒØļØ§ØąØ¨ Ų…ØŽØ˛ŲˆŲ† ؁؊ iCloud", + "asset_not_found_on_icloud": "Ø§Ų„ØŖØĩŲ„ Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦØ§Ø¯Ų‡ ؁؊ Ø§Ų„ØŦŲ‡Ø§Ø˛, Ø§Ų„ØŖØĩŲ„ Ų‚Ø¯ Ų„Ø§ ؊ØĒŲ… Ø§Ų„ŲˆØĩŲˆŲ„ Ų„Ų‡ Ø¨ØŗØ¨Ø¨ ؅؄؁ Ų…ØĒØļØ§ØąØ¨ Ų…ØŽØ˛ŲˆŲ† ؁؊ iCloud", "asset_offline": "Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ ØēŲŠØą اØĒØĩØ§Ų„", "asset_offline_description": "Ų„Ų… ŲŠØšØ¯ Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ Ø§Ų„ØŽØ§ØąØŦ؊ Ų…ŲˆØŦŲˆØ¯Ų‹Ø§ ØšŲ„Ų‰ Ø§Ų„Ų‚ØąØĩ. ŲŠØąØŦŲ‰ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø¨Ų…ØŗØ¤ŲˆŲ„ Immich Ų„Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ.", "asset_restored_successfully": "ØĒŲ… Ø§ØŗØĒؚاد؊ Ø§Ų„Ø§ØĩŲ„ Ø¨Ų†ØŦاح", @@ -612,7 +653,7 @@ "backup_controller_page_background_turn_off": "Ų‚Ų… بØĨŲŠŲ‚Ø§Ų ØĒØ´ØēŲŠŲ„ ØŽØ¯Ų…ØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ", "backup_controller_page_background_turn_on": "Ų‚Ų… بØĒØ´ØēŲŠŲ„ ØŽØ¯Ų…ØŠ Ø§Ų„ØŽŲ„ŲŲŠØŠ", "backup_controller_page_background_wifi": "ŲŲ‚Øˇ ØšŲ„Ų‰ Wi-Fi", - "backup_controller_page_backup": "Ø¯ØšŲ…", + "backup_controller_page_backup": "Ų†ØŗØŽ احØĒŲŠØ§ØˇŲŠ", "backup_controller_page_backup_selected": "Ø§Ų„Ų…Ø­Ø¯Ø¯: ", "backup_controller_page_backup_sub": "Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", "backup_controller_page_created": "Ø§Ų†Ø´ØĻ ؁؊ :{date}", @@ -646,6 +687,7 @@ "backup_options_page_title": "ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "backup_setting_subtitle": "Ø§Ø¯Ø§ØąØŠ اؚداداØĒ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ ؁؊ Ø§Ų„ØŽŲ„ŲŲŠØŠ ŲˆØ§Ų„Ų…Ų‚Ø¯Ų…ØŠ", "backup_settings_subtitle": "ØĨØ¯Ø§ØąØŠ ØĨؚداداØĒ Ø§Ų„ØĒØ­Ų…ŲŠŲ„", + "backup_upload_details_page_more_details": "اØļØēØˇ Ų„ØĒŲØ§ØĩŲŠŲ„ اØļØ§ŲŲŠØŠ", "backward": "Ø§Ų„Ų‰ Ø§Ų„ŲˆØąØ§ØĄ", "biometric_auth_enabled": "Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨Ø§ŲŠŲˆŲ…ØĒØąŲŠØŠ Ų…ŲØšŲ„Ų‡", "biometric_locked_out": "Ų„Ų‚Ø¯ ؂؁؄ØĒ ØšŲ†Ųƒ Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨ŲŠŲˆŲ…ØĒØąŲŠØŠ", @@ -704,6 +746,8 @@ "change_password_form_password_mismatch": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą ØēŲŠØą Ų…ØˇØ§Ø¨Ų‚ØŠ", "change_password_form_reenter_new_password": "ØŖØšØ¯ ØĨØ¯ØŽØ§Ų„ ŲƒŲ„Ų…ØŠ Ų…ØąŲˆØą ØŦØ¯ŲŠØ¯ØŠ", "change_pin_code": "ØĒØēŲŠŲŠØą ØąŲ…Ø˛ PIN", + "change_trigger": "ØĒØēŲŠŲŠØą Ø§Ų„Ų…ŲØšŲ„", + "change_trigger_prompt": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØ§ŲƒØ¯ Ø§Ų†Ųƒ ØĒØąŲŠØ¯ ØĒØēŲŠŲŠØą Ø§Ų„Ų…ŲØšŲ„ØŸ Ų‡Ø°Ø§ ØŗŲŠØ˛ŲŠŲ„ ŲƒŲ„ Ø§Ų„Ø§ØŦØąØ§ØĻاØĒ ŲˆØ§Ų„ØĒØĩŲŲŠØ§ØĒ.", "change_your_password": "ØēŲŠØą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "changed_visibility_successfully": "ØĒŲ… ØĒØēŲŠŲŠØą Ø§Ų„ØąØ¤ŲŠØŠ Ø¨Ų†ØŦاح", "charging": "Ø§Ų„Ø´Ø­Ų†", @@ -712,8 +756,21 @@ "check_corrupt_asset_backup_button": "اØŦØąØ§ØĄ ŲØ­Øĩ", "check_corrupt_asset_backup_description": "Ų‚Ų… بØĨØŦØąØ§ØĄ Ų‡Ø°Ø§ Ø§Ų„ŲØ­Øĩ ŲŲ‚Øˇ ØšØ¨Øą Ø´Ø¨ŲƒØŠ Wi-Fi ŲˆØ¨ØšØ¯ Ų†ØŗØŽ ØŦŲ…ŲŠØš Ø§Ų„ØŖØĩŲˆŲ„ احØĒŲŠØ§ØˇŲŠŲ‹Ø§. Ų‚Ø¯ ŲŠØŗØĒØēØąŲ‚ Ø§Ų„ØĨØŦØąØ§ØĄ بØļØš Ø¯Ų‚Ø§ØĻŲ‚.", "check_logs": "ØĒØ­Ų‚Ų‚ Ų…Ų† Ø§Ų„ØŗØŦŲ„Ø§ØĒ", + "checksum": "Ų…ØŦŲ…ŲˆØš Ø§Ų„ØĒØ­Ų‚Ų‚", "choose_matching_people_to_merge": "ا؎ØĒØą Ø§Ų„ØŖØ´ØŽØ§Øĩ Ø§Ų„Ų…ØĒØˇØ§Ø¨Ų‚ŲŠŲ† Ų„Ø¯Ų…ØŦŲ‡Ų…", "city": "Ø§Ų„Ų…Ø¯ŲŠŲ†ØŠ", + "cleanup_confirm_description": "Immich ؈ØŦد {count} اØĩŲˆŲ„ (Ø§Ų†Ø´ØĻØĒ Ų‚Ø¨Ų„ {date}) ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§ Ø§Ų„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų…. Ø§Ø˛Ø§Ų„ØŠ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ Ų…Ų† Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛?", + "cleanup_confirm_prompt_title": "Ø§Ø˛Ø§Ų„ØŠ Ų…Ų† Ų‡Ø°Ø§ Ø§Ų„ØŦŲ‡Ø§Ø˛ØŸ", + "cleanup_deleted_assets": "ØĒŲ… Ų†Ų‚Ų„ {count} اØĩŲˆŲ„ Ø§Ų„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", + "cleanup_deleting": "ØŦØ§ØąŲŠ Ø§Ų„Ų†Ų‚Ų„ Ø§Ų„Ų‰ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ...", + "cleanup_found_assets": "ØĒŲ… Ø§ŲŠØŦاد {count} اØĩŲˆŲ„ ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§", + "cleanup_found_assets_with_size": "ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„ {count} ØšŲ†Ø§ØĩØą ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§ ({size})", + "cleanup_icloud_shared_albums_excluded": "Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ iCloud Ø§Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…ØŗØĒØĢŲ†Ø§ØŠ Ų…Ų† Ø§Ų„Ø¨Ø­ØĢ", + "cleanup_no_assets_found": "Â­Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦاد اØĩŲˆŲ„ ØĒØˇØ§Ø¨Ų‚ Ø§Ų„Ų…ØšØ§ŲŠŲŠØą. Ø¨Ø§Ų„Ø§ØļØ§ŲŲ‡. ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ ŲŠŲ…ŲƒŲ† Ø§Ų† ŲŠØ­Ø°Ų ŲŲ‚Øˇ Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§Ų‹ Ø§Ų„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų…", + "cleanup_preview_title": "اØĩŲˆŲ„ Ų„ŲŠØĒŲ… Ø§Ø˛Ø§Ų„ØĒŲ‡Ø§ ({count})", + "cleanup_step3_description": "ابحØĢ ØšŲ† اØĩŲˆŲ„ ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§ ØĒØˇØ§Ø¨Ų‚ Ø¨ŲŠØ§Ų†Ø§ØĒ؃ ؈ احØĒŲØ¸ Ø¨Ø§Ų„Ø§ØšØ¯Ø§Ø¯Ø§ØĒ.", + "cleanup_step4_summary": "{count} اØĩŲˆŲ„ (ØŖŲ†Ø´ØŖØĒ Ų‚Ø¨Ų„ {date}) Ų„ŲŠØĒŲ… Ø§Ø˛Ø§Ų„ØĒŲ‡Ø§ Ų…Ų† ØŦŲ‡Ø§Ø˛Ųƒ Ø§Ų„Ų…Ø­Ų„ŲŠ. ØŗØĒØ¸Ų„ Ø§Ų„ØĩŲˆØą Ų…ØĒاح؊ Ų…Ų† ØŽŲ„Ø§Ų„ ØĒØˇØ¨ŲŠŲ‚ Immich .", + "cleanup_trash_hint": "Ų„Ø§ØŗØĒؚاد؊ Ų…ØŗØ§Ø­ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† Ø¨Ø§Ų„ŲƒØ§Ų…Ų„ØŒ Ø§ŲØĒØ­ ØĒØˇØ¨ŲŠŲ‚ Ų…ØšØąØļ Ø§Ų„Ų†Ø¸Ø§Ų… ŲˆØŖŲØąØē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "clear": "ØĨØŽŲ„Ø§ØĄ", "clear_all": "ØĨØŽŲ„Ø§ØĄ Ø§Ų„ŲƒŲ„", "clear_all_recent_searches": "Ų…ØŗØ­ ØŦŲ…ŲŠØš ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", @@ -725,6 +782,8 @@ "client_cert_import": "Ø§ØŗØĒŲŠØąØ§Ø¯", "client_cert_import_success_msg": "ØĒŲ… Ø§ØŗØĒŲŠØąØ§Ø¯ Ø´Ų‡Ø§Ø¯ØŠ Ø§Ų„ØšŲ…ŲŠŲ„", "client_cert_invalid_msg": "؅؄؁ Ø´Ų‡Ø§Ø¯ØŠ ØšŲ…ŲŠŲ„ ØēŲŠØą ØĩØ§Ų„Ø­ØŠ Ø§Ųˆ ŲƒŲ„Ų…ØŠ ØŗØą ØēŲŠØą ØĩØ­ŲŠØ­ØŠ", + "client_cert_password_message": "ØŖØ¯ØŽŲ„ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ų‡Ø°Ų‡ Ø§Ų„Ø´Ų‡Ø§Ø¯ØŠ", + "client_cert_password_title": "ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ø§Ų„Ø´Ų‡Ø§Ø¯ØŠ", "client_cert_remove_msg": "ØĒŲ… Ø§Ø˛Ø§Ų„ØŠ Ø´Ų‡Ø§Ø¯ØŠ Ø§Ų„ØšŲ…ŲŠŲ„", "client_cert_subtitle": "ŲŠØ¯ØšŲ… Øĩ؊Øē PKCS12 (.p12, .pfx)ŲŲ‚Øˇ. Ø§ØŗØĒŲŠØąØ§Ø¯/Ø§Ø˛Ø§Ų„ØŠ Ø§Ų„Ø´Ų‡Ø§Ø¯Ø§ØĒ Ų…ØĒاح ŲŲ‚Øˇ Ų‚Ø¨Ų„ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„", "client_cert_title": "Ø´Ų‡Ø§Ø¯ØŠ Ų…ØŗØĒØŽØ¯Ų… SSL [ØĒØŦØąŲŠØ¨ŲŠØŠ]", @@ -779,6 +838,7 @@ "create_album": "ØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ…", "create_album_page_untitled": "Ø¨Ø¯ŲˆŲ† Ø§ØŗŲ…", "create_api_key": "ØĨŲ†Ø´Ø§ØĄ ؅؁ØĒاح API", + "create_first_workflow": "ØĨŲ†Ø´Ø§ØĄ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ø§Ų„ØŖŲˆŲ„", "create_library": "ØĨŲ†Ø´Ø§ØĄ Ų…ŲƒØĒب؊", "create_link": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ", "create_link_to_share": "ØĨŲ†Ø´Ø§ØĄ ØąØ§Ø¨Øˇ Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ", @@ -793,17 +853,25 @@ "create_tag": "ØĨŲ†Ø´Ø§ØĄ ØšŲ„Ø§Ų…ØŠ", "create_tag_description": "ØŖŲ†Ø´ØĻ ØšŲ„Ø§Ų…ØŠ ØŦØ¯ŲŠØ¯ØŠ. Ø¨Ø§Ų„Ų†ØŗØ¨ØŠ Ų„Ų„ØšŲ„Ø§Ų…Ø§ØĒ Ø§Ų„Ų…ØĒØ¯Ø§ØŽŲ„ØŠØŒ ŲŠØąØŦŲ‰ ØĨØ¯ØŽØ§Ų„ Ø§Ų„Ų…ØŗØ§Øą Ø§Ų„ŲƒØ§Ų…Ų„ Ų„Ų„ØšŲ„Ø§Ų…ØŠ Ø¨Ų…Ø§ ؁؊ Ø°Ų„Ųƒ Ø§Ų„ØŽØˇŲˆØˇ Ø§Ų„Ų…Ø§ØĻŲ„ØŠ Ų„Ų„ØŖŲ…Ø§Ų….", "create_user": "ØĨŲ†Ø´Ø§ØĄ Ų…ØŗØĒØŽØ¯Ų…", + "create_workflow": "ØĨŲ†Ø´Ø§ØĄ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "created": "ØĒŲ… Ø§Ų„ØĨŲ†Ø´Ø§ØĄ", "created_at": "Ų…ØŽŲ„ŲˆŲ‚", "creating_linked_albums": "ØŦØ§ØąŲŠ ØĨŲ†Ø´Ø§ØĄ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ų…ØąØĒØ¨ØˇØŠ...", "crop": "Ų‚Øĩ", + "crop_aspect_ratio_fixed": "ØĒŲ… Ø§Ų„Ø§ØĩŲ„Ø§Ø­", + "crop_aspect_ratio_free": "Ø­Øą", + "crop_aspect_ratio_original": "اØĩŲ„ŲŠ", "curated_object_page_title": "ØŖØ´ŲŠØ§ØĄ", "current_device": "Ø§Ų„ØŦŲ‡Ø§Ø˛ Ø§Ų„Ø­Ø§Ų„ŲŠ", "current_pin_code": "ØąŲ…Ø˛ PIN Ø§Ų„Ø­Ø§Ų„ŲŠ", "current_server_address": "ØšŲ†ŲˆØ§Ų† Ø§Ų„ØŽØ§Ø¯Ų… Ø§Ų„Ø­Ø§Ų„ŲŠ", + "custom_date": "ØĒØ§ØąŲŠØŽ Ų…ØŽØĩØĩ", "custom_locale": "Ų„ØēØŠ Ų…ØŽØĩØĩØŠ", "custom_locale_description": "ØĒŲ†ØŗŲŠŲ‚ Ø§Ų„ØĒŲˆØ§ØąŲŠØŽ ŲˆØ§Ų„ØŖØąŲ‚Ø§Ų… Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„Ų„ØēØŠ ŲˆØ§Ų„Ų…Ų†ØˇŲ‚ØŠ", "custom_url": "ØąØ§Ø¨Øˇ Ų…ØŽØĩØĩ", + "cutoff_date_description": "احØĒŲØ¸ Ø¨Ø§Ų„ØĩŲˆØą Ų…Ų† ØĸØŽØąâ€Ļ", + "cutoff_day": "{count, plural, one {ŲŠŲˆŲ…} other {Ø§ŲŠØ§Ų…}}", + "cutoff_year": "{count, plural, one {ØŗŲ†ØŠ} other {ØŗŲ†ŲˆØ§ØĒ}}", "daily_title_text_date": "E ، MMM DD", "daily_title_text_date_year": "E ، MMM DD ، yyyy", "dark": "Ų…ØšØĒŲ…", @@ -859,6 +927,7 @@ "deselect_all": "Ø§Ų„ØēØ§ØĄ ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„", "details": "ØĒŲØ§ØĩŲŠŲ„", "direction": "Ø§Ų„ØĨØĒØŦØ§Ų‡", + "disable": "Ø§Ø¨ØˇØ§Ų„", "disabled": "Ų…ØšØˇŲ„", "disallow_edits": "Ų…Ų†Øš Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ", "discord": "Ø¯ØŗŲƒŲˆØąØ¯", @@ -884,16 +953,18 @@ "download_include_embedded_motion_videos": "Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…Ø¯Ų…ØŦØŠ", "download_include_embedded_motion_videos_description": "ØĒØļŲ…ŲŠŲ† Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØļŲ…Ų†ØŠ ؁؊ Ø§Ų„ØĩŲˆØą Ø§Ų„Ų…ØĒØ­ØąŲƒØŠ ŲƒŲ…Ų„Ų ؅؆؁ØĩŲ„", "download_notfound": "Ų„Ų… ŲŠØšØĢØą ØšŲ„Ų‰ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", - "download_paused": "Ø§ŲˆŲ‚Ų Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", - "download_settings": "Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„Ø§ØĒ", + "download_original": "ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲ„ŲŠ", + "download_paused": "ØĒŲˆŲ‚Ų Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_settings": "Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "download_settings_description": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØĨؚداداØĒ Ø§Ų„Ų…ØĒØšŲ„Ų‚ØŠ بØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ", - "download_started": "بدا Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", + "download_started": "Ø¨Ø¯ØŖ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "download_sucess": "Ų†ØŦØ­ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", "download_sucess_android": "ØĒŲ… ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ Ø§Ų„Ų‰ DCIM/Immich", - "download_waiting_to_retry": "Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ų„Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ", + "download_waiting_to_retry": "Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ų„Ø§ØšØ§Ø¯ØŠ Ø§Ų„Ų…Ø­Ø§ŲˆŲ„ØŠ", "downloading": "ØŦØ§ØąŲ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", - "downloading_asset_filename": "{filename} Ų‚ŲŠØ¯ Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„", - "downloading_media": "ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ", + "downloading_asset_filename": "ØŦØ§ØąŲŠ ØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„Ø§ØĩŲ„ {filename}", + "downloading_from_icloud": "Ø§Ų„ØĒŲ†Ø˛ŲŠŲ„ Ų…Ų† iCloud", + "downloading_media": "ØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„ŲˆØŗØ§ØĻØˇ", "drop_files_to_upload": "Ų‚Ų… بØĨØŗŲ‚Ø§Øˇ Ø§Ų„Ų…Ų„ŲØ§ØĒ ؁؊ ØŖŲŠ Ų…ŲƒØ§Ų† Ų„ØąŲØšŲ‡Ø§", "duplicates": "Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ", "duplicates_description": "Ų‚Ų… Ø¨Ø­Ų„ ŲƒŲ„ Ų…ØŦŲ…ŲˆØšØŠ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„ØĨØ´Ø§ØąØŠ ØĨŲ„Ų‰ Ø§Ų„ØĒŲƒØąØ§ØąØ§ØĒ، ØĨŲ† ؈ØŦدØĒ", @@ -921,11 +992,22 @@ "edit_tag": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØšŲ„Ø§Ų…ØŠ", "edit_title": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„ØšŲ†ŲˆØ§Ų†", "edit_user": "ØĒØšØ¯ŲŠŲ„ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", + "edit_workflow": "ØĒØšØ¯ŲŠŲ„ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "editor": "Ų…Ø­ØąØą", "editor_close_without_save_prompt": "Ų„Ų† ؊ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ", "editor_close_without_save_title": "ØĨØēŲ„Ø§Ų‚ Ø§Ų„Ų…Ø­ØąØąØŸ", - "editor_crop_tool_h2_aspect_ratios": "Ų†ØŗØ¨ Ø§Ų„ØšØąØļ ØĨŲ„Ų‰ Ø§Ų„Ø§ØąØĒŲØ§Øš", - "editor_crop_tool_h2_rotation": "Ø§Ų„ØĒØ¯ŲˆŲŠØą", + "editor_confirm_reset_all_changes": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØĨؚاد؊ ØļØ¨Øˇ ØŦŲ…ŲŠØš Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ؟", + "editor_discard_edits_confirm": "ØĒØŦØ§Ų‡Ų„ Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ", + "editor_discard_edits_prompt": "Ų„Ø¯ŲŠŲƒ ØĒØšØ¯ŲŠŲ„Ø§ØĒ ØēŲŠØą Ų…Ø­ŲŲˆØ¸ØŠ. Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† ØąØēبØĒ؃ ؁؊ ØĒØŦØ§Ų‡Ų„Ų‡Ø§ØŸ", + "editor_discard_edits_title": "ØĒØŦØ§Ų‡Ų„ Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ؟", + "editor_edits_applied_error": "ŲØ´Ų„ ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ", + "editor_edits_applied_success": "ØĒŲ… ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØĒØšØ¯ŲŠŲ„Ø§ØĒ Ø¨Ų†ØŦاح", + "editor_flip_horizontal": "Ø§Ų‚Ų„Ø¨ ØŖŲŲ‚ŲŠŲ‹Ø§", + "editor_flip_vertical": "Ø§Ų‚Ų„Ø¨ ØšŲ…ŲˆØ¯ŲŠŲ‹Ø§", + "editor_orientation": "اØĒØŦØ§Ų‡", + "editor_reset_all_changes": "اؚاد؊ Ø¸Ø¨Øˇ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ", + "editor_rotate_left": "ØŖØ¯Øą 90° ØšŲƒØŗ اØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", + "editor_rotate_right": "Ø§Ø¯Øą 90° باØĒØŦØ§Ų‡ ØšŲ‚Ø§ØąØ¨ Ø§Ų„ØŗØ§ØšØŠ", "email": "Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„ØĨŲ„ŲƒØĒØąŲˆŲ†ŲŠ", "email_notifications": "ØĒŲ†Ø¨ŲŠŲ‡Ø§ØĒ Ø§Ų„Ø¨ØąŲŠØ¯ Ø§Ų„Ø§Ų„ŲƒØĒØąŲˆŲ†ŲŠ", "empty_folder": "Ų‡Ø°Ø§ Ø§Ų„Ų…ØŦŲ„Ø¯ ŲØ§ØąØē", @@ -934,7 +1016,7 @@ "enable": "ØĒŲØšŲŠŲ„", "enable_backup": "ØĒØ´ØēŲŠŲ„ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", "enable_biometric_auth_description": "ØŖØ¯ØŽŲ„ ØąŲ…Ø˛ PIN Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ Ø§Ų„Ø¨ŲŠŲˆŲ…ØĒØąŲŠØŠ", - "enabled": "Ų…ŲØšŲ„", + "enabled": "Ų…ŲØšŲŽŲ„", "end_date": "ØĒØ§ØąŲŠØŽ Ø§Ų„ØĨŲ†ØĒŲ‡Ø§ØĄ", "enqueued": "Ų…ŲØ¯ØąØŦ ؁؊ Ø§Ų„ØˇØ§Ø¨ŲˆØą", "enter_wifi_name": "Ø§Ø¯ØŽŲ„ Ø§ØŗŲ… Wi-Fi", @@ -944,11 +1026,14 @@ "error_change_sort_album": "ŲØ´Ų„ ؁؊ ØĒØēŲŠŲŠØą ØĒØąØĒŲŠØ¨ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "error_delete_face": "حدØĢ ØŽØˇØŖ ؁؊ Ø­Ø°Ų Ø§Ų„ŲˆØŦŲ‡ Ų…Ų† Ø§Ų„ØŖØĩŲˆŲ„", "error_getting_places": "ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ Ø§ØŗØĒØąØŦاؚ Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ų…ŲˆØ§Ų‚Øš", + "error_loading_albums": "ØŽØˇØŖ ؁؊ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", "error_loading_image": "حدØĢ ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ", "error_loading_partners": "ØŽØˇØŖ بØĒØ­Ų…ŲŠŲ„ Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ø´ØąŲƒØ§ØĄ: {error}", + "error_retrieving_asset_information": "ØŽØˇØŖ ؁؊ Ø§ØŗØĒؚاد؊ Ų…ØšŲ„ŲˆŲ…Ø§ØĒ Ø§Ų„Ø§ØĩŲ„", "error_saving_image": "ØŽØˇØŖ: {error}", "error_tag_face_bounding_box": "ØŽØˇØŖ ؁؊ ؈ØļØš ØšŲ„Ø§Ų…ØŠ ØšŲ„Ų‰ Ø§Ų„ŲˆØŦŲ‡ - Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ ØĨحداØĢŲŠØ§ØĒ Ø§Ų„Ų…ØąØ¨Øš Ø§Ų„Ų…Ø­ŲŠØˇ", "error_title": "ØŽØˇØŖ - حدØĢ ØŽŲ„Ų„ŲŒ Ų…Ø§", + "error_while_navigating": "حدØĢ ØŽØˇØŖ ØŖØĢŲ†Ø§ØĄ Ø§Ų„Ø§Ų†ØĒŲ‚Ø§Ų„ ØĨŲ„Ų‰ Ø§Ų„ØŖØĩŲ„", "errors": { "cannot_navigate_next_asset": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø§Ų†ØĒŲ‚Ø§Ų„ ØĨŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠ", "cannot_navigate_previous_asset": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ø§Ų†ØĒŲ‚Ø§Ų„ ØĨŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲ‰ Ø§Ų„ØŗØ§Ø¨Ų‚", @@ -1006,6 +1091,7 @@ "unable_to_complete_oauth_login": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲƒŲ…Ø§Ų„ ØĒØŗØŦŲŠŲ„ Ø§Ų„Ø¯ØŽŲˆŲ„ ØšØ¨Øą OAuth", "unable_to_connect": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ø§Ų„ØĨØĒØĩØ§Ų„", "unable_to_copy_to_clipboard": "Ų„Ø§ ŲŠŲ…ŲƒŲ† Ø§Ų„Ų†ØŗØŽ ØĨŲ„Ų‰ Ø§Ų„Ø­Ø§ŲØ¸ØŠØŒ ØĒØŖŲƒØ¯ Ų…Ų† Ø§ØŗØĒØŽØ¯Ø§Ų…Ųƒ Ų„Ų„ØĩŲØ­ØŠ ØšØ¨Øą https", + "unable_to_create": "ØĒØšØ°Øą ØĨŲ†Ø´Ø§ØĄ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "unable_to_create_admin_account": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲ†Ø´Ø§ØĄ Ø­ØŗØ§Ø¨ Ø§Ų„Ų…ØŗØ¤ŲˆŲ„", "unable_to_create_api_key": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲ†Ø´Ø§ØĄ ؅؁ØĒاح API ØŦØ¯ŲŠØ¯", "unable_to_create_library": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲ†Ø´Ø§ØĄ Ų…ŲƒØĒب؊", @@ -1016,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ø­Ø°Ų Ų†Ų…Øˇ Ø§Ų„Ø§ØŗØĒبؚاد", "unable_to_delete_shared_link": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ø­Ø°Ų Ø§Ų„ØąØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒ", "unable_to_delete_user": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ø­Ø°Ų Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", + "unable_to_delete_workflow": "ØĒØšØ°Øą Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "unable_to_download_files": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„Ų…Ų„ŲØ§ØĒ", "unable_to_edit_exclusion_pattern": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØšØ¯ŲŠŲ„ Ų†Ų…Øˇ Ø§Ų„Ø§ØŗØĒبؚاد", "unable_to_empty_trash": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲØąØ§Øē ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", @@ -1055,6 +1142,7 @@ "unable_to_scan_library": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ŲØ­Øĩ Ø§Ų„Ų…ŲƒØĒب؊", "unable_to_set_feature_photo": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØšŲŠŲŠŲ† Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…Ų…ŲŠØ˛ØŠ", "unable_to_set_profile_picture": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØšŲŠŲŠŲ† ØĩŲˆØąØŠ Ø§Ų„Ų…Ų„Ų Ø§Ų„Ø´ØŽØĩ؊", + "unable_to_set_rating": "ØĒØšØ°Øą ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĒŲ‚ŲŠŲŠŲ…", "unable_to_submit_job": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒŲ‚Ø¯ŲŠŲ… Ø§Ų„ŲˆØ¸ŲŠŲØŠ", "unable_to_trash_asset": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ Ų†Ų‚Ų„ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", "unable_to_unlink_account": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĨŲ„ØēØ§ØĄ ØąØ¨Øˇ Ø§Ų„Ø­ØŗØ§Ø¨", @@ -1066,8 +1154,10 @@ "unable_to_update_settings": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĨؚداداØĒ", "unable_to_update_timeline_display_status": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØ­Ø¯ŲŠØĢ Ø­Ø§Ų„ØŠ ØšØąØļ Ø§Ų„Ų…ØŽØˇØˇ Ø§Ų„Ø˛Ų…Ų†ŲŠ", "unable_to_update_user": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ ØĒØ­Ø¯ŲŠØĢ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…", + "unable_to_update_workflow": "ØĒØšØ°Øą ØĒØ­Ø¯ŲŠØĢ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", "unable_to_upload_file": "ØĒØšØ°Øą ØąŲØš Ø§Ų„Ų…Ų„Ų" }, + "errors_text": "Ø§ØŽØˇØ§ØĄ", "exclusion_pattern": "Ų†Ų…Øˇ Ø§ØŗØĒبؚاد", "exif": "Exif (Øĩ؊ØēØŠ ؅؄؁ ØĩŲˆØąŲŠ Ų‚Ø§Ø¨Ų„ Ų„Ų„ØĒØ¨Ø§Ø¯Ų„)", "exif_bottom_sheet_description": "اØļ؁ ؈ØĩŲØ§...", @@ -1099,6 +1189,7 @@ "external_network_sheet_info": "ØšŲ†Ø¯Ų…Ø§ Ų„Ø§ ؊ØĒŲˆØ§ØŦد ØšŲ„Ų‰ Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„Ų…ŲØļŲ„ØŠØŒ ؁ØĨŲ†Ų‡ ØŗŲŠØĒØĩŲ„ Ø¨Ø§Ų„ØŽØ§Ø¯Ų… Ų…Ų† ØŽŲ„Ø§Ų„ ØŖŲˆŲ„ ØšŲ†Ø§ŲˆŲŠŲ† URL ØŖØ¯Ų†Ø§Ų‡ Ø§Ų„ØĒ؊ ŲŠŲ…ŲƒŲ†Ų‡ Ø§Ų„ŲˆØĩŲˆŲ„ ØĨŲ„ŲŠŲ‡Ø§ØŒ Ø¨Ø¯ØĄŲ‹Ø§ Ų…Ų† Ø§Ų„ØŖØšŲ„Ų‰ ØĨŲ„Ų‰ Ø§Ų„ØŖØŗŲŲ„", "face_unassigned": "ØēŲŠØą Ų…ØšŲŠŲ†", "failed": "ŲØ´Ų„", + "failed_count": "ŲØ´Ų„: {count}", "failed_to_authenticate": "ŲØ´Ų„ ؁؊ Ø§Ų„Ų…ØĩØ§Ø¯Ų‚ØŠ", "failed_to_load_assets": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲˆŲ„", "failed_to_load_folder": "ŲØ´Ų„ ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…ØŦŲ„Ø¯", @@ -1111,14 +1202,17 @@ "features": "Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ", "features_in_development": "Ø§Ų„Ų…ŲŠØ˛Ø§ØĒ Ų‚ŲŠØ¯ Ø§Ų„ØĒØˇŲˆŲŠØą", "features_setting_description": "ØĨØ¯Ø§ØąØŠ Ų…ŲŠØ˛Ø§ØĒ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", - "file_name": "ØĨØŗŲ… Ø§Ų„Ų…Ų„Ų", "file_name_or_extension": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų ØŖŲˆ Ø§Ų…ØĒØ¯Ø§Ø¯Ų‡", + "file_name_text": "ØŖØŗŲ… Ø§Ų„Ų…Ų„Ų", + "file_name_with_value": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų: {file_name}", "file_size": "Ø­ØŦŲ… Ø§Ų„Ų…Ų„Ų", "filename": "Ø§ØŗŲ… Ø§Ų„Ų…Ų„Ų", "filetype": "Ų†ŲˆØš Ø§Ų„Ų…Ų„Ų", "filter": "ØĒØĩŲŲŠØŠ", + "filter_description": "Ø´ØąŲˆØˇ ØĒØĩŲŲŠØŠ Ø§Ų„ØŖØĩŲˆŲ„ Ø§Ų„Ų…ØŗØĒŲ‡Ø¯ŲØŠ", "filter_people": "ØĒØĩŲŲŠØŠ Ø§Ų„Ø§Ø´ØŽØ§Øĩ", "filter_places": "ØĒØĩŲŲŠØŠ Ø§Ų„Ø§Ų…Ø§ŲƒŲ†", + "filters": "Ø§Ų„ØĒØĩŲŲŠØ§ØĒ", "find_them_fast": "ŲŠŲ…ŲƒŲ†Ųƒ Ø§Ų„ØšØĢŲˆØą ØšŲ„ŲŠŲ‡Ø§ Ø¨ØŗØąØšØŠ Ø¨Ø§Ų„Ø§ØŗŲ… Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„Ø¨Ø­ØĢ", "first": "Ø§Ų„Ø§ŲˆŲ„", "fix_incorrect_match": "ØĨØĩŲ„Ø§Ø­ Ø§Ų„Ų…ØˇØ§Ø¨Ų‚ØŠ ØēŲŠØą Ø§Ų„ØĩØ­ŲŠØ­ØŠ", @@ -1128,12 +1222,16 @@ "folders_feature_description": "ØĒØĩŲØ­ ØšØąØļ Ø§Ų„Ų…ØŦŲ„Ø¯ Ų„Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠ ØšŲ„Ų‰ Ų†Ø¸Ø§Ų… Ø§Ų„Ų…Ų„ŲØ§ØĒ", "forgot_pin_code_question": "Ų‡Ų„ Ų†ØŗŲŠØĒ ØąŲ…Ø˛ Ø§Ų„PIN Ø§Ų„ØŽØ§Øĩ Ø¨ŲƒØŸ", "forward": "ØĨŲ„Ų‰ Ø§Ų„ØŖŲ…Ø§Ų…", + "free_up_space": "ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ", + "free_up_space_description": "Ų†Ų‚Ų„ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲØ¯ŲŠŲˆØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØŽØ˛Ų†Ų‡Ø§ احØĒŲŠØ§ØˇŲŠØ§Ø§Ų„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ Ø§Ų„ØŽØ§ØĩŲ‡ بØŦŲ‡Ø§Ø˛Ųƒ Ų„ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ. Ų†ØŗØŽŲƒ ØšŲ„Ų‰ Ø§Ų‰ØŽØ§Ø¯Ų… ØŗØĒØ¨Ų‚Ų‰ Ø¨ØŖŲ…Ø§Ų†.", + "free_up_space_settings_subtitle": "ØĒØ­ØąŲŠØą ØŽØ˛Ų† Ø§Ų„ØŦŲ‡Ø§Ø˛", "full_path": "Ų…ØŗØ§Øą ŲƒØ§Ų…Ų„:{path}", "gcast_enabled": "ŲƒŲˆŲƒŲ„ ŲƒØ§ØŗØĒ", "gcast_enabled_description": "ØĒŲ‚ŲˆŲ… Ų‡Ø°Ų‡ Ø§Ų„Ų…ŲŠØ˛ØŠ بØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ų…ŲˆØ§ØąØ¯ Ø§Ų„ØŽØ§ØąØŦŲŠØŠ Ų…Ų† Google Ø­ØĒŲ‰ ØĒØšŲ…Ų„.", "general": "ØšØ§Ų…", "geolocation_instruction_location": "Ø§Ų†Ų‚Øą ØšŲ„Ų‰ Ø§Ų„Ø§ØĩŲ„ Ø§Ų„Ø°ŲŠ ŲŠØ­ØĒ؈؊ ØšŲ„Ų‰ ØĨحداØĢŲŠØ§ØĒ Ų†Ø¸Ø§Ų… ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„Ų…ŲˆØ§Ų‚Øš Ų„Ø§ØŗØĒØŽØ¯Ø§Ų… Ų…ŲˆŲ‚ØšŲ‡ØŒ ØŖŲˆ ا؎ØĒØą Ø§Ų„Ų…ŲˆŲ‚Øš Ų…Ø¨Ø§Ø´ØąØŠ Ų…Ų† Ø§Ų„ØŽØąŲŠØˇØŠ", "get_help": "Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§Ų„Ų…ØŗØ§ØšØ¯ØŠ", + "get_people_error": "ØŽØˇØŖ Ø§ØŗØĒؚاد؊ Ø§Ų„ØŖØ´ØŽØ§Øĩ", "get_wifiname_error": "ØĒØšØ°Øą Ø§Ų„Ø­ØĩŲˆŲ„ ØšŲ„Ų‰ Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi. ØĒØŖŲƒØ¯ Ų…Ų† Ų…Ų†Ø­ Ø§Ų„ØŖØ°ŲˆŲ†Ø§ØĒ Ø§Ų„Ų„Ø§Ø˛Ų…ØŠ ŲˆØ§ØĒØĩØ§Ų„Ųƒ Ø¨Ø´Ø¨ŲƒØŠ Wi-Fi", "getting_started": "Ø§Ų„Ø¨Ø¯ØĄ", "go_back": "Ø§Ų„ØąØŦŲˆØš Ų„Ų„ØŽŲ„Ų", @@ -1159,12 +1257,14 @@ "header_settings_header_name_input": "Ø§ØŗŲ… Ø§Ų„ØąØŖØŗ", "header_settings_header_value_input": "Ų‚ŲŠŲ…ØŠ Ø§Ų„ØąØŖØŗ", "headers_settings_tile_title": "ØąØ¤ŲˆØŗ ŲˆŲƒŲŠŲ„ Ų…ØŽØĩØĩØŠ", + "height": "Ø§Ų„ØˇŲˆŲ„", "hi_user": "Ų…ØąØ­Ø¨Ø§ {name} ({email})", "hide_all_people": "ØĨØŽŲØ§ØĄ ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", "hide_gallery": "Ø§ØŽŲØ§ØĄ Ø§Ų„Ų…ØšØąØļ", "hide_named_person": "ØĨØŽŲØ§ØĄ Ø§Ų„Ø´ØŽØĩ {name}", "hide_password": "Ø§ØŽŲØ§ØĄ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "hide_person": "Ø§ØŽŲØ§ØĄ Ø§Ų„Ø´ØŽØĩ", + "hide_schema": "Ø§ØŽŲØ§ØĄ Ø§Ų„Ų…ØŽØˇØˇ", "hide_text_recognition": "Ø§ØŽŲØ§ØĄ Ø§Ų„ØĒØšØąŲ ØšŲ„Ų‰ Ø§Ų„Ų†Øĩ", "hide_unnamed_people": "ØĨØŽŲØ§ØĄ Ø§Ų„ØŖØ´ØŽØ§Øĩ Ø¨Ø¯ŲˆŲ† ØĨØŗŲ…", "home_page_add_to_album_conflicts": "ØĒŲ…ØĒ ØĨØļØ§ŲØŠ {added} ØŖØĩŲˆŲ„ ØĨŲ„Ų‰ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ… {album}. {failed} ØŖØĩŲˆŲ„ Ų…ŲˆØŦŲˆØ¯ØŠ Ø¨Ø§Ų„ŲØšŲ„ ؁؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ….", @@ -1237,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Ø§Ų„Ų…ØšØ§Ų„ØŦØŠ ØŦØąØĒ ؁؊ {dateTime}", "items_count": "{count, plural, one {# ØšŲ†ØĩØą} other {# ØšŲ†Ø§ØĩØą}}", "jobs": "Ø§Ų„ŲˆØ¸Ø§ØĻ؁", + "json_editor": "Ų…Ø­ØąØą JSON", + "json_error": "ØŽØˇØŖ JSON", "keep": "احØĒŲØ¸", + "keep_albums": "Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", + "keep_albums_count": "Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ ب{count} {count, plural, one {Ø§Ų„Ø¨ŲˆŲ…} other {Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ}}", "keep_all": "احØĒŲØ¸ Ø¨Ø§Ų„ŲƒŲ„", + "keep_description": "ا؎ØĒØą Ų…Ø§ ŲŠØ¨Ų‚Ų‰ ØšŲ„Ų‰ ØŦŲ‡Ø§Ø˛Ųƒ ØšŲ†Ø¯ ØĒØ­ØąŲŠØą Ø§Ų„Ų…ØŗØ§Ø­ØŠ.", + "keep_favorites": "Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ø§Ų„Ų…ŲØļŲ„Ø§ØĒ", + "keep_on_device": "احØĒŲØ¸ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛", + "keep_on_device_hint": "ا؎ØĒØą Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„ØĒ؊ ØĒØąŲŠØ¯ Ø§Ø¨Ų‚Ø§ØĻŲ‡Ø§ ØšŲ„Ų‰ Ø§Ų„ØŦŲ‡Ø§Ø˛", "keep_this_delete_others": "احØĒŲØ¸ Ø¨Ų‡Ø°Ø§ØŒ ŲˆØ§Ø­Ø°Ų Ø§Ų„ØĸØŽØąŲŠŲ†", + "keeping": "Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ ب: {items}", "kept_this_deleted_others": "ØĒŲ… Ø§Ų„Ø§Ø­ØĒŲØ§Ø¸ Ø¨Ų‡Ø°Ø§ Ø§Ų„ØŖØĩŲ„ ŲˆØ­Ø°Ų {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "ا؎ØĒØĩØ§ØąØ§ØĒ Ų„ŲˆØ­ØŠ Ø§Ų„Ų…ŲØ§ØĒŲŠØ­", "language": "Ø§Ų„Ų„ØēØŠ", @@ -1249,7 +1358,7 @@ "language_setting_description": "ا؎ØĒØą Ų„ØēØĒ؃ Ø§Ų„Ų…ŲØļŲ„ØŠ", "large_files": "Ų…Ų„ŲØ§ØĒ ŲƒØ¨ŲŠØąØŠ", "last": "Ø§Ų„Ø§ØŽŲŠØą", - "last_months": "{count, plural, one {Ø´Ų‡Øą ŲØ§ØĻØĒ} other {Ø§Ø´Ų‡Øą # ŲØ§ØĻØĒØŠ}}", + "last_months": "{count, plural, one {Ø´Ų‡Øą ŲØ§ØĻØĒ} other {ŲØ§ØĻØĒØŠ # Ø§Ø´Ų‡Øą}}", "last_seen": "Ø§ØŽØą Ø¸Ų‡ŲˆØą", "latest_version": "احدØĢ اØĩØ¯Ø§Øą", "latitude": "ØŽØˇ Ø§Ų„ØšØąØļ", @@ -1281,6 +1390,7 @@ "local": "Ų…Ø­Ų„Ų‘ŲŠ", "local_asset_cast_failed": "ØēŲŠØą Ų‚Ø§Ø¯Øą ØšŲ„Ų‰ بØĢ ØŖØĩŲ„ Ų„Ų… ؊ØĒŲ… ØĒØ­Ų…ŲŠŲ„Ų‡ ØĨŲ„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų…", "local_assets": "ØŖŲØĩŲˆŲ„ (Ų…Ų„ŲØ§ØĒ) Ų…Ø­Ų„ŲŠØŠ", + "local_id": "Ø§Ų„Ų‡ŲˆŲŠØŠ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ", "local_media_summary": "Ų…Ų„ØŽØĩ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ", "local_network": "Ø´Ø¨ŲƒØŠ Ų…Ø­Ų„ŲŠØŠ", "local_network_sheet_info": "ØŗŲŠØĒØĩŲ„ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø¨Ø§Ų„ØŽØ§Ø¯Ų… Ų…Ų† ØŽŲ„Ø§Ų„ ØšŲ†ŲˆØ§Ų† URL Ų‡Ø°Ø§ ØšŲ†Ø¯ Ø§ØŗØĒØŽØ¯Ø§Ų… Ø´Ø¨ŲƒØŠ Wi-Fi Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ", @@ -1332,10 +1442,28 @@ "loop_videos_description": "ŲŲŽØšŲ’Ų„ Ų„ØĒŲƒØąØ§Øą Ų…Ų‚ØˇØš ŲŲŠØ¯ŲŠŲˆ ØĒŲ„Ų‚Ø§ØĻŲŠŲ‹Ø§ ؁؊ ØšØ§ØąØļ Ø§Ų„ØĒŲØ§ØĩŲŠŲ„.", "main_branch_warning": "ØŖŲ†ØĒ ØĒØŗØĒØŽØ¯Ų… ØĨØĩØ¯Ø§ØąØ§Ų‹ Ų‚ŲŠØ¯ Ø§Ų„ØĒØˇŲˆŲŠØąØ› ŲˆŲ†Ø­Ų† Ų†ŲˆØĩ؊ بشد؊ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… ØĨØĩØ¯Ø§Øą Ø§Ų„Ų†Ø´Øą!", "main_menu": "Ø§Ų„Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„ØąØĻŲŠØŗŲŠØŠ", + "maintenance_action_restore": "Ø§ØŗØĒؚاد؊ Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", "maintenance_description": "؊ØŦب ؈ØļØš Immich ؁؊ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ.", "maintenance_end": "Ø§Ų†Ų‡Ø§ØĄ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ", "maintenance_end_error": "ŲØ´Ų„ ؁؊ Ø§Ų†Ų‡Ø§ØĄ ؈ØļØš Ø§Ų„ØĩŲŠØ§Ų†ØŠ.", "maintenance_logged_in_as": "Ø­Ø§Ų„ŲŠØ§ Ų…ØŗØŦŲ„ Ø¨Ø§ØŗŲ… {user}", + "maintenance_restore_from_backup": "Ø§ØŗØĒؚاد؊ Ų…Ų† Ø§Ų„ØŽØ˛Ų† Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ", + "maintenance_restore_library": "Ø§ØŗØĒؚاد؊ Ø§Ų„Ų…ŲƒØĒØ¨Ų‡ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", + "maintenance_restore_library_confirm": "ØĨذا بدا Ų‡Ø°Ø§ ØĩØ­ŲŠØ­Ø§ØŒ ؁ØĒابؚ ØšŲ…Ų„ŲŠØŠ Ø§ØŗØĒؚاد؊ Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ!", + "maintenance_restore_library_description": "Ø§ØŗØĒؚاد؊ Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ", + "maintenance_restore_library_folder_has_files": "{folder} ŲŠØ­ØĒ؈؊ {count} Ų…ØŦŲ„Ø¯(اØĒ)", + "maintenance_restore_library_folder_no_files": "{folder} Ų„Ø§ ŲŠØ­ØĒ؈؊ ØšŲ„Ų‰ Ų…Ų„ŲØ§ØĒ!", + "maintenance_restore_library_folder_pass": "Ų‚Ø§Ø¨Ų„ Ų„Ų„Ų‚ØąØ§ØĄØŠ ŲˆØ§Ų„ŲƒØĒاب؊", + "maintenance_restore_library_folder_read_fail": "ØēŲŠØą Ų‚Ø§Ø¨Ų„ Ų„Ų„Ų‚ØąØ§ØĄØŠ", + "maintenance_restore_library_folder_write_fail": "ØēŲŠØą Ų‚Ø§Ø¨Ų„ Ų„Ų„ŲƒØĒاب؊", + "maintenance_restore_library_hint_missing_files": "Ų‚Ø¯ ØĒŲƒŲˆŲ† بؚØļ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ø§Ų„Ų…Ų‡Ų…ØŠ Ų…ŲŲ‚ŲˆØ¯ØŠ", + "maintenance_restore_library_hint_regenerate_later": "ŲŠŲ…ŲƒŲ†Ųƒ ØĨؚاد؊ ØĨŲ†Ø´Ø§ØĄ Ų‡Ø°Ų‡ Ų„Ø§Ø­Ų‚Ų‹Ø§ ؁؊ Ø§Ų„ØĨؚداداØĒ", + "maintenance_restore_library_hint_storage_template_missing_files": "Ų‡Ų„ ØĒØŗØĒØŽØ¯Ų… Ų‚Ø§Ų„Ø¨ ØĒØŽØ˛ŲŠŲ†ØŸ Ų‚Ø¯ ØĒŲƒŲˆŲ† بؚØļ Ø§Ų„Ų…Ų„ŲØ§ØĒ Ų…ŲŲ‚ŲˆØ¯ØŠ", + "maintenance_restore_library_loading": "ØŦØ§ØąŲ ØĒØ­Ų…ŲŠŲ„ ŲØ­ŲˆØĩاØĒ Ø§Ų„ØŗŲ„Ø§Ų…ØŠ ŲˆØ§Ų„ØŖØŗØ§Ų„ŲŠØ¨ Ø§Ų„Ø§ØŗØĒØ¯Ų„Ø§Ų„ŲŠØŠâ€Ļ", + "maintenance_task_backup": "ØŦØ§ØąŲŠ Ø§Ų†Ø´Ø§ØĄ Ų†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ Ų„Ų‚Ø§ØšØ¯ØŠ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ų…ŲˆØŦŲˆØ¯ØŠâ€Ļ", + "maintenance_task_migrations": "ØĒØ´ØēŲŠŲ„ ØšŲ…Ų„ŲŠØ§ØĒ ØĒØąØ­ŲŠŲ„ Ų‚ŲˆØ§ØšØ¯ Ø§Ų„Ø¨ŲŠØ§Ų†Ø§ØĒâ€Ļ", + "maintenance_task_restore": "ØŦØ§ØąŲ Ø§ØŗØĒؚاد؊ Ø§Ų„Ų†ØŗØŽØŠ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠØŠ Ø§Ų„Ų…ØŽØĒØ§ØąØŠâ€Ļ", + "maintenance_task_rollback": "ŲØ´Ų„ØĒ ØšŲ…Ų„ŲŠØŠ Ø§Ų„Ø§ØŗØĒؚاد؊، ØŦØ§ØąŲ Ø§Ų„ØĒØąØ§ØŦØš ØĨŲ„Ų‰ Ų†Ų‚ØˇØŠ Ø§Ų„Ø§ØŗØĒؚاد؊â€Ļ", "maintenance_title": "ØēŲŠØą Ų…ØĒŲˆŲØą Ų…Ø¤Ų‚ØĒا", "make": "ØĩŲ†Øš", "manage_geolocation": "ØĨØ¯Ø§ØąØŠ Ø§Ų„Ų…ŲˆŲ‚Øš", @@ -1397,6 +1525,8 @@ "minimize": "ØĒØĩØēŲŠØą", "minute": "Ø¯Ų‚ŲŠŲ‚ØŠ", "minutes": "Ø¯Ų‚Ø§ØĻŲ‚", + "mirror_horizontal": "Ø§ŲŲ‚ŲŠ", + "mirror_vertical": "ØšŲ…ŲˆØ¯ŲŠ", "missing": "Ø§Ų„Ų…ŲŲ‚ŲˆØ¯ØŠ", "mobile_app": "ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„ØŦŲˆØ§Ų„", "mobile_app_download_onboarding_note": "Ų‚Ų… بØĒŲ†Ø˛ŲŠŲ„ Ø§Ų„ØĒØˇØ¨ŲŠŲ‚ Ø§Ų„Ų…Øĩاحب Ų„Ų„Ų‡Ø§ØĒ؁ Ø§Ų„Ų…Ø­Ų…ŲˆŲ„ Ø¨Ø§ØŗØĒØŽØ¯Ø§Ų… Ø§Ų„ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„ØĒØ§Ų„ŲŠØŠ", @@ -1405,11 +1535,14 @@ "monthly_title_text_date_format": "Øˇ Øˇ Øˇ", "more": "Ø§Ų„Ų…Ø˛ŲŠØ¯", "move": "ØĒØ­ØąŲŠŲƒ", + "move_down": "Ø§Ų†Ø˛Ų„ Ø§Ų„Ų‰ Ø§Ų„Ø§ØŗŲŲ„", "move_off_locked_folder": "ØĒØ­ØąŲŠŲƒ ØŽØ§ØąØŦ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", "move_to": "Ų†Ų‚Ų„ Ø§Ų„Ų‰", + "move_to_device_trash": "Ų†Ų‚Ų„ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ų…Ų‡Ų…Ų„Ø§ØĒ Ø§Ų„ØŦŲ‡Ø§Ø˛", "move_to_lock_folder_action_prompt": "{count} اØļ؊؁ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", "move_to_locked_folder": "Ø§Ų„Ų†Ų‚Ų„ Ø§Ų„Ų‰ Ų…ØŦŲ„Ø¯ Ų…ØēŲ„Ų‚", "move_to_locked_folder_confirmation": "Ų‡Ø°Ų‡ Ø§Ų„ØĩŲˆØą ŲˆØ§Ų„ŲØ¯ŲŠŲˆØ§ØĒ ØŗØĒØĒŲ… Ø§Ø˛Ø§Ų„ØĒŲ‡Ø§ Ų…Ų† ØŦŲ…ŲŠØš Ø§Ų„Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ، ŲˆŲŠŲ…ŲƒŲ†Ø§Ų† ØĒØĒŲ… Ų…Ø´Ø§Ų‡Ø¯ØĒŲ‡Ø§ ŲŲ‚Øˇ Ų…Ų† ØŽŲ„Ø§Ų„ Ø§Ų„Ų…ØŦŲ„Ø¯ Ø§Ų„Ų…Ų‚ŲŲ„", + "move_up": "ØĒØ­ØąŲƒ Ø§Ų„Ų‰ Ø§Ų„Ø§ØšŲ„Ų‰", "moved_to_archive": "ØĒŲ… Ų†Ų‚Ų„ {count, plural, one {# اØĩŲ„} other {# اØĩŲˆŲ„}} Ø§Ų„Ų‰ Ø§Ų„Ø§ØąØ´ŲŠŲ", "moved_to_library": "ØĒŲ… Ų†Ų‚Ų„ {count, plural, one {# اØĩŲ„} other {# اØĩŲˆŲ„}} Ø§Ų„Ų‰ Ø§Ų„Ų…ŲƒØĒب؊", "moved_to_trash": "ØĒŲ… Ø§Ų„Ų†Ų‚Ų„ ØĨŲ„Ų‰ ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ", @@ -1419,6 +1552,7 @@ "my_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ؊", "name": "Ø§Ų„Ø§ØŗŲ…", "name_or_nickname": "Ø§Ų„Ø§ØŗŲ… ØŖŲˆ Ø§Ų„Ų„Ų‚Ø¨", + "name_required": "Ø§Ų„Ø§ØŗŲ… Ų…ØˇŲ„ŲˆØ¨", "navigate": "Ø§Ų„ØĒŲ†Ų‚Ų„", "navigate_to_time": "Ø§Ų†ØĒŲ‚Ų„ ØĨŲ„Ų‰ Ø§Ų„ŲˆŲ‚ØĒ", "network_requirement_photos_upload": "Ø§ØŗØĒØŽØ¯Ø§Ų… Ø¨ŲŠØ§Ų†Ø§ØĒ Ø§Ų„Ų‡Ø§ØĒ؁ Ø§Ų„Ų…Ø­Ų…ŲˆŲ„ Ų„ØšŲ…Ų„ Ų†ØŗØŽØŠ احØĒŲŠØ§ØˇŲŠØŠ Ų„Ų„ØĩŲˆØą", @@ -1443,6 +1577,8 @@ "next": "Ø§Ų„ØĒØ§Ų„ŲŠ", "next_memory": "Ø§Ų„Ø°ŲƒØąŲ‰ Ø§Ų„ØĒØ§Ų„ŲŠØŠ", "no": "Ų„Ø§", + "no_actions_added": "Ų„Ų… ØĒØĒŲ… ØĨØļØ§ŲØŠ ØĨØŦØąØ§ØĄØ§ØĒ Ø­ØĒŲ‰ Ø§Ų„Ø§Ų†", + "no_albums_found": "Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦاد Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", "no_albums_message": "Ų‚Ų… بØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… Ų„ØĒŲ†Ø¸ŲŠŲ… Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "no_albums_with_name_yet": "ŲŠØ¨Ø¯Ųˆ ØŖŲ†Ų‡ Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø¨Ų‡Ø°Ø§ Ø§Ų„Ø§ØŗŲ… Ø­ØĒŲ‰ Ø§Ų„ØĸŲ†.", "no_albums_yet": "ŲŠØ¨Ø¯Ųˆ ØŖŲ†Ų‡ Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø­ØĒŲ‰ Ø§Ų„ØĸŲ†.", @@ -1452,11 +1588,13 @@ "no_cast_devices_found": "Ų„Ų… ؊ØĒŲ… Ø§ŲŠØŦاد ØŦŲ‡Ø§Ø˛ بØĢ", "no_checksum_local": "Ų„Ø§ ØĒ؈ØŦد Ø¨ŲŠØ§Ų†Ø§ØĒ ØĒØ­Ų‚Ų‚ Ų…ØĒاح؊ - ؊ØĒØšØ°Øą ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ø§ØĩŲˆŲ„ Ø§Ų„Ų…Ø­Ų„ŲŠØŠ", "no_checksum_remote": "Ų„Ø§ ؊؈ØŦد ØąŲ…Ø˛ ØĒØ­Ų‚Ų‚ Ų…ØĒاح - ؊ØĒØšØ°Øą ØĒØ­Ų…ŲŠŲ„ Ø§Ų„Ø§ØĩŲ„ Ų…Ų† Ø§Ų„Ų…ŲˆŲ‚Øš Ø§Ų„Ø¨ØšŲŠØ¯", + "no_configuration_needed": "Ų„Ø§ حاØŦØŠ ØĨŲ„Ų‰ ØŖŲŠ ØĨؚداداØĒ", "no_devices": "Ų„Ø§ ؊؈ØŦد اØŦŲ‡Ø˛ØŠ Ų…ØąØŽØĩØŠ", "no_duplicates_found": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ ØŖŲŠ ØĒŲƒØąØ§ØąØ§ØĒ.", "no_exif_info_available": "Ų„Ø§ ØĒØĒŲˆŲØą Ų…ØšŲ„ŲˆŲ…Ø§ØĒ exif", "no_explore_results_message": "Ų‚Ų… Ø¨ØąŲØš Ø§Ų„Ų…Ø˛ŲŠØ¯ Ų…Ų† Ø§Ų„ØĩŲˆØą Ų„Ø§ØŗØĒŲƒØ´Ø§Ų Ų…ØŦŲ…ŲˆØšØĒ؃.", "no_favorites_message": "ØŖØļ؁ Ø§Ų„Ų…ŲØļŲ„ØŠ Ų„Ų„ØšØĢŲˆØą Ø¨ØŗØąØšØŠ ØšŲ„Ų‰ ØŖŲØļŲ„ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", + "no_filters_added": "Ų„Ų… ØĒØĒŲ… ØĨØļØ§ŲØŠ ØŖŲŠ ؁؄ØĒØą بؚد", "no_libraries_message": "ØĨŲ†Ø´Ø§ØĄ Ų…ŲƒØĒب؊ ØŽØ§ØąØŦŲŠØŠ Ų„ØšØąØļ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„ØŽØ§ØĩØŠ Ø¨Ųƒ", "no_local_assets_found": "Ų„Ų… ؊ØĒŲ… Ø§Ų„ØšØĢŲˆØą ØšŲ„Ų‰ ØŖŲŠ اØĩŲˆŲ„ Ų…Ø­Ų„ŲŠØŠ ØĒØĒØˇØ§Ø¨Ų‚ Ų…Øš Ų‚ŲŠŲ…ØŠ Ø§Ų„ØĒØ­Ų‚Ų‚ Ų‡Ø°Ų‡", "no_location_set": "Ų„Ų… ؊ØĒŲ… ØĒØ­Ø¯ŲŠØ¯ Ų…ŲˆŲ‚Øš", @@ -1470,11 +1608,11 @@ "no_results_description": "ØŦØąØ¨ ŲƒŲ„Ų…ØŠ ØąØĻŲŠØŗŲŠØŠ Ų…ØąØ§Ø¯ŲØŠ ØŖŲˆ ØŖŲƒØĢØą ØšŲ…ŲˆŲ…ŲŠØŠ", "no_shared_albums_message": "Ų‚Ų… بØĨŲ†Ø´Ø§ØĄ ØŖŲ„Ø¨ŲˆŲ… Ų„Ų…Ø´Ø§ØąŲƒØŠ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ų…Øš Ø§Ų„ØŖØ´ØŽØ§Øĩ ؁؊ Ø´Ø¨ŲƒØĒ؃", "no_uploads_in_progress": "Ų„Ø§ ؊؈ØŦد Ø§ŲŠ Ų…Ų„ŲØ§ØĒ Ų‚ŲŠØ¯ Ø§Ų„ØąŲØš", + "none": "Ų„Ø§ ؊؈ØŦد", "not_allowed": "ØēŲŠØą Ų…ØŗŲ…ŲˆØ­", "not_available": "ØēŲŠØą Ų…ØĒاح", "not_in_any_album": "Ų„ŲŠØŗØĒ ؁؊ ØŖŲŠ ØŖŲ„Ø¨ŲˆŲ…", "not_selected": "Ų„Ų… ŲŠØŽØĒØ§Øą", - "note_apply_storage_label_to_previously_uploaded assets": "Ų…Ų„Ø§Ø­Ø¸ØŠ: Ų„ØĒØˇØ¨ŲŠŲ‚ ØŗŲ…ØŠ Ø§Ų„ØĒØŽØ˛ŲŠŲ† ØšŲ„Ų‰ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§ Ų…ØŗØ¨Ų‚Ų‹Ø§ØŒ Ų‚Ų… بØĒØ´ØēŲŠŲ„", "notes": "Ų…Ų„Ø§Ø­Ø¸Ø§ØĒ", "nothing_here_yet": "Ų„Ø§ ؊؈ØŦد Ø´ŲŠØĄ Ų‡Ų†Ø§ بؚد", "notification_permission_dialog_content": "Ų„ØĒŲ…ŲƒŲŠŲ† Ø§Ų„ØĨØŽØˇØ§ØąØ§ØĒ ، Ø§Ų†ØĒŲ‚Ų„ ØĨŲ„Ų‰ Ø§Ų„ØĨؚداداØĒ ؈ ا؎ØĒØ§Øą Ø§Ų„ØŗŲ…Ø§Ø­.", @@ -1552,6 +1690,7 @@ "people": "Ø§Ų„ØŖØ´ØŽØ§Øĩ", "people_edits_count": "ØĒŲ… ØĒØšØ¯ŲŠŲ„ {count, plural, one {# Ø´ØŽØĩ } other {# ØŖØ´ØŽØ§Øĩ }}", "people_feature_description": "ØĒØĩŲØ­ Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ų…ØŦŲ…ØšØŠ Ø­ØŗØ¨ Ø§Ų„ØŖØ´ØŽØ§Øĩ", + "people_selected": "{count, plural, one {# Ø´ØŽØĩ Ų…ØŽØĒØ§Øą} other {# اش؎اØĩ Ų…ØŽØĒØ§ØąŲŠŲ†}}", "people_sidebar_description": "ØšØąØļ ØąØ§Ø¨Øˇ Ų„Ų„ØŖØ´ØŽØ§Øĩ ؁؊ Ø§Ų„Ø´ØąŲŠØˇ Ø§Ų„ØŦØ§Ų†Ø¨ŲŠ", "permanent_deletion_warning": "ØĒØ­Ø°ŲŠØą Ø§Ų„Ø­Ø°Ų Ø§Ų„Ø¯Ø§ØĻŲ…", "permanent_deletion_warning_setting_description": "ØĨØ¸Ų‡Ø§Øą ØĒØ­Ø°ŲŠØą ØšŲ†Ø¯ Ø­Ø°Ų Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ų†Ų‡Ø§ØĻŲŠŲ‹Ø§", @@ -1576,11 +1715,14 @@ "person_age_years": "{years, plural, other {# Ø§ØšŲˆØ§Ų…}} Ų…Ų† Ø§Ų„ØšŲ…Øą", "person_birthdate": "ŲˆŲ„Ø¯ ؁؊ {date}", "person_hidden": "{name}{hidden, select, true { (Ų…ØŽŲŲŠ)} other {}}", + "person_recognized": "Ø´ØŽØĩ ØĒŲ… Ø§Ų„ØĒØšØąŲ ØšŲ„ŲŠŲ‡", + "person_selected": "Ø´ØŽØĩ Ų…ØŽØĒØ§Øą", "photo_shared_all_users": "ŲŠØ¨Ø¯Ųˆ ØŖŲ†Ųƒ Ø´Ø§ØąŲƒØĒ ØĩŲˆØąŲƒ Ų…Øš ØŦŲ…ŲŠØš Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ† ØŖŲˆ Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ Ų…ØŗØĒØŽØ¯Ų… Ų„Ų„Ų…Ø´Ø§ØąŲƒØŠ Ų…ØšŲ‡.", "photos": "Ø§Ų„ØĩŲˆØą", "photos_and_videos": "Ø§Ų„ØĩŲˆØą ŲˆŲ…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ", "photos_count": "{count, plural, one {{count, number} ØĩŲˆØąØŠ} other {{count, number} ØĩŲˆØą}}", "photos_from_previous_years": "ØĩŲˆØą Ų…Ų† Ø§Ų„ØŗŲ†ŲˆØ§ØĒ Ø§Ų„ØŗØ§Ø¨Ų‚ØŠ", + "photos_only": "ØĩŲˆØą ŲŲ‚Øˇ", "pick_a_location": "ا؎ØĒØą Ų…ŲˆŲ‚ØšŲ‹Ø§", "pick_custom_range": "Ų†ØˇØ§Ų‚ Ų…ØŽØĩØĩ", "pick_date_range": "حدد Ų†ØˇØ§Ų‚ Ø§Ų„ØĒØ§ØąŲŠØŽ", @@ -1656,10 +1798,12 @@ "purchase_settings_server_activated": "؊ØĒŲ… ØĨØ¯Ø§ØąØŠ ؅؁ØĒاح Ų…Ų†ØĒØŦ Ø§Ų„ØŽØ§Ø¯Ų… Ų…Ų† Ų‚Ø¨Ų„ Ų…Ø¯ŲŠØą Ø§Ų„Ų†Ø¸Ø§Ų…", "query_asset_id": "Ø§ØŗØĒØšŲ„Ø§Ų… ØšŲ† Ų…ØšØąŲ Ø§Ų„ØŖØĩŲ„", "queue_status": "؊ØĒŲ… Ø§Ų„Ø§ØļØ§ŲØŠ Ø§Ų„Ų‰ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų†ØĒØ¸Ø§Øą Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ {count}/{total}", + "rate_asset": "ØĒŲ‚ŲŠŲŠŲ… Ø§Ų„Ø§ØĩŲ„", "rating": "ØĒŲ‚ŲŠŲŠŲ… Ų†ØŦŲ…ŲŠ", "rating_clear": "Ų…ØŗØ­ Ø§Ų„ØĒŲ‚ŲŠŲŠŲ…", "rating_count": "{count, plural, one {# Ų†ØŦŲ…ØŠ} other {# Ų†ØŦŲˆŲ…}}", "rating_description": "â€Ģâ€ŒØ§ØšØąØļ ØĒŲ‚ŲŠŲŠŲ… EXIF ؁؊ Ų„ŲˆØ­ØŠ Ø§Ų„Ų…ØšŲ„ŲˆŲ…Ø§ØĒ", + "rating_set": "ØĒŲ… ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĒØĩŲ†ŲŠŲ {rating, plural, one {# Ų†ØŦŲ…ØŠ} other {# Ų†ØŦŲˆŲ…}}", "reaction_options": "ØŽŲŠØ§ØąØ§ØĒ ØąØ¯ Ø§Ų„ŲØšŲ„", "read_changelog": "Ų‚ØąØ§ØĄØŠ ØŗØŦŲ„ Ø§Ų„ØĒØēŲŠŲŠØą", "readonly_mode_disabled": "ØĒŲ… ØĒØšØˇŲŠŲ„ ؈ØļØš Ø§Ų„Ų‚ØąØ§ØĄØŠ ŲŲ‚Øˇ", @@ -1670,7 +1814,7 @@ "reassigned_assets_to_new_person": "ØĒŲ…ØĒ ØĨؚاد؊ ØĒØšŲŠŲŠŲ† {count, plural, one {# Ø§Ų„Ų…Ø­ØĒŲˆŲ‰} other {# Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ}} ØĨŲ„Ų‰ Ø´ØŽØĩ ØŦØ¯ŲŠØ¯", "reassing_hint": "ØĒØšŲŠŲŠŲ† Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ Ų„Ø´ØŽØĩ Ų…ŲˆØŦŲˆØ¯", "recent": "Ø­Ø¯ŲŠØĢ", - "recent-albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø­Ø¯ŲŠØĢØŠ", + "recent_albums": "ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø§Ų„Ø­Ø¯ŲŠØĢØŠ", "recent_searches": "ØšŲ…Ų„ŲŠØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ Ø§Ų„ØŖØŽŲŠØąØŠ", "recently_added": "اØļ؊؁ Ų…Ø¤ØŽØąØ§", "recently_added_page_title": "ØŖØļ؊؁ Ų…Ø¤ØŽØąØ§", @@ -1759,9 +1903,11 @@ "saved_settings": "ØĒŲ… Ø­ŲØ¸ Ø§Ų„ØĨؚداداØĒ", "say_something": "Ų‚Ų„ Ø´ŲŠØĻŲ‹Ø§", "scaffold_body_error_occurred": "حدØĢ ØŽØˇØŖ", + "scan": "بحØĢ", "scan_all_libraries": "ŲØ­Øĩ ŲƒŲ„ Ø§Ų„Ų…ŲƒØĒباØĒ", "scan_library": "Ų…ØŗØ­", "scan_settings": "ØĨؚداداØĒ Ø§Ų„ŲØ­Øĩ", + "scanning": "ØŦØ§ØąŲŠ Ø§Ų„Ø¨Ø­ØĢ", "scanning_for_album": "ØŦØ§ØąŲ Ø§Ų„ŲØ­Øĩ ØšŲ† ØŖŲ„Ø¨ŲˆŲ…...", "search": "Ø§Ų„Ø¨Ø­ØĢ", "search_albums": "Ø§Ų„Ø¨Ø­ØĢ ؁؊ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ", @@ -1791,6 +1937,7 @@ "search_filter_media_type_title": "ا؎ØĒØą Ų†ŲˆØš Ø§Ų„ŲˆØŗØ§ØĻØˇ", "search_filter_ocr": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† ØˇØąŲŠŲ‚ Ø§Ų„ØĒØšØąŲ Ø§Ų„Ø¨ØĩØąŲŠ ØšŲ„Ų‰ Ø§Ų„Ø­ØąŲˆŲ", "search_filter_people_title": "ا؎ØĒØą Ø§Ų„Ø§Ø´ØŽØ§Øĩ", + "search_filter_star_rating": "ØĒŲ‚ŲŠŲŠŲ… Ø§Ų„Ų†ØŦŲˆŲ…", "search_for": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ†", "search_for_existing_person": "Ø§Ų„Ø¨Ø­ØĢ ØšŲ† Ø´ØŽØĩ Ų…ŲˆØŦŲˆØ¯", "search_no_more_result": "Ų„Ø§ ØĒ؈ØŦد Ų†ØĒاØĻØŦ اØļØ§ŲŲŠØŠ", @@ -1825,17 +1972,23 @@ "second": "ØĢØ§Ų†ŲŠØŠ", "see_all_people": "ØšØąØļ ØŦŲ…ŲŠØš Ø§Ų„ØŖØ´ØŽØ§Øĩ", "select": "ØĨØŽØĒØą", + "select_album": "ا؎ØĒØą Ø§Ų„Ø¨ŲˆŲ…", "select_album_cover": "ØĒØ­Ø¯ŲŠØ¯ ØēŲ„Ø§Ų Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", + "select_albums": "ا؎ØĒØą Ø§Ų„Ø¨ŲˆŲ…Ø§ØĒ", "select_all": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ŲƒŲ„", "select_all_duplicates": "ØĒØ­Ø¯ŲŠØ¯ ØŦŲ…ŲŠØš Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ų…ŲƒØąØąØŠ", "select_all_in": "ا؎ØĒØą Ø§Ų„ŲƒŲ„ ؁؊ {group}", "select_avatar_color": "ØĒØ­Ø¯ŲŠØ¯ Ų„ŲˆŲ† Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ø´ØŽØĩŲŠØŠ", + "select_count": "{count, plural, one {ا؎ØĒØą #} other {ا؎ØĒØą #}}", + "select_cutoff_date": "حدد ØĒØ§ØąŲŠØŽ Ø§Ų„Ų‚ØˇØš", "select_face": "ØĒØ­Ø¯ŲŠØ¯ ؈ØŦŲ‡", "select_featured_photo": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…Ų…ŲŠØ˛ØŠ", "select_from_computer": "ØĒØ­Ø¯ŲŠØ¯ Ų…Ų† Ø§Ų„Ø­Ø§ØŗØ¨ Ø§Ų„ØĸŲ„ŲŠ", "select_keep_all": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØŖØ­ØĒŲØ§Ø¸ Ø¨Ø§Ų„ŲƒŲ„", "select_library_owner": "ØĒØ­Ø¯ŲŠØ¯ Ų…Ø§Ų„ŲŲƒ Ø§Ų„Ų…ŲƒØĒب؊", "select_new_face": "ØĒØ­Ø¯ŲŠØ¯ ؈ØŦŲ‡ ØŦØ¯ŲŠØ¯", + "select_people": "ا؎ØĒØą Ø§Ų„Ø§Ø´ØŽØ§Øĩ", + "select_person": "ا؎ØĒØą Ø´ØŽØĩ", "select_person_to_tag": "ا؎ØĒØą Ø´ØŽØĩ Ų„ŲˆØļØš ØšŲ„Ø§Ų…ØŠ", "select_photos": "ØĒØ­Ø¯ŲŠØ¯ Ø§Ų„ØĩŲˆØą", "select_trash_all": "ØĒØ­Ø¯ŲŠØ¯ Ø­Ø°Ų Ø§Ų„ŲƒŲ„Ų", @@ -1971,6 +2124,7 @@ "show_password": "ØĨØ¸Ų‡Ø§Øą ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "show_person_options": "ØĨØ¸Ų‡Ø§Øą ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ø´ØŽØĩ", "show_progress_bar": "ØĨØ¸Ų‡Ø§Øą Ø´ØąŲŠØˇ Ø§Ų„ØĒŲ‚Ø¯Ų…", + "show_schema": "ØŖØ¸Ų‡Øą Ø§Ų„Ų…ØŽØˇØˇ", "show_search_options": "ØĨØ¸Ų‡Ø§Øą ØŽŲŠØ§ØąØ§ØĒ Ø§Ų„Ø¨Ø­ØĢ", "show_shared_links": "ØšØąØļ Ø§Ų„ØąŲˆØ§Ø¨Øˇ Ø§Ų„Ų…Ø´ØĒØąŲƒØŠ", "show_slideshow_transition": "ØĨØ¸Ų‡Ø§Øą Ø§Ų†ØĒŲ‚Ø§Ų„ ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­", @@ -1988,6 +2142,8 @@ "skip_to_folders": "ØĒØŽØˇŲŠ ØĨŲ„Ų‰ Ø§Ų„Ų…ØŦŲ„Ø¯Ø§ØĒ", "skip_to_tags": "ØĒØŽØˇŲŠ ØĨŲ„Ų‰ Ø§Ų„ØšŲ„Ø§Ų…Ø§ØĒ", "slideshow": "ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­", + "slideshow_repeat": "اؚاد؊ ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­", + "slideshow_repeat_description": "Ø§Ų„ØšŲˆØ¯ØŠ ØĨŲ„Ų‰ Ø§Ų„Ø¨Ø¯Ø§ŲŠØŠ ØšŲ†Ø¯ Ø§Ų†ØĒŲ‡Ø§ØĄ ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­", "slideshow_settings": "ØĨؚداداØĒ ØšØąØļ Ø§Ų„Ø´ØąØ§ØĻØ­", "sort_albums_by": "ØąØĒب Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…Ø§ØĒ Ø­ØŗØ¨...", "sort_created": "ØĒØ§ØąŲŠØŽ Ø§Ų„ØĨŲ†Ø´Ø§ØĄ", @@ -2064,6 +2220,7 @@ "theme_setting_theme_subtitle": "ا؎ØĒØą ØĨؚداداØĒ Ų…Ø¸Ų‡Øą Ø§Ų„ØĒØˇØ¨ŲŠŲ‚", "theme_setting_three_stage_loading_subtitle": "Ų‚Ø¯ ŲŠØ˛ŲŠØ¯ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ Ų…Ų† ØĢŲ„Ø§ØĢ Ų…ØąØ§Ø­Ų„ Ų…Ų† ØŖØ¯Ø§ØĄ Ø§Ų„ØĒØ­Ų…ŲŠŲ„ ŲˆŲ„ŲƒŲ†Ų‡ ŲŠØŗØ¨Ø¨ ØĒØ­Ų…ŲŠŲ„ Ø´Ø¨ŲƒØŠ ØŖØšŲ„Ų‰ Ø¨ŲƒØĢŲŠØą", "theme_setting_three_stage_loading_title": "ØĒŲ…ŲƒŲŠŲ† ØĒØ­Ų…ŲŠŲ„ ØĢŲ„Ø§ØĢ Ų…ØąØ§Ø­Ų„", + "then": "ØĢŲ…", "they_will_be_merged_together": "ØŗŲŠØĒŲ… Ø¯Ų…ØŦŲ‡Ų… Ų…ØšŲ‹Ø§", "third_party_resources": "Ų…ŲˆØ§ØąØ¯ Ø§Ų„ØˇØąŲ Ø§Ų„ØĢØ§Ų„ØĢ", "time": "ŲˆŲ‚ØĒ", @@ -2098,6 +2255,13 @@ "trash_page_select_assets_btn": "ا؎ØĒØą Ø§Ų„ØŖØĩŲˆŲ„", "trash_page_title": "ØŗŲ„ØŠ Ø§Ų„Ų…Ų‡Ų…Ų„Ø§ØĒ ({count})", "trashed_items_will_be_permanently_deleted_after": "ØŗŲŠØĒŲ… Ø­Ø°ŲŲ Ø§Ų„ØšŲ†Ø§ØĩØą Ø§Ų„Ų…Ø­Ø°ŲˆŲØŠ Ų†ŲŲ‡Ø§ØĻŲŠŲ‹Ø§ بؚد {days, plural, one {# ŲŠŲˆŲ…} other {# ØŖŲŠØ§Ų… }}.", + "trigger": "Ų…ŲØšŲŲ„", + "trigger_asset_uploaded": "ØĒŲ… ØąŲØš Ø§Ų„Ø§ØĩŲ„", + "trigger_asset_uploaded_description": "؊ØĒŲ… ØĒŲØšŲŠŲ„Ų‡ ØšŲ†Ø¯ ØĒØ­Ų…ŲŠŲ„ ØŖØĩŲ„ ØŦØ¯ŲŠØ¯", + "trigger_description": "حدØĢ ŲŠØ¨Ø¯ØŖ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "trigger_person_recognized": "ØĒŲ… Ø§Ų„ØĒØšØąŲ ØšŲ„Ų‰ Ø´ØŽØĩ", + "trigger_person_recognized_description": "؊ØĒŲ… ØĒŲØšŲŠŲ„Ų‡ ØšŲ†Ø¯ Ø§ŲƒØĒØ´Ø§Ų Ø´ØŽØĩ", + "trigger_type": "Ų†ŲˆØš Ø§Ų„Ų…ŲØšŲ„", "troubleshoot": "Ø§ØŗØĒŲƒØ´Ø§Ų Ø§Ų„Ų…Ø´Ø§ŲƒŲ„", "type": "Ø§Ų„Ų†ŲˆØš", "unable_to_change_pin_code": "ØĒŲŲŠŲŠØą ØąŲ…Ø˛ PIN ØēŲŠØą Ų…Ų…ŲƒŲ†", @@ -2112,6 +2276,7 @@ "unhide_person": "ØŖØ¸Ų‡Øą Ø§Ų„Ø´ØŽØĩ", "unknown": "ØēŲŠØą Ų…ØšØąŲˆŲ", "unknown_country": "Ø¨Ų„Ø¯ ØēŲŠØą Ų…ØšØąŲˆŲ", + "unknown_date": "ØĒØ§ØąŲŠØŽ ØēŲŠØą Ų…ØšØąŲˆŲ", "unknown_year": "ØŗŲ†ØŠ ØēŲŠØą Ų…ØšØąŲˆŲØŠ", "unlimited": "ØēŲŠØą Ų…Ø­Ø¯ŲˆØ¯", "unlink_motion_video": "ØĨŲ„ØēØ§ØĄ ØąØ¨Øˇ ŲŲŠØ¯ŲŠŲˆ Ø§Ų„Ø­ØąŲƒØŠ", @@ -2128,17 +2293,19 @@ "unstack": "؁؃ Ø§Ų„ŲƒŲˆŲ…Ų‡", "unstack_action_prompt": "ØĒŲ… Ø§Ø˛Ø§Ų„ØŠ ØĒŲƒØ¯ŲŠØŗ {count}", "unstacked_assets_count": "ØĒŲ… ØĨØŽØąØ§ØŦ {count, plural, one {# Ø§Ų„ØŖØĩŲ„} other {# Ø§Ų„ØŖØĩŲˆŲ„}} Ų…Ų† Ø§Ų„ØĒŲƒØ¯ŲŠØŗ", + "unsupported_field_type": "Ų†ŲˆØš Ø­Ų‚Ų„ ØēŲŠØą Ų…Ø¯ØšŲˆŲ…", "untagged": "ØēŲŠØą Ų…ŲØšŲŽŲ„ŲŽŲ‘Ų…", + "untitled_workflow": "ØŽØˇØŠ ØŗŲŠØą ØšŲ…Ų„ Ø¨Ø¯ŲˆŲ† ØšŲ†ŲˆØ§Ų†", "up_next": "Ø§Ų„ØĒØ§Ų„ŲŠ", "update_location_action_prompt": "ØĒØ­Ø¯ŲŠØĢ Ų…ŲˆŲ‚Øš {count} ØšŲ†Ø§ØĩØą Ų…Ø­Ø¯Ø¯ØŠ ØšŲ„Ų‰ Ø§Ų„Ų†Ø­Ųˆ Ø§Ų„ØĒØ§Ų„ŲŠ:", "updated_at": "ØĒŲ… Ø§Ų„ØĒØ­Ø¯ŲŠØĢ", "updated_password": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ŲƒŲ„Ų…ØŠ Ø§Ų„Ų…ØąŲˆØą", "upload": "ØąŲØš", - "upload_action_prompt": "{count} ؅؄؁ ؁؊ Ų‚Ø§ØĻŲ…ØŠ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą Ų„Ų„ØąŲØš", "upload_concurrency": "Ø§Ų„ØąŲØš Ø§Ų„Ų…ØĒØ˛Ø§Ų…Ų†", "upload_details": "ØĒŲØ§ØĩŲŠŲ„ Ø§Ų„ØąŲØš", "upload_dialog_info": "Ų‡Ų„ ØĒØąŲŠØ¯ Ø§Ų„Ų†ØŗØŽ Ø§Ų„Ø§Ø­ØĒŲŠØ§ØˇŲŠ Ų„Ų„ØŖØĩŲˆŲ„ (Ø§Ų„ØŖØĩŲˆŲ„) Ø§Ų„Ų…Ø­Ø¯Ø¯ØŠ ØĨŲ„Ų‰ Ø§Ų„ØŽØ§Ø¯Ų…ØŸ", "upload_dialog_title": "ØĒØ­Ų…ŲŠŲ„ Ø§Ų„ØŖØĩŲˆŲ„", + "upload_error_with_count": "ØŽØˇØŖ ؁؊ ØąŲØš {count, plural, one {# اØĩŲ„} other {# اØĩŲˆŲ„}}", "upload_errors": "ØĨ؃ØĒŲ…Ų„ Ø§Ų„ØąŲØš Ų…Øš {count, plural, one {# ØŽØˇØŖ} other {# ØŖØŽØˇØ§ØĄ}}, Ų‚Ų… بØĒØ­Ø¯ŲŠØĢ Ø§Ų„ØĩŲØ­ØŠ Ų„ØąØ¤ŲŠØŠ Ø§Ų„Ų…Ø­ØĒŲˆŲŠØ§ØĒ Ø§Ų„ØŦØ¯ŲŠØ¯ØŠ Ø§Ų„ØĒ؊ ØĒŲ… ØąŲØšŲ‡Ø§.", "upload_finished": "ØĒŲ… Ø§Ų„Ø§Ų†ØĒŲ‡Ø§ØĄ Ų…Ų† Ø§Ų„ØąŲØš", "upload_progress": "Ų…ØĒØ¨Ų‚ŲŠØŠ {remaining, number} - Ų…ØšØ§Ų„ØŦØŠ {processed, number}/{total, number}", @@ -2174,6 +2341,7 @@ "utilities": "ØŖØ¯ŲˆØ§ØĒ", "validate": "ØĒØ­Ų‚Ų’Ų‚", "validate_endpoint_error": "Ø§Ų„ØąØŦØ§ØĄ Ø§Ø¯ØŽØ§Ų„ ØšŲ†ŲˆØ§Ų† URL ØĩØ§Ų„Ø­", + "validation_error": "ØŽØˇØŖ ؁؊ Ø§Ų„ØĒØ­Ų‚Ų‚", "variables": "Ø§Ų„Ų…ØĒØēŲŠØąØ§ØĒ", "version": "Ø§Ų„ØĨØĩØ¯Ø§Øą", "version_announcement_closing": "ØĩØ¯ŲŠŲ‚ŲƒØŒ ØŖŲ„ŲŠŲƒØŗ", @@ -2185,10 +2353,12 @@ "video_hover_setting_description": "ØĒØ´ØēŲŠŲ„ Ø§Ų„ØĩŲˆØąØŠ Ø§Ų„Ų…ØĩØēØąØŠ Ų„Ų„ŲŲŠØ¯ŲŠŲˆ ØšŲ†Ø¯ ØĒØ­ØąŲŠŲƒ Ø§Ų„Ų…Ø§ŲˆØŗ ŲŲˆŲ‚ Ø§Ų„ØšŲ†ØĩØą. Ø­ØĒŲ‰ ØšŲ†Ø¯ Ø§Ų„ØĒØšØˇŲŠŲ„ØŒ ŲŠŲ…ŲƒŲ† Ø¨Ø¯ØĄ Ø§Ų„ØĒØ´ØēŲŠŲ„ ØšŲ† ØˇØąŲŠŲ‚ Ø§Ų„ØĒŲ…ØąŲŠØą ŲŲˆŲ‚ ØąŲ…Ø˛ Ø§Ų„ØĒØ´ØēŲŠŲ„.", "videos": "ŲŲŠØ¯ŲŠŲˆŲ‡Ø§ØĒ", "videos_count": "{count, plural, one {# Ų…Ų‚ØˇØš ŲŲŠØ¯ŲŠŲˆ } other {# Ų…Ų‚Ø§ØˇØš Ø§Ų„ŲŲŠØ¯ŲŠŲˆ }}", + "videos_only": "Ø§Ų„ŲØ¯ŲŠŲˆØ§ØĒ ŲŲ‚Øˇ", "view": "ØšØąØļ", "view_album": "ØšØąØļ Ø§Ų„ØŖŲ„Ø¨ŲˆŲ…", "view_all": "ØšØąØļ Ø§Ų„ŲƒŲ„", "view_all_users": "ØšØąØļ ŲƒØ§ŲØŠ Ø§Ų„Ų…ØŗØĒØŽØ¯Ų…ŲŠŲ†", + "view_asset_owners": "ØšØąØļ Ų…Ø§Ų„ŲƒŲŠ Ø§Ų„ØŖØĩŲˆŲ„", "view_details": "ØąØ¤ŲŠØŠ Ø§Ų„ØĒŲØ§ØĩŲŠŲ„", "view_in_timeline": "ØšØąØļ ؁؊ Ø§Ų„ØŦØ¯ŲˆŲ„ Ø§Ų„Ø˛Ų…Ų†ŲŠ", "view_link": "ØšØąØļ Ø§Ų„ØąØ§Ø¨Øˇ", @@ -2204,19 +2374,36 @@ "viewer_stack_use_as_main_asset": "Ø§ØŗØĒØŽØ¯Ų… ŲƒØŖØĩŲ„ ØąØĻŲŠØŗŲŠ", "viewer_unstack": "؁؃ Ø§Ų„ŲƒŲˆŲ…Ų‡", "visibility_changed": "Ø§Ų„ØąØ¤ŲŠØŠ ØĒØēŲŠØąØĒ Ų„Ų€ {count, plural, one {Ø´ØŽØĩ ŲˆØ§Ø­Ø¯} other {# ؚد؊ ØŖØ´ØŽØ§Øĩ}}", + "visual": "Ų…ØąØĻ؊", + "visual_builder": "ادا؊ Ų†Ø´Ø§ØĄ Ų…ØąØĻŲŠØŠ", "waiting": "؁؊ Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą", + "waiting_count": "Ø§Ų„Ø§Ų†ØĒØ¸Ø§Øą: {count}", "warning": "ØĒØ­Ø°ŲŠØą", "week": "ØŖØŗØ¨ŲˆØš", "welcome": "Ų…ØąØ­Ø¨Ø§Ų‹", "welcome_to_immich": "Ų…ØąØ­Ø¨Ø§Ų‹ Ø¨Ųƒ ؁؊ Immich", + "width": "ØšŲØąØļ", "wifi_name": "Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Wi-Fi", - "workflow": "ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_delete_prompt": "Ų‡Ų„ ØŖŲ†ØĒ Ų…ØĒØŖŲƒØ¯ Ų…Ų† Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ų‡Ø°Ø§ØŸ", + "workflow_deleted": "ØĒŲ… Ø­Ø°Ų ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_description": "؈Øĩ؁ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_info": "Ų…ØšŲ„ŲˆŲ…Ø§ØĒ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_json": "؅؄؁ JSON Ų„ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_json_help": "Ų‚Ų… بØĒØšØ¯ŲŠŲ„ ØĨؚداداØĒ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ بØĩ؊ØēØŠ JSON. ØŗØĒØĒŲ… Ų…Ø˛Ø§Ų…Ų†ØŠ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ Ų…Øš ØŖØ¯Ø§ØŠ Ø§Ų„ØĨŲ†Ø´Ø§ØĄ Ø§Ų„Ų…ØąØĻŲŠØŠ.", + "workflow_name": "Ø§ØŗŲ… ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_navigation_prompt": "Ų‡Ų„ Ø§Ų†ØĒ Ų…ØĒØ§ŲƒØ¯ Ų…Ų† Ø§Ų„Ų…ØēØ§Ø¯ØąØŠ Ø¨Ø¯ŲˆŲ† Ø­ŲØ¸ Ø§Ų„ØĒØēŲŠŲŠØąØ§ØĒ؟", + "workflow_summary": "Ų…Ų„ØŽØĩ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflow_update_success": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ Ø¨Ų†ØŦاح", + "workflow_updated": "ØĒŲ… ØĒØ­Ø¯ŲŠØĢ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflows": "ØŗŲŠØą Ø§Ų„ØšŲ…Ų„", + "workflows_help_text": "ØĒØšŲ…Ų„ ØŗŲŠØą Ø§Ų„ØšŲ…Ų„ ØšŲ„Ų‰ ØŖØĒŲ…ØĒØŠ Ø§Ų„ØĨØŦØąØ§ØĄØ§ØĒ ØšŲ„Ų‰ ØŖØĩŲˆŲ„Ųƒ Ø¨Ų†Ø§ØĄŲ‹ ØšŲ„Ų‰ Ø§Ų„Ų…ŲØšŲ„Ø§ØĒ ŲˆØ§Ų„ŲŲ„Ø§ØĒØą", "wrong_pin_code": "ØąŲ…Ø˛ Ø§Ų„ØĒØšØąŲŠŲ Ø§Ų„Ø´ØŽØĩ؊ ØŽØ§ØˇØĻ", "year": "ØŗŲ†ØŠ", "years_ago": "{years, plural, one {# ØŗŲ†ØŠ} other {# ØŗŲ†ŲˆØ§ØĒ}} Ų…ØļØĒ", "yes": "Ų†ØšŲ…", "you_dont_have_any_shared_links": "Ų„ŲŠØŗ Ų„Ø¯ŲŠŲƒ ØŖŲŠ ØąŲˆØ§Ø¨Øˇ Ų…Ø´ØĒØąŲƒØŠ", "your_wifi_name": "Ø§ØŗŲ… Ø´Ø¨ŲƒØŠ Ø§Ų„Ø§ØĒØĩØ§Ų„ Ø§Ų„Ų„Ø§ØŗŲ„ŲƒŲŠ Ø§Ų„ØŽØ§Øĩ Ø¨Ųƒ", + "zero_to_clear_rating": "اØļØēØˇ 0 Ų„Ų…ØŗØ­ ØĒØĩŲ†ŲŠŲ Ø§Ų„Ø§ØĩŲˆŲ„", "zoom_image": "ØĒŲƒØ¨ŲŠØą Ø§Ų„ØĩŲˆØąØŠ", "zoom_to_bounds": "ØĒŲƒØ¨ŲŠØą Ø­ØĒŲ‰ Ø­Ø¯ŲˆØ¯ Ø§Ų„Ų…Ų†ØˇŲ‚ØŠ" } diff --git a/i18n/be.json b/i18n/be.json index 84a3e517e9..1c446c0cbd 100644 --- a/i18n/be.json +++ b/i18n/be.json @@ -1,12 +1,14 @@ { - "about": "Ай", + "about": "Ай ĐŋŅ€Đ°Đ´ŅƒĐē҆Đĩ", "account": "ĐŖĐģŅ–ĐēĐžĐ˛Ņ‹ СаĐŋҖҁ", "account_settings": "НаĐģĐ°Đ´Ņ‹ ŅžĐģŅ–ĐēĐžĐ˛Đ°ĐŗĐ° СаĐŋŅ–ŅŅƒ", "acknowledge": "ĐŸĐ°Ņ†Đ˛ĐĩŅ€Đ´ĐˇŅ–Ņ†ŅŒ", "action": "ДзĐĩŅĐŊĐŊĐĩ", "action_common_update": "АйĐŊĐ°Đ˛Ņ–Ņ†ŅŒ", + "action_description": "ДзĐĩŅĐŊĐŊŅ–, ŅĐēŅ–Ņ Đ˛Ņ‹ĐēĐžĐŊĐ˛Đ°ŅŽŅ†Ņ†Đ° С Đ°Đ´Đ°ĐąŅ€Đ°ĐŊŅ‹ĐŧŅ– аб’ĐĩĐēŅ‚Đ°ĐŧŅ–", "actions": "ДзĐĩŅĐŊĐŊŅ–", - "active": "АĐēŅ‚Ņ‹ŅžĐŊҋ҅", + "active": "АĐŋŅ€Đ°Ņ†ĐžŅžĐ˛Đ°ŅŽŅ†Ņ†Đ°", + "active_count": "АĐŋŅ€Đ°Ņ†ĐžŅžĐ˛Đ°ŅŽŅ†Ņ†Đ°: {count}", "activity": "АĐēŅ‚Ņ‹ŅžĐŊĐ°ŅŅ†ŅŒ", "activity_changed": "АĐēŅ‚Ņ‹ŅžĐŊĐ°ŅŅ†ŅŒ {enabled, select, true {҃ĐēĐģŅŽŅ‡Đ°ĐŊа} other {адĐēĐģŅŽŅ‡Đ°ĐŊа}}", "add": "Đ”Đ°Đ´Đ°Ņ†ŅŒ", @@ -14,10 +16,15 @@ "add_a_location": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐŧĐĩŅŅ†Đ°", "add_a_name": "Đ”Đ°Đ´Đ°Ņ†ŅŒ Ņ–ĐŧŅ", "add_a_title": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐˇĐ°ĐŗĐ°ĐģОваĐē", + "add_action": "Đ”Đ°Đ´Đ°Ņ†ŅŒ дСĐĩŅĐŊĐŊĐĩ", + "add_action_description": "ĐĐ°Ņ†Ņ–ŅĐŊҖ҆Đĩ Đ´ĐģŅ дадаĐŊĐŊŅ дСĐĩŅĐŊĐŊŅ", + "add_assets": "Đ”Đ°Đ´Đ°Ņ†ŅŒ аб’ĐĩĐē҂ҋ", "add_birthday": "Đ”Đ°Đ´Đ°Ņ†ŅŒ дСĐĩĐŊҌ ĐŊĐ°Ņ€Đ°Đ´ĐļŅĐŊĐŊŅ", "add_endpoint": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐēŅ€ĐžĐŋĐē҃ Đ´ĐžŅŅ‚ŅƒĐŋ҃", "add_exclusion_pattern": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ŅˆĐ°ĐąĐģĐžĐŊ Đ˛Ņ‹ĐēĐģŅŽŅ‡ŅĐŊĐŊŅ", - "add_location": "Đ”Đ°Đ´Đ°ĐšŅ†Đĩ ĐŧĐĩŅŅ†Đ°", + "add_filter": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҄ҖĐģŅŒŅ‚Ņ€", + "add_filter_description": "ĐĐ°Ņ†Ņ–ŅĐŊҖ҆Đĩ Đ´ĐģŅ дадаĐŊĐŊŅ ŅžĐŧĐžĐ˛Ņ‹ Đ°Đ´ĐąĐžŅ€Ņƒ", + "add_location": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐŧĐĩŅŅ†Đ°", "add_more_users": "Đ”Đ°Đ´Đ°Ņ†ŅŒ йОĐģҌ҈ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–ĐēĐ°Ņž", "add_partner": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐŋĐ°Ņ€Ņ‚ĐŊŅ‘Ņ€Đ°", "add_path": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҈ĐģŅŅ…", @@ -27,12 +34,15 @@ "add_to_album": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃ аĐģŅŒĐąĐžĐŧ", "add_to_album_bottom_sheet_added": "ДададзĐĩĐŊа да {album}", "add_to_album_bottom_sheet_already_exists": "ĐŖĐļĐž СĐŊĐ°Ņ…ĐžĐ´ĐˇŅ–Ņ†Ņ†Đ° Ņž {album}", - "add_to_album_bottom_sheet_some_local_assets": "НĐĩĐēĐ°Ņ‚ĐžŅ€Ņ‹Ņ ĐģаĐēаĐģҌĐŊŅ‹Ņ аĐēŅ‚Ņ‹Đ˛Ņ‹ ĐŊĐĩ ĐŧĐžĐŗŅƒŅ†ŅŒ ĐąŅ‹Ņ†ŅŒ дададСĐĩĐŊŅ‹ Ņž аĐģŅŒĐąĐžĐŧ", + "add_to_album_bottom_sheet_some_local_assets": "НĐĩĐēĐ°Ņ‚ĐžŅ€Ņ‹Ņ ĐģаĐēаĐģҌĐŊŅ‹Ņ аб’ĐĩĐē҂ҋ ĐŊĐĩ ĐŧĐžĐŗŅƒŅ†ŅŒ ĐąŅ‹Ņ†ŅŒ дададСĐĩĐŊŅ‹ Ņž аĐģŅŒĐąĐžĐŧ", "add_to_album_toggle": "ПĐĩŅ€Đ°ĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Đ˛Ņ‹ĐąĐ°Ņ€ Đ´ĐģŅ {album}", "add_to_albums": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃ аĐģŅŒĐąĐžĐŧŅ‹", "add_to_albums_count": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃ аĐģŅŒĐąĐžĐŧŅ‹ ({count})", + "add_to_bottom_bar": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃", "add_to_shared_album": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ҃ Đ°ĐŗŅƒĐģҌĐŊŅ‹ аĐģŅŒĐąĐžĐŧ", + "add_upload_to_stack": "ЗаĐŋаĐŧĐŋĐ°Đ˛Đ°Ņ†ŅŒ Ņ– Đ´Đ°Đ´Đ°Ņ†ŅŒ ҃ ĐŊĐ°ĐąĐžŅ€", "add_url": "Đ”Đ°Đ´Đ°Ņ†ŅŒ URL", + "add_workflow_step": "Đ”Đ°Đ´Đ°Ņ†ŅŒ ĐēŅ€ĐžĐē ĐŋŅ€Đ°Ņ†ĐžŅžĐŊĐ°ĐŗĐ° ĐŋŅ€Đ°Ņ†ŅŅŅƒ", "added_to_archive": "ДададзĐĩĐŊа Ņž Đ°Ņ€Ņ…Ņ–Ņž", "added_to_favorites": "ДададзĐĩĐŊа Ņž Đ°ĐąŅ€Đ°ĐŊŅ‹Ņ", "added_to_favorites_count": "ДададзĐĩĐŊа {count, number} да Đ°ĐąŅ€Đ°ĐŊĐ°ĐŗĐ°", @@ -40,13 +50,13 @@ "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": "НаĐģĐ°Đ´Ņ‹ Đ°ŅžŅ‚ŅĐŊ҂ҋ҄ҖĐēĐ°Ņ†Ņ‹Ņ–", + "authentication_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŋĐ°Ņ€ĐžĐģŅĐŧŅ–, OAuth Ņ– Ņ–ĐŊŅˆŅ‹Ņ ĐŊаĐģĐ°Đ´Ņ‹ Đ°ŅžŅ‚ŅĐŊ҂ҋ҄ҖĐēĐ°Ņ†Ņ‹Ņ–", + "authentication_settings_disable_all": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ адĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ҃ҁĐĩ ҁĐŋĐžŅĐ°ĐąŅ‹ ŅžĐ˛Đ°Ņ…ĐžĐ´Ņƒ? ĐŖĐ˛Đ°Ņ…ĐžĐ´ ĐąŅƒĐ´ĐˇĐĩ Ņ†Đ°ĐģĐēаĐŧ адĐēĐģŅŽŅ‡Đ°ĐŊŅ‹.", "authentication_settings_reenable": "Каб СĐŊĐžŅž ҃ĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ, Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐšŅ†Đĩ КаĐŧаĐŊĐ´Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", "background_task_job": "ФОĐŊĐ°Đ˛Ņ‹Ņ СадаĐŊĐŊŅ–", "backup_database": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛ŅƒŅŽ ĐēĐžĐŋŅ–ŅŽ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅", - "backup_database_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ€ŅĐˇĐĩŅ€Đ˛Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅", + "backup_database_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ŅŅ‚Đ˛Đ°Ņ€ŅĐŊĐŊĐĩ даĐŧĐŋĐ°Ņž ĐąĐ°ĐˇŅ‹ даĐŊҋ҅", "backup_keep_last_amount": "КоĐģҌĐēĐ°ŅŅ†ŅŒ ĐŋаĐŋŅŅ€ŅĐ´ĐŊŅ–Ņ… Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛Ņ‹Ņ… ĐēĐžĐŋŅ–Đš Đ´ĐģŅ ĐˇĐ°Ņ…Đ°Đ˛Đ°ĐŊĐŊŅ", "backup_onboarding_1_description": "СĐŊŅŅˆĐŊŅŅ ĐēĐžĐŋŅ–Ņ Ņž вОйĐģаĐē҃ айО Ņž Ņ–ĐŊŅˆŅ‹Đŧ Ņ„Ņ–ĐˇŅ–Ņ‡ĐŊŅ‹Đŧ ĐŧĐĩҁ҆ҋ.", "backup_onboarding_2_description": "ĐģаĐēаĐģҌĐŊŅ‹Ņ ĐēĐžĐŋŅ–Ņ– ĐŊа Ņ–ĐŊŅˆŅ‹Ņ… ĐŋҀҋĐģĐ°Đ´Đ°Ņ…. Đ“ŅŅ‚Đ° ŅžĐēĐģŅŽŅ‡Đ°Đĩ Ņž ŅŅĐąĐĩ Đ°ŅĐŊĐžŅžĐŊŅ‹Ņ Ņ„Đ°ĐšĐģŅ‹ Ņ– ĐģаĐēаĐģҌĐŊŅƒŅŽ Ņ€ŅĐˇĐĩŅ€Đ˛ĐžĐ˛ŅƒŅŽ ĐēĐžĐŋŅ–ŅŽ ĐŗŅŅ‚Ņ‹Ņ… Ņ„Đ°ĐšĐģĐ°Ņž.", @@ -59,12 +69,13 @@ "backup_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– Ņ€ŅĐˇĐĩŅ€Đ˛Đ°Đ˛Đ°ĐŊĐŊŅ ĐąĐ°ĐˇŅ‹ даĐŊҋ҅.", "cleared_jobs": "ĐŅ‡Ņ‹ŅˆŅ‡Đ°ĐŊŅ‹ СадаĐŊĐŊŅ– Đ´ĐģŅ: {job}", "config_set_by_file": "КаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ ĐˇĐ°Ņ€Đ°Đˇ ŅƒŅŅ‚Đ°ĐģŅĐ˛Đ°ĐŊа ĐŋŅ€Đ°Đˇ Ņ„Đ°ĐšĐģ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹Ņ–", - "confirm_delete_library": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ ŅˆŅ‚Đž ĐļадаĐĩ҆Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐē҃ {library}?", + "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}?", + "confirm_reprocess_all_faces": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ ĐŋĐĩŅ€Đ°Đ°ĐŋŅ€Đ°Ņ†Đ°Đ˛Đ°Ņ†ŅŒ ҃ҁĐĩ Ņ‚Đ˛Đ°Ņ€Ņ‹? Đ“ŅŅ‚Đ° Ņ‚Đ°ĐēŅĐ°Đŧа ĐŋŅ€Ņ‹Đ˛ŅĐ´ĐˇĐĩ да Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊĐŊŅ Ņ–ĐŧŅ‘ĐŊ ĐģŅŽĐ´ĐˇĐĩĐš.", + "confirm_user_password_reset": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ Ņž ҂ҋĐŧ, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ ҁĐēŅ–ĐŊŅƒŅ†ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ {user}?", + "confirm_user_pin_code_reset": "Đ’Ņ‹ ŅžĐŋŅŅžĐŊĐĩĐŊŅ‹ Ņž ҂ҋĐŧ, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ ҁĐēŅ–ĐŊŅƒŅ†ŅŒ PIN-ĐēОд {user}?", + "copy_config_to_clipboard_description": "КаĐŋŅ–Ņ€Đ°Đ˛Đ°Ņ†ŅŒ ĐąŅĐŗŅƒŅ‡ŅƒŅŽ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹ŅŽ ŅŅ–ŅŅ‚ŅĐŧŅ‹ Ņž JSON ҃ ĐąŅƒŅ„ĐĩŅ€ айĐŧĐĩĐŊ҃", "create_job": "ĐĄŅ‚Đ˛Đ°Ņ€Ņ‹Ņ†ŅŒ СадаĐŊĐŊĐĩ", "cron_expression": "Đ’Ņ‹Ņ€Đ°Đˇ Cron", "cron_expression_description": "Đ—Đ°Đ´Đ°ĐšŅ†Đĩ Ņ–ĐŊŅ‚ŅŅ€Đ˛Đ°Đģ ҁĐēаĐŊаваĐŊĐŊŅ, Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ŅŽŅ‡Ņ‹ Ņ„Đ°Ņ€ĐŧĐ°Ņ‚ cron. ДĐģŅ Đ°Ņ‚Ņ€Ņ‹ĐŧаĐŊĐŊŅ Đ´Đ°Đ´Đ°Ņ‚ĐēОваК Ņ–ĐŊŅ„Đ°Ņ€ĐŧĐ°Ņ†Ņ‹Ņ–, ĐˇĐ˛ŅŅ€ĐŊҖ҆ĐĩŅŅ, ĐŊаĐŋҀҋĐēĐģад, да Crontab Guru", @@ -72,6 +83,8 @@ "disable_login": "АдĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ŅƒĐ˛Đ°Ņ…ĐžĐ´", "duplicate_detection_job_description": "ЗаĐŋŅƒŅŅ†Ņ–Ņ†ŅŒ ĐŧĐ°ŅˆŅ‹ĐŊĐŊаĐĩ ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊĐĩ ĐŊа аĐēŅ‚Ņ‹Đ˛Đ°Ņ… Đ´ĐģŅ Đ˛Ņ‹ŅŅžĐģĐĩĐŊĐŊŅ ĐŋадОйĐŊҋ҅ Đ˛Ņ‹ŅŅž. ЗаĐģĐĩĐļŅ‹Ņ†ŅŒ ад Smart Search", "exclusion_pattern_description": "ШайĐģĐžĐŊŅ‹ Đ˛Ņ‹ĐēĐģŅŽŅ‡ŅĐŊĐŊŅ даСваĐģŅŅŽŅ†ŅŒ Ņ–ĐŗĐŊĐ°Ņ€Đ°Đ˛Đ°Ņ†ŅŒ Ņ„Đ°ĐšĐģŅ‹ Ņ– ĐŋаĐŋĐēŅ– ĐŋҀҋ ҁĐēаĐŊаваĐŊĐŊŅ– Đ˛Đ°ŅˆĐ°Đš ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–. Đ“ŅŅ‚Đ° ĐēĐ°Ņ€Ņ‹ŅĐŊа, ĐēаĐģŅ– Ņž Đ˛Đ°Ņ Ņ‘ŅŅ†ŅŒ ĐŋаĐŋĐēŅ–, ŅĐēŅ–Ņ СĐŧŅŅˆŅ‡Đ°ŅŽŅ†ŅŒ Ņ„Đ°ĐšĐģŅ‹, ŅĐēŅ–Ņ Đ˛Ņ‹ ĐŊĐĩ Ņ…ĐžŅ‡Đ°Ņ†Đĩ Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Đ°Đ˛Đ°Ņ†ŅŒ, ĐŊаĐŋҀҋĐēĐģад, Ņ„Đ°ĐšĐģŅ‹ RAW.", + "export_config_as_json_description": "Đ—Đ°Ņ…Đ°Đ˛Đ°Ņ†ŅŒ ĐąŅĐŗŅƒŅ‡ŅƒŅŽ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹ŅŽ ŅŅ–ŅŅ‚ŅĐŧŅ‹ Ņž Ņ„Đ°ĐšĐģ JSON", + "external_libraries_page_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ СĐŊĐĩ҈ĐŊŅ–ĐŧŅ– ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēаĐŧŅ–", "face_detection": "Đ’Ņ‹ŅŅžĐģĐĩĐŊĐŊĐĩ Ņ‚Đ˛Đ°Ņ€Đ°Ņž", "face_detection_description": "Đ’Ņ‹ŅŅžĐģŅŅ†ŅŒ Ņ‚Đ˛Đ°Ņ€Ņ‹ ĐŊа Ņ„ĐžŅ‚Đ°ĐˇĐ´Ņ‹ĐŧĐēĐ°Ņ… Ņ– Đ˛Ņ–Đ´ŅĐ° С даĐŋаĐŧĐžĐŗĐ°Đš ĐŧĐ°ŅˆŅ‹ĐŊĐŊĐ°ĐŗĐ° ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊŅ. ДĐģŅ Đ˛Ņ–Đ´ŅĐ° ŅžĐģŅ–Ņ‡Đ˛Đ°ĐĩŅ†Ņ†Đ° Ņ‚ĐžĐģҌĐēŅ– ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€Đ°. \"АйĐŊĐ°Đ˛Ņ–Ņ†ŅŒ\" (ĐŋĐĩŅ€Đ°)аĐŋŅ€Đ°Ņ†ĐžŅžĐ˛Đ°Đĩ ŅžŅĐĩ ĐŧĐĩĐ´Ņ‹Ņ. \"ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ\" Đ´Đ°Đ´Đ°Ņ‚ĐēОва Đ°Ņ‡Ņ‹ŅˆŅ‡Đ°Đĩ ŅžŅĐĩ ĐąŅĐŗŅƒŅ‡Ņ‹Ņ даĐŊŅ‹Ņ ĐŋŅ€Đ° Ņ‚Đ˛Đ°Ņ€Ņ‹. \"ĐĐ´ŅŅƒŅ‚ĐŊŅ–Ņ‡Đ°Đĩ\" ŅŅ‚Đ°Đ˛Ņ–Ņ†ŅŒ ҃ Ņ‡Đ°Ņ€ĐŗŅƒ ĐŧĐĩĐ´Ņ‹Ņ, ŅĐēŅ–Ņ ŅŅˆŅ‡Ņ ĐŊĐĩ ĐąŅ‹ĐģŅ– аĐŋŅ€Đ°Ņ†Đ°Đ˛Đ°ĐŊŅ‹Ņ. Đ’Ņ‹ŅŅžĐģĐĩĐŊŅ‹Ņ Ņ‚Đ˛Đ°Ņ€Ņ‹ ĐąŅƒĐ´ŅƒŅ†ŅŒ ĐŋĐ°ŅŅ‚Đ°ŅžĐģĐĩĐŊŅ‹ Ņž Ņ‡Đ°Ņ€ĐŗŅƒ Đ´ĐģŅ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊŅ Đ°ŅĐžĐą ĐŋĐ°ŅĐģŅ ĐˇĐ°Đ˛ŅŅ€ŅˆŅĐŊĐŊŅ Đ˛Ņ‹ŅŅžĐģĐĩĐŊĐŊŅ Ņ‚Đ˛Đ°Ņ€Đ°Ņž, С ĐŗŅ€ŅƒĐŋаваĐŊĐŊĐĩĐŧ Ņ–Ņ… Đŋа ҖҁĐŊŅƒŅŽŅ‡Ņ‹Ņ… айО ĐŊĐžĐ˛Ņ‹Ņ… ĐģŅŽĐ´ĐˇŅŅ….", "facial_recognition_job_description": "Đ“Ņ€ŅƒĐŋĐ°Đ˛Đ°Ņ†ŅŒ Đ˛Ņ‹ŅŅžĐģĐĩĐŊŅ‹Ņ Ņ‚Đ˛Đ°Ņ€Ņ‹ Đŋа Đ°ŅĐžĐąĐ°Ņ…. Đ“ŅŅ‚Ņ‹ ŅŅ‚Đ°Đŋ Đ˛Ņ‹ĐēĐžĐŊваĐĩŅ†Ņ†Đ° ĐŋĐ°ŅĐģŅ ĐˇĐ°Đ˛ŅŅ€ŅˆŅĐŊĐŊŅ Đ˛Ņ‹ŅŅžĐģĐĩĐŊĐŊŅ Ņ‚Đ˛Đ°Ņ€Đ°Ņž. \"ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ\" (ĐŋĐ°ŅžŅ‚ĐžŅ€ĐŊа) ĐŋĐĩŅ€Đ°ĐŗŅ€ŅƒĐŋĐžŅžĐ˛Đ°Đĩ ŅžŅĐĩ Ņ‚Đ˛Đ°Ņ€Ņ‹. \"ĐĐ´ŅŅƒŅ‚ĐŊŅ–Ņ‡Đ°Đĩ\" ŅŅ‚Đ°Đ˛Ņ–Ņ†ŅŒ ҃ Ņ‡Đ°Ņ€ĐŗŅƒ Ņ‚Đ˛Đ°Ņ€Ņ‹, ŅĐēŅ–Ņ ŅŅˆŅ‡Ņ ĐŊĐĩ ĐŋҀҋĐŋŅ–ŅĐ°ĐŊŅ‹Ņ да ŅĐēОК-ĐŊĐĩĐąŅƒĐ´ĐˇŅŒ Đ°ŅĐžĐąŅ‹.", @@ -87,40 +100,68 @@ "image_prefer_embedded_preview": "ĐĐ´Đ´Đ°Đ˛Đ°Ņ†ŅŒ ĐŋĐĩŅ€Đ°Đ˛Đ°ĐŗŅƒ ŅžĐąŅƒĐ´Đ°Đ˛Đ°ĐŊаК ĐŋŅ€Đ°ŅĐ˛Đĩ", "image_prefer_embedded_preview_setting_description": "Đ’Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°Ņ†ŅŒ ŅƒĐąŅƒĐ´Đ°Đ˛Đ°ĐŊŅ‹Ņ ĐŋŅ€Đ°ŅĐ˛Ņ‹ Ņž RAW-Ņ„ĐžŅ‚Đ°ĐˇĐ´Ņ‹ĐŧĐēĐ°Ņ… Ņž ŅĐēĐ°ŅŅ†Ņ– ŅžĐ˛Đ°Ņ…ĐžĐ´ĐŊҋ҅ даĐŊҋ҅ Đ´ĐģŅ аĐŋŅ€Đ°Ņ†ĐžŅžĐēŅ– ĐŧаĐģŅŽĐŊĐēĐ°Ņž, ĐēаĐģŅ– ĐŧĐ°ĐŗŅ‡Ņ‹Đŧа. Đ“ŅŅ‚Đ° даСваĐģŅĐĩ Đ°Ņ‚Ņ€Ņ‹ĐŧĐ°Ņ†ŅŒ йОĐģҌ҈ даĐēĐģадĐŊŅ‹Ņ ĐēĐžĐģĐĩҀҋ Đ´ĐģŅ ĐŊĐĩĐēĐ°Ņ‚ĐžŅ€Ņ‹Ņ… Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž, аĐģĐĩ Đļ ŅĐēĐ°ŅŅ†ŅŒ ĐŋŅ€Đ°ŅŅž СаĐģĐĩĐļŅ‹Ņ†ŅŒ ад ĐēаĐŧĐĩҀҋ, Ņ– ĐŊа Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐĩ ĐŧĐžĐļа ĐąŅ‹Ņ†ŅŒ йОĐģҌ҈ Đ°Ņ€Ņ‚ŅŅ„Đ°ĐēŅ‚Đ°Ņž ҁ҆ҖҁĐē҃.", "image_prefer_wide_gamut": "ĐĐ´Đ´Đ°Ņ†ŅŒ ĐŋĐĩŅ€Đ°Đ˛Đ°ĐŗŅƒ ŅˆŅ‹Ņ€ĐžĐēаК ĐŗĐ°ĐŧĐĩ", + "image_prefer_wide_gamut_setting_description": "Đ’Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ĐšŅ†Đĩ Display P3 Đ´ĐģŅ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€. Đ“ŅŅ‚Đ° ĐģĐĩĐŋĐĩĐš ĐˇĐ°Ņ…ĐžŅžĐ˛Đ°Đĩ ŅŅ€ĐēĐ°ŅŅ†ŅŒ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž С ŅˆŅ‹Ņ€ĐžĐēаК ĐēĐžĐģĐĩŅ€Đ°Đ˛Đ°Đš ĐŋŅ€Đ°ŅŅ‚ĐžŅ€Đ°Đš, аĐģĐĩ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅŅ‹ ĐŧĐžĐŗŅƒŅ†ŅŒ Đ˛Ņ‹ĐŗĐģŅĐ´Đ°Ņ†ŅŒ Đŋа-Ņ–ĐŊŅˆĐ°Đŧ҃ ĐŊа ŅŅ‚Đ°Ņ€Ņ‹Ņ… ĐŋҀҋĐģĐ°Đ´Đ°Ņ… ŅĐ° ŅŅ‚Đ°Ņ€Đ°Đš вĐĩŅ€ŅŅ–ŅĐš ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°. Đ’Ņ–Đ´Đ°Ņ€Ņ‹ŅŅ‹ sRGB ĐˇĐ°Ņ…ĐžŅžĐ˛Đ°ŅŽŅ†Ņ†Đ° Ņž Ņ„Đ°Ņ€ĐŧĐ°Ņ†Đĩ sRGB, ŅˆŅ‚Đž даСваĐģŅĐĩ ĐŋаСйĐĩĐŗĐŊŅƒŅ†ŅŒ ĐēĐžĐģĐĩŅ€Đ°Đ˛Ņ‹Ņ… ĐˇŅ€ŅƒŅ…Đ°Ņž.", "image_preview_description": "Đ’Ņ–Đ´Đ°Ņ€Ņ‹Ņ ŅŅŅ€ŅĐ´ĐŊŅĐŗĐ° ĐŋаĐŧĐĩŅ€Ņƒ С Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊŅ‹ĐŧŅ– ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ‹ĐŧŅ–, Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ĐĩŅ†Ņ†Đ° ĐŋҀҋ ĐŋŅ€Đ°ĐŗĐģŅĐ´ĐˇĐĩ Đ°ŅĐžĐąĐŊĐ°ĐŗĐ° Ņ€ŅŅŅƒŅ€ŅŅƒ Ņ– Đ´ĐģŅ ĐŧĐ°ŅˆŅ‹ĐŊĐŊĐ°ĐŗĐ° ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊŅ", "image_preview_quality_description": "Đ¯ĐēĐ°ŅŅ†ŅŒ ĐŋŅ€Đ°ŅĐ˛Ņ‹ ад 1 да 100. Đ§Ņ‹Đŧ Đ˛Ņ‹ŅˆŅĐš, ҂ҋĐŧ ĐģĐĩĐŋ҈, аĐģĐĩ ĐŋҀҋ ĐŗŅŅ‚Ņ‹Đŧ ŅŅ‚Đ˛Đ°Ņ€Đ°ŅŽŅ†Ņ†Đ° Ņ„Đ°ĐšĐģŅ‹ йОĐģŅŒŅˆĐ°ĐŗĐ° ĐŋаĐŧĐĩŅ€Ņƒ Ņ– ĐŧĐžĐļа СĐŊŅ–ĐˇŅ–Ņ†Ņ†Đ° Ņ…ŅƒŅ‚ĐēĐ°ŅŅ†ŅŒ Đ˛ĐžĐ´ĐŗŅƒĐē҃ ĐŋҀҋĐēĐģадаĐŊĐŊŅ. ĐŽŅŅ‚Đ°ĐŊĐžŅžĐēа ĐŊŅ–ĐˇĐēĐ°ĐŗĐ° СĐŊĐ°Ņ‡ŅĐŊĐŊŅ ĐŧĐžĐļа ĐŋĐ°ŅžĐŋĐģŅ‹Đ˛Đ°Ņ†ŅŒ ĐŊа ŅĐēĐ°ŅŅ†ŅŒ ĐŧĐ°ŅˆŅ‹ĐŊĐŊĐ°ĐŗĐ° ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊŅ.", "image_preview_title": "НаĐģĐ°Đ´Ņ‹ ĐŋаĐŋŅŅ€ŅĐ´ĐŊŅĐŗĐ° ĐŋŅ€Đ°ĐŗĐģŅĐ´Ņƒ", "image_quality": "Đ¯ĐēĐ°ŅŅ†ŅŒ", "image_resolution": "Đ Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", + "image_resolution_description": "БоĐģҌ҈ Đ˛Ņ‹ŅĐžĐēĐ°Ņ Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ даСваĐģŅĐĩ ĐˇĐ°Ņ…Đ°Đ˛Đ°Ņ†ŅŒ йОĐģҌ҈ Đ´ŅŅ‚Đ°ĐģŅŅž, аĐģĐĩ ĐŋĐ°Ņ‚Ņ€Đ°ĐąŅƒĐĩ йОĐģҌ҈ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ĐēадаваĐŊĐŊŅ, ĐŋŅ€Ņ‹Đ˛ĐžĐ´ĐˇŅ–Ņ†ŅŒ да ĐŋĐ°Đ˛ŅĐģŅ–Ņ‡Đ˛Đ°ĐŊĐŊŅ ĐŋаĐŧĐĩŅ€Ņƒ Ņ„Đ°ĐšĐģĐ°Ņž Ņ– ĐŧĐžĐļа СĐŊŅ–ĐˇŅ–Ņ†ŅŒ Ņ…ŅƒŅ‚ĐēĐ°ŅŅ†ŅŒ Đ˛ĐžĐ´ĐŗŅƒĐē҃ Đ´Đ°Đ´Đ°Ņ‚Đē҃.", "image_settings": "НаĐģĐ°Đ´Ņ‹ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°", "image_settings_description": "ĐšŅ–Ņ€ŅƒĐšŅ†Đĩ ŅĐēĐ°ŅŅ†ŅŽ Ņ– Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŽ ŅĐŗĐĩĐŊĐĩŅ€Ņ‹Ņ€Đ°Đ˛Đ°ĐŊҋ҅ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž", "image_thumbnail_description": "МаĐģĐĩĐŊҌĐēĐ°Ņ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€Đ° С Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊŅ‹ĐŧŅ– ĐŧĐĩŅ‚Đ°Đ´Đ°Đ´ĐˇĐĩĐŊŅ‹ĐŧŅ–, ŅĐēĐ°Ņ Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ĐĩŅ†Ņ†Đ° ĐŋҀҋ ĐŋŅ€Đ°ĐŗĐģŅĐ´ĐˇĐĩ ĐŗŅ€ŅƒĐŋ Ņ„Đ°Ņ‚Đ°ĐŗŅ€Đ°Ņ„Ņ–Đš, Ņ‚Đ°ĐēŅ–Ņ… ŅĐē Đ°ŅĐŊĐžŅžĐŊĐ°Ņ Ņ…Ņ€ĐžĐŊŅ–Đēа", "image_thumbnail_quality_description": "Đ¯ĐēĐ°ŅŅ†ŅŒ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€ ад 1 да 100. Đ§Ņ‹Đŧ Đ˛Ņ‹ŅˆŅĐš ŅĐēĐ°ŅŅ†ŅŒ, ҂ҋĐŧ ĐģĐĩĐŋ҈, аĐģĐĩ ĐŋҀҋ ĐŗŅŅ‚Ņ‹Đŧ ŅŅ‚Đ˛Đ°Ņ€Đ°ŅŽŅ†Ņ†Đ° Ņ„Đ°ĐšĐģŅ‹ йОĐģŅŒŅˆĐ°ĐŗĐ° ĐŋаĐŧĐĩŅ€Ņƒ Ņ– ĐŧĐžĐļа СĐŊŅ–ĐˇŅ–Ņ†Ņ†Đ° Ņ…ŅƒŅ‚ĐēĐ°ŅŅ†ŅŒ Đ˛ĐžĐ´ĐŗŅƒĐē҃ ĐŋҀҋĐēĐģадаĐŊĐŊŅ.", "image_thumbnail_title": "НаĐģĐ°Đ´Ņ‹ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€", - "job_concurrency": "{job} ĐēаĐŊĐēŅƒŅ€ŅĐŊŅ‚ĐŊĐ°ŅŅ†ŅŒ", + "import_config_from_json_description": "ІĐŧĐŋĐ°Ņ€Ņ‚Đ°Đ˛Đ°Ņ†ŅŒ ĐēаĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ‹ŅŽ ŅŅ–ŅŅ‚ŅĐŧŅ‹ ĐŋŅ€Đ°Đˇ СаĐŋаĐŧĐŋĐžŅžĐ˛Đ°ĐŊĐŊĐĩ JSON Ņ„Đ°ĐšĐģа ĐŊĐ°ŅŅ‚Ņ€ĐžĐĩĐē", + "job_concurrency": "КоĐģҌĐēĐ°ŅŅ†ŅŒ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊҋ҅ ĐŋĐ°Ņ‚ĐžĐēĐ°Ņž СадаĐŊĐŊŅ {job}", "job_created": "ЗадаĐŊĐŊĐĩ ŅŅ‚Đ˛ĐžŅ€Đ°ĐŊа", - "job_not_concurrency_safe": "Đ“ŅŅ‚Đ° СадаĐŊĐŊĐĩ ĐŊĐĩĐąŅŅĐŋĐĩ҇ĐŊаĐĩ Đ´ĐģŅ ĐēаĐŊĐēŅƒŅ€ŅĐŊŅ‚ĐŊĐ°ĐŗĐ°(адĐŊĐ°Ņ‡Đ°ŅĐžĐ˛Đ°ĐŗĐ°, ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊĐ°ĐŗĐ°) Đ˛Ņ‹ĐēаĐŊаĐŊĐŊŅ.", + "job_not_concurrency_safe": "Đ“ŅŅ‚Đ° СадаĐŊĐŊĐĩ ĐŊĐĩĐąŅŅĐŋĐĩ҇ĐŊаĐĩ Đ´ĐģŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊĐ°ĐŗĐ° Đ˛Ņ‹ĐēаĐŊаĐŊĐŊŅ.", "job_settings": "НаĐģĐ°Đ´Ņ‹ СадаĐŊĐŊŅŅž", - "job_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°Ņ†ŅŒ ĐŊаĐģадаĐŧŅ– адĐŊĐ°Ņ‡Đ°ŅĐžĐ˛Đ°ĐŗĐ° (ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊĐ°ĐŗĐ°) Đ˛Ņ‹ĐēаĐŊаĐŊĐŊŅ СадаĐŊĐŊŅ", + "job_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°Ņ†ŅŒ ĐŊаĐģадаĐŧŅ– ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊĐ°ĐŗĐ° Đ˛Ņ‹ĐēаĐŊаĐŊĐŊŅ СадаĐŊĐŊŅŅž", "jobs_delayed": "{jobCount, plural, other {# адĐēĐģадСĐĩĐŊа}}", "jobs_failed": "{jobCount, plural, other {# ĐŊĐĩ Đ˛Ņ‹ĐēаĐŊаĐģĐ°ŅŅ}}", "library_created": "ĐĄŅ‚Đ˛ĐžŅ€Đ°ĐŊа ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēа: {library}", "library_deleted": "Đ‘Ņ–ĐąĐģŅ–ŅŅ‚ŅĐēа Đ˛Ņ‹Đ´Đ°ĐģĐĩĐŊа", + "library_details": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–", + "library_folder_description": "Đ’Ņ‹ĐˇĐŊĐ°Ņ‡Ņ†Đĩ ĐŋаĐŋĐē҃ Đ´ĐģŅ Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Ņƒ. Đ“ŅŅ‚Đ° ĐŋаĐŋĐēа, ҃ĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Ņ‹ ĐŋадĐŋаĐŋĐēŅ–, ĐąŅƒĐ´ĐˇĐĩ ĐŋŅ€Đ°ŅĐēаĐŊаваĐŊа ĐŊа ĐŊĐ°ŅŅžĐŊĐ°ŅŅ†ŅŒ Ņ„ĐžŅ‚Đ° Ņ– Đ˛Ņ–Đ´ŅĐ°.", + "library_remove_exclusion_pattern_prompt": "Đ’Ņ‹ ҃ĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐŗŅŅ‚Ņ‹ ŅˆĐ°ĐąĐģĐžĐŊ Đ˛Ņ‹ĐēĐģŅŽŅ‡ŅĐŊĐŊŅ?", + "library_remove_folder_prompt": "Đ’Ņ‹ ҃ĐŋŅŅžĐŊĐĩĐŊŅ‹, ŅˆŅ‚Đž Ņ…ĐžŅ‡Đ°Ņ†Đĩ Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ ĐŗŅŅ‚Ņƒ ĐŋаĐŋĐē҃ Ņ–ĐŧĐŋĐ°Ņ€Ņ‚Ņƒ?", "library_scanning": "ĐĄĐēаĐŊаваĐŊĐŊĐĩ Đŋа Ņ€Đ°ŅĐēĐģадСĐĩ", "library_scanning_description": "НаĐģĐ°Đ´ĐˇŅŒŅ†Đĩ ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ҁĐēаĐŊаваĐŊĐŊŅ Đ˛Đ°ŅˆĐ°Đš ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–", - "library_scanning_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ҁĐēаĐŊаваĐŊĐŊĐĩ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ– Đŋа Ņ€Đ°ŅĐēĐģадСĐĩ", + "library_scanning_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ĐŋĐĩŅ€Ņ‹ŅĐ´Ņ‹Ņ‡ĐŊаĐĩ ҁĐēаĐŊаваĐŊĐŊĐĩ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–", "library_settings": "ЗĐŊĐĩ҈ĐŊŅŅ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēа", "library_settings_description": "НаĐģĐ°Đ´ĐˇŅŒŅ†Đĩ ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ СĐŊĐĩ҈ĐŊŅĐš ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ–", "library_tasks_description": "ĐĄĐēаĐŊĐ°Đ˛Đ°Ņ†ŅŒ СĐŊĐĩ҈ĐŊŅ–Ņ ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēŅ– ĐŊа ĐŊĐ°ŅŅžĐŊĐ°ŅŅ†ŅŒ ĐŊĐžĐ˛Ņ‹Ņ… Ņ–/айО СĐŧĐĩĐŊĐĩĐŊҋ҅ Ņ€ŅŅŅƒŅ€ŅĐ°Ņž", + "library_updated": "Đ‘Ņ–ĐąĐģŅ–ŅŅ‚ŅĐēа айĐŊĐžŅžĐģĐĩĐŊа", "library_watching_enable_description": "ĐĐ°ĐˇŅ–Ņ€Đ°Ņ†ŅŒ Са СĐŧĐĩĐŊаĐŧŅ– Ņ„Đ°ĐšĐģĐ°Ņž ҃ СĐŊĐĩ҈ĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēĐ°Ņ…", - "library_watching_settings": "ĐĄĐ°Ņ‡Ņ‹Ņ†ŅŒ Са ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēаК (ŅĐēҁĐŋĐĩҀҋĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊŅ‹)", + "library_watching_settings": "[ЭКСПЕРĐĢМЕНĐĸАЛĐŦНА] ĐĄĐ°Ņ‡Ņ‹Ņ†ŅŒ Са ĐąŅ–ĐąĐģŅ–ŅŅ‚ŅĐēаК", "library_watching_settings_description": "ĐŅžŅ‚Đ°ĐŧĐ°Ņ‚Ņ‹Ņ‡ĐŊа ŅĐ°Ņ‡Ņ‹Ņ†ŅŒ Са СĐŧĐĩĐŊаĐŧŅ– Ņž Ņ„Đ°ĐšĐģĐ°Ņ…", "logging_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Đ˛ŅĐ´ĐˇĐĩĐŊĐŊĐĩ ĐļŅƒŅ€ĐŊаĐģа", "logging_level_description": "КаĐģŅ– ҃ĐēĐģŅŽŅ‡Đ°ĐŊа, ŅĐēŅ– ŅžĐˇŅ€ĐžĐ˛ĐĩĐŊҌ ĐļŅƒŅ€ĐŊаĐģŅĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°Ņ†ŅŒ.", "logging_settings": "Đ’ŅĐ´ĐˇĐĩĐŊĐŊĐĩ ĐļŅƒŅ€ĐŊаĐģа", + "machine_learning_availability_checks": "ĐŸŅ€Đ°Đ˛ĐĩŅ€Đēа Đ´Đ°ŅŅ‚ŅƒĐŋĐŊĐ°ŅŅ†Ņ–", + "machine_learning_availability_checks_description": "ĐŅžŅ‚Đ°ĐŧĐ°Ņ‚Ņ‹Ņ‡ĐŊа Đ˛Ņ‹ŅŅžĐģŅŅ†ŅŒ Ņ– ĐŊĐ°Đ´Đ°Đ˛Đ°Ņ†ŅŒ ĐŋĐĩŅ€Đ°Đ˛Đ°ĐŗŅƒ Đ´Đ°ŅŅ‚ŅƒĐŋĐŊŅ‹Đŧ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°Đŧ ĐŧĐ°ŅˆŅ‹ĐŊĐŊĐ°ĐŗĐ° ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊŅ", + "machine_learning_availability_checks_enabled": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ĐŋŅ€Đ°Đ˛ĐĩŅ€Đē҃ Đ´Đ°ŅŅ‚ŅƒĐŋĐŊĐ°ŅŅ†Ņ–", + "machine_learning_availability_checks_interval": "ІĐŊŅ‚ŅŅ€Đ˛Đ°Đģ ĐŋŅ€Đ°Đ˛ĐĩŅ€ĐēŅ–", + "machine_learning_availability_checks_interval_description": "ІĐŊŅ‚ŅŅ€Đ˛Đ°Đģ ҃ ĐŧŅ–ĐģҖҁĐĩĐē҃ĐŊĐ´Đ°Ņ… ĐŋаĐŧŅ–Đļ ĐŋŅ€Đ°Đ˛ĐĩŅ€ĐēаĐŧŅ– Đ´Đ°ŅŅ‚ŅƒĐŋĐŊĐ°ŅŅ†Ņ–", + "machine_learning_availability_checks_timeout": "Đ§Đ°Ņ Ņ‡Đ°ĐēаĐŊĐŊŅ СаĐŋŅ‹Ņ‚Ņƒ", + "machine_learning_availability_checks_timeout_description": "Đ§Đ°Ņ Ņ‡Đ°ĐēаĐŊĐŊŅ Ņž ĐŧŅ–ĐģҖҁĐĩĐē҃ĐŊĐ´Đ°Ņ… Đ´ĐģŅ ĐŋŅ€Đ°Đ˛ĐĩŅ€ĐēŅ– Đ´Đ°ŅŅ‚ŅƒĐŋĐŊĐ°ŅŅ†Ņ–", "machine_learning_clip_model": "CLIP ĐŧĐ°Đ´ŅĐģҌ", "machine_learning_clip_model_description": "Назва CLIP ĐŧĐ°Đ´ŅĐģŅ– ĐŋаĐēаСаĐŊа Ņ‚ŅƒŅ‚. Đ—Đ˛ŅŅ€ĐŊҖ҆Đĩ ŅžĐ˛Đ°ĐŗŅƒ, ŅˆŅ‚Đž ĐŋҀҋ СĐŧĐĩĐŊĐĩ ĐŧĐ°Đ´ŅĐģŅ– ĐŊĐĩĐ°ĐąŅ…ĐžĐ´ĐŊа ĐŋĐ°ŅžŅ‚ĐžŅ€ĐŊа СаĐŋŅƒŅŅ†Ņ–Ņ†ŅŒ СадаĐŊĐŊĐĩ \"Smart Search\" Đ´ĐģŅ ŅžŅŅ–Ņ… Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž.", "machine_learning_duplicate_detection": "Đ’Ņ‹ŅŅžĐģĐĩĐŊĐŊĐĩ ĐŋадОйĐŊҋ҅", + "machine_learning_duplicate_detection_enabled": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Đ˛Ņ‹ŅŅžĐģĐĩĐŊĐŊĐĩ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Ņž", + "machine_learning_duplicate_detection_enabled_description": "КаĐģŅ– адĐēĐģŅŽŅ‡Đ°ĐŊа, Đ°ĐąŅĐ°ĐģŅŽŅ‚ĐŊа Ņ–Đ´ŅĐŊ҂ҋ҇ĐŊŅ‹Ņ Ņ„Đ°ĐšĐģŅ‹ ŅžŅŅ‘ Ņ€ĐžŅžĐŊа ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ†ŅŒ СаĐŋаĐŧĐŋĐžŅžĐ˛Đ°Ņ†Ņ†Đ°.", + "machine_learning_duplicate_detection_setting_description": "Đ’Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐŊĐŊĐĩ ŅžĐąŅƒĐ´Đ°Đ˛Đ°ĐŊĐŊŅŅž CLIP Đ´ĐģŅ ĐŋĐžŅˆŅƒĐē҃ вĐĩŅ€Đ°ĐŗĐžĐ´ĐŊҋ҅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Ņž", + "machine_learning_enabled": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ ĐŧĐ°ŅˆŅ‹ĐŊĐŊаĐĩ ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊĐĩ", + "machine_learning_enabled_description": "КаĐģŅ– адĐēĐģŅŽŅ‡Đ°ĐŊа, ҃ҁĐĩ Ņ„ŅƒĐŊĐē҆ҋҖ ĐŧĐ°ŅˆŅ‹ĐŊĐŊĐ°ĐŗĐ° ĐŊĐ°Đ˛ŅƒŅ‡Đ°ĐŊĐŊŅ ĐąŅƒĐ´ŅƒŅ†ŅŒ адĐēĐģŅŽŅ‡Đ°ĐŊŅ‹ ĐŊĐĩСаĐģĐĩĐļĐŊа ад ĐŊаĐģад ĐŊŅ–ĐļŅĐš.", + "machine_learning_facial_recognition": "Đ Đ°ŅĐŋаСĐŊаваĐŊĐŊĐĩ Ņ‚Đ˛Đ°Ņ€Đ°Ņž", + "machine_learning_facial_recognition_description": "Đ’Ņ‹ŅŅžĐģĐĩĐŊĐŊĐĩ, Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊĐĩ Ņ– ĐŗŅ€ŅƒĐŋаваĐŊĐŊĐĩ Ņ‚Đ˛Đ°Ņ€Đ°Ņž ĐŊа Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņ…", + "machine_learning_facial_recognition_model": "ĐœĐ°Đ´ŅĐģҌ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊŅ Ņ‚Đ˛Đ°Ņ€Đ°Ņž", + "machine_learning_facial_recognition_model_description": "ĐœĐ°Đ´ŅĐģŅ– ĐŋĐĩŅ€Đ°ĐģŅ–Ņ‡Đ°ĐŊŅ‹ Ņž ĐŋĐ°Ņ€Đ°Đ´Đē҃ ŅžĐąŅ‹Đ˛Đ°ĐŊĐŊŅ Ņ–Ņ… ĐŋаĐŧĐĩŅ€Ņƒ. БоĐģŅŒŅˆŅ‹Ņ ĐŧĐ°Đ´ŅĐģŅ– ĐŋавОĐģҌĐŊĐĩĐš Ņ– Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ŅŽŅ†ŅŒ йОĐģҌ҈ ĐŋаĐŧŅŅ†Ņ–, аĐģĐĩ Đ´Đ°ŅŽŅ†ŅŒ ĐģĐĩĐŋŅˆŅ‹Ņ Đ˛Ņ‹ĐŊŅ–ĐēŅ–. Đ—Đ˛ŅŅ€ĐŊҖ҆Đĩ ŅƒĐ˛Đ°ĐŗŅƒ, ŅˆŅ‚Đž ĐŋĐ°ŅĐģŅ СĐŧĐĩĐŊŅ‹ ĐŧĐ°Đ´ŅĐģŅ– Ņ‚Ņ€ŅĐąĐ° СĐŊĐžŅž СаĐŋŅƒŅŅ†Ņ–Ņ†ŅŒ СадаĐŊĐŊĐĩ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊŅ Ņ‚Đ˛Đ°Ņ€Đ°Ņž Đ´ĐģŅ ŅžŅŅ–Ņ… Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ°Ņž.", + "machine_learning_facial_recognition_setting": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊĐĩ Ņ‚Đ˛Đ°Ņ€Đ°Ņž", + "machine_learning_facial_recognition_setting_description": "КаĐģŅ– адĐēĐģŅŽŅ‡Đ°ĐŊа, Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅŅ‹ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ†ŅŒ ĐēĐ°Đ´Đ°Đ˛Đ°Ņ†Ņ†Đ° Đ´ĐģŅ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊŅ Ņ‚Đ˛Đ°Ņ€Đ°Ņž, Ņ– ĐŊĐĩ ĐąŅƒĐ´ĐˇĐĩ СаĐŋĐ°ŅžĐŊŅŅ†Ņ†Đ° Ņ€Đ°ĐˇĐ´ĐˇĐĩĐģ \"Đ›ŅŽĐ´ĐˇŅ–\" ĐŊа ŅŅ‚Đ°Ņ€ĐžĐŊ҆ҋ \"ĐĐŗĐģŅĐ´\".", + "machine_learning_ocr_max_resolution": "МаĐēҁҖĐŧаĐģҌĐŊĐ°Ņ Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", + "machine_learning_ocr_max_resolution_description": "Đ’Ņ–Đ´Đ°Ņ€Ņ‹ŅŅ‹ С Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŽ йОĐģҌ҈ ĐŗŅŅ‚Đ°Đš ĐąŅƒĐ´ŅƒŅ†ŅŒ ĐŋаĐŧĐĩĐŊŅˆĐ°ĐŊŅ‹ С ĐˇĐ°Ņ…Đ°Đ˛Đ°ĐŊĐŊĐĩĐŧ ŅŅƒĐ°Đ´ĐŊĐžŅŅ–ĐŊŅ‹ йаĐēĐžŅž. БоĐģҌ҈ Đ˛Ņ‹ŅĐžĐēŅ–Ņ СĐŊĐ°Ņ‡ŅĐŊĐŊŅ– ĐŋĐ°Đ˛Ņ‹ŅˆĐ°ŅŽŅ†ŅŒ даĐēĐģадĐŊĐ°ŅŅ†ŅŒ Ņ€Đ°ŅĐŋаСĐŊаваĐŊĐŊŅ, аĐģĐĩ ĐŋĐ°Ņ‚Ņ€Đ°ĐąŅƒŅŽŅ†ŅŒ йОĐģҌ҈ Ņ‡Đ°ŅŅƒ ĐŊа аĐŋŅ€Đ°Ņ†ĐžŅžĐē҃ Ņ– Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚ĐžŅžĐ˛Đ°ŅŽŅ†ŅŒ йОĐģҌ҈ ĐŋаĐŧŅŅ†Ņ–.", "map_dark_style": "ĐĻŅ‘ĐŧĐŊŅ‹ ҁ҂ҋĐģҌ", "map_enable_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ„ŅƒĐŊĐē҆ҋҖ ĐēĐ°Ņ€Ņ‚Ņ‹", "map_gps_settings": "НаĐģĐ°Đ´Ņ‹ ĐēĐ°Ņ€Ņ‚Ņ‹ Ņ– GPS", @@ -128,6 +169,7 @@ "map_settings": "ĐšĐ°Ņ€Ņ‚Đ°", "map_settings_description": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŊаĐģадаĐŧŅ– ĐēĐ°Ņ€Ņ‚Ņ‹", "map_style_description": "URL-Đ°Đ´Ņ€Đ°Ņ style.json Ņ‚ŅĐŧŅ‹ ĐēĐ°Ņ€Ņ‚Ņ‹", + "metadata_extraction_job_description": "Đ’Ņ‹ĐŊŅŅ†ŅŒ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ‹Ņ С Ņ„Đ°ĐšĐģĐ°Ņž, Ņ‚Đ°ĐēŅ–Ņ ŅĐē ĐŧĐĩŅŅ†Đ°ĐˇĐŊĐ°Ņ…ĐžĐ´ĐļаĐŊĐŊĐĩ, Ņ‚Đ˛Đ°Ņ€Ņ‹ Ņ– Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", "metadata_settings": "НаĐģĐ°Đ´Ņ‹ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊҋ҅", "oauth_button_text": "ĐĸŅĐēҁ҂ ĐēĐŊĐžĐŋĐēŅ–", "oauth_settings": "OAuth", @@ -153,7 +195,11 @@ "transcoding_accepted_video_codecs": "ĐŸŅ€Ņ‹ĐŊŅŅ‚Ņ‹Ņ Đ˛Ņ–Đ´ŅĐ°ĐēĐžĐ´ŅĐēŅ–", "transcoding_advanced_options_description": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ, ŅĐēŅ–Ņ йОĐģŅŒŅˆĐ°ŅŅ†Ņ– ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–ĐēĐ°Ņž ĐŊĐĩ Ņ‚Ņ€ŅĐąĐ° СĐŧŅĐŊŅŅ†ŅŒ", "transcoding_audio_codec": "ĐŅƒĐ´Ņ‹ŅĐēĐžĐ´ŅĐē", - "transcoding_encoding_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐēĐ°Đ´ĐˇŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊŅ", + "transcoding_encoding_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐēадаваĐŊĐŊŅ", + "transcoding_encoding_options_description": "Đ—Đ°Đ´Đ°ĐšŅ†Đĩ ĐēĐžĐ´ŅĐēŅ–, Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ, ŅĐēĐ°ŅŅ†ŅŒ Ņ– Ņ–ĐŊŅˆŅ‹Ņ ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ Đ´ĐģŅ ĐēадаваĐŊĐŊŅ Đ˛Ņ–Đ´ŅĐ°", + "transcoding_optimal_description": "Đ’Ņ–Đ´ŅĐ° С Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŽ Đ˛Ņ‹ŅˆŅĐš ĐŧŅŅ‚Đ°Đ˛Đ°Đš ҆Җ Ņž ĐŊĐĩĐŋҀҋĐŊŅŅ‚Ņ‹Đŧ Ņ„Đ°Ņ€ĐŧĐ°Ņ†Đĩ", + "transcoding_target_resolution": "ĐœŅŅ‚Đ°Đ˛Đ°Ņ Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", + "transcoding_target_resolution_description": "Đ’Ņ‹ŅˆŅĐšŅˆŅ‹Ņ Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†Ņ– ĐŧĐžĐŗŅƒŅ†ŅŒ ĐˇĐ°Ņ…Đ°Đ˛Đ°Ņ†ŅŒ йОĐģҌ҈ Đ´ŅŅ‚Đ°ĐģĐĩĐš, аĐģĐĩ ĐŋĐ°Ņ‚Ņ€Đ°ĐąŅƒŅŽŅ†ŅŒ йОĐģҌ҈ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ĐēадаваĐŊĐŊŅ, ĐŧĐ°ŅŽŅ†ŅŒ йОĐģŅŒŅˆŅ‹ ĐŋаĐŧĐĩŅ€ Ņ„Đ°ĐšĐģĐ°Ņž Ņ– ĐŧĐžĐŗŅƒŅ†ŅŒ СĐŧĐĩĐŊŅˆŅ‹Ņ†ŅŒ Ņ…ŅƒŅ‚ĐēĐ°ŅŅ†ŅŒ адĐēĐ°ĐˇŅƒ ĐŋŅ€Đ°ĐŗŅ€Đ°ĐŧŅ‹.", "transcoding_video_codec": "Đ’Ņ–Đ´ŅĐ°ĐēĐžĐ´ŅĐē", "trash_enabled_description": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Ņ„ŅƒĐŊĐē҆ҋҖ ҁĐŧĐĩŅ‚ĐŊҖ҆ҋ", "trash_number_of_days": "КоĐģҌĐēĐ°ŅŅ†ŅŒ Đ´ĐˇŅ‘ĐŊ", @@ -179,7 +225,7 @@ "administration": "ĐšŅ–Ņ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°Đŧ", "advanced": "ĐŸĐ°ŅˆŅ‹Ņ€Đ°ĐŊŅ‹Ņ", "advanced_settings_log_level_title": "ĐŖĐˇŅ€ĐžĐ˛ĐĩĐŊҌ Đ˛ŅĐ´ĐˇĐĩĐŊĐŊŅ ĐļŅƒŅ€ĐŊаĐģа: {level}", - "advanced_settings_proxy_headers_title": "Đ—Đ°ĐŗĐ°ĐģĐžŅžĐēŅ– ĐŋŅ€ĐžĐēҁҖ", + "advanced_settings_proxy_headers_title": "[ЭКСПЕРĐĢМЕНĐĸАЛĐŦНА] ĐŖĐģĐ°ŅĐŊŅ‹Ņ ĐˇĐ°ĐŗĐ°ĐģĐžŅžĐēŅ– ĐŋŅ€ĐžĐēҁҖ", "advanced_settings_tile_subtitle": "ĐŸĐ°ŅˆŅ‹Ņ€Đ°ĐŊŅ‹Ņ ĐŊаĐģĐ°Đ´Ņ‹ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", "advanced_settings_troubleshooting_subtitle": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†ŅŒ Đ´Đ°Đ´Đ°Ņ‚ĐēĐžĐ˛Ņ‹Ņ Ņ„ŅƒĐŊĐē҆ҋҖ Đ´ĐģŅ Đ˛Ņ‹ĐŋŅ€Đ°ŅžĐģĐĩĐŊĐŊŅ ĐŊĐĩĐŋаĐģадаĐē", "advanced_settings_troubleshooting_title": "Đ’Ņ‹ĐŋŅ€Đ°ŅžĐģĐĩĐŊĐŊĐĩ ĐŊĐĩĐŋаĐģадаĐē", @@ -326,16 +372,14 @@ "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": "Đ”Đ°Đ´Đ°Ņ†ŅŒ аĐŋŅ–ŅĐ°ĐŊĐŊĐĩ...", + "explore": "ĐĐŗĐģŅĐ´", "favorite": "ĐŖ Đ°ĐąŅ€Đ°ĐŊŅ‹Đŧ", "favorite_or_unfavorite_photo": "Đ”Đ°Đ´Đ°Ņ†ŅŒ айО Đ˛Ņ‹Đ´Đ°ĐģŅ–Ņ†ŅŒ Ņ„ĐžŅ‚Đ° С Đ°ĐąŅ€Đ°ĐŊĐ°ĐŗĐ°", "favorites": "ĐĐąŅ€Đ°ĐŊŅ‹Ņ", - "file_name": "Назва Ņ„Đ°ĐšĐģа", "filename": "Назва Ņ„Đ°ĐšĐģа", "filetype": "ĐĸŅ‹Đŋ Ņ„Đ°ĐšĐģа", "filter": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€", @@ -413,7 +457,7 @@ "reassign": "ПĐĩŅ€Đ°ĐŋŅ€Ņ‹ĐˇĐŊĐ°Ņ‡Ņ‹Ņ†ŅŒ", "reassing_hint": "ĐŸŅ€Ņ‹ĐŋŅ–ŅĐ°Ņ†ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊŅ‹Ņ аĐēŅ‚Ņ‹Đ˛Ņ‹ ҖҁĐŊŅƒŅŽŅ‡Đ°Đš Đ°ŅĐžĐąĐĩ", "recent": "ĐŅĐ´Đ°ŅžĐŊŅ–", - "recent-albums": "ĐŅĐ´Đ°ŅžĐŊŅ–Ņ аĐģŅŒĐąĐžĐŧŅ‹", + "recent_albums": "ĐŅĐ´Đ°ŅžĐŊŅ–Ņ аĐģŅŒĐąĐžĐŧŅ‹", "recent_searches": "ĐŅĐ´Đ°ŅžĐŊŅ–Ņ ĐŋĐžŅˆŅƒĐēŅ–", "recently_added": "ĐŅĐ´Đ°ŅžĐŊа дададСĐĩĐŊа", "refresh_faces": "АйĐŊĐ°Đ˛Ņ–Ņ†ŅŒ Ņ‚Đ˛Đ°Ņ€Ņ‹", @@ -427,6 +471,7 @@ "repository": "Đ ŅĐŋĐ°ĐˇŅ–Ņ‚ĐžŅ€Ņ‹Đš", "reset": "ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ", "reset_password": "ĐĄĐēŅ–ĐŊŅƒŅ†ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", + "resolution": "Đ Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†ŅŒ", "restore": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ", "restore_all": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ ŅƒŅŅ‘", "restore_user": "АдĐŊĐ°Đ˛Ņ–Ņ†ŅŒ ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐģҌĐŊŅ–Đēа", @@ -447,6 +492,8 @@ "search_page_your_map": "Đ’Đ°ŅˆĐ° ĐēĐ°Ņ€Ņ‚Đ°", "second": "ĐĄĐĩĐē҃ĐŊда", "send_message": "АдĐŋŅ€Đ°Đ˛Ņ–Ņ†ŅŒ ĐŋавĐĩдаĐŧĐģĐĩĐŊĐŊĐĩ", + "setting_image_viewer_original_subtitle": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†Đĩ Đ´ĐģŅ СаĐŋаĐŧĐŋаваĐŊĐŊŅ ĐˇŅ‹Ņ…ĐžĐ´ĐŊĐ°ĐŗĐ° Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ° ҃ ĐŋĐžŅžĐŊаК Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†Ņ– (҈ĐŧĐ°Ņ‚!). АдĐēĐģŅŽŅ‡Ņ‹Ņ†Đĩ Đēай СĐŧĐĩĐŊŅˆŅ‹Ņ†ŅŒ Đ˛Ņ‹ĐēĐ°Ņ€Ņ‹ŅŅ‚Đ°ĐŊĐŊĐĩ Ņ‚Ņ€Đ°Ņ„Ņ–Đēа (ŅĐē ҁĐĩŅ‚ĐēŅ–, Ņ‚Đ°Đē Ņ– ĐēŅŅˆĐ° ĐŋҀҋĐģĐ°Đ´Ņ‹).", + "setting_image_viewer_preview_subtitle": "ĐŖĐēĐģŅŽŅ‡Ņ‹Ņ†Đĩ Đ´ĐģŅ СаĐŋаĐŧĐŋаваĐŊĐŊŅ Đ˛Ņ–Đ´Đ°Ņ€Ņ‹ŅĐ° ŅŅŅ€ŅĐ´ĐŊŅĐš Ņ€Đ°ĐˇĐ´ĐˇŅĐģŅĐģҌĐŊĐ°ŅŅ†Ņ–. АдĐēĐģŅŽŅ‡Ņ‹Ņ†Đĩ, Đēай ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°Ņ†ŅŒ Ņ‚ĐžĐģҌĐēŅ– Đ°Ņ€Ņ‹ĐŗŅ–ĐŊаĐģ ҆Җ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€Ņƒ.", "setting_languages_apply": "ĐŖĐļŅ‹Ņ†ŅŒ", "setting_notifications_notify_never": "ĐŊŅ–ĐēĐžĐģŅ–", "settings": "НаĐģĐ°Đ´Ņ‹", @@ -498,7 +545,7 @@ "video_hover_setting": "ĐŸŅ€Đ°ĐšĐŗŅ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€Ņ‹ Đ˛Ņ–Đ´ŅĐ° ĐŋҀҋ ĐŊĐ°Đ˛ŅĐ´ĐˇĐĩĐŊĐŊŅ– ĐēŅƒŅ€ŅĐžŅ€Đ°", "video_hover_setting_description": "ĐŸŅ€Đ°ĐšĐŗŅ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŧŅ–ĐŊŅ–ŅŅ†ŅŽŅ€Ņ‹ Đ˛Ņ–Đ´ŅĐ° ĐŋҀҋ ĐŊĐ°Đ˛ŅĐ´ĐˇĐĩĐŊĐŊŅ– ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа ŅĐģĐĩĐŧĐĩĐŊŅ‚. ĐĐ°Đ˛Đ°Ņ‚ ĐēаĐģŅ– Ņ„ŅƒĐŊĐēŅ†Ņ‹Ņ адĐēĐģŅŽŅ‡Đ°ĐŊа, ĐŋŅ€Đ°ĐšĐŗŅ€Đ°Đ˛Đ°ĐŊĐŊĐĩ ĐŧĐžĐļĐŊа ĐŋĐ°Ņ‡Đ°Ņ†ŅŒ, ĐŊĐ°Đ˛Ņ‘ŅžŅˆŅ‹ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа СĐŊĐ°Ņ‡ĐžĐē ĐŋŅ€Đ°ĐšĐŗŅ€Đ°Đ˛Đ°ĐŊĐŊŅ.", "videos": "Đ’Ņ–Đ´ŅĐ°", - "videos_count": "{count, plural, one {# Đ˛Ņ–Đ´ŅĐ°} Đ°ŅŅ‚Đ°Ņ‚ĐŊŅ–Ņ {# Đ˛Ņ–Đ´ŅĐ°}}", + "videos_count": "{count, plural, one {# Đ˛Ņ–Đ´ŅĐ°} other {# Đ˛Ņ–Đ´ŅĐ°}}", "view": "ĐŸŅ€Đ°ĐŗĐģŅĐ´", "view_album": "ĐŸŅ€Đ°ĐŗĐģŅĐ´ĐˇĐĩŅ†ŅŒ аĐģŅŒĐąĐžĐŧ", "view_all": "ĐŸŅ€Đ°ĐŗĐģŅĐ´ĐˇĐĩŅ†ŅŒ ŅƒŅŅ‘", diff --git a/i18n/bg.json b/i18n/bg.json index 0bf54f1ee7..0d39878cad 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -5,6 +5,7 @@ "acknowledge": "ĐŸĐžŅ‚Đ˛ŅŠŅ€ĐļдаваĐŧ", "action": "ДĐĩĐšŅŅ‚Đ˛Đ¸Đĩ", "action_common_update": "ОбĐŊОви", + "action_description": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ Са иСĐŋҊĐģĐŊĐĩĐŊиĐĩ ҁ Ņ„Đ¸ĐģŅ‚Ņ€Đ¸Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ ОйĐĩĐēŅ‚Đ¸", "actions": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ", "active": "АĐēŅ‚Đ¸Đ˛ĐŊи", "active_count": "АĐēŅ‚Đ¸Đ˛ĐŊи: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Добави ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "add_a_name": "Добави иĐŧĐĩ", "add_a_title": "Добaви ĐˇĐ°ĐŗĐģавиĐĩ", + "add_action": "Добави Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ", + "add_action_description": "ĐĐ°Ņ‚Đ¸ŅĐŊĐĩŅ‚Đĩ Са да Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚Đĩ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ", + "add_assets": "Đ”ĐžĐąĐ°Đ˛ŅĐŊĐĩ ĐŊа ОйĐĩĐēŅ‚Đ¸", "add_birthday": "Добави Đ´Đ°Ņ‚Đ° ĐŊа Ņ€Đ°ĐļдаĐŊĐĩ", "add_endpoint": "Добави ĐēŅ€Đ°ĐšĐŊа Ņ‚ĐžŅ‡Đēа", "add_exclusion_pattern": "Добави ĐŧОдĐĩĐģ Са иСĐēĐģŅŽŅ‡Đ˛Đ°ĐŊĐĩ", + "add_filter": "Добави Ņ„Đ¸ĐģŅ‚ŅŠŅ€", + "add_filter_description": "ĐĐ°Ņ‚Đ¸ŅĐŊĐĩŅ‚Đĩ Са да Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚Đĩ ҃ҁĐģОвиĐĩ Са Ņ„Đ¸ĐģŅ‚ŅŠŅ€", "add_location": "Дoйави ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "add_more_users": "Добави ĐžŅ‰Đĩ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи", "add_partner": "Добави ĐŋĐ°Ņ€Ņ‚ĐŊŅŒĐžŅ€", @@ -36,6 +42,7 @@ "add_to_shared_album": "Добави ĐēҊĐŧ ҁĐŋОдĐĩĐģĐĩĐŊ аĐģĐąŅƒĐŧ", "add_upload_to_stack": "Добави ĐēĐ°Ņ‡ĐĩĐŊĐ¸Ņ‚Đĩ в ĐŗŅ€ŅƒĐŋа", "add_url": "Добави URL", + "add_workflow_step": "Добави ŅŅ‚ŅŠĐŋĐēа ĐžŅ‚ Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", "added_to_archive": "ДобавĐĩĐŊĐž ĐēҊĐŧ Đ°Ņ€Ņ…Đ¸Đ˛Đ°", "added_to_favorites": "ДобавĐĩĐŊи ĐēҊĐŧ ĐģŅŽĐąĐ¸ĐŧĐ¸Ņ‚Đĩ ви", "added_to_favorites_count": "ДобавĐĩĐŊи {count, number} ĐēҊĐŧ ĐģŅŽĐąĐ¸Đŧи", @@ -97,6 +104,8 @@ "image_preview_description": "ĐĄŅ€ĐĩĐ´ĐĩĐŊ Ņ€Đ°ĐˇĐŧĐĩŅ€ ĐŊа Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩŅ‚Đž ҁ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊи, иСĐŋĐžĐģСваĐŊĐž ĐŋŅ€Đ¸ ĐŋŅ€ĐĩĐŗĐģĐĩĐ´ ĐŊа ĐĩдиĐŊ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ и Са ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐž ĐžĐąŅƒŅ‡ĐĩĐŊиĐĩ", "image_preview_quality_description": "ĐšĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž ĐŊа ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģĐŊĐ¸Ņ ĐŋŅ€ĐĩĐŗĐģĐĩĐ´ ĐžŅ‚ 1 Đ´Đž 100. По-Đ˛Đ¸ŅĐžĐēĐ°Ņ‚Đ° ŅŅ‚ĐžĐšĐŊĐžŅŅ‚ Đĩ ĐŋĐž-Đ´ĐžĐąŅ€Đ°, ĐŊĐž вОди Đ´Đž ĐŋĐž-ĐŗĐžĐģĐĩĐŧи Ņ„Đ°ĐšĐģОвĐĩ и ĐŧĐžĐļĐĩ да ĐŊаĐŧаĐģи ĐąŅŠŅ€ĐˇĐžĐ´ĐĩĐšŅŅ‚Đ˛Đ¸ĐĩŅ‚Đž ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž. ЗадаваĐŊĐĩŅ‚Đž ĐŊа ĐŊĐ¸ŅĐēа ŅŅ‚ĐžĐšĐŊĐžŅŅ‚ ĐŧĐžĐļĐĩ да ĐŋОвĐģĐ¸ŅĐĩ ĐŊа ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛ĐžŅ‚Đž ĐŊа ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžŅ‚Đž ĐžĐąŅƒŅ‡ĐĩĐŊиĐĩ.", "image_preview_title": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐŊа ĐŋŅ€ĐĩĐŗĐģĐĩда", + "image_progressive": "ĐŸŅ€ĐžĐŗŅ€ĐĩŅĐ¸Đ˛ĐĩĐŊ JPEG", + "image_progressive_description": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸ŅŅ‚Đ°, ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊи в ĐŋŅ€ĐžĐŗŅ€ĐĩŅĐ¸Đ˛ĐĩĐŊ JPEG Ņ„ĐžŅ€ĐŧĐ°Ņ‚, ҁĐĩ ĐˇĐ°Ņ€ĐĩĐļĐ´Đ°Ņ‚ ĐŋĐž-ĐąŅŠŅ€ĐˇĐž, ҁ ĐŋĐžŅŅ‚ĐĩĐŋĐĩĐŊĐŊĐž ĐŋĐžĐ´ĐžĐąŅ€ŅĐ˛Đ°Ņ‰Đž ҁĐĩ ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž. ĐĸОва ĐŊŅĐŧа вĐģĐ¸ŅĐŊиĐĩ ĐŊа ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ ĐēĐ°Ņ‚Đž WebP Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ.", "image_quality": "ĐšĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž", "image_resolution": "Đ ĐĩСОĐģŅŽŅ†Đ¸Ņ", "image_resolution_description": "По-Đ˛Đ¸ŅĐžĐēĐ¸Ņ‚Đĩ Ņ€ĐĩСОĐģŅŽŅ†Đ¸Đ¸ ĐŧĐžĐŗĐ°Ņ‚ да СаĐŋĐ°ĐˇŅŅ‚ ĐŋОвĐĩ҇Đĩ Đ´ĐĩŅ‚Đ°ĐšĐģи, ĐŊĐž Đ¸ĐˇĐ¸ŅĐēĐ˛Đ°Ņ‚ ĐŋОвĐĩ҇Đĩ Đ˛Ņ€ĐĩĐŧĐĩ Са ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩ, иĐŧĐ°Ņ‚ ĐŋĐž-ĐŗĐžĐģĐĩĐŧи Ņ€Đ°ĐˇĐŧĐĩŅ€Đ¸ ĐŊа Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ и ĐŧĐžĐŗĐ°Ņ‚ да ĐŊаĐŧаĐģŅŅ‚ ĐąŅŠŅ€ĐˇĐžĐ´ĐĩĐšŅŅ‚Đ˛Đ¸ĐĩŅ‚Đž ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "ВĐēĐģŅŽŅ‡Đ˛Đ°ĐŊĐĩ ĐŊа ИĐŊŅ‚ĐĩĐģĐ¸ĐŗĐĩĐŊŅ‚ĐŊĐž ĐĸŅŠŅ€ŅĐĩĐŊĐĩ", "machine_learning_smart_search_enabled_description": "АĐēĐž Đĩ Đ´ĐĩаĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐž, Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸ŅŅ‚Đ° ĐŊŅĐŧа да ĐąŅŠĐ´Đ°Ņ‚ ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊи Са ИĐŊŅ‚ĐĩĐģĐ¸ĐŗĐĩĐŊŅ‚ĐŊĐž ĐĸŅŠŅ€ŅĐĩĐŊĐĩ.", "machine_learning_url_description": "URL ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° Са ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐž ĐžĐąŅƒŅ‡ĐĩĐŊиĐĩ. АĐēĐž ŅĐ° ĐŋŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛ĐĩĐŊи ĐŋОвĐĩ҇Đĩ ĐžŅ‚ ĐĩдиĐŊ URL, Đ˛ŅĐĩĐēи ŅŅŠŅ€Đ˛ŅŠŅ€ ҉Đĩ ĐąŅŠĐ´Đĩ ĐžĐŋĐ¸Ņ‚Đ°ĐŊ ĐĩдиĐŊ ĐŋĐž ĐĩдиĐŊ, Đ´ĐžĐēĐ°Ņ‚Đž ĐĩдиĐŊ ĐžŅ‚ĐŗĐžĐ˛ĐžŅ€Đ¸ ҃ҁĐŋĐĩ҈ĐŊĐž, в Ņ€Đĩда ĐžŅ‚ ĐŋŅŠŅ€Đ˛Đ¸Ņ Đ´Đž ĐŋĐžŅĐģĐĩĐ´ĐŊĐ¸Ņ. ĐĄŅŠŅ€Đ˛ŅŠŅ€Đ¸, ĐēĐžĐ¸Ņ‚Đž ĐŊĐĩ ĐžŅ‚ĐŗĐžĐ˛ĐžŅ€ŅŅ‚, ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊĐž Đ¸ĐŗĐŊĐžŅ€Đ¸Ņ€Đ°ĐŊи, Đ´ĐžĐēĐ°Ņ‚Đž ĐŊĐĩ ҁĐĩ Đ˛ŅŠŅ€ĐŊĐ°Ņ‚ ĐžĐŊĐģаКĐŊ.", + "maintenance_delete_backup": "Đ˜ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛", + "maintenance_delete_backup_description": "ĐĸОСи Ņ„Đ°ĐšĐģ ҉Đĩ ĐąŅŠĐ´Đĩ ĐąĐĩĐˇĐ˛ŅŠĐˇĐ˛Ņ€Đ°Ņ‚ĐŊĐž Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚.", + "maintenance_delete_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž Đ¸ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛.", + "maintenance_restore_backup": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛", + "maintenance_restore_backup_description": "Immich ҉Đĩ Đ¸ĐˇŅ‚Ņ€Đ¸Đĩ Đ˛ŅĐ¸Ņ‡Đēи Ņ‚ĐĩĐēŅƒŅ‰Đ¸ даĐŊĐŊи и ĐŋĐžŅĐģĐĩ ҉Đĩ Đ˛ŅŠĐˇŅŅ‚Đ°ĐŊОви даĐŊĐŊĐ¸Ņ‚Đĩ ĐžŅ‚ Đ¸ĐˇĐąŅ€Đ°ĐŊĐ¸Ņ Đ°Ņ€Ņ…Đ¸Đ˛. ĐŸŅŠŅ€Đ˛Đž ҉Đĩ ĐŊаĐŋŅ€Đ°Đ˛Đ¸ ĐŊОв Đ°Ņ€Ņ…Đ¸Đ˛.", + "maintenance_restore_backup_different_version": "ĐĸОСи Đ°Ņ€Ņ…Đ¸Đ˛ Đĩ ŅŅŠĐˇĐ´Đ°Đ´ĐĩĐŊ ҁ Ņ€Đ°ĐˇĐģĐ¸Ņ‡ĐŊа вĐĩŅ€ŅĐ¸Ņ ĐŊа Immich!", + "maintenance_restore_backup_unknown_version": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐžĐŋŅ€ĐĩĐ´ĐĩĐģŅĐŊĐĩ ĐŊа вĐĩŅ€ŅĐ¸ŅŅ‚Đ° ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛Đ°.", + "maintenance_restore_database_backup": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа даĐŊĐŊĐ¸Ņ‚Đĩ ĐžŅ‚ Đ°Ņ€Ņ…Đ¸Đ˛", + "maintenance_restore_database_backup_description": "Đ’Ņ€ŅŠŅ‰Đ°ĐŊĐĩ ĐēҊĐŧ ĐŋŅ€ĐĩĐ´Đ¸ŅˆĐŊĐž ŅŅŠŅŅ‚ĐžŅĐŊиĐĩ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи ҇ҀĐĩС иСĐŋĐžĐģСваĐŊĐĩ ĐŊа Ņ„Đ°ĐšĐģ-Đ°Ņ€Ņ…Đ¸Đ˛", "maintenance_settings": "ĐžĐąŅĐģ҃ĐļваĐŊĐĩ", "maintenance_settings_description": "ĐŸŅ€ĐĩĐēвĐģŅŽŅ‡Đ˛Đ°ĐŊĐĩ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° Immich в Ņ€ĐĩĐļиĐŧ ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ.", - "maintenance_start": "ЗаĐŋĐžŅ‡ĐŊи Ņ€ĐĩĐļиĐŧ ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ", + "maintenance_start": "ĐŸŅ€ĐĩĐŧиĐŊи ĐēҊĐŧ Ņ€ĐĩĐļиĐŧ ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ", "maintenance_start_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐĩĐŧиĐŊаваĐŊĐĩ в Ņ€ĐĩĐļиĐŧ ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ.", + "maintenance_upload_backup": "Đ—Đ°Ņ€Đĩди Ņ„Đ°ĐšĐģ-Đ°Ņ€Ņ…Đ¸Đ˛ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи", + "maintenance_upload_backup_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛, Ņ‚ĐžĐ˛Đ° Ņ„Đ°ĐšĐģ .sql/.sql.gz Đģи Đĩ?", "manage_concurrency": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŋĐ°Ņ€Đ°ĐģĐĩĐģĐŊĐžŅŅ‚", "manage_concurrency_description": "ĐžŅ‚Đ¸Đ´ĐĩŅ‚Đĩ ĐŊа ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ°Ņ‚Đ° ҁҊҁ ĐˇĐ°Đ´Đ°Ņ‡Đ¸, Са да ҃ĐŋŅ€Đ°Đ˛ĐģŅĐ˛Đ°Ņ‚Đĩ ĐĩĐ´ĐŊĐžĐ˛Ņ€ĐĩĐŧĐĩĐŊĐŊĐžŅŅ‚Ņ‚Đ° иĐŧ", "manage_log_settings": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ ĐŊа СаĐŋĐ¸ŅĐ˛Đ°ĐŊĐĩ", @@ -252,7 +272,7 @@ "oauth_auto_register": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊа Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ", "oauth_auto_register_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŊОви ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи ҁĐģĐĩĐ´ вĐģиСаĐŊĐĩ ҁ OAuth", "oauth_button_text": "ĐĸĐĩĐēҁ҂ ĐŊа ĐąŅƒŅ‚ĐžĐŊа", - "oauth_client_secret_description": "Đ˜ĐˇĐ¸ŅĐēва ҁĐĩ, ĐēĐžĐŗĐ°Ņ‚Đž Đ´ĐžŅŅ‚Đ°Đ˛Ņ‡Đ¸Đēа ĐŊа OAuth ĐŊĐĩ ĐŋĐžĐ´Đ´ŅŠŅ€Đļа PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Đ—Đ°Đ´ŅŠĐģĐļĐ¸Ņ‚ĐĩĐģĐŊĐž Са ĐŋОвĐĩŅ€Đ¸Ņ‚ĐĩĐģĐĩĐŊ ĐēĐģиĐĩĐŊŅ‚ иĐģи ĐēĐžĐŗĐ°Ņ‚Đž ĐŊĐĩ ҁĐĩ ĐŋĐžĐ´Đ´ŅŠŅ€Đļа PKCE (Proof Key for Code Exchange) Са ĐŋŅƒĐąĐģĐ¸Ņ‡ĐĩĐŊ ĐēĐģиĐĩĐŊŅ‚.", "oauth_enable_description": "ВĐģиСаĐŊĐĩ ҁ OAuth", "oauth_mobile_redirect_uri": "URI Са ĐŧОйиĐģĐŊĐž ĐŋŅ€ĐĩĐŊĐ°ŅĐžŅ‡Đ˛Đ°ĐŊĐĩ", "oauth_mobile_redirect_uri_override": "URI ĐŋŅ€ĐĩĐŊĐ°ŅĐžŅ‡Đ˛Đ°ĐŊĐĩ Са ĐŧОйиĐģĐŊи ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", @@ -326,7 +346,7 @@ "template_email_invite_album": "ШайĐģĐžĐŊ Са ĐŋĐžĐēаĐŊа Са аĐģĐąŅƒĐŧ", "template_email_preview": "ĐŸŅ€ĐĩĐŗĐģĐĩĐ´", "template_email_settings": "ШайĐģĐžĐŊи Са иĐŧĐĩĐšĐģи", - "template_email_update_album": "ШайĐģĐžĐŊ Са аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ°Ņ†Đ¸Ņ ĐŊа аĐģĐąŅƒĐŧ", + "template_email_update_album": "ШайĐģĐžĐŊ Са ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа аĐģĐąŅƒĐŧ", "template_email_welcome": "ШайĐģĐžĐŊ Са ĐŋŅ€Đ¸Đ˛ĐĩŅ‚ŅŅ‚Đ˛Đ°Ņ‰ иĐŧĐĩĐšĐģ", "template_settings": "ШайĐģĐžĐŊи Са иСвĐĩŅŅ‚Đ¸Ņ", "template_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ŅˆĐ°ĐąĐģĐžĐŊи Са иСвĐĩŅŅ‚Đ¸Ņ", @@ -363,7 +383,7 @@ "transcoding_hardware_acceleration": "ĐĨĐ°Ņ€Đ´ŅƒĐĩŅ€ĐŊĐž ҃ҁĐēĐžŅ€ĐĩĐŊиĐĩ", "transcoding_hardware_acceleration_description": "ЕĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģĐŊĐž: ĐŧĐŊĐžĐŗĐž ĐŋĐž-ĐąŅŠŅ€ĐˇĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩ, ĐŊĐž ĐŧĐžĐļĐĩ да ĐŋĐžĐŊиĐļи ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛ĐžŅ‚Đž ĐŋŅ€Đ¸ ŅŅŠŅ‰Đ¸Ņ ĐąĐ¸Ņ‚Ņ€ĐĩĐšŅ‚", "transcoding_hardware_decoding": "ĐĨĐ°Ņ€Đ´ŅƒĐĩŅ€ĐŊĐž Đ´ĐĩĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩ", - "transcoding_hardware_decoding_setting_description": "ĐŸŅ€Đ¸ĐģĐ°ĐŗĐ° ҁĐĩ ŅĐ°ĐŧĐž Са NVENC, QSV и RKMPP. АĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ° ҃ҁĐēĐžŅ€ĐĩĐŊиĐĩ ĐžŅ‚ ĐēŅ€Đ°Đš Đ´Đž ĐēŅ€Đ°Đš, вĐŧĐĩŅŅ‚Đž ŅĐ°ĐŧĐž да ҃ҁĐēĐžŅ€ŅĐ˛Đ° ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩŅ‚Đž. МоĐļĐĩ да ĐŊĐĩ Ņ€Đ°ĐąĐžŅ‚Đ¸ ҁ Đ˛ŅĐ¸Ņ‡Đēи видĐĩĐžĐēĐģиĐŋОвĐĩ.", + "transcoding_hardware_decoding_setting_description": "АĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ° ҃ҁĐēĐžŅ€ĐĩĐŊиĐĩ ĐžŅ‚ ĐēŅ€Đ°Đš Đ´Đž ĐēŅ€Đ°Đš, вĐŧĐĩŅŅ‚Đž ŅĐ°ĐŧĐž да ҃ҁĐēĐžŅ€ŅĐ˛Đ° ĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩŅ‚Đž. МоĐļĐĩ да ĐŊĐĩ Ņ€Đ°ĐąĐžŅ‚Đ¸ ҁ Đ˛ŅĐ¸Ņ‡Đēи видĐĩĐžĐēĐģиĐŋОвĐĩ.", "transcoding_max_b_frames": "МаĐēŅĐ¸ĐŧаĐģĐŊи B-҄ҀĐĩĐšĐŧа", "transcoding_max_b_frames_description": "По-Đ˛Đ¸ŅĐžĐēĐ¸Ņ‚Đĩ ŅŅ‚ĐžĐšĐŊĐžŅŅ‚Đ¸ ĐŋĐžĐ´ĐžĐąŅ€ŅĐ˛Đ°Ņ‚ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊĐžŅŅ‚Ņ‚Đ° ĐŊа ĐēĐžĐŧĐŋŅ€ĐĩŅĐ¸ŅŅ‚Đ°, ĐŊĐž ĐˇĐ°ĐąĐ°Đ˛ŅŅ‚ Ņ€Đ°ĐˇĐēĐžĐ´Đ¸Ņ€Đ°ĐŊĐĩŅ‚Đž. МоĐļĐĩ да ĐŊĐĩ Đĩ ŅŅŠĐ˛ĐŧĐĩŅŅ‚Đ¸Đŧ ҁ Ņ…Đ°Ņ€Đ´ŅƒĐĩŅ€ĐŊĐžŅ‚Đž ҃ҁĐēĐžŅ€ĐĩĐŊиĐĩ ĐŊа ĐŋĐž-ŅŅ‚Đ°Ņ€Đ¸ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°. 0 Đ´ĐĩаĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ° B-҄ҀĐĩĐšĐŧа, Đ´ĐžĐēĐ°Ņ‚Đž -1 Садава Ņ‚Đ°ĐˇĐ¸ ŅŅ‚ĐžĐšĐŊĐžŅŅ‚ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž.", "transcoding_max_bitrate": "МаĐēŅĐ¸ĐŧаĐģĐĩĐŊ ĐąĐ¸Ņ‚Ņ€ĐĩĐšŅ‚", @@ -431,6 +451,9 @@ "admin_password": "АдĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ŅĐēа ĐŋĐ°Ņ€ĐžĐģа", "administration": "АдĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ", "advanced": "Đ Đ°ĐˇŅˆĐ¸Ņ€ĐĩĐŊĐž", + "advanced_settings_clear_image_cache": "Đ˜ĐˇŅ‡Đ¸ŅŅ‚Đ¸ ĐēĐĩŅˆĐ° Са Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", + "advanced_settings_clear_image_cache_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž Đ¸ĐˇŅ‡Đ¸ŅŅ‚Đ˛Đ°ĐŊĐĩ ĐŊа ĐēĐĩŅˆĐ° Са Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", + "advanced_settings_clear_image_cache_success": "ĐŖŅĐŋĐĩ҈ĐŊĐž Đ¸ĐˇŅ‡Đ¸ŅŅ‚ĐĩĐŊи {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "ĐŸŅ€Đ¸ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ, иСĐŋĐžĐģĐˇĐ˛Đ°ĐšŅ‚Đĩ Ņ‚Đ°ĐˇĐ¸ ĐžĐŋŅ†Đ¸Ņ ĐēĐ°Ņ‚Đž Ņ„Đ¸ĐģŅ‚ŅŠŅ€, ĐžŅĐŊОваĐŊ ĐŊа ĐŋŅ€ĐžĐŧŅĐŊа ĐŊа дадĐĩĐŊ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸Đ¸. ОĐŋĐ¸Ņ‚Đ°ĐšŅ‚Đĩ ŅĐ°ĐŧĐž в ҁĐģŅƒŅ‡Đ°Đš, ҇Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž иĐŧа ĐŋŅ€ĐžĐąĐģĐĩĐŧ ҁ ĐžŅ‚ĐēŅ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа Đ˛ŅĐ¸Ņ‡Đēи аĐģĐąŅƒĐŧи.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНĐĸАЛНО] ИСĐŋĐžĐģСваК Ņ„Đ¸ĐģŅ‚ŅŠŅ€Đ° ĐŊа аĐģŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊĐžŅ‚Đž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž Са ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Ņ ĐŊа аĐģĐąŅƒĐŧи", "advanced_settings_log_level_title": "Ниво ĐŊа СаĐŋĐ¸Ņ в Đ´ĐŊĐĩвĐŊиĐēа: {level}", @@ -453,13 +476,13 @@ "album": "АĐģĐąŅƒĐŧ", "album_added": "АĐģĐąŅƒĐŧŅŠŅ‚ Đĩ дОйавĐĩĐŊ", "album_added_notification_setting_description": "ПоĐģŅƒŅ‡Đ°Đ˛Đ°ĐšŅ‚Đĩ иСвĐĩŅŅ‚Đ¸Đĩ ĐŋĐž иĐŧĐĩĐšĐģ, ĐēĐžĐŗĐ°Ņ‚Đž ĐąŅŠĐ´ĐĩŅ‚Đĩ дОйавĐĩĐŊи ĐēҊĐŧ ҁĐŋОдĐĩĐģĐĩĐŊ аĐģĐąŅƒĐŧ", - "album_cover_updated": "ОбĐģĐžĐļĐēĐ°Ņ‚Đ° ĐŊа аĐģĐąŅƒĐŧа Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊа", + "album_cover_updated": "ОбĐģĐžĐļĐēĐ°Ņ‚Đ° ĐŊа аĐģĐąŅƒĐŧа Đĩ ОйĐŊОвĐĩĐŊа", "album_delete_confirmation": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ аĐģĐąŅƒĐŧа {album}?", "album_delete_confirmation_description": "АĐēĐž Ņ‚ĐžĐˇĐ¸ аĐģĐąŅƒĐŧ Đĩ ҁĐŋОдĐĩĐģĐĩĐŊ, Đ´Ņ€ŅƒĐŗĐ¸ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи вĐĩ҇Đĩ ĐŊŅĐŧа да иĐŧĐ°Ņ‚ Đ´ĐžŅŅ‚ŅŠĐŋ Đ´Đž ĐŊĐĩĐŗĐž.", "album_deleted": "АĐģĐąŅƒĐŧа Đĩ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚", "album_info_card_backup_album_excluded": "ИЗКЛЮЧЕН", "album_info_card_backup_album_included": "ВКЛЮЧЕН", - "album_info_updated": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŅ‚Đ° Са аĐģĐąŅƒĐŧа Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊа", + "album_info_updated": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸ŅŅ‚Đ° Са аĐģĐąŅƒĐŧа Đĩ ОйĐŊОвĐĩĐŊа", "album_leave": "Да ĐŊаĐŋ҃ҁĐŊа Đģи аĐģĐąŅƒĐŧа?", "album_leave_confirmation": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да ĐŊаĐŋ҃ҁĐŊĐĩŅ‚Đĩ {album}?", "album_name": "ИĐŧĐĩ ĐŊа аĐģĐąŅƒĐŧа", @@ -467,10 +490,12 @@ "album_remove_user": "ĐŸŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐŊа ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ?", "album_remove_user_confirmation": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐĩŅ‚Đĩ {user}?", "album_search_not_found": "ĐŅĐŧа ĐŊаĐŧĐĩŅ€ĐĩĐŊи аĐģĐąŅƒĐŧи, ĐžŅ‚ĐŗĐžĐ˛Đ°Ņ€ŅŅ‰Đ¸ ĐŊа Ņ‚ŅŠŅ€ŅĐĩĐŊĐĩŅ‚Đž ви", + "album_selected": "АĐģĐąŅƒĐŧа Đĩ Đ¸ĐˇĐąŅ€Đ°ĐŊ", "album_share_no_users": "Đ˜ĐˇĐŗĐģĐĩĐļда, ҇Đĩ ҁ҂Đĩ ҁĐŋОдĐĩĐģиĐģи Ņ‚ĐžĐˇĐ¸ аĐģĐąŅƒĐŧ ҁ Đ˛ŅĐ¸Ņ‡Đēи ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи иĐģи ĐŊŅĐŧĐ°Ņ‚Đĩ Đ´Ņ€ŅƒĐŗ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ, ҁ ĐēĐžĐŗĐžŅ‚Đž да ĐŗĐž ҁĐŋОдĐĩĐģĐ¸Ņ‚Đĩ.", "album_summary": "ĐžĐąĐžĐąŅ‰ĐĩĐŊиĐĩ ĐŊа аĐģĐąŅƒĐŧа", - "album_updated": "АĐģĐąŅƒĐŧŅŠŅ‚ Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊ", + "album_updated": "АĐģĐąŅƒĐŧŅŠŅ‚ Đĩ ОйĐŊОвĐĩĐŊ", "album_updated_setting_description": "ПоĐģŅƒŅ‡Đ°Đ˛Đ°ĐšŅ‚Đĩ иСвĐĩŅŅ‚Đ¸Đĩ ĐŋĐž иĐŧĐĩĐšĐģ, ĐēĐžĐŗĐ°Ņ‚Đž ҁĐŋОдĐĩĐģĐĩĐŊ аĐģĐąŅƒĐŧ иĐŧа ĐŊОви Ņ„Đ°ĐšĐģОвĐĩ", + "album_upload_assets": "Đ—Đ°Ņ€ĐĩĐ´ĐĩŅ‚Đĩ ОйĐĩĐēŅ‚Đ¸ ĐžŅ‚ ĐēĐžĐŧĐŋŅŽŅ‚ŅŠŅ€Đ° в ŅŅŠŅ€Đ˛ŅŠŅ€Đ° и ĐŗĐ¸ дОйавĐĩŅ‚Đĩ в аĐģĐąŅƒĐŧ", "album_user_left": "НаĐŋ҃ҁĐŊа {album}", "album_user_removed": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚ {user}", "album_viewer_appbar_delete_confirm": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚ĐžĐˇĐ¸ аĐģĐąŅƒĐŧ ĐžŅ‚ ŅĐ˛ĐžŅ ĐŋŅ€ĐžŅ„Đ¸Đģ?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "ĐŸŅŠŅ€Đ˛ĐžĐŊĐ°Ņ‡Đ°ĐģĐĩĐŊ Ņ€ĐĩĐ´ ĐŊа ŅĐžŅ€Ņ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŋŅ€Đ¸ ŅŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐŊОв аĐģĐąŅƒĐŧ.", "albums_feature_description": "КоĐģĐĩĐēŅ†Đ¸Đ¸ ĐžŅ‚ ОйĐĩĐēŅ‚Đ¸, ĐēĐžĐ¸Ņ‚Đž ĐŧĐžĐŗĐ°Ņ‚ да ĐąŅŠĐ´Đ°Ņ‚ ҁĐŋОдĐĩĐģŅĐŊи ҁ Đ´Ņ€ŅƒĐŗĐ¸ ĐŋĐžŅ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи.", "albums_on_device_count": "АĐģĐąŅƒĐŧи ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž ({count})", + "albums_selected": "{count, plural, one {Đ˜ĐˇĐąŅ€Đ°ĐŊ Đĩ # аĐģĐąŅƒĐŧ} other {Đ˜ĐˇĐąŅ€Đ°ĐŊи ŅĐ° # аĐģĐąŅƒĐŧа}}", "all": "Đ’ŅĐ¸Ņ‡Đēи", "all_albums": "Đ’ŅĐ¸Ņ‡Đēи аĐģĐąŅƒĐŧи", "all_people": "Đ’ŅĐ¸Ņ‡Đēи Ņ…ĐžŅ€Đ°", + "all_photos": "Đ’ŅĐ¸Ņ‡Đēи ҁĐŊиĐŧĐēи", "all_videos": "Đ’ŅĐ¸Ņ‡Đēи видĐĩĐžĐēĐģиĐŋОвĐĩ", "allow_dark_mode": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸ Ņ‚ŅŠĐŧĐĩĐŊ Ņ€ĐĩĐļиĐŧ", "allow_edits": "ПозвоĐģŅĐ˛Đ°ĐŊĐĩ ĐŊа Ņ€ĐĩдаĐēŅ†Đ¸Đ¸", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "ПозвоĐģĐĩŅ‚Đĩ ĐŊа ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊĐ¸Ņ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ да ĐŧĐžĐļĐĩ да ĐēĐ°Ņ‡Đ˛Đ°", "allowed": "Đ Đ°ĐˇŅ€Đĩ҈ĐĩĐŊĐž", "alt_text_qr_code": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩ ĐŊа QR ĐēОд", + "always_keep": "ВиĐŊĐ°ĐŗĐ¸ ĐŋаСи", + "always_keep_photos_hint": "ĐŸŅ€Đ¸ ĐžŅĐ˛ĐžĐąĐžĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ СаĐŋаСĐĩĐŊи Đ˛ŅĐ¸Ņ‡Đēи ҁĐŊиĐŧĐēи ĐŊа Ņ‚ĐžĐ˛Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž.", + "always_keep_videos_hint": "ĐŸŅ€Đ¸ ĐžŅĐ˛ĐžĐąĐžĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ СаĐŋаСĐĩĐŊи Đ˛ŅĐ¸Ņ‡Đēи видĐĩа ĐŊа Ņ‚ĐžĐ˛Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž.", "anti_clockwise": "ĐžĐąŅ€Đ°Ņ‚ĐŊĐž ĐŊа Ņ‡Đ°ŅĐžĐ˛ĐŊиĐēĐžĐ˛Đ°Ņ‚Đ° ҁ҂ҀĐĩĐģĐēа", "api_key": "API ĐēĐģŅŽŅ‡", "api_key_description": "ĐĸаСи ŅŅ‚ĐžĐšĐŊĐžŅŅ‚ ҉Đĩ ĐąŅŠĐ´Đĩ ĐŋĐžĐēаСаĐŊа ŅĐ°ĐŧĐž вĐĩĐ´ĐŊҊĐļ. МоĐģŅ, ĐŊĐĩ ĐˇĐ°ĐąŅ€Đ°Đ˛ŅĐšŅ‚Đĩ да ĐŗĐž ĐēĐžĐŋĐ¸Ņ€Đ°Ņ‚Đĩ, ĐŋŅ€Đĩди да ĐˇĐ°Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đĩ ĐŋŅ€ĐžĐˇĐžŅ€ĐĩŅ†Đ°.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {ĐŅ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи #}}", "are_these_the_same_person": "ĐĸОва ĐĩĐ´ĐŊĐž и ŅŅŠŅ‰Đž ĐģĐ¸Ņ†Đĩ Đģи Đĩ?", "are_you_sure_to_do_this": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да ĐŊаĐŋŅ€Đ°Đ˛Đ¸Ņ‚Đĩ Ņ‚ĐžĐ˛Đ°?", + "array_field_not_fully_supported": "ПоĐģĐĩŅ‚Đ°Ņ‚Đ° ĐŊа ĐŧĐ°ŅĐ¸Đ˛Đ° Đ¸ĐˇĐ¸ŅĐēĐ˛Đ°Ņ‚ Ņ€ŅŠŅ‡ĐŊĐž Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа JSON", "asset_action_delete_err_read_only": "НĐĩ ĐŧĐžĐŗĐ°Ņ‚ да ҁĐĩ Đ¸ĐˇŅ‚Ņ€Đ¸Đ˛Đ°Ņ‚ ОйĐĩĐēŅ‚Đ¸ ŅĐ°ĐŧĐž-Са-҇ĐĩŅ‚ĐĩĐŊĐĩ, ĐŋŅ€ĐžĐŋ҃ҁĐēаĐŊĐĩ", "asset_action_share_err_offline": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐžŅ„ĐģаКĐŊ ОйĐĩĐēŅ‚/и, ĐŋŅ€ĐžĐŋ҃ҁĐēаĐŧĐĩ", "asset_added_to_album": "ДобавĐĩĐŊĐž в аĐģĐąŅƒĐŧ", "asset_adding_to_album": "Đ”ĐžĐąĐ°Đ˛ŅĐŊĐĩ в аĐģĐąŅƒĐŧâ€Ļ", + "asset_created": "ОбĐĩĐēŅ‚ŅŠŅ‚ Đĩ ŅŅŠĐˇĐ´Đ°Đ´ĐĩĐŊ", "asset_description_updated": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩŅ‚Đž ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° Đĩ ОйĐŊОвĐĩĐŊĐž", "asset_filename_is_offline": "АĐēŅ‚Đ¸Đ˛ŅŠŅ‚ {filename} Đĩ ĐžŅ„ĐģаКĐŊ", "asset_has_unassigned_faces": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ŅŠŅ‚ иĐŧа ĐŊĐĩСададĐĩĐŊи ĐģĐ¸Ņ†Đ°", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "РаСĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "asset_list_settings_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐŊа ĐŧŅ€ĐĩĐļĐ°Ņ‚Đ° ĐŊа Ņ€Đ°ĐˇĐŋĐžĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи", "asset_list_settings_title": "РаСĐŋĐžĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи", + "asset_not_found_on_device_android": "ОбĐĩĐēŅ‚ŅŠŅ‚ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅ€ĐĩĐŊ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", + "asset_not_found_on_device_ios": "ОбĐĩĐēŅ‚ŅŠŅ‚ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅ€ĐĩĐŊ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž. АĐēĐž иСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ iCloud, ОйĐĩĐēŅ‚ŅŠŅ‚ ĐŧĐžĐļĐĩ да Đĩ ĐŊĐĩĐ´ĐžŅŅ‚ŅŠĐŋĐĩĐŊ ĐŋĐžŅ€Đ°Đ´Đ¸ ĐŋĐžĐ˛Ņ€ĐĩĐ´ĐĩĐŊ Ņ„Đ°ĐšĐģ, ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊ в iCloud", + "asset_not_found_on_icloud": "ОбĐĩĐēŅ‚ŅŠŅ‚ ĐŊĐĩ Đĩ ĐŊаĐŧĐĩŅ€ĐĩĐŊ в iCloud. ОбĐĩĐēŅ‚ŅŠŅ‚ ĐŧĐžĐļĐĩ да Đĩ ĐŊĐĩĐ´ĐžŅŅ‚ŅŠĐŋĐĩĐŊ ĐŋĐžŅ€Đ°Đ´Đ¸ ĐŋĐžĐ˛Ņ€ĐĩĐ´ĐĩĐŊ Ņ„Đ°ĐšĐģ, ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊ в iCloud", "asset_offline": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ŅŠŅ‚ Đĩ ĐžŅ„ĐģаКĐŊ", "asset_offline_description": "ĐĸОСи Đ˛ŅŠĐŊ҈ĐĩĐŊ аĐēŅ‚Đ¸Đ˛ вĐĩ҇Đĩ ĐŊĐĩ ҁĐĩ ĐŊаĐŧĐ¸Ņ€Đ° ĐŊа Đ´Đ¸ŅĐēа. МоĐģŅ, ŅĐ˛ŅŠŅ€ĐļĐĩŅ‚Đĩ ҁĐĩ ҁ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° ĐŊа Immich Са ĐŋĐžĐŧĐžŅ‰.", "asset_restored_successfully": "ĐŖŅĐŋĐĩ҈ĐŊĐž Đ˛ŅŠĐˇŅŅ‚Đ°ĐŊОвĐĩĐŊ ОйĐĩĐēŅ‚", @@ -691,7 +726,7 @@ "canceling": "АĐŊ҃ĐģĐ¸Ņ€Đ°ĐŊĐĩ", "cannot_merge_people": "НĐĩ ĐŧĐžĐļĐĩ да ОйĐĩдиĐŊŅĐ˛Đ° Ņ…ĐžŅ€Đ°", "cannot_undo_this_action": "НĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ да ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚Đĩ Ņ‚ĐžĐ˛Đ° Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ!", - "cannot_update_the_description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩŅ‚Đž ĐŊĐĩ ĐŧĐžĐļĐĩ да ĐąŅŠĐ´Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐž", + "cannot_update_the_description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩŅ‚Đž ĐŊĐĩ ĐŧĐžĐļĐĩ да ĐąŅŠĐ´Đĩ ОйĐŊОвĐĩĐŊĐž", "cast": "ĐŸĐžŅ‚ĐžŅ‡ĐŊĐž ĐŋŅ€ĐĩдаваĐŊĐĩ", "cast_description": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŊа ĐŊаĐģĐ¸Ņ‡ĐŊĐ¸Ņ‚Đĩ ҆ĐĩĐģи Са ĐŋŅ€ĐĩдаваĐŊĐĩ", "change_date": "ĐŸŅ€ĐžĐŧĐĩĐŊи Đ´Đ°Ņ‚Đ°Ņ‚Đ°", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "ĐŸĐ°Ņ€ĐžĐģĐ¸Ņ‚Đĩ ĐŊĐĩ ŅŅŠĐ˛ĐŋĐ°Đ´Đ°Ņ‚", "change_password_form_reenter_new_password": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸ ĐŊĐžĐ˛Đ°Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģа", "change_pin_code": "ĐĄĐŧĐĩĐŊи PIN ĐēОда", + "change_trigger": "ĐŸŅ€ĐžĐŧŅĐŊа ĐŊа Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ°", + "change_trigger_prompt": "ĐĐ°Đ¸ŅŅ‚Đ¸ĐŊа Đģи Đ¸ŅĐēĐ°Ņ‚Đĩ да ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ°? ĐĸОва ҉Đĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐĩ Đ˛ŅĐ¸Ņ‡Đēи ĐŊаĐģĐ¸Ņ‡ĐŊи Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ и Ņ„Đ¸ĐģŅ‚Ņ€Đ¸.", "change_your_password": "ĐŸŅ€ĐžĐŧĐĩĐŊĐĩŅ‚Đĩ ĐŋĐ°Ņ€ĐžĐģĐ°Ņ‚Đ° ŅĐ¸", "changed_visibility_successfully": "ВидиĐŧĐžŅŅ‚Ņ‚Đ° Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐĩĐŊа ҃ҁĐŋĐĩ҈ĐŊĐž", "charging": "ĐŸŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ", @@ -722,6 +759,18 @@ "checksum": "КоĐŊŅ‚Ņ€ĐžĐģĐŊа ҁ҃Đŧа", "choose_matching_people_to_merge": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐŋĐžĐ´Ņ…ĐžĐ´ŅŅ‰Đ¸ Ņ…ĐžŅ€Đ° Са ҁĐģиваĐŊĐĩ", "city": "Đ“Ņ€Đ°Đ´", + "cleanup_confirm_description": "Immich ĐŊаĐŧĐĩŅ€Đ¸ {count} ОйĐĩĐēŅ‚Đ° (ŅŅŠĐˇĐ´Đ°Đ´ĐĩĐŊи ĐŋŅ€Đĩди {date}), ĐēĐžĐ¸Ņ‚Đž ŅĐ° Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ°. Да ҁĐĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚ Đģи ĐģĐžĐēаĐģĐŊĐ¸Ņ‚Đĩ ĐēĐžĐŋĐ¸Ņ ĐžŅ‚ Ņ‚ĐžĐ˛Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž?", + "cleanup_confirm_prompt_title": "Да ҁĐĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚ Đģи ĐžŅ‚ Ņ‚ĐžĐ˛Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž?", + "cleanup_deleted_assets": "В ĐēĐžŅˆŅ‡ĐĩŅ‚Đž ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи {count} ОйĐĩĐēŅ‚Đ°", + "cleanup_deleting": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ˛Đ°ĐŊĐĩ в ĐēĐžŅˆŅ‡ĐĩŅ‚Đž...", + "cleanup_found_assets": "НаĐŧĐĩŅ€ĐĩĐŊи ŅĐ° {count} Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ОйĐĩĐēŅ‚Đ°", + "cleanup_found_assets_with_size": "НаĐŧĐĩŅ€ĐĩĐŊи ŅĐ° {count} Đ°Ņ€Ņ…Đ¸Đ˛Đ° ҁ Ņ€Đ°ĐˇĐŧĐĩŅ€ ({size})", + "cleanup_icloud_shared_albums_excluded": "ĐĄĐŋОдĐĩĐģĐĩĐŊĐ¸Ņ‚Đĩ iCloud аĐģĐąŅƒĐŧи ŅĐ° иСĐēĐģŅŽŅ‡ĐĩĐŊи ĐžŅ‚ ҁĐēаĐŊĐ¸Ņ€Đ°ĐŊĐĩŅ‚Đž", + "cleanup_no_assets_found": "НĐĩ ŅĐ° ĐŊаĐŧĐĩŅ€ĐĩĐŊи ОйĐĩĐēŅ‚Đ¸, ĐēĐžĐ¸Ņ‚Đž да ĐžŅ‚ĐŗĐžĐ˛Đ°Ņ€ŅŅ‚ ĐŊа СададĐĩĐŊĐ¸Ņ‚Đĩ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸Đ¸. За ĐžŅĐ˛ĐžĐąĐžĐļдваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž ĐŧĐžĐļĐĩ да ҁĐĩ ĐŋŅ€ĐĩĐŧĐ°Đ˛Đ°Ņ‚ ŅĐ°ĐŧĐž Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ОйĐĩĐēŅ‚Đ¸", + "cleanup_preview_title": "ОбĐĩĐēŅ‚Đ¸ Са ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ({count})", + "cleanup_step3_description": "ĐĄĐēаĐŊĐ¸Ņ€Đ°ĐŊĐĩ Са Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊи ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ҁĐŊиĐŧĐēи и видĐĩа, ҁĐŋĐžŅ€ĐĩĐ´ Đ¸ĐˇĐąŅ€Đ°ĐŊĐ°Ņ‚Đ° Đ´Đ°Ņ‚Đ° и СададĐĩĐŊĐ¸Ņ‚Đĩ ĐžĐŋŅ†Đ¸Đ¸ ĐŊа Ņ„Đ¸ĐģŅ‚ŅŠŅ€Đ°.", + "cleanup_step4_summary": "{count} ОйĐĩĐēŅ‚Đ° (ŅŅŠĐˇĐ´Đ°Đ´ĐĩĐŊи ĐŋŅ€Đĩди {date}) Са ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐžŅ‚ Ņ‚ĐžĐ˛Đ° ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž. ĐĄĐŊиĐŧĐēĐ¸Ņ‚Đĩ ҉Đĩ ĐžŅŅ‚Đ°ĐŊĐ°Ņ‚ Đ´ĐžŅŅ‚ŅŠĐŋĐŊи ҇ҀĐĩС ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž Immich.", + "cleanup_trash_hint": "За да ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚Đĩ ĐŊаĐŋҊĐģĐŊĐž ĐŧŅŅŅ‚ĐžŅ‚Đž Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ, ĐžŅ‚Đ˛ĐžŅ€ĐĩŅ‚Đĩ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊĐžŅ‚Đž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ „ГаĐģĐĩŅ€Đ¸Ņâ€œ и иСĐŋŅ€Đ°ĐˇĐŊĐĩŅ‚Đĩ ĐēĐžŅˆŅ‡ĐĩŅ‚Đž", "clear": "Đ˜ĐˇŅ‡Đ¸ŅŅ‚Đ¸", "clear_all": "Đ˜ĐˇŅ‡Đ¸ŅŅ‚Đ¸ Đ˛ŅĐ¸Ņ‡ĐēĐž", "clear_all_recent_searches": "Đ˜ĐˇŅ‡Đ¸ŅŅ‚ĐĩŅ‚Đĩ Đ˛ŅĐ¸Ņ‡Đēи ҁĐēĐžŅ€ĐžŅˆĐŊи Ņ‚ŅŠŅ€ŅĐĩĐŊĐ¸Ņ", @@ -733,6 +782,8 @@ "client_cert_import": "ИĐŧĐŋĐžŅ€Ņ‚", "client_cert_import_success_msg": "КĐģиĐĩĐŊ҂ҁĐēĐ¸Ņ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ Đĩ иĐŧĐŋĐžŅ€Ņ‚Đ¸Ņ€Đ°ĐŊ", "client_cert_invalid_msg": "НĐĩваĐģидĐĩĐŊ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ иĐģи ĐŗŅ€Đĩ҈ĐŊа ĐŋĐ°Ņ€ĐžĐģа", + "client_cert_password_message": "Đ’ŅŠĐ˛ĐĩĐ´ĐĩŅ‚Đĩ ĐŋĐ°Ņ€ĐžĐģа Са Ņ‚ĐžĐˇĐ¸ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚", + "client_cert_password_title": "ĐŸĐ°Ņ€ĐžĐģа Са ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚", "client_cert_remove_msg": "КĐģиĐĩĐŊ҂ҁĐēĐ¸Ņ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ Đĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚", "client_cert_subtitle": "ĐŸĐžĐ´Đ´ŅŠŅ€Đļа ҁĐĩ ŅĐ°ĐŧĐž Ņ„ĐžŅ€ĐŧĐ°Ņ‚ PKCS12 (.p12, .pfx). ИĐŧĐŋĐžŅ€Ņ‚/ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐŊа ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ ĐŧĐžĐļĐĩ ŅĐ°ĐŧĐž ĐŋŅ€Đĩди вĐŋĐ¸ŅĐ˛Đ°ĐŊĐĩ в ŅĐ¸ŅŅ‚ĐĩĐŧĐ°Ņ‚Đ°", "client_cert_title": "КĐģиĐĩĐŊ҂ҁĐēи SSL ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ [ЕКСПЕРИМЕНĐĸАЛНО]", @@ -787,6 +838,7 @@ "create_album": "ĐĄŅŠĐˇĐ´Đ°Đš аĐģĐąŅƒĐŧ", "create_album_page_untitled": "БĐĩС ĐˇĐ°ĐŗĐģавиĐĩ", "create_api_key": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ API ĐēĐģŅŽŅ‡", + "create_first_workflow": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ ĐŋŅŠŅ€Đ˛Đ¸ Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", "create_library": "ĐĄŅŠĐˇĐ´Đ°Đš йийĐģĐ¸ĐžŅ‚ĐĩĐēа", "create_link": "ĐĄŅŠĐˇĐ´Đ°Đš ĐģиĐŊĐē", "create_link_to_share": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐģиĐŊĐē Са ҁĐŋОдĐĩĐģŅĐŊĐĩ", @@ -801,17 +853,25 @@ "create_tag": "ĐĄŅŠĐˇĐ´Đ°Đš Ņ‚Đ°Đŗ", "create_tag_description": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ ĐŊОв Ņ‚Đ°Đŗ. За вĐģĐžĐļĐĩĐŊи Ņ‚Đ°ĐŗĐžĐ˛Đĩ, ĐŧĐžĐģŅ, Đ˛ŅŠĐ˛ĐĩĐ´ĐĩŅ‚Đĩ ĐŋҊĐģĐŊĐ¸Ņ ĐŋŅŠŅ‚ ĐŊа Ņ‚Đ°ĐŗĐ°, вĐēĐģŅŽŅ‡Đ¸Ņ‚ĐĩĐģĐŊĐž ĐŊаĐēĐģĐžĐŊĐĩĐŊĐ¸Ņ‚Đĩ ҇ĐĩŅ€Ņ‚Đ¸.", "create_user": "ĐĄŅŠĐˇĐ´Đ°Đš ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", + "create_workflow": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", "created": "ĐĄŅŠĐˇĐ´Đ°Đ´ĐĩĐŊĐž", "created_at": "ĐĄŅŠĐˇĐ´Đ°Đ´ĐĩĐŊ", "creating_linked_albums": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ŅĐ˛ŅŠŅ€ĐˇĐ°ĐŊи аĐģĐąŅƒĐŧи...", "crop": "Đ˜ĐˇŅ€ĐĩĐļи", + "crop_aspect_ratio_fixed": "ФиĐēŅĐ¸Ņ€Đ°ĐŊ", + "crop_aspect_ratio_free": "ХвОйОдĐĩĐŊ", + "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģĐĩĐŊ", "curated_object_page_title": "НĐĩŅ‰Đ°", "current_device": "ĐĸĐĩĐēŅƒŅ‰Đž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž", "current_pin_code": "ĐĄĐĩĐŗĐ°ŅˆĐĩĐŊ PIN ĐēОд", "current_server_address": "ĐĐ°ŅŅ‚ĐžŅŅ‰ Đ°Đ´Ņ€Đĩҁ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ°", + "custom_date": "ПĐĩŅ€ŅĐžĐŊаĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊа Đ´Đ°Ņ‚Đ°", "custom_locale": "ПĐĩŅ€ŅĐžĐŊаĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊ ĐģĐžĐēаĐģ", "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа Đ´Đ°Ņ‚Đ¸ и Ņ‡Đ¸ŅĐģа в ĐˇĐ°Đ˛Đ¸ŅĐ¸ĐŧĐžŅŅ‚ ĐžŅ‚ ĐĩСиĐēа и Ņ€ĐĩĐŗĐ¸ĐžĐŊа", "custom_url": "ПĐĩŅ€ŅĐžĐŊаĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊ URL Đ°Đ´Ņ€Đĩҁ", + "cutoff_date_description": "ЗаĐŋаСваĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи ĐžŅ‚ ĐŋĐžŅĐģĐĩĐ´ĐŊĐ¸Ņ‚Đĩâ€Ļ", + "cutoff_day": "{count, plural, one {Đ´ĐĩĐŊ} other {Đ´ĐŊи}}", + "cutoff_year": "{count, plural, one {ĐŗĐžĐ´Đ¸ĐŊа} other {ĐŗĐžĐ´Đ¸ĐŊи}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "ĐĸҊĐŧĐĩĐŊ", @@ -867,6 +927,7 @@ "deselect_all": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи Đ¸ĐˇĐąĐžŅ€Đ° ĐžŅ‚ Đ˛ŅĐ¸Ņ‡Đēи", "details": "ДĐĩŅ‚Đ°ĐšĐģи", "direction": "ĐŸĐžŅĐžĐēа", + "disable": "Đ—Đ°ĐąŅ€Đ°ĐŊи", "disabled": "ИСĐēĐģŅŽŅ‡ĐĩĐŊĐž", "disallow_edits": "Đ—Đ°ĐąŅ€Đ°ĐŊŅĐ˛Đ°ĐŊĐĩ ĐŊа Ņ€ĐĩдаĐēŅ†Đ¸Đ¸Ņ‚Đĩ", "discord": "НаĐŧĐĩŅ€Đ¸ ĐŊи в Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Đ’ĐŗŅ€Đ°Đ´ĐĩĐŊи видĐĩа", "download_include_embedded_motion_videos_description": "ВĐēĐģŅŽŅ‡ĐĩŅ‚Đĩ видĐĩĐ°Ņ‚Đ°, Đ˛ĐŗŅ€Đ°Đ´ĐĩĐŊи в диĐŊаĐŧĐ¸Ņ‡ĐŊи ҁĐŊиĐŧĐēи, ĐēĐ°Ņ‚Đž ĐžŅ‚Đ´ĐĩĐģĐĩĐŊ Ņ„Đ°ĐšĐģ", "download_notfound": "НĐĩ Đĩ ĐŊаĐŧĐĩŅ€ĐĩĐŊĐž Са Đ¸ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩ", + "download_original": "ХваĐģŅĐŊĐĩ ĐŊа ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", "download_paused": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩŅ‚Đž Đĩ ĐŊа ĐŋĐ°ŅƒĐˇĐ°", "download_settings": "Đ˜ĐˇŅ‚ĐĩĐŗĐģи", "download_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ, ŅĐ˛ŅŠŅ€ĐˇĐ°ĐŊи ҁ Đ¸ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩŅ‚Đž ĐŊа Ņ„Đ°ĐšĐģОвĐĩ", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Đ˜ĐˇŅ‡Đ°ĐēваĐŊĐĩ Са ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐĩĐŊиĐĩ", "downloading": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩ", "downloading_asset_filename": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩ ĐŊа Ņ„Đ°ĐšĐģ {filename}", + "downloading_from_icloud": "ХваĐģŅĐŊĐĩ ĐžŅ‚ iCloud", "downloading_media": "Đ˜ĐˇŅ‚ĐĩĐŗĐģŅĐŊĐĩ ĐŊа ĐŧĐĩĐ´Đ¸Ņ", "drop_files_to_upload": "ĐŸŅƒŅĐŊĐĩŅ‚Đĩ Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ, Са да ĐŗĐ¸ ĐēĐ°Ņ‡Đ¸Ņ‚Đĩ", "duplicates": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", @@ -929,11 +992,22 @@ "edit_tag": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°Đš Ņ‚Đ°Đŗ", "edit_title": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐˇĐ°ĐŗĐģавиĐĩŅ‚Đž", "edit_user": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", + "edit_workflow": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", "editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "editor_close_without_save_prompt": "ĐŸŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ ĐŊŅĐŧа да ĐąŅŠĐ´Đ°Ņ‚ СаĐŋаСĐĩĐŊи", "editor_close_without_save_title": "Đ—Đ°Ņ‚Đ˛Đ°Ņ€ŅĐŊĐĩ ĐŊа Ņ€ĐĩдаĐēŅ‚ĐžŅ€Đ°?", - "editor_crop_tool_h2_aspect_ratios": "ĐĄŅŠĐžŅ‚ĐŊĐžŅˆĐĩĐŊĐ¸Ņ ĐŊа ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ", - "editor_crop_tool_h2_rotation": "Đ—Đ°Đ˛ŅŠŅ€Ņ‚Đ°ĐŊĐĩ", + "editor_confirm_reset_all_changes": "ĐĄĐ¸ĐŗŅƒŅ€ĐŊи Đģи ҁ҂Đĩ, ҇Đĩ Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ˛ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đĩ Đ˛ŅĐ¸Ņ‡Đēи ĐŋŅ€ĐžĐŧĐĩĐŊи?", + "editor_discard_edits_confirm": "ĐžŅ‚Ņ…Đ˛ŅŠŅ€Đģи ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ", + "editor_discard_edits_prompt": "ИĐŧĐ°Ņ‚Đĩ ĐŊĐĩСаĐŋаСĐĩĐŊи ĐŋŅ€ĐžĐŧĐĩĐŊи. ĐĐ°Đ¸ŅŅ‚Đ¸ĐŊа Đģи Đ¸ŅĐēĐ°Ņ‚Đĩ да ĐŗĐ¸ ĐžŅ‚Ņ…Đ˛ŅŠŅ€ĐģĐ¸Ņ‚Đĩ?", + "editor_discard_edits_title": "ĐžŅ‚Ņ…Đ˛ŅŠŅ€ĐģŅĐŧĐĩ Đģи ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ?", + "editor_edits_applied_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€Đ¸ĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ", + "editor_edits_applied_success": "ĐŖŅĐŋĐĩ҈ĐŊĐž ĐŋŅ€Đ¸ĐģĐ°ĐŗĐ°ĐŊĐĩ ĐŊа ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ", + "editor_flip_horizontal": "ĐžĐąŅŠŅ€ĐŊи Ņ…ĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģĐŊĐž", + "editor_flip_vertical": "ĐžĐąŅŠŅ€ĐŊи вĐĩŅ€Ņ‚Đ¸ĐēаĐģĐŊĐž", + "editor_orientation": "ĐžŅ€Đ¸ĐĩĐŊŅ‚Đ°Ņ†Đ¸Ņ", + "editor_reset_all_changes": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊОви Đ˛ŅĐ¸Ņ‡Đēи ĐŋŅ€ĐžĐŧĐĩĐŊи", + "editor_rotate_left": "Đ—Đ°Đ˛ŅŠŅ€Ņ‚Đ¸ 90° ĐžĐąŅ€Đ°Ņ‚ĐŊĐž ĐŊа Ņ‡Đ°ŅĐžĐ˛ĐŊиĐēĐžĐ˛Đ°Ņ‚Đ° ҁ҂ҀĐĩĐģĐēа", + "editor_rotate_right": "Đ—Đ°Đ˛ŅŠŅ€Ņ‚Đ¸ 90° ĐŋĐž Ņ‡Đ°ŅĐžĐ˛ĐŊиĐēĐžĐ˛Đ°Ņ‚Đ° ҁ҂ҀĐĩĐģĐēа", "email": "ИĐŧĐĩĐšĐģ", "email_notifications": "ИСвĐĩŅŅ‚Đ¸Ņ ĐŊа иĐŧĐĩĐšĐģ", "empty_folder": "ĐĸаСи ĐŋаĐŋĐēа Đĩ ĐŋŅ€Đ°ĐˇĐŊа", @@ -952,11 +1026,14 @@ "error_change_sort_album": "НĐĩ҃ҁĐŋĐĩ҈ĐŊа ĐŋŅ€ĐžĐŧŅĐŊа ĐŊа Ņ€Đĩда ĐŊа ŅĐžŅ€Ņ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа аĐģĐąŅƒĐŧ", "error_delete_face": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ Đ¸ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа ĐģĐ¸Ņ†Đĩ ĐžŅ‚ аĐēŅ‚Đ¸Đ˛Đ°", "error_getting_places": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ŅŅŠĐąĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐŧĐĩŅŅ‚Đ°Ņ‚Đ°", + "error_loading_albums": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа аĐģĐąŅƒĐŧи", "error_loading_image": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩŅ‚Đž", "error_loading_partners": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ĐŋĐ°Ņ€Ņ‚ĐŊŅŒĐžŅ€Đ¸: {error}", + "error_retrieving_asset_information": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐŋĐžĐģŅƒŅ‡Đ°Đ˛Đ°ĐŊĐĩ ĐŊа иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Са ОйĐĩĐēŅ‚", "error_saving_image": "Đ“Ņ€Đĩ҈Đēа: {error}", "error_tag_face_bounding_box": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐžŅ‚ĐąĐĩĐģŅĐˇĐ˛Đ°ĐŊĐĩ ĐŊа ĐģĐ¸Ņ†Đĩ - ĐŊĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ°Đ˛Đ°ĐŊĐĩ ĐŊа ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸ ĐŊа Ņ€Đ°ĐŧĐēĐ°Ņ‚Đ°", "error_title": "Đ“Ņ€Đĩ҈Đēа - ĐŊĐĩŅ‰Đž ҁĐĩ ĐžĐąŅŠŅ€Đēа", + "error_while_navigating": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐŊĐ°Đ˛Đ¸ĐŗĐ¸Ņ€Đ°ĐŊĐĩ ĐēҊĐŧ ОйĐĩĐēŅ‚", "errors": { "cannot_navigate_next_asset": "НĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ да ĐŋŅ€ĐĩĐŧиĐŊĐĩŅ‚Đĩ ĐēҊĐŧ ҁĐģĐĩĐ´Đ˛Đ°Ņ‰Đ¸Ņ Ņ„Đ°ĐšĐģ", "cannot_navigate_previous_asset": "НĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ да ĐŋŅ€ĐĩĐŧиĐŊĐĩŅ‚Đĩ ĐēҊĐŧ ĐŋŅ€ĐĩĐ´Đ¸ŅˆĐŊĐ¸Ņ аĐēŅ‚Đ¸Đ˛", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ĐˇĐ°Đ˛ŅŠŅ€ŅˆĐ¸ OAuth вĐģиСаĐŊĐĩ", "unable_to_connect": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ŅĐ˛ŅŠŅ€ĐļĐĩ", "unable_to_copy_to_clipboard": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ĐēĐžĐŋĐ¸Ņ€Đ° в ĐēĐģиĐŋĐąĐžŅ€Đ´Đ°, ŅƒĐ˛ĐĩŅ€ĐĩŅ‚Đĩ ҁĐĩ, ҇Đĩ иĐŧĐ°Ņ‚Đĩ Đ´ĐžŅŅ‚ŅŠĐŋ Đ´Đž ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ°Ņ‚Đ° ĐŋŅ€ĐĩС https", + "unable_to_create": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ŅŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", "unable_to_create_admin_account": "НĐĩ ĐŧĐžĐļĐĩ да ŅŅŠĐˇĐ´Đ°Đ´Đĩ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ŅĐēи аĐēĐ°ŅƒĐŊŅ‚", "unable_to_create_api_key": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ŅŅŠĐˇĐ´Đ°Đ´Đĩ ĐŊОв API ĐēĐģŅŽŅ‡", "unable_to_create_library": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ŅŅŠĐˇĐ´Đ°Đ´Đĩ йийĐģĐ¸ĐžŅ‚ĐĩĐēа", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "НĐĩ ĐŧĐžĐļĐĩ да Đ¸ĐˇŅ‚Ņ€Đ¸Đĩ ŅˆĐ°ĐąĐģĐžĐŊ Са иСĐēĐģŅŽŅ‡Đ˛Đ°ĐŊĐĩ", "unable_to_delete_shared_link": "ĐĄĐŋОдĐĩĐģĐĩĐŊĐ°Ņ‚Đ° Đ˛Ņ€ŅŠĐˇĐēа ĐŊĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ Đ¸ĐˇŅ‚Ņ€Đ¸Đĩ", "unable_to_delete_user": "НĐĩ ĐŧĐžĐļĐĩ да Đ¸ĐˇŅ‚Ņ€Đ¸Đĩ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", + "unable_to_delete_workflow": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", "unable_to_download_files": "НĐĩ ĐŧĐžĐŗĐ°Ņ‚ да ҁĐĩ Đ¸ĐˇŅ‚ĐĩĐŗĐģŅŅ‚ Ņ„Đ°ĐšĐģОвĐĩŅ‚Đĩ", "unable_to_edit_exclusion_pattern": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€Đ° ŅˆĐ°ĐąĐģĐžĐŊ Са иСĐēĐģŅŽŅ‡Đ˛Đ°ĐŊĐĩ", "unable_to_empty_trash": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž иСĐŋŅ€Đ°ĐˇĐ˛Đ°ĐŊĐĩ ĐŊа ĐēĐžŅˆŅ‡ĐĩŅ‚Đž", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ҁĐēаĐŊĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа йийĐģĐ¸ĐžŅ‚ĐĩĐēĐ°Ņ‚Đ°", "unable_to_set_feature_photo": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž СадаваĐŊĐĩ ĐŊа ĐŋŅ€ĐĩĐ´ŅŅ‚Đ°Đ˛Đ¸Ņ‚ĐĩĐģĐŊа ҁĐŊиĐŧĐēа", "unable_to_set_profile_picture": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž СадаваĐŊĐĩ ĐŊа ĐŋŅ€ĐžŅ„Đ¸ĐģĐŊа ҁĐŊиĐŧĐēа", + "unable_to_set_rating": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž СадаваĐŊĐĩ ĐŊа Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "unable_to_submit_job": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž СадаваĐŊĐĩ ĐŊа ĐˇĐ°Đ´Đ°Ņ‡Đ°", "unable_to_trash_asset": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐĩĐŧĐ°Ņ…Đ˛Đ°ĐŊĐĩ ĐŊа Ņ„Đ°ĐšĐģа", "unable_to_unlink_account": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐžŅ‚Đ´ĐĩĐģŅĐŊĐĩ ĐŊа аĐēĐ°ŅƒĐŊŅ‚Đ°", @@ -1072,12 +1152,14 @@ "unable_to_update_library": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа йийĐģĐ¸ĐžŅ‚ĐĩĐēĐ°Ņ‚Đ°", "unable_to_update_location": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐģĐžĐēĐ°Ņ†Đ¸ŅŅ‚Đ°", "unable_to_update_settings": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ", - "unable_to_update_timeline_display_status": "НĐĩĐ˛ŅŠĐˇĐŧĐžĐļĐŊĐž Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐĩŅ‚Đž ĐŊа ŅŅŠŅŅ‚ĐžŅĐŊиĐĩŅ‚Đž ĐŊа Đ´Đ¸ŅĐŋĐģĐĩŅ ĐŊа Đ˛Ņ€ĐĩĐŧĐĩĐ˛Đ°Ņ‚Đ° ĐģиĐŊĐ¸Ņ", + "unable_to_update_timeline_display_status": "НĐĩĐ˛ŅŠĐˇĐŧĐžĐļĐŊĐž Đĩ ОйĐŊОваваĐŊĐĩ ĐŊа ŅŅŠŅŅ‚ĐžŅĐŊиĐĩŅ‚Đž ĐŊа Đ´Đ¸ŅĐŋĐģĐĩŅ ĐŊа Đ˛Ņ€ĐĩĐŧĐĩĐ˛Đ°Ņ‚Đ° ĐģиĐŊĐ¸Ņ", "unable_to_update_user": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģŅ", + "unable_to_update_workflow": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", "unable_to_upload_file": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐĩ ĐŊа Ņ„Đ°ĐšĐģ" }, + "errors_text": "Đ“Ņ€Đĩ҈Đēи", "exclusion_pattern": "ШайĐģĐžĐŊ Са иСĐēĐģŅŽŅ‡ĐĩĐŊиĐĩ", - "exif": "Exif", + "exif": "Еxif", "exif_bottom_sheet_description": "Добави ОĐŋĐ¸ŅĐ°ĐŊиĐĩ...", "exif_bottom_sheet_description_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ", "exif_bottom_sheet_details": "ПОДРОБНОСĐĸИ", @@ -1116,18 +1198,21 @@ "favorite_or_unfavorite_photo": "Добави иĐģи ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊи ҁĐŊиĐŧĐēа ĐžŅ‚ Đ›ŅŽĐąĐ¸Đŧи", "favorites": "Đ›ŅŽĐąĐ¸Đŧи", "favorites_page_no_favorites": "НĐĩ ŅĐ° ĐŊаĐŧĐĩŅ€ĐĩĐŊи ĐģŅŽĐąĐ¸Đŧи ОйĐĩĐēŅ‚Đ¸", - "feature_photo_updated": "ĐŸŅ€ĐĩĐ´ŅŅ‚Đ°Đ˛Đ¸Ņ‚ĐĩĐģĐŊĐ°Ņ‚Đ° ҁĐŊиĐŧĐēа Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐĩĐŊа", + "feature_photo_updated": "ĐŸŅ€ĐĩĐ´ŅŅ‚Đ°Đ˛Đ¸Ņ‚ĐĩĐģĐŊĐ°Ņ‚Đ° ҁĐŊиĐŧĐēа Đĩ ОйĐŊОвĐĩĐŊа", "features": "Đ¤ŅƒĐŊĐēŅ†Đ¸Đ¸", "features_in_development": "Đ¤ŅƒĐŊĐēŅ†Đ¸Đ¸ в ĐŋŅ€ĐžŅ†Đĩҁ ĐŊа Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Đēа", "features_setting_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸Ņ‚Đĩ ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž", - "file_name": "ИĐŧĐĩ ĐŊа Ņ„Đ°ĐšĐģа", "file_name_or_extension": "ИĐŧĐĩ ĐŊа Ņ„Đ°ĐšĐģ иĐģи Ņ€Đ°ĐˇŅˆĐ¸Ņ€ĐĩĐŊиĐĩ", + "file_name_text": "ИĐŧe ĐŊа Ņ„Đ°ĐšĐģ", + "file_name_with_value": "ИĐŧĐĩ ĐŊа Ņ„Đ°ĐšĐģ: {file_name}", "file_size": "РаСĐŧĐĩŅ€ ĐŊа Ņ„Đ°ĐšĐģа", "filename": "ИĐŧĐĩ ĐŊа Ņ„Đ°ĐšĐģ", "filetype": "ĐĸиĐŋ ĐŊа Ņ„Đ°ĐšĐģ", "filter": "ФиĐģŅ‚ŅŠŅ€", + "filter_description": "ĐŖŅĐģĐžĐ˛Đ¸Ņ Са Ņ„Đ¸ĐģŅ‚Ņ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ОйĐĩĐēŅ‚Đ¸", "filter_people": "ФиĐģŅ‚Ņ€Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа Ņ…ĐžŅ€Đ°", "filter_places": "ФиĐģŅ‚ŅŠŅ€ ĐŋĐž ĐŧŅŅŅ‚Đž", + "filters": "ФиĐģŅ‚Ņ€Đ¸", "find_them_fast": "НаĐŧĐĩŅ€ĐĩŅ‚Đĩ ĐŗĐ¸ ĐąŅŠŅ€ĐˇĐž ĐŋĐž иĐŧĐĩ ҁ Ņ‚ŅŠŅ€ŅĐĩĐŊĐĩ", "first": "ĐŸŅŠŅ€Đ˛Đ¸", "fix_incorrect_match": "ПоĐŋŅ€Đ°Đ˛ŅĐŊĐĩ ĐŊа ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģĐŊĐž ŅŅŠĐ˛ĐŋадĐĩĐŊиĐĩ", @@ -1137,12 +1222,16 @@ "folders_feature_description": "ĐŸŅ€ĐĩĐŗĐģĐĩĐļдаĐŊĐĩ ĐŊа ĐŋаĐŋĐēĐ°Ņ‚Đ° Са ҁĐŊиĐŧĐēĐ¸Ņ‚Đĩ и видĐĩĐžĐēĐģиĐŋОвĐĩŅ‚Đĩ в Ņ„Đ°ĐšĐģĐžĐ˛Đ°Ņ‚Đ° ŅĐ¸ŅŅ‚ĐĩĐŧа", "forgot_pin_code_question": "Đ—Đ°ĐąŅ€Đ°Đ˛Đ¸Đģи ҁ҂Đĩ ŅĐ˛ĐžŅ ПИН ĐēОд?", "forward": "НаĐŋŅ€ĐĩĐ´", + "free_up_space": "ĐžŅĐ˛ĐžĐąĐžĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž", + "free_up_space_description": "ĐŸŅ€ĐĩĐŧĐĩҁ҂ĐĩŅ‚Đĩ Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ ҁĐŊиĐŧĐēи и видĐĩа в ĐēĐžŅˆŅ‡ĐĩŅ‚Đž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž, Са да ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚Đĩ ĐŧŅŅŅ‚Đž. КоĐŋĐ¸ŅŅ‚Đ° ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ СаĐŋаСĐĩĐŊи.", + "free_up_space_settings_subtitle": "ĐžŅĐ˛ĐžĐąĐžĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", "full_path": "ĐŸŅŠĐģĐĩĐŊ ĐŋŅŠŅ‚: {path}", - "gcast_enabled": "Google Cast", + "gcast_enabled": "GООgle Cast", "gcast_enabled_description": "За да Ņ€Đ°ĐąĐžŅ‚Đ¸ Ņ‚Đ°ĐˇĐ¸ Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ ĐˇĐ°Ņ€ĐĩĐļда Đ˛ŅŠĐŊ҈ĐŊи Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐžŅ‚ Google.", "general": "ĐžĐąŅ‰Đ¸", "geolocation_instruction_location": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ОйĐĩĐēŅ‚ ҁ GPS ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸ Са да иСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ Ņ‚ŅŅ… иĐģи иСйĐĩŅ€ĐĩŅ‚Đĩ ĐŧŅŅŅ‚Đž Đ´Đ¸Ņ€ĐĩĐēŅ‚ĐŊĐž ĐžŅ‚ ĐēĐ°Ņ€Ņ‚Đ°Ņ‚Đ°", "get_help": "ПоĐŧĐžŅ‰", + "get_people_error": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐŋĐžĐģŅƒŅ‡Đ°Đ˛Đ°ĐŊĐĩ ĐŊа Ņ…ĐžŅ€Đ°", "get_wifiname_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ°Đ˛Đ°ĐŊĐĩ иĐŧĐĩŅ‚Đž ĐŊа Wi-Fi ĐŧŅ€ĐĩĐļĐ°Ņ‚Đ°. МоĐģŅ, ŅƒĐąĐĩĐ´ĐĩŅ‚Đĩ ҁĐĩ, ҇Đĩ ŅĐ° ĐŋŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛ĐĩĐŊи ĐŊ҃ĐļĐŊĐ¸Ņ‚Đĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊĐ¸Ņ ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž и иĐŧа Đ˛Ņ€ŅŠĐˇĐēа ҁ Wi-Fi", "getting_started": "КаĐē да СаĐŋĐžŅ‡ĐŊĐĩĐŧ", "go_back": "Đ’Ņ€ŅŠŅ‰Đ°ĐŊĐĩ ĐŊаСад", @@ -1175,6 +1264,7 @@ "hide_named_person": "ĐĄĐēŅ€Đ¸Đš Ņ‡ĐžĐ˛ĐĩĐē {name}", "hide_password": "ĐĄĐēŅ€Đ¸Đš ĐŋĐ°Ņ€ĐžĐģа", "hide_person": "ĐĄĐēŅ€Đ¸Đš Ņ‡ĐžĐ˛ĐĩĐē", + "hide_schema": "ĐĄĐēŅ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа ҁ҅ĐĩĐŧĐ°Ņ‚Đ°", "hide_text_recognition": "ĐĄĐēŅ€Đ¸Đš Ņ€Đ°ĐˇĐŋОСĐŊĐ°Ņ‚Đ¸Ņ Ņ‚ĐĩĐēҁ҂", "hide_unnamed_people": "ĐĄĐēŅ€Đ¸Đš ĐŊĐĩĐŊаСОваĐŊи Ņ…ĐžŅ€Đ°", "home_page_add_to_album_conflicts": "ДобавĐĩĐŊи ŅĐ° {added} ОйĐĩĐēŅ‚Đ° в аĐģĐąŅƒĐŧа {album}. ВĐĩ҇Đĩ иĐŧа {failed} ОйĐĩĐēŅ‚Đ°.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "ЗаĐŋĐžŅ‡ĐŊĐ°Ņ‚Đ° ĐžĐąŅ€Đ°ĐąĐžŅ‚Đēа ĐŊа {dateTime}", "items_count": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}}", "jobs": "Đ—Đ°Đ´Đ°Ņ‡Đ¸", + "json_editor": "JSON Ņ€ĐĩдаĐēŅ‚ĐžŅ€", + "json_error": "Đ“Ņ€Đĩ҈Đēа в JSON", "keep": "Đ—Đ°Đ´Ņ€ŅŠĐļ", + "keep_albums": "ЗаĐŋаСи аĐģĐąŅƒĐŧи", + "keep_albums_count": "ЗаĐŋаСваĐŊĐĩ ĐŊа {count} {count, plural, one {аĐģĐąŅƒĐŧ} other {аĐģĐąŅƒĐŧа}}", "keep_all": "Đ—Đ°Đ´Ņ€ŅŠĐļ Đ˛ŅĐ¸Ņ‡Đēи", + "keep_description": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐēаĐēвО да ĐžŅŅ‚Đ°ĐŊĐĩ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž ĐŋŅ€Đ¸ ĐžŅĐ˛ĐžĐąĐžĐļдаваĐŊĐĩ ĐŊа ĐŧŅŅŅ‚Đž.", + "keep_favorites": "ЗаĐŋаСваĐŊĐĩ ĐŊа ĐģŅŽĐąĐ¸Đŧи", + "keep_on_device": "ЗаĐŋаСи ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", + "keep_on_device_hint": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ОйĐĩĐēŅ‚Đ¸Ņ‚Đĩ, ĐēĐžĐ¸Ņ‚Đž да ĐąŅŠĐ´Đ°Ņ‚ СаĐŋаСĐĩĐŊи ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", "keep_this_delete_others": "ЗаĐŋаСи Ņ‚ĐžĐ˛Đ°, Đ¸ĐˇŅ‚Ņ€Đ¸Đš Đ´Ņ€ŅƒĐŗĐ¸Ņ‚Đĩ", + "keeping": "ЗаĐŋаСваĐŊĐĩ: {items}", "kept_this_deleted_others": "ЗаĐŋаСи Ņ‚ĐžĐˇĐ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚ и Đ´Ņ€ŅƒĐŗĐ¸Ņ‚Đĩ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°}}", "keyboard_shortcuts": "Đ‘ŅŠŅ€ĐˇĐ¸ ĐēĐģĐ°Đ˛Đ¸ŅˆĐŊи ĐēĐžĐŧйиĐŊĐ°Ņ†Đ¸Đ¸", "language": "ЕзиĐē", @@ -1316,7 +1415,7 @@ "login_form_api_exception": "Đ“Ņ€Đĩ҈Đēа в ĐēĐžĐŧ҃ĐŊиĐēĐ°Ņ†Đ¸ŅŅ‚Đ°. МоĐģŅ, ĐŋŅ€ĐžĐ˛ĐĩŅ€Đ¸ URL ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° и ĐžĐŋĐ¸Ņ‚Đ°Đš ĐŋаĐē.", "login_form_back_button_text": "ĐžĐąŅ€Đ°Ņ‚ĐŊĐž", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port", + "login_form_endpoint_hint": "http://yĐžur-server-ip:port", "login_form_endpoint_url": "URL Đ°Đ´Ņ€Đĩҁ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ°", "login_form_err_http": "МоĐģŅ, ĐžĐŋŅ€ĐĩĐ´ĐĩĐģи ĐŋŅ€ĐžŅ‚ĐžĐēĐžĐģа http:// иĐģи https://", "login_form_err_invalid_email": "НĐĩваĐģидĐĩĐŊ иĐŧĐĩĐšĐģ Đ°Đ´Ņ€Đĩҁ", @@ -1343,10 +1442,28 @@ "loop_videos_description": "ПозвоĐģи Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ĐŋĐžĐ˛Ņ‚Đ°Ņ€ŅĐŊĐĩ ĐŊа видĐĩĐžŅ‚Đž в Đ¸ĐˇĐŗĐģĐĩда ĐŊа Đ´ĐĩŅ‚Đ°ĐšĐģĐ¸Ņ‚Đĩ.", "main_branch_warning": "ИСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ вĐĩŅ€ŅĐ¸Ņ Са Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Ņ‡Đ¸Ņ†Đ¸, ŅĐ¸ĐģĐŊĐž ĐŋŅ€ĐĩĐŋĐžŅ€ŅŠŅ‡Đ˛Đ°ĐŧĐĩ да иСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ ĐžŅ„Đ¸Ņ†Đ¸Đ°ĐģĐŊа вĐĩŅ€ŅĐ¸Ņ!", "main_menu": "ГĐģавĐŊĐž ĐŧĐĩĐŊŅŽ", + "maintenance_action_restore": "Đ’ŅŠĐˇĐ˛ŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи", "maintenance_description": "ĐĄŅŠŅ€Đ˛ŅŠŅ€Đ° Immich Đĩ ĐŋĐžŅŅ‚Đ°Đ˛ĐĩĐŊ в Ņ€ĐĩĐļиĐŧ ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ.", "maintenance_end": "ĐšŅ€Đ°Đš ĐŊа Ņ€ĐĩĐļиĐŧа ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ", "maintenance_end_error": "НĐĩ҃ҁĐŋĐĩ҈ĐŊĐž ĐˇĐ°Đ˛ŅŠŅ€ŅˆĐ˛Đ°ĐŊĐĩ ĐŊа Ņ€ĐĩĐļиĐŧа ĐŊа ĐžĐąŅĐģ҃ĐļваĐŊĐĩ.", "maintenance_logged_in_as": "ĐĸĐĩĐēŅƒŅ‰Đ¸Ņ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ Đĩ {user}", + "maintenance_restore_from_backup": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐžŅ‚ Đ°Ņ€Ņ…Đ¸Đ˛", + "maintenance_restore_library": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа йийĐģĐ¸ĐžŅ‚ĐĩĐēа", + "maintenance_restore_library_confirm": "АĐēĐž Ņ‚ĐžĐ˛Đ° Đ¸ĐˇĐŗĐģĐĩĐļда ĐŋŅ€Đ°Đ˛Đ¸ĐģĐŊĐž, ĐŊаĐŋŅ€Đ°Đ˛ĐĩŅ‚Đĩ Đ˛ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐžŅ‚ Đ°Ņ€Ņ…Đ¸Đ˛!", + "maintenance_restore_library_description": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊи", + "maintenance_restore_library_folder_has_files": "{folder} иĐŧа {count} ĐŋаĐŋĐēи", + "maintenance_restore_library_folder_no_files": "В {folder} ĐŊŅĐŧа Ņ„Đ°ĐšĐģОвĐĩ!", + "maintenance_restore_library_folder_pass": "Са ҇ĐĩŅ‚ĐĩĐŊĐĩ и Са СаĐŋĐ¸Ņ", + "maintenance_restore_library_folder_read_fail": "ĐŊĐĩ Đĩ Ņ‡Đ¸Ņ‚Đ°ĐĩĐŧ", + "maintenance_restore_library_folder_write_fail": "ĐŊĐĩ Đĩ СаĐŋĐ¸ŅĐ˛Đ°ĐĩĐŧ", + "maintenance_restore_library_hint_missing_files": "МоĐļĐĩ да ĐģиĐŋŅĐ˛Đ°Ņ‚ ваĐļĐŊи Ņ„Đ°ĐšĐģОвĐĩ", + "maintenance_restore_library_hint_regenerate_later": "МоĐļĐĩŅ‚Đĩ да ĐŗĐ¸ ĐŗĐĩĐŊĐĩŅ€Đ¸Ņ€Đ°Ņ‚Đĩ ĐžŅ‚ĐŊОвО ĐŋĐž-ĐēҊҁĐŊĐž в ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ", + "maintenance_restore_library_hint_storage_template_missing_files": "ИСĐŋĐžĐģĐˇĐ˛Đ°Ņ‚Đĩ Đģи ŅˆĐ°ĐąĐģĐžĐŊ Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ? МоĐļĐĩ да ĐģиĐŋŅĐ˛Đ°Ņ‚ Ņ„Đ°ĐšĐģОвĐĩ", + "maintenance_restore_library_loading": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēи Са Ņ†ŅĐģĐžŅŅ‚ и ĐĩĐ˛Ņ€Đ¸ŅŅ‚Đ¸Đēаâ€Ļ", + "maintenance_task_backup": "ĐĄŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ ĐŊа Đ°Ņ€Ņ…Đ¸Đ˛ ĐŊа ŅŅŠŅ‰ĐĩŅŅ‚Đ˛ŅƒĐ˛Đ°Ņ‰Đ°Ņ‚Đ° йаСа даĐŊĐŊиâ€Ļ", + "maintenance_task_migrations": "ИСĐŋҊĐģĐŊŅĐ˛Đ°Ņ‚ ҁĐĩ ĐŧĐ¸ĐŗŅ€Đ°Ņ†Đ¸Đ¸ ĐŊа ĐąĐ°ĐˇĐ°Ņ‚Đ° даĐŊĐŊиâ€Ļ", + "maintenance_task_restore": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩ ĐžŅ‚ Đ¸ĐˇĐąŅ€Đ°ĐŊĐ¸Ņ Đ°Ņ€Ņ…Đ¸Đ˛â€Ļ", + "maintenance_task_rollback": "Đ’ŅŠĐˇŅŅ‚Đ°ĐŊĐžĐ˛ŅĐ˛Đ°ĐŊĐĩŅ‚Đž ĐŊĐĩ Đĩ ҃ҁĐŋĐĩ҈ĐŊĐž, Đ˛Ņ€ŅŠŅ‰Đ°ĐŊĐĩ ĐēҊĐŧ ĐŊĐ°Ņ‡Đ°ĐģĐŊа ĐŋĐžĐˇĐ¸Ņ†Đ¸Ņâ€Ļ", "maintenance_title": "Đ’Ņ€ĐĩĐŧĐĩĐŊĐŊĐž ĐŊĐĩĐ´ĐžŅŅ‚ŅŠĐŋĐĩĐŊ", "make": "ĐœĐ°Ņ€Đēа", "manage_geolocation": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊа ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊĐ¸ŅŅ‚Đ°", @@ -1408,6 +1525,8 @@ "minimize": "МиĐŊиĐŧĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐĩ", "minute": "МиĐŊŅƒŅ‚Đ°", "minutes": "МиĐŊŅƒŅ‚Đ¸", + "mirror_horizontal": "ĐĨĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģĐŊĐž", + "mirror_vertical": "ВĐĩŅ€Ņ‚Đ¸ĐēаĐģĐŊĐž", "missing": "ЛиĐŋŅĐ˛Đ°Ņ‰Đ¸", "mobile_app": "МобиĐģĐŊĐž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ", "mobile_app_download_onboarding_note": "ХваĐģĐĩŅ‚Đĩ ĐŧОйиĐģĐŊĐžŅ‚Đž ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ Immich ҁ ĐŊŅĐēĐžŅ ĐžŅ‚ ҁĐģĐĩĐ´ĐŊĐ¸Ņ‚Đĩ ĐžĐŋŅ†Đ¸Đ¸", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM Đŗ", "more": "ĐžŅ‰Đĩ", "move": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸", + "move_down": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸ ĐŊадОĐģ҃", "move_off_locked_folder": "ИСвади ĐžŅ‚ СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", "move_to": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸ ĐēҊĐŧ", + "move_to_device_trash": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ˛Đ°ĐŊĐĩ в ĐēĐžŅˆŅ‡ĐĩŅ‚Đž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ĐžŅ‚Đž", "move_to_lock_folder_action_prompt": "{count} ŅĐ° дОйавĐĩĐŊи в СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", "move_to_locked_folder": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸ в СаĐēĐģŅŽŅ‡ĐĩĐŊа ĐŋаĐŋĐēа", "move_to_locked_folder_confirmation": "ĐĸĐĩСи ҁĐŊиĐŧĐēи и видĐĩа ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ ĐžŅ‚ Đ˛ŅĐ¸Ņ‡Đēи аĐģĐąŅƒĐŧи и ҉Đĩ ŅĐ° Đ´ĐžŅŅ‚ŅŠĐŋĐŊи ŅĐ°ĐŧĐž в СаĐēĐģŅŽŅ‡ĐĩĐŊĐ°Ņ‚Đ° ĐŋаĐŋĐēа", + "move_up": "ĐŸŅ€ĐĩĐŧĐĩŅŅ‚Đ¸ ĐŊĐ°ĐŗĐžŅ€Đĩ", "moved_to_archive": "{count, plural, one {# ОйĐĩĐēŅ‚ Đĩ ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊ} many {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи} other {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи}} в Đ°Ņ€Ņ…Đ¸Đ˛Đ°", "moved_to_library": "{count, plural, one {# ОйĐĩĐēŅ‚ Đĩ ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊ} many {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи} other {# ОйĐĩĐēŅ‚Đ° ŅĐ° ĐŋŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊи}} в йийĐģĐ¸ĐžŅ‚ĐĩĐēĐ°Ņ‚Đ°", "moved_to_trash": "ĐŸŅ€ĐĩĐŧĐĩҁ҂ĐĩĐŊĐž в ĐēĐžŅˆŅ‡ĐĩŅ‚Đž", @@ -1430,6 +1552,7 @@ "my_albums": "Мои аĐģĐąŅƒĐŧи", "name": "ИĐŧĐĩ", "name_or_nickname": "ИĐŧĐĩ иĐģи ĐŋŅ€ŅĐēĐžŅ€", + "name_required": "Đ—Đ°Đ´ŅŠĐģĐļĐ¸Ņ‚ĐĩĐģĐŊĐž Đĩ ИĐŧĐĩ", "navigate": "ĐŸŅ€Đ¸Đ´Đ˛Đ¸ĐļваĐŊĐĩ", "navigate_to_time": "ĐŸŅ€Đ¸Đ´Đ˛Đ¸ĐļваĐŊĐĩ Đ´Đž ĐŧĐžĐŧĐĩĐŊŅ‚ Đ˛ŅŠĐ˛ Đ˛Ņ€ĐĩĐŧĐĩŅ‚Đž", "network_requirement_photos_upload": "ИСĐŋĐžĐģСваК ĐŧОйиĐģĐŊи даĐŊĐŊи Са Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи", @@ -1454,20 +1577,24 @@ "next": "ĐĄĐģĐĩĐ´Đ˛Đ°Ņ‰Đž", "next_memory": "ĐĄĐģĐĩĐ´Đ˛Đ°Ņ‰ ҁĐŋĐžĐŧĐĩĐŊ", "no": "НĐĩ", + "no_actions_added": "Đ’ŅĐĩ ĐžŅ‰Đĩ ĐŊĐĩ ŅĐ° дОйавĐĩĐŊи Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ", + "no_albums_found": "НĐĩ ŅĐ° ĐŊаĐŧĐĩŅ€ĐĩĐŊи аĐģĐąŅƒĐŧи", "no_albums_message": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ аĐģĐąŅƒĐŧ Са ĐžŅ€ĐŗĐ°ĐŊĐ¸ĐˇĐ¸Ņ€Đ°ĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ", "no_albums_with_name_yet": "Đ˜ĐˇĐŗĐģĐĩĐļда, ҇Đĩ Đ˛ŅĐĩ ĐžŅ‰Đĩ ĐŊŅĐŧĐ°Ņ‚Đĩ аĐģĐąŅƒĐŧи ҁ Ņ‚ĐžĐ˛Đ° иĐŧĐĩ.", "no_albums_yet": "Đ˜ĐˇĐŗĐģĐĩĐļда, ҇Đĩ Đ˛ŅĐĩ ĐžŅ‰Đĩ ĐŊŅĐŧĐ°Ņ‚Đĩ аĐģĐąŅƒĐŧи.", "no_archived_assets_message": "ĐŅ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°ĐšŅ‚Đĩ ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ, Са да ĐŗĐ¸ ҁĐēŅ€Đ¸ĐĩŅ‚Đĩ ĐžŅ‚ Đ¸ĐˇĐŗĐģĐĩда ĐŊа ĐĄĐŊиĐŧĐēи", - "no_assets_message": "КЛИКНЕĐĸЕ, ЗА ДА КАЧИĐĸЕ ПĐĒРВАĐĸА ХИ СНИМКА", + "no_assets_message": "КĐģиĐēĐŊĐĩŅ‚Đĩ, Са да ĐēĐ°Ņ‡Đ¸Ņ‚Đĩ ĐŋŅŠŅ€Đ˛Đ°Ņ‚Đ° ҁĐŊиĐŧĐēа", "no_assets_to_show": "ĐŅĐŧа ОйĐĩĐēŅ‚Đ¸ Са ĐŋĐžĐēаСваĐŊĐĩ", "no_cast_devices_found": "ĐŅĐŧа ĐŊаĐŧĐĩŅ€ĐĩĐŊи ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ° Са ĐŋŅ€ĐĩдаваĐŊĐĩ", "no_checksum_local": "ЛиĐŋŅĐ˛Đ°Ņ‚ ĐēĐžĐŊŅ‚Ņ€ĐžĐģĐŊи ҁ҃Đŧи - ĐŊĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ĐŋĐžĐģŅƒŅ‡Đ°Ņ‚ ĐģĐžĐēаĐģĐŊи ОйĐĩĐēŅ‚Đ¸", "no_checksum_remote": "ЛиĐŋŅĐ˛Đ°Ņ‚ ĐēĐžĐŊŅ‚Ņ€ĐžĐģĐŊи ҁ҃Đŧи - ĐŊĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ ĐŋĐžĐģŅƒŅ‡Đ°Ņ‚ ОйĐĩĐēŅ‚Đ¸ ĐžŅ‚ ŅŅŠŅ€Đ˛ŅŠŅ€Đ°", + "no_configuration_needed": "НĐĩ Đĩ ĐŊ҃ĐļĐŊа ĐēĐžĐŊŅ„Đ¸ĐŗŅƒŅ€Đ°Ņ†Đ¸Ņ", "no_devices": "ĐŅĐŧа ĐžŅ‚ĐžŅ€Đ¸ĐˇĐ¸Ņ€Đ°ĐŊи ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", "no_duplicates_found": "НĐĩ ĐąŅŅ…Đ° ĐžŅ‚ĐēŅ€Đ¸Ņ‚Đ¸ Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸.", "no_exif_info_available": "ĐŅĐŧа exif иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ", "no_explore_results_message": "ĐšĐ°Ņ‡ĐĩŅ‚Đĩ ĐžŅ‰Đĩ ҁĐŊиĐŧĐēи, Са да Ņ€Đ°ĐˇĐŗĐģĐĩĐ´Đ°Ņ‚Đĩ ĐēĐžĐģĐĩĐēŅ†Đ¸ŅŅ‚Đ° ŅĐ¸.", "no_favorites_message": "ДобавĐĩŅ‚Đĩ в ĐģŅŽĐąĐ¸Đŧи, Са да ĐŊаĐŧĐ¸Ņ€Đ°Ņ‚Đĩ ĐąŅŠŅ€ĐˇĐž ĐŊаК-Đ´ĐžĐąŅ€Đ¸Ņ‚Đĩ ŅĐ¸ ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ", + "no_filters_added": "Đ’ŅĐĩ ĐžŅ‰Đĩ ĐŊĐĩ ŅĐ° дОйавĐĩĐŊи Ņ„Đ¸ĐģŅ‚Ņ€Đ¸", "no_libraries_message": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ Đ˛ŅŠĐŊ҈ĐŊа йийĐģĐ¸ĐžŅ‚ĐĩĐēа Са да Ņ€Đ°ĐˇĐŗĐģĐĩĐļĐ´Đ°Ņ‚Đĩ ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ", "no_local_assets_found": "НĐĩ Đĩ ĐŊаĐŧĐĩŅ€ĐĩĐŊ ĐģĐžĐēаĐģĐĩĐŊ ОйĐĩĐēŅ‚ ҁ Ņ‚Đ°Đēава ĐēĐžĐŊŅ‚Ņ€ĐžĐģĐŊа ҁ҃Đŧа", "no_location_set": "НĐĩ Đĩ СададĐĩĐŊĐž ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", @@ -1481,11 +1608,11 @@ "no_results_description": "ОĐŋĐ¸Ņ‚Đ°ĐšŅ‚Đĩ ҁҊҁ ŅĐ¸ĐŊĐžĐŊиĐŧ иĐģи ĐŋĐž-ĐžĐąŅ‰Đ° ĐēĐģŅŽŅ‡ĐžĐ˛Đ° Đ´ŅƒĐŧа", "no_shared_albums_message": "ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ аĐģĐąŅƒĐŧ, Са да ҁĐŋОдĐĩĐģŅŅ‚Đĩ ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ ҁ Ņ…ĐžŅ€Đ°Ņ‚Đ° в ĐŧŅ€ĐĩĐļĐ°Ņ‚Đ° ŅĐ¸", "no_uploads_in_progress": "ĐŅĐŧа ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐĩ в ĐŧĐžĐŧĐĩĐŊŅ‚Đ°", + "none": "ĐĐ¸Ņ‰Đž", "not_allowed": "НĐĩ Đĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊĐž", "not_available": "НĐĩĐŊаĐģĐ¸Ņ‡ĐŊĐž", "not_in_any_album": "НĐĩ Đĩ в ĐŊиĐēОК аĐģĐąŅƒĐŧ", "not_selected": "НĐĩ Đĩ Đ¸ĐˇĐąŅ€Đ°ĐŊĐž", - "note_apply_storage_label_to_previously_uploaded assets": "ЗабĐĩĐģĐĩĐļĐēа: За да ĐŋŅ€Đ¸ĐģĐžĐļĐ¸Ņ‚Đĩ ĐĩŅ‚Đ¸ĐēĐĩŅ‚Đ° Са ŅŅŠŅ…Ņ€Đ°ĐŊĐĩĐŊиĐĩ ĐēҊĐŧ ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģĐŊĐž ĐēĐ°Ņ‡ĐĩĐŊи аĐēŅ‚Đ¸Đ˛Đ¸, ŅŅ‚Đ°Ņ€Ņ‚Đ¸Ņ€Đ°ĐšŅ‚Đĩ", "notes": "БĐĩĐģĐĩĐļĐēи", "nothing_here_yet": "Đ—Đ°ŅĐĩĐŗĐ° Ņ‚ŅƒĐē ĐŊŅĐŧа ĐŊĐ¸Ņ‰Đž", "notification_permission_dialog_content": "За да вĐēĐģŅŽŅ‡Đ¸Ņˆ иСвĐĩŅŅ‚Đ¸ŅŅ‚Đ°, ĐžŅ‚Đ¸Đ´Đ¸ в ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи и иСйĐĩŅ€Đ¸ Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸.", @@ -1563,6 +1690,7 @@ "people": "ĐĨĐžŅ€Đ°", "people_edits_count": "ĐŸŅ€ĐžĐŧĐĩĐŊи {count, plural, one {# Ņ‡ĐžĐ˛ĐĩĐē} other {# Ņ‡ĐžĐ˛ĐĩĐēа}}", "people_feature_description": "ĐŸŅ€ĐĩĐŗĐģĐĩĐļдаĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ, ĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи ĐŋĐž Ņ…ĐžŅ€Đ°", + "people_selected": "{count, plural, one {Đ˜ĐˇĐąŅ€Đ°ĐŊ Đĩ # Ņ‡ĐžĐ˛ĐĩĐē} other {Đ˜ĐˇĐąŅ€Đ°ĐŊи ŅĐ° # Ņ‡ĐžĐ˛ĐĩĐēа}}", "people_sidebar_description": "ПоĐēаСваĐŊĐĩ ĐŊа Đ˛Ņ€ŅŠĐˇĐēа ĐēҊĐŧ Ņ…ĐžŅ€Đ°Ņ‚Đ° в ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ‡ĐŊĐ°Ņ‚Đ° ĐģĐĩĐŊŅ‚Đ°", "permanent_deletion_warning": "ĐŸŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´ĐĩĐŊиĐĩ Са Ņ‚Ņ€Đ°ĐšĐŊĐž Đ¸ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ", "permanent_deletion_warning_setting_description": "ПоĐēаСваĐŊĐĩ ĐŊа ĐŋŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´ĐĩĐŊиĐĩ ĐŋŅ€Đ¸ Ņ‚Ņ€Đ°ĐšĐŊĐž Đ¸ĐˇŅ‚Ņ€Đ¸Đ˛Đ°ĐŊĐĩ ĐŊа аĐēŅ‚Đ¸Đ˛Đ¸", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# ĐŗĐžĐ´Đ¸ĐŊи}}", "person_birthdate": "Đ”Đ°Ņ‚Đ° ĐŊа Ņ€Đ°ĐļдаĐŊĐĩ {date}", "person_hidden": "{name}{hidden, select, true { (ҁĐēŅ€Đ¸Ņ‚)} other {}}", + "person_recognized": "РаСĐŋОСĐŊĐ°Ņ‚Đž e ĐģĐ¸Ņ†Đĩ", + "person_selected": "Đ˜ĐˇĐąŅ€Đ°ĐŊĐž Đĩ ĐģĐ¸Ņ†Đĩ", "photo_shared_all_users": "Đ˜ĐˇĐŗĐģĐĩĐļда, ҇Đĩ ҁ҂Đĩ ҁĐŋОдĐĩĐģиĐģи ҁĐŊиĐŧĐēĐ¸Ņ‚Đĩ ŅĐ¸ ҁ Đ˛ŅĐ¸Ņ‡Đēи ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи иĐģи ĐŊŅĐŧĐ°Ņ‚Đĩ ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģи, ҁ ĐēĐžĐ¸Ņ‚Đž да ҁĐŋОдĐĩĐģŅŅ‚Đĩ.", "photos": "ĐĄĐŊиĐŧĐēи", "photos_and_videos": "ĐĄĐŊиĐŧĐēи и ВидĐĩа", "photos_count": "{count, plural, one {{count, number} ĐĄĐŊиĐŧĐēа} other {{count, number} ĐĄĐŊиĐŧĐēи}}", "photos_from_previous_years": "ĐĄĐŊиĐŧĐēи ĐžŅ‚ ĐŋŅ€ĐĩĐ´Ņ…ĐžĐ´ĐŊи ĐŗĐžĐ´Đ¸ĐŊи", + "photos_only": "ХаĐŧĐž ҁĐŊиĐŧĐēи", "pick_a_location": "ИСйĐĩŅ€Đ¸ ĐģĐžĐēĐ°Ņ†Đ¸Ņ", "pick_custom_range": "ĐŸŅ€ĐžĐ¸ĐˇĐ˛ĐžĐģĐĩĐŊ ĐŋĐĩŅ€Đ¸ĐžĐ´", "pick_date_range": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐŋĐĩŅ€Đ¸ĐžĐ´", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "ĐŸŅ€ĐžĐ´ŅƒĐēŅ‚ĐžĐ˛Đ¸ŅŅ‚ ĐēĐģŅŽŅ‡ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° ҁĐĩ ҃ĐŋŅ€Đ°Đ˛ĐģŅĐ˛Đ° ĐžŅ‚ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "query_asset_id": "Buscar item per ID", "queue_status": "В ĐžĐŋĐ°ŅˆĐēа {count} ĐžŅ‚ {total}", + "rate_asset": "ЗадаваĐŊĐĩ ĐŊа Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating": "ĐžŅ†ĐĩĐŊĐēа ҁҊҁ СвĐĩСди", "rating_clear": "Đ˜ĐˇŅ‡Đ¸ŅŅ‚Đ¸ ĐžŅ†ĐĩĐŊĐēĐ°Ņ‚Đ°", "rating_count": "{count, plural, one {# СвĐĩСда} other {# СвĐĩСди}}", "rating_description": "ПоĐēаĐļи EXIF ĐžŅ†ĐĩĐŊĐēĐ°Ņ‚Đ° в ĐŋаĐŊĐĩĐģа ҁ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ", + "rating_set": "ЗададĐĩĐŊ Đĩ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ {rating, plural, one {# СвĐĩСда} other {# СвĐĩСди}}", "reaction_options": "Đ˜ĐˇĐąĐžŅ€ ĐŊа Ņ€ĐĩаĐēŅ†Đ¸Ņ", "read_changelog": "ĐŸŅ€ĐžŅ‡ĐĩŅ‚Đ¸ ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ", "readonly_mode_disabled": "Đ ĐĩĐļиĐŧа ŅĐ°ĐŧĐž Са ҇ĐĩŅ‚ĐĩĐŊĐĩ Đĩ Đ´ĐĩаĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐŊ", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "ĐŸŅ€ĐĩĐŊаСĐŊĐ°Ņ‡ĐĩĐŊи {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°}} ĐŊа ĐŊОв Ņ‡ĐžĐ˛ĐĩĐē", "reassing_hint": "НазĐŊĐ°Ņ‡Đ¸ Đ¸ĐˇĐąŅ€Đ°ĐŊĐ¸Ņ‚Đĩ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊа ŅŅŠŅ‰ĐĩŅŅ‚Đ˛ŅƒĐ˛Đ°Ņ‰Đž ĐģĐ¸Ņ†Đĩ", "recent": "ĐĄĐēĐžŅ€ĐžŅˆĐŊи", - "recent-albums": "ĐĄĐēĐžŅ€ĐžŅˆĐŊи АĐģĐąŅƒĐŧи", + "recent_albums": "ĐĄĐēĐžŅ€ĐžŅˆĐŊи АĐģĐąŅƒĐŧи", "recent_searches": "ĐĄĐēĐžŅ€ĐžŅˆĐŊи Ņ‚ŅŠŅ€ŅĐĩĐŊĐ¸Ņ", "recently_added": "ĐĐ°ŅĐēĐžŅ€Đž дОйавĐĩĐŊĐž", "recently_added_page_title": "ĐĐ°ŅĐēĐžŅ€Đž дОйавĐĩĐŊĐž", @@ -1770,9 +1903,11 @@ "saved_settings": "ЗаĐŋаСĐĩĐŊи ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи", "say_something": "КаĐļи ĐŊĐĩŅ‰Đž", "scaffold_body_error_occurred": "Đ’ŅŠĐˇĐŊиĐēĐŊа ĐŗŅ€Đĩ҈Đēа", + "scan": "ĐĄĐēаĐŊĐ¸Ņ€Đ°ĐŊe", "scan_all_libraries": "ĐĄĐēаĐŊĐ¸Ņ€Đ°Đš Đ˛ŅĐ¸Ņ‡Đēи йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "scan_library": "ĐĄĐēаĐŊĐ¸Ņ€Đ°Đš", "scan_settings": "ĐĄĐēаĐŊĐ¸Ņ€Đ°Đš ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ¸Ņ‚Đĩ", + "scanning": "ĐĄĐēаĐŊĐ¸Ņ€Đ°ĐŊĐĩ", "scanning_for_album": "ĐĄĐēаĐŊĐ¸Ņ€Đ°Đš Са аĐģĐąŅƒĐŧ...", "search": "ĐĸŅŠŅ€ŅĐĩĐŊĐĩ", "search_albums": "ĐĸŅŠŅ€ŅĐ¸ аĐģĐąŅƒĐŧи", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "ИСйĐĩŅ€Đ¸ Ņ‚Đ¸Đŋ ĐŊа Ņ„Đ°ĐšĐģа", "search_filter_ocr": "ĐĸŅŠŅ€ŅĐĩĐŊĐĩ ĐŊa Ņ‚ĐĩĐēҁ҂", "search_filter_people_title": "ИСйĐĩŅ€Đ¸ Ņ…ĐžŅ€Đ°", + "search_filter_star_rating": "КĐģĐ°ŅĐ°Ņ†Đ¸Ņ ҁҊҁ СвĐĩСди", "search_for": "ĐĸŅŠŅ€ŅĐ¸ Са", "search_for_existing_person": "ĐĸŅŠŅ€ŅĐ¸ ŅŅŠŅ‰ĐĩŅŅ‚Đ˛ŅƒĐ˛Đ°Ņ‰ Ņ‡ĐžĐ˛ĐĩĐē", "search_no_more_result": "ĐŅĐŧа Đ´Ņ€ŅƒĐŗĐ¸ Ņ€ĐĩĐˇŅƒĐģŅ‚Đ°Ņ‚Đ¸", @@ -1836,17 +1972,23 @@ "second": "ĐĄĐĩĐē҃ĐŊда", "see_all_people": "ВиĐļŅ‚Đĩ Đ˛ŅĐ¸Ņ‡Đēи Ņ…ĐžŅ€Đ°", "select": "ИСйĐĩŅ€Đ¸", + "select_album": "ИСйĐĩŅ€ĐĩŅ‚Đĩ аĐģĐąŅƒĐŧ", "select_album_cover": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ОйĐģĐžĐļĐēа ĐŊа аĐģĐąŅƒĐŧ", + "select_albums": "ИСйĐĩŅ€ĐĩŅ‚Đĩ аĐģĐąŅƒĐŧи", "select_all": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Đ˛ŅĐ¸Ņ‡Đēи", "select_all_duplicates": "ИСйĐĩŅ€Đ¸ Đ˛ŅĐ¸Ņ‡Đēи Đ´ŅƒĐąĐģиĐēĐ°Ņ‚Đ¸", "select_all_in": "ИСйĐĩŅ€Đ¸ Đ˛ŅĐ¸Ņ‡Đēи ĐžŅ‚ ĐŗŅ€ŅƒĐŋĐ°Ņ‚Đ° {group}", "select_avatar_color": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Ņ†Đ˛ŅŅ‚ ĐŊа Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°", + "select_count": "{count, plural, one {Đ˜ĐˇĐąŅ€Đ°ĐŊ Đĩ #} other {Đ˜ĐˇĐąŅ€Đ°ĐŊи ŅĐ° #}}", + "select_cutoff_date": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐēŅ€Đ°ĐšĐŊа Đ´Đ°Ņ‚Đ°", "select_face": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐģĐ¸Ņ†Đĩ", "select_featured_photo": "ИСйĐĩŅ€Đ¸ ĐŋŅ€ĐĩĐ´ŅŅ‚Đ°Đ˛Đ¸Ņ‚ĐĩĐģĐŊа ҁĐŊиĐŧĐēа", "select_from_computer": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐžŅ‚ ĐēĐžĐŧĐŋŅŽŅ‚ŅŠŅ€Đ°", "select_keep_all": "ИСйĐĩŅ€Đ¸ \"ĐˇĐ°Đ´Ņ€ŅŠĐļ Đ˛ŅĐ¸Ņ‡Đēи\"", "select_library_owner": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ŅĐžĐąŅŅ‚Đ˛ĐĩĐŊиĐē ĐŊа йийĐģĐ¸ĐžŅ‚ĐĩĐēа", "select_new_face": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐŊОвО ĐģĐ¸Ņ†Đĩ", + "select_people": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ĐģĐ¸Ņ†Đ°", + "select_person": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Ņ‡ĐžĐ˛ĐĩĐē", "select_person_to_tag": "ИСйĐĩŅ€Đ¸ ĐģĐ¸Ņ†Đĩ, ĐēĐžĐĩŅ‚Đž да ĐŧĐ°Ņ€ĐēĐ¸Ņ€Đ°Ņˆ", "select_photos": "ИСйĐĩŅ€ĐĩŅ‚Đĩ ҁĐŊиĐŧĐēи", "select_trash_all": "ИСйĐĩŅ€ĐĩŅ‚Đĩ Đ˛ŅĐ¸Ņ‡ĐēĐž Са ĐēĐžŅˆŅ‡ĐĩŅ‚Đž", @@ -1938,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} ĐŗĐžĐ´Đ¸ĐŊи", "shared_link_edit_password_hint": "Đ’ŅŠĐ˛Đĩди ĐŋĐ°Ņ€ĐžĐģа Са Đ´ĐžŅŅ‚ŅŠĐŋ Đ´Đž ҁĐŋОдĐĩĐģĐĩĐŊ Ņ€ĐĩŅŅƒŅ€Ņ", "shared_link_edit_submit_button": "ОбĐŊОви Đ˛Ņ€ŅŠĐˇĐēĐ°Ņ‚Đ°", - "shared_link_error_server_url_fetch": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ иСвĐģĐĩ҇Đĩ URL Đ°Đ´Ņ€ĐĩŅŅŠŅ‚ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ°", + "shared_link_error_server_url_fetch": "НĐĩ ĐŧĐžĐļĐĩ да ҁĐĩ иСвĐģĐĩ҇Đĩ url-Đ°Đ´Ņ€ĐĩŅŅŠŅ‚ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ°", "shared_link_expires_day": "Đ˜ĐˇŅ‚Đ¸Ņ‡Đ° ҁĐģĐĩĐ´ {count} Đ´ĐĩĐŊ", "shared_link_expires_days": "Đ˜ĐˇŅ‚Đ¸Ņ‡Đ° ҁĐģĐĩĐ´ {count} Đ´ĐŊи", "shared_link_expires_hour": "Đ˜ĐˇŅ‚Đ¸Ņ‡Đ° ҁĐģĐĩĐ´ {count} Ņ‡Đ°Ņ", @@ -1982,6 +2124,7 @@ "show_password": "ПоĐēаĐļи ĐŋĐ°Ņ€ĐžĐģĐ°Ņ‚Đ°", "show_person_options": "ПоĐēаСваĐŊĐĩ ĐŊа ĐžĐŋŅ†Đ¸Đ¸ Са ĐģĐ¸Ņ†Đ°", "show_progress_bar": "ПоĐēаСваĐŊĐĩ ĐŊа ĐŋŅ€ĐžĐŗŅ€Đĩҁ ĐąĐ°Ņ€Đ°", + "show_schema": "ПоĐēаĐļи ҁ҅ĐĩĐŧа", "show_search_options": "ПоĐēаСваĐŊĐĩ ĐŊа ĐžĐŋŅ†Đ¸Đ¸Ņ‚Đĩ Са Ņ‚ŅŠŅ€ŅĐĩĐŊĐĩ", "show_shared_links": "ПоĐēаĐļи ҁĐŋОдĐĩĐģĐĩĐŊи ĐģиĐŊĐēОвĐĩ", "show_slideshow_transition": "ПоĐēаĐļи ĐŋŅ€ĐĩŅ…ĐžĐ´Đ° ĐŊа ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒŅ‚Đž", @@ -1999,6 +2142,8 @@ "skip_to_folders": "ĐŸŅ€ĐĩĐŧиĐŊи ĐēҊĐŧ ĐŋаĐŋĐēĐ¸Ņ‚Đĩ", "skip_to_tags": "ĐŸŅ€ĐĩĐŧиĐŊи ĐēҊĐŧ ĐĩŅ‚Đ¸ĐēĐĩŅ‚Đ¸Ņ‚Đĩ", "slideshow": "ĐĄĐģĐ°ĐšĐ´ŅˆĐžŅƒ", + "slideshow_repeat": "ĐŸĐžĐ˛Ņ‚Đ°Ņ€ŅĐš ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒŅ‚Đž", + "slideshow_repeat_description": "ЗаĐŋĐžŅ‡Đ˛Đ°Đš ĐžŅ‚ĐŊОвО, ĐēĐžĐŗĐ°Ņ‚Đž ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒŅ‚Đž ĐŋŅ€Đ¸ĐēĐģŅŽŅ‡Đ¸", "slideshow_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи Са ҁĐģĐ°ĐšĐ´ŅˆĐžŅƒ", "sort_albums_by": "ĐĄĐžŅ€Ņ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа аĐģĐąŅƒĐŧи ĐŋĐž...", "sort_created": "Đ”Đ°Ņ‚Đ° ĐŊа ŅŅŠĐˇĐ´Đ°Đ˛Đ°ĐŊĐĩ", @@ -2053,7 +2198,7 @@ "tag_feature_description": "Đ Đ°ĐˇĐŗĐģĐĩĐļдаĐŊĐĩ ĐŊа ҁĐŊиĐŧĐēи и видĐĩĐžĐēĐģиĐŋОвĐĩ, ĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи ĐŋĐž Ņ‚ĐĩĐŧи ҁ ĐģĐžĐŗĐ¸Ņ‡ĐĩҁĐēи Ņ‚Đ°ĐŗĐžĐ˛Đĩ", "tag_not_found_question": "НĐĩ ĐŧĐžĐļĐĩŅ‚Đĩ да ĐŊаĐŧĐĩŅ€Đ¸Ņ‚Đĩ ĐĩŅ‚Đ¸ĐēĐĩŅ‚? ĐĄŅŠĐˇĐ´Đ°ĐšŅ‚Đĩ Ņ‚Đ°ĐēŅŠĐ˛ Ņ‚ŅƒĐē", "tag_people": "ĐžŅ‚ĐąĐĩĐģĐĩĐļи ĐĨĐžŅ€Đ°", - "tag_updated": "АĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊ ĐĩŅ‚Đ¸ĐēĐĩŅ‚: {tag}", + "tag_updated": "ОбĐŊОвĐĩĐŊ ĐĩŅ‚Đ¸ĐēĐĩŅ‚: {tag}", "tagged_assets": "ĐĸĐ°ĐŗĐŊĐ°Ņ‚Đ¸ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}}", "tags": "Đ•Ņ‚Đ¸ĐēĐĩŅ‚", "tap_to_run_job": "ДоĐēĐžŅĐŊĐĩŅ‚Đĩ, Са да ŅŅ‚Đ°Ņ€Ņ‚Đ¸Ņ€Đ°Ņ‚Đĩ ĐˇĐ°Đ´Đ°Ņ‡Đ°Ņ‚Đ°", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Задай ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐŊа Ņ†Đ˛ĐĩŅ‚ĐžĐ˛Đ°Ņ‚Đ° Ņ‚ĐĩĐŧа ĐŊа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩŅ‚Đž", "theme_setting_three_stage_loading_subtitle": "ĐĸŅ€Đ¸-ҁ҂ĐĩĐŋĐĩĐŊĐŊĐžŅ‚Đž ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŧĐžĐļĐĩ да ŅƒĐ˛ĐĩĐģĐ¸Ņ‡Đ¸ ĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ĐĩĐģĐŊĐžŅŅ‚Ņ‚Đ°, ĐŊĐž ҉Đĩ ŅƒĐ˛ĐĩĐģĐ¸Ņ‡Đ¸ СĐŊĐ°Ņ‡Đ¸Ņ‚ĐĩĐģĐŊĐž и ĐŧŅ€ĐĩĐļĐžĐ˛Đ¸Ņ Ņ‚Ņ€Đ°Ņ„Đ¸Đē", "theme_setting_three_stage_loading_title": "ВĐēĐģŅŽŅ‡Đ¸ Ņ‚Ņ€Đ¸-ҁ҂ĐĩĐŋĐĩĐŊĐŊĐž ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ", + "then": "ĐĄĐģĐĩĐ´ Ņ‚ĐžĐ˛Đ°", "they_will_be_merged_together": "ĐĸĐĩ ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ ОйĐĩдиĐŊĐĩĐŊи", "third_party_resources": "Đ ĐĩŅŅƒŅ€ŅĐ¸ ĐžŅ‚ ҂ҀĐĩŅ‚Đ¸ ŅŅ‚Ņ€Đ°ĐŊи", "time": "Đ’Ņ€ĐĩĐŧĐĩ", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "ИСйĐĩŅ€Đ¸ ОйĐĩĐēŅ‚Đ¸", "trash_page_title": "В ĐēĐžŅˆĐ° ({count})", "trashed_items_will_be_permanently_deleted_after": "Đ˜ĐˇŅ…Đ˛ŅŠŅ€ĐģĐĩĐŊĐ¸Ņ‚Đĩ в ĐēĐžŅˆŅ‡ĐĩŅ‚Đž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚Đ¸ Са ĐŋĐžŅŅ‚ĐžŅĐŊĐŊĐž ҁĐģĐĩĐ´ {days, plural, one {# Đ´ĐĩĐŊ} other {# Đ´ĐŊи}}.", + "trigger": "ĐĸŅ€Đ¸ĐŗĐĩŅ€", + "trigger_asset_uploaded": "ОбĐĩĐēŅ‚ŅŠŅ‚ Đĩ ĐˇĐ°Ņ€ĐĩĐ´ĐĩĐŊ", + "trigger_asset_uploaded_description": "ĐĄŅ€Đ°ĐąĐžŅ‚Đ˛Đ° ĐŋŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ĐŊОв ОйĐĩĐēŅ‚", + "trigger_description": "ĐĄŅŠĐąĐ¸Ņ‚Đ¸Đĩ, ĐēĐžĐĩŅ‚Đž ŅŅ‚Đ°Ņ€Ņ‚Đ¸Ņ€Đ° Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "trigger_person_recognized": "РаСĐŋОСĐŊĐ°Ņ‚Đž Đĩ ĐģĐ¸Ņ†Đĩ", + "trigger_person_recognized_description": "ĐĄŅ€Đ°ĐąĐžŅ‚Đ˛Đ° ĐŋŅ€Đ¸ Ņ€Đ°ĐˇĐŋОСĐŊаваĐŊĐĩ ĐŊа ĐģĐ¸Ņ†Đĩ", + "trigger_type": "ĐĸиĐŋ ĐŊа Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ°", "troubleshoot": "ĐžŅ‚ŅŅ‚Ņ€Đ°ĐŊŅĐ˛Đ°ĐŊĐĩ ĐŊа ĐŋŅ€ĐžĐąĐģĐĩĐŧи", "type": "ĐĸиĐŋ", "unable_to_change_pin_code": "НĐĩĐ˛ŅŠĐˇĐŧĐžĐļĐŊа ĐŋŅ€ĐžĐŧŅĐŊа ĐŊа PIN ĐēОда", @@ -2123,6 +2276,7 @@ "unhide_person": "ПоĐēаĐļи ĐžŅ‚ĐŊОвО Ņ‡ĐžĐ˛ĐĩĐēа", "unknown": "НĐĩиСвĐĩҁ҂ĐŊĐž", "unknown_country": "НĐĩĐŋОСĐŊĐ°Ņ‚Đ° Đ”ŅŠŅ€Đļава", + "unknown_date": "НĐĩиСвĐĩҁ҂ĐŊа Đ´Đ°Ņ‚Đ°", "unknown_year": "НĐĩиСвĐĩҁ҂ĐŊа ĐŗĐžĐ´Đ¸ĐŊа", "unlimited": "НĐĩĐžĐŗŅ€Đ°ĐŊĐ¸Ņ‡ĐĩĐŊĐž", "unlink_motion_video": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи Đ˛Ņ€ŅŠĐˇĐēĐ°Ņ‚Đ° ҁ видĐĩĐž", @@ -2139,17 +2293,19 @@ "unstack": "РаСĐēĐ°Ņ‡Đ¸", "unstack_action_prompt": "{count} ŅĐ° Ņ€Đ°ĐˇĐŗŅ€ŅƒĐŋĐ¸Ņ€Đ°ĐŊи", "unstacked_assets_count": "РаСĐēĐ°Ņ‡ĐĩĐŊи {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}}", + "unsupported_field_type": "ĐĸиĐŋа ĐŊа ĐŋĐžĐģĐĩŅ‚Đž ĐŊĐĩ ҁĐĩ ĐŋĐžĐ´Đ´ŅŠŅ€Đļа", "untagged": "НĐĩĐŧĐ°Ņ€ĐēĐ¸Ņ€Đ°ĐŊи", + "untitled_workflow": "Đ Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ ĐąĐĩС иĐŧĐĩ", "up_next": "ĐĄĐģĐĩĐ´Đ˛Đ°Ņ‰", "update_location_action_prompt": "ОбĐŊОви ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸Ņ‚Đĩ ĐŊа {count} Đ¸ĐˇĐąŅ€Đ°ĐŊи ОйĐĩĐēŅ‚Đ° ҁ:", "updated_at": "ОбĐŊОвĐĩĐŊĐž", - "updated_password": "ĐŸĐ°Ņ€ĐžĐģĐ°Ņ‚Đ° Đĩ аĐēŅ‚ŅƒĐ°ĐģĐ¸ĐˇĐ¸Ņ€Đ°ĐŊа", + "updated_password": "ĐŸĐ°Ņ€ĐžĐģĐ°Ņ‚Đ° Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐĩĐŊа", "upload": "ĐšĐ°Ņ‡Đ˛Đ°ĐŊĐĩ", - "upload_action_prompt": "{count} ĐŊа ĐžĐŋĐ°ŅˆĐēа Са ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐĩ", "upload_concurrency": "ĐŖŅĐŋĐžŅ€ĐĩĐ´ĐŊи ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐ¸Ņ", "upload_details": "ДĐĩŅ‚Đ°ĐšĐģи Са ĐēĐ°Ņ‡Đ˛Đ°ĐŊĐĩŅ‚Đž", "upload_dialog_info": "Đ˜ŅĐēĐ°Ņ‚Đĩ Đģи да Đ°Ņ€Ņ…Đ¸Đ˛Đ¸Ņ€Đ°Ņ‚Đĩ ĐŊа ŅŅŠŅ€Đ˛ŅŠŅ€Đ° Đ¸ĐˇĐąŅ€Đ°ĐŊĐ¸Ņ‚Đĩ ОйĐĩĐēŅ‚Đ¸?", "upload_dialog_title": "ĐšĐ°Ņ‡Đ¸ ОйĐĩĐēŅ‚", + "upload_error_with_count": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ĐˇĐ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа {count, plural, one {# ОйĐĩĐēŅ‚} other {# ОйĐĩĐēŅ‚Đ°}}", "upload_errors": "ĐšĐ°Ņ‡Đ˛Đ°ĐŊĐĩŅ‚Đž Đĩ ĐˇĐ°Đ˛ŅŠŅˆĐĩĐŊĐž ҁ {count, plural, one {# ĐŗŅ€Đĩ҈Đēа} other {# ĐŗŅ€Đĩ҈Đēи}}, ОйĐŊОвĐĩŅ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ°Ņ‚Đ° Са да Đ˛Đ¸Đ´Đ¸Ņ‚Đĩ ĐŊĐžĐ˛Đ¸Ņ‚Đĩ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸.", "upload_finished": "ĐšĐ°Ņ‡Đ˛Đ°ĐŊĐĩŅ‚Đž ĐˇĐ°Đ˛ŅŠŅ€ŅˆĐ¸", "upload_progress": "ĐžŅŅ‚Đ°Đ˛Đ°Ņ‚ {remaining, number} - ĐžĐąŅ€Đ°ĐąĐžŅ‚ĐĩĐŊи {processed, number}/{total, number}", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "ĐŸĐžŅ‚Ņ€ĐĩĐąĐģĐĩĐŊиĐĩ", "use_biometric": "ИСĐŋĐžĐģСваК йиОĐŧĐĩŅ‚Ņ€Đ¸Ņ", - "use_current_connection": "иСĐŋĐžĐģСваК Ņ‚ĐĩĐēŅƒŅ‰Đ°Ņ‚Đ° Đ˛Ņ€ŅŠĐˇĐēа", + "use_current_connection": "ИСĐŋĐžĐģСваК Ņ‚ĐĩĐēŅƒŅ‰Đ°Ņ‚Đ° Đ˛Ņ€ŅŠĐˇĐēа", "use_custom_date_range": "ИСĐŋĐžĐģĐˇĐ˛Đ°ĐšŅ‚Đĩ ŅĐžĐąŅŅ‚Đ˛ĐĩĐŊ диаĐŋаСОĐŊ ĐžŅ‚ Đ´Đ°Ņ‚Đ¸ вĐŧĐĩŅŅ‚Đž Ņ‚ĐžĐ˛Đ°", "user": "ĐŸĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ", "user_has_been_deleted": "ĐĸОСи ĐŋĐžŅ‚Ņ€ĐĩĐąĐ¸Ņ‚ĐĩĐģ Đĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚.", @@ -2185,6 +2341,7 @@ "utilities": "ИĐŊŅŅ‚Ņ€ŅƒĐŧĐĩĐŊŅ‚Đ¸", "validate": "ВаĐģĐ¸Đ´Đ¸Ņ€Đ°ĐŊĐĩ", "validate_endpoint_error": "МоĐģŅ, Đ˛ŅŠĐ˛Đĩди ĐŋŅ€Đ°Đ˛Đ¸ĐģĐĩĐŊ URL", + "validation_error": "Đ“Ņ€Đĩ҈Đēа ĐŋŅ€Đ¸ ваĐģĐ¸Đ´Đ¸Ņ€Đ°ĐŊĐĩ", "variables": "ĐŸŅ€ĐžĐŧĐĩĐŊĐģиви", "version": "ВĐĩŅ€ŅĐ¸Ņ", "version_announcement_closing": "ĐĸвОК ĐŋŅ€Đ¸ŅŅ‚ĐĩĐģ, АĐģĐĩĐēҁ", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Đ’ŅŠĐˇĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐļдаĐŊĐĩ ĐŊа видĐĩĐžĐēĐģиĐŋа, ĐēĐžĐŗĐ°Ņ‚Đž ĐŧĐ¸ŅˆĐēĐ°Ņ‚Đ° ҁĐĩ двиĐļи ĐŊад ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°. Đ”ĐžŅ€Đ¸ ĐēĐžĐŗĐ°Ņ‚Đž Đĩ Đ´ĐĩаĐēŅ‚Đ¸Đ˛Đ¸Ņ€Đ°ĐŊĐž, Đ˛ŅŠĐˇĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐļдаĐŊĐĩŅ‚Đž ĐŧĐžĐļĐĩ да ĐąŅŠĐ´Đĩ ŅŅ‚Đ°Ņ€Ņ‚Đ¸Ņ€Đ°ĐŊĐž ҇ҀĐĩС ĐˇĐ°Đ´ŅŠŅ€ĐļаĐŊĐĩ ĐŊа ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа ĐŧĐ¸ŅˆĐēĐ°Ņ‚Đ° Đ˛ŅŠŅ€Ņ…Ņƒ иĐēĐžĐŊĐ°Ņ‚Đ° Са Đ˛ŅŠĐˇĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐļдаĐŊĐĩ.", "videos": "ВидĐĩĐžĐēĐģиĐŋОвĐĩ", "videos_count": "{count, plural, one {# ВидĐĩĐž} other {# ВидĐĩа}}", + "videos_only": "ХаĐŧĐž видĐĩа", "view": "ĐŸŅ€ĐĩĐŗĐģĐĩĐ´", "view_album": "Đ Đ°ĐˇĐŗĐģĐĩдаК аĐģĐąŅƒĐŧа", "view_all": "ĐŸŅ€ĐĩĐŗĐģĐĩĐ´ ĐŊа Đ˛ŅĐ¸Ņ‡Đēи", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "ИСĐŋĐžĐģСваК ĐēĐ°Ņ‚Đž ĐžŅĐŊОвĐĩĐŊ", "viewer_unstack": "ĐŸŅ€ĐĩĐŧĐ°Ņ…ĐŊи ĐžŅ‚ ĐžĐŋĐ°ŅˆĐēĐ°Ņ‚Đ°", "visibility_changed": "ВидиĐŧĐžŅŅ‚Ņ‚Đ° Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐĩĐŊа Са {count, plural, one {# Ņ‡ĐžĐ˛ĐĩĐē} other {# Ņ‡ĐžĐ˛ĐĩĐēа}}", + "visual": "Đ’Đ¸ĐˇŅƒĐ°ĐģĐĩĐŊ", + "visual_builder": "Đ’Đ¸ĐˇŅƒĐ°ĐģĐĩĐŊ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", "waiting": "в Đ¸ĐˇŅ‡Đ°ĐēваĐŊĐĩ", "waiting_count": "В Đ¸ĐˇŅ‡Đ°ĐēваĐŊĐĩ: {count}", "warning": "ВĐŊиĐŧаĐŊиĐĩ", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Đ”ĐžĐąŅ€Đĩ Đ´ĐžŅˆĐģи в Immich", "width": "Đ¨Đ¸Ņ€Đ¸ĐŊa", "wifi_name": "Wi-Fi ĐŧŅ€ĐĩĐļа", - "workflow": "Đ Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_delete_prompt": "ĐĐ°Đ¸ŅŅ‚Đ¸ĐŊа Đģи Đ¸ŅĐēĐ°Ņ‚Đĩ да Đ¸ĐˇŅ‚Ņ€Đ¸ĐĩŅ‚Đĩ Ņ‚ĐžĐˇĐ¸ Ņ€Đ°ĐąĐžŅ‚ĐĩĐŊ ĐŋŅ€ĐžŅ†Đĩҁ?", + "workflow_deleted": "Đ Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ Đĩ Đ¸ĐˇŅ‚Ņ€Đ¸Ņ‚", + "workflow_description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Са Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_json": "JSON ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_json_help": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€Đ°ĐŊĐĩ ĐŊа ĐēĐžĐŊŅ„Đ¸ĐŗŅƒŅ€Đ°Ņ†Đ¸ŅŅ‚Đ° ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ в JSON Ņ„ĐžŅ€ĐŧĐ°Ņ‚. ĐŸŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ ҉Đĩ ĐąŅŠĐ´Đ°Ņ‚ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€Đ°ĐŊи ҁ Đ˛Đ¸ĐˇŅƒĐ°ĐģĐŊĐ¸Ņ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€.", + "workflow_name": "ИĐŧĐĩ ĐŊа Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_navigation_prompt": "ĐĐ°Đ¸ŅŅ‚Đ¸ĐŊа Đģи Đ¸ŅĐēĐ°Ņ‚Đĩ да иСĐģĐĩСĐĩŅ‚Đĩ ĐąĐĩС да ŅŅŠŅ…Ņ€Đ°ĐŊĐ¸Ņ‚Đĩ ĐŋŅ€ĐžĐŧĐĩĐŊĐ¸Ņ‚Đĩ?", + "workflow_summary": "ĐžĐąĐžĐąŅ‰ĐĩĐŊиĐĩ Са Ņ€Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_update_success": "Đ Đ°ĐąĐžŅ‚ĐŊĐ¸ŅŅ‚ ĐŋŅ€ĐžŅ†Đĩҁ Đĩ ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊОвĐĩĐŊ", + "workflow_updated": "Đ Đ°ĐąĐžŅ‚ĐŊĐ¸ŅŅ‚ ĐŋŅ€ĐžŅ†Đĩҁ Đĩ ОйĐŊОвĐĩĐŊ", + "workflows": "Đ Đ°ĐąĐžŅ‚ĐŊи ĐŋŅ€ĐžŅ†ĐĩŅĐ¸", + "workflows_help_text": "Đ Đ°ĐąĐžŅ‚ĐŊĐ¸Ņ‚Đĩ ĐŋŅ€ĐžŅ†ĐĩŅĐ¸ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ¸Ņ€Đ°Ņ‚ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸ŅŅ‚Đ° ҁ Đ˛Đ°ŅˆĐ¸Ņ‚Đĩ ОйĐĩĐēŅ‚Đ¸ ҇ҀĐĩС Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ¸ и Ņ„Đ¸ĐģŅ‚Ņ€Đ¸", "wrong_pin_code": "Đ“Ņ€Đĩ҈ĐĩĐŊ PIN ĐēОд", "year": "ГодиĐŊа", "years_ago": "ĐŋŅ€Đĩди {years, plural, one {# ĐŗĐžĐ´Đ¸ĐŊа} other {# ĐŗĐžĐ´Đ¸ĐŊи}}", "yes": "Да", "you_dont_have_any_shared_links": "ĐŅĐŧĐ°Ņ‚Đĩ ҁĐŋОдĐĩĐģĐĩĐŊи Đ˛Ņ€ŅŠĐˇĐēи", "your_wifi_name": "Đ’Đ°ŅˆĐ°Ņ‚Đ° Wi-Fi ĐŧŅ€ĐĩĐļа", + "zero_to_clear_rating": "ĐŊĐ°Ņ‚Đ¸ŅĐŊĐĩŅ‚Đĩ 0, Са да ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐĩŅ‚Đĩ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗĐ°", "zoom_image": "ĐŖĐ˛ĐĩĐģĐ¸Ņ‡Đ°Đ˛Đ°ĐŊĐĩ ĐŊа Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩŅ‚Đž", "zoom_to_bounds": "ĐŸŅ€Đ¸ĐąĐģиĐļи Đ´Đž ŅŅŠĐąĐ¸Ņ€Đ°ĐŊĐĩ в ĐŗŅ€Đ°ĐŊĐ¸Ņ†Đ¸Ņ‚Đĩ" } diff --git a/i18n/bi.json b/i18n/bi.json index c5c9edbbb1..290b816cc6 100644 --- a/i18n/bi.json +++ b/i18n/bi.json @@ -17,7 +17,7 @@ "readonly_mode_enabled": "Mod blo yu no save janjem i on", "reassigned_assets_to_new_person": "Janjem{count, plural, one {# asset} other {# assets}} blo nu man", "reassing_hint": "janjem ol sumtin yu bin joos i go blo wan man", - "recent-albums": "album i no old tu mas", + "recent_albums": "album i no old tu mas", "recent_searches": "lukabout wea i no old tu mas", "time_based_memories_duration": "hao mus second blo wan wan imij i stap lo scrin.", "timezone": "taemzon", diff --git a/i18n/bn.json b/i18n/bn.json index a785993f0a..7ba6c0a467 100644 --- a/i18n/bn.json +++ b/i18n/bn.json @@ -5,18 +5,25 @@ "acknowledge": "āĻ¸ā§āĻŦā§€āĻ•ā§ƒāϤāĻŋ", "action": "āĻ•āĻžāĻ°ā§āϝ", "action_common_update": "āφāĻĒāĻĄā§‡āϟ", + "action_description": "āĻŦāĻžāĻ›āĻžāχāĻ•ā§ƒāϤ āϏāĻŽā§āĻĒāĻĻāϏāĻŽā§‚āĻšā§‡āϰ āωāĻĒāϰ āϏāĻŽā§āĻĒāĻžāĻĻāύāϝ⧋āĻ—ā§āϝ āĻ•āĻžāĻœā§‡āϰ āϤāĻžāϞāĻŋāĻ•āĻž", "actions": "āĻ•āĻ°ā§āĻŽ", "active": "āϏāϚāϞ", + "active_count": "Active: {count}", "activity": "āĻ•āĻžāĻ°ā§āϝāĻ•āϞāĻžāĻĒ", - "activity_changed": "āĻāĻ•āϟāĻŋāĻ­āĻŋāϟāĻŋ āĻāĻ–āύ {enabled, select, true {āϚāĻžāϞ⧁} other {āĻŦāĻ¨ā§āϧ}} āφāϛ⧇", + "activity_changed": "āĻāĻ•āϟāĻŋāĻ­āĻŋāϟāĻŋ āĻāĻ–āύ {enabled, select, true {enabled} other {disabled}} āφāϛ⧇", "add": "āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_description": "āĻāĻ•āϟāĻŋ āĻŦāĻŋāĻŦāϰāĻŖ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_location": "āĻāĻ•āϟāĻŋ āĻ…āĻŦāĻ¸ā§āĻĨāĻžāύ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_name": "āĻāĻ•āϟāĻŋ āύāĻžāĻŽ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_a_title": "āĻāĻ•āϟāĻŋ āĻļāĻŋāϰ⧋āύāĻžāĻŽ āϝ⧋āĻ— āĻ•āϰ⧁āύ", - "add_birthday": "āĻāĻ•āϟāĻŋ āϜāĻ¨ā§āĻŽāĻĻāĻŋāύ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_action": "āĻ•āĻ°ā§āĻŽ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_action_description": "āϏāĻŽā§āĻĒāĻžāĻĻāύ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āĻāĻ•āϟāĻŋ āĻ•āĻžāϜ āϝ⧋āĻ— āĻ•āϰāϤ⧇ āĻ•ā§āϞāĻŋāĻ• āĻ•āϰ⧁āύ", + "add_assets": "āϏāĻŽā§āĻĒāĻĻ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_birthday": "āϜāĻ¨ā§āĻŽāĻĻāĻŋāύ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_endpoint": "āĻāĻ¨ā§āĻĄāĻĒāϝāĻŧ⧇āĻ¨ā§āϟ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_exclusion_pattern": "āĻŦāĻšāĻŋāĻ°ā§āĻ­ā§‚āϤāĻ•āϰāĻŖ āύāĻŽā§āύāĻž", + "add_filter": "āĻĢāĻŋāĻ˛ā§āϟāĻžāϰ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_filter_description": "āĻāĻ•āϟāĻŋ āĻĢāĻŋāĻ˛ā§āϟāĻžāϰ āĻļāĻ°ā§āϤ āϝ⧋āĻ— āĻ•āϰāϤ⧇ āĻ•ā§āϞāĻŋāĻ• āĻ•āϰ⧁āύ", "add_location": "āĻ…āĻŦāĻ¸ā§āĻĨāĻžāύ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", "add_more_users": "āφāϰ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀ āϝ⧁āĻ•ā§āϤ āĻ•āϰ⧁āύ", "add_partner": "āĻ…āĻ‚āĻļā§€āĻĻāĻžāϰ āϝ⧋āĻ— āĻ•āϰ⧁āύ", @@ -31,8 +38,11 @@ "add_to_album_toggle": "{album} - āĻāϰ āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰ⧁āύ", "add_to_albums": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_to_albums_count": "āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧋āĻ— āĻ•āϰ⧁āύ ({count})", + "add_to_bottom_bar": "āĻ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_to_shared_album": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_upload_to_stack": "āφāĻĒāϞ⧋āĻĄ āĻ¸ā§āĻŸā§āϝāĻžāϕ⧇ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "add_url": "āϞāĻŋāĻ™ā§āĻ• āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "add_workflow_step": "āĻ•āĻžāĻœā§‡āϰ āϧāĻžāĻĒ āϝ⧋āĻ— āĻ•āϰ⧁āύ", "added_to_archive": "āφāĻ°ā§āĻ•āĻžāχāĻ­ āĻ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "added_to_favorites": "āĻĢ⧇āĻ­āĻžāϰāĻŋāĻŸā§‡ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "added_to_favorites_count": "āĻĒāĻ›āĻ¨ā§āĻĻ⧇āϰ āϤāĻžāϞāĻŋāĻ•āĻžā§Ÿ {count, number} āϝ⧋āĻ— āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇", @@ -65,6 +75,7 @@ "confirm_reprocess_all_faces": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āϏāĻŽāĻ¸ā§āϤ āĻŽā§āĻ– āĻĒ⧁āύāϰāĻžāϝāĻŧ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰāϤ⧇ āϚāĻžāύ? āĻāϟāĻŋ āύāĻžāĻŽāϝ⧁āĻ•ā§āϤ āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰāĻ“ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻŦ⧇āĨ¤", "confirm_user_password_reset": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āϰāĻŋāϏ⧇āϟ āĻ•āϰāϤ⧇ āϚāĻžāύ?", "confirm_user_pin_code_reset": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ {user} āĻāϰ āĻĒāĻŋāύ āϕ⧋āĻĄ āϰāĻŋāϏ⧇āϟ āĻ•āϰāϤ⧇ āϚāĻžāύ?", + "copy_config_to_clipboard_description": "āĻŦāĻ°ā§āϤāĻŽāĻžāύ āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻ•āύāĻĢāĻŋāĻ—āĻžāϰ⧇āĻļāύ āĻāĻ•āϟāĻŋ JSON āĻ…āĻŦāĻœā§‡āĻ•ā§āϟ āĻšāĻŋāϏ⧇āĻŦ⧇ āĻ•ā§āϞāĻŋāĻĒāĻŦā§‹āĻ°ā§āĻĄā§‡ āĻ•āĻĒāĻŋ āĻ•āϰ⧁āύ", "create_job": "job āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ", "cron_expression": "āĻ•ā§āϰ⧋āύ āĻāĻ•ā§āϏāĻĒā§āϰ⧇āĻļāύ", "cron_expression_description": "āĻ•ā§āϰ⧋āύ āĻĢāĻ°ā§āĻŽā§āϝāĻžāϟ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻ¸ā§āĻ•ā§āϝāĻžāύāĻŋāĻ‚ āĻŦā§āϝāĻŦāϧāĻžāύ āϏ⧇āϟ āĻ•āϰ⧁āύāĨ¤ āφāϰāĻ“ āϤāĻĨā§āϝ⧇āϰ āϜāĻ¨ā§āϝ āĻĻāϝāĻŧāĻž āĻ•āϰ⧇ āĻĻ⧇āϖ⧁āύ āϝ⧇āĻŽāύ Crontab Guru", @@ -72,6 +83,8 @@ "disable_login": "āϞāĻ—āχāύ āĻ…āĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", "duplicate_detection_job_description": "āĻ…āύ⧁āϰ⧂āĻĒ āĻ›āĻŦāĻŋ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰāϤ⧇ āϏāĻŽā§āĻĒāĻĻāϗ⧁āϞāĻŋāϤ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϚāĻžāϞāĻžāύāĨ¤ āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ⧇āϰ āωāĻĒāϰ āύāĻŋāĻ°ā§āĻ­āϰ āĻ•āϰ⧇", "exclusion_pattern_description": "āĻāĻ•ā§āϏāĻ•ā§āϞ⧁āĻļāύ āĻĒā§āϝāĻžāϟāĻžāĻ°ā§āύ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āφāĻĒāύāĻŋ āφāĻĒāύāĻžāϰ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύ āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āĻĢāĻžāχāϞ āĻāĻŦāĻ‚ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰāϗ⧁āϞāĻŋāϕ⧇ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāϤ⧇ āĻĒāĻžāϰāĻŦ⧇āύāĨ¤ āϝāĻĻāĻŋ āφāĻĒāύāĻžāϰ āĻāĻŽāύ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻĨāĻžāϕ⧇ āϝ⧇āĻ–āĻžāύ⧇ āĻāĻŽāύ āĻĢāĻžāχāϞ āĻĨāĻžāϕ⧇ āϝāĻž āφāĻĒāύāĻŋ āφāĻŽāĻĻāĻžāύāĻŋ āĻ•āϰāϤ⧇ āϚāĻžāύ āύāĻž, āϝ⧇āĻŽāύ RAW āĻĢāĻžāχāϞāĨ¤", + "export_config_as_json_description": "āĻŦāĻ°ā§āϤāĻŽāĻžāύ āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻ•āύāĻĢāĻŋāĻ—āĻžāϰ⧇āĻļāύ āĻāĻ•āϟāĻŋ JSON āĻĢāĻžāχāϞ āĻšāĻŋāϏ⧇āĻŦ⧇ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻ•āϰ⧁āύ", + "external_libraries_page_description": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ external āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻĒ⧇āϜ", "face_detection": "āĻŽā§āĻ– āϏāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ", "face_detection_description": "āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡ āĻĨāĻžāĻ•āĻž āĻŽā§āĻ–/āĻšā§‡āĻšāĻžāϰāĻž āϗ⧁āϞāĻŋ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰ⧁āύāĨ¤ āĻ­āĻŋāĻĄāĻŋāĻ“ āϗ⧁āϞāĻŋāϰ āϜāĻ¨ā§āϝ, āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āĻŦāĻŋāĻŦ⧇āϚāύāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ \"āϰāĻŋāĻĢā§āϰ⧇āĻļ\" (āĻĒ⧁āύāϰāĻžāϝāĻŧ) āϏāĻŽāĻ¸ā§āϤ āĻ…ā§āϝāĻžāϏ⧇āϟ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰ⧇āĨ¤ \"āϰāĻŋāϏ⧇āϟ\" āĻ•āϰāĻžāϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ āĻ…āϤāĻŋāϰāĻŋāĻ•ā§āϤāĻ­āĻžāĻŦ⧇ āϏāĻŽāĻ¸ā§āϤ āĻŦāĻ°ā§āϤāĻŽāĻžāύ āĻŽā§āϖ⧇āϰ āĻĄā§‡āϟāĻž āϏāĻžāĻĢ āĻ•āϰ⧇āĨ¤ \"āĻ…āύ⧁āĻĒāĻ¸ā§āĻĨāĻŋāϤ\" āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞāĻŋāϕ⧇ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻ•āϰ⧇ āϝāĻž āĻāĻ–āύāĻ“ āĻĒā§āϰāĻ•ā§āϰāĻŋāϝāĻŧāĻž āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤ āϏāύāĻžāĻ•ā§āϤ āĻ•āϰāĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ⧇āϰ āϜāĻ¨ā§āϝ āϏāĻžāϰāĻŋāĻŦāĻĻā§āϧ āĻ•āϰāĻž āĻšāĻŦ⧇, āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āĻĄāĻŋāĻŸā§‡āĻ•āĻļāύ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšāĻ“āϝāĻŧāĻžāϰ āĻĒāϰ⧇, āĻŦāĻŋāĻĻā§āϝāĻŽāĻžāύ āĻŦāĻž āύāϤ⧁āύ āĻŦā§āϝāĻ•ā§āϤāĻŋāĻĻ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻ—ā§‹āĻˇā§āĻ ā§€āĻŦāĻĻā§āϧ āĻ•āϰ⧇āĨ¤", "facial_recognition_job_description": "āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāĻž āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āĻŽāĻžāύ⧁āώ⧇āϰ āĻŽāĻ§ā§āϝ⧇ āĻ—ā§‹āĻˇā§āĻ ā§€āϭ⧁āĻ•ā§āϤ/āĻ—ā§āϰ⧁āĻĒ āĻ•āϰ⧁āύāĨ¤ āĻŽā§āĻ– āϏāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšāĻ“āϝāĻŧāĻžāϰ āĻĒāϰ⧇ āĻāχ āϧāĻžāĻĒāϟāĻŋ āϚāϞ⧇āĨ¤ \"āϰāĻŋāϏ⧇āϟ\" (āĻĒ⧁āύāϰāĻžāϝāĻŧ) āϏāĻŽāĻ¸ā§āϤ āĻŽā§āĻ–āϕ⧇ āĻ•ā§āϞāĻžāĻ¸ā§āϟāĻžāϰ āĻ•āϰ⧇āĨ¤ \"āĻ…āύ⧁āĻĒāĻ¸ā§āĻĨāĻŋāϤ/āĻŽāĻŋāϏāĻŋāĻ‚\" āĻŽā§āĻ–āϗ⧁āϞāĻŋāϕ⧇ āϏāĻžāϰāĻŋāϤ⧇ āϰāĻžāϖ⧇ āϝ⧇āϗ⧁āϞ⧋ āϕ⧋āύāĻ“ āĻŦā§āϝāĻ•ā§āϤāĻŋāϕ⧇ āĻāϏāĻžāχāύ/āĻŦāϰāĻžāĻĻā§āĻĻ āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋāĨ¤", @@ -91,6 +104,8 @@ "image_preview_description": "āĻ¸ā§āĻŸā§āϰāĻŋāĻĒāĻĄ āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āϏāĻš āĻŽāĻžāĻāĻžāϰāĻŋ āφāĻ•āĻžāϰ⧇āϰ āĻ›āĻŦāĻŋ, āĻāĻ•āϟāĻŋ āĻāĻ•āĻ• āϏāĻŽā§āĻĒāĻĻ āĻĻ⧇āĻ–āĻžāϰ āϏāĻŽāϝāĻŧ āĻāĻŦāĻ‚ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚āϝāĻŧ⧇āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšāϝāĻŧ", "image_preview_quality_description": "ā§§-ā§§ā§Ļā§Ļ āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻĒā§āϰāĻŋāĻ­āĻŋāω āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋāĨ¤ āĻŦ⧇āĻļāĻŋ āĻšāϞ⧇ āĻ­āĻžāϞ⧋, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤ āĻ•āĻŽ āĻŽāĻžāύ āϏ⧇āϟ āĻ•āϰāϞ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϕ⧋āϝāĻŧāĻžāϞāĻŋāϟāĻŋāϰ āωāĻĒāϰ āĻĒā§āϰāĻ­āĻžāĻŦ āĻĒāĻĄāĻŧāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", "image_preview_title": "āĻĒā§āϰāĻŋāĻ­āĻŋāω āϏ⧇āϟāĻŋāĻ‚āϏ", + "image_progressive": "āĻĒā§āϰāĻ—ā§āϰ⧇āϏāĻŋāĻ­", + "image_progressive_description": "āϧ⧀āϰ⧇ āϧ⧀āϰ⧇ āϞ⧋āĻĄ āĻšāĻ“ā§ŸāĻžāϰ āϏ⧁āĻŦāĻŋāϧāĻžāĻ°ā§āĻĨ⧇ JPEG āĻ›āĻŦāĻŋāϗ⧁āϞ⧋ āĻĒā§āϰāĻ—ā§āϰ⧇āϏāĻŋāĻ­āĻ­āĻžāĻŦ⧇ āĻāύāϕ⧋āĻĄ āĻ•āϰ⧁āύāĨ¤ WebP āĻ›āĻŦāĻŋāϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āĻāϟāĻŋ āϕ⧋āύ⧋ āĻĒā§āϰāĻ­āĻžāĻŦ āĻĢ⧇āϞāĻŦ⧇ āύāĻž", "image_quality": "āϗ⧁āĻŖāĻŽāĻžāύ", "image_resolution": "āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ", "image_resolution_description": "āωāĻšā§āϚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ⧇āϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āφāϰāĻ“ āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āϤāĻĨā§āϝ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āϏāĻŽā§āĻ­āĻŦ āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāύāϕ⧋āĻĄ āĻ•āϰāϤ⧇ āĻŦ⧇āĻļāĻŋ āϏāĻŽāϝāĻŧ āϞāĻžāϗ⧇, āĻĢāĻžāχāϞ⧇āϰ āφāĻ•āĻžāϰ āĻŦāĻĄāĻŧ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", @@ -99,6 +114,7 @@ "image_thumbnail_description": "āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āĻŦāĻžāĻĻ āĻĻ⧇āĻ“ā§ŸāĻž āϛ⧋āϟ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ, āĻŽā§‚āϞ āϟāĻžāχāĻŽāϞāĻžāχāύ⧇āϰ āĻŽāϤ⧋ āĻ›āĻŦāĻŋāϰ āĻ—ā§āϰ⧁āĻĒ āĻĻ⧇āĻ–āĻžāϰ āϏāĻŽāϝāĻŧ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšā§Ÿ", "image_thumbnail_quality_description": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ⧇āϰ āĻŽāĻžāύ ā§§-ā§§ā§Ļā§ĻāĨ¤ āĻŦ⧇āĻļāĻŋ āĻšāϞ⧇ āĻ­āĻžāϞ⧋, āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻŦāĻĄāĻŧ āĻĢāĻžāχāϞ āϤ⧈āϰāĻŋ āĻšāϝāĻŧ āĻāĻŦāĻ‚ āĻ…ā§āϝāĻžāĻĒ⧇āϰ āĻĒā§āϰāϤāĻŋāĻ•ā§āϰāĻŋāϝāĻŧāĻžāĻļā§€āϞāϤāĻž āĻ•āĻŽāĻžāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", "image_thumbnail_title": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āϞ āϏ⧇āϟāĻŋāĻ‚āϏ", + "import_config_from_json_description": "āĻāĻ•āϟāĻŋ JSON āĻ•āύāĻĢāĻŋāĻ— āĻĢāĻžāχāϞ āφāĻĒāϞ⧋āĻĄ āĻ•āϰ⧇ āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻ•āύāĻĢāĻŋāĻ—āĻžāϰ⧇āĻļāύ āχāĻŽāĻĒā§‹āĻ°ā§āϟ āĻ•āϰ⧁āύāĨ¤", "job_concurrency": "{job} āĻ•āύāĻ•āĻžāϰ⧇āĻ¨ā§āϏāĻŋ", "job_created": "Job āϤ⧈āϰāĻŋ āĻšāϝāĻŧ⧇āϛ⧇", "job_not_concurrency_safe": "āĻāχ āĻ•āĻžāϜāϟāĻŋ āϏāĻŽāĻžāĻ¨ā§āϤāϰāĻžāϞāĻ­āĻžāĻŦ⧇ āϚāĻžāϞāĻžāύ⧋ āύāĻŋāϰāĻžāĻĒāĻĻ āύ⧟", @@ -106,14 +122,20 @@ "job_settings_description": "āĻ•āĻžāĻœā§‡āϰ āϏāĻŽāĻžāĻ¨ā§āϤāϰāĻžāϞāϤāĻž āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", "jobs_delayed": "{jobCount, plural, other {# āĻŦāĻŋāϞāĻŽā§āĻŦāĻŋāϤ}}", "jobs_failed": "{jobCount, plural, other {# āĻŦā§āϝāĻ°ā§āĻĨ}}", + "jobs_over_time": "āϏāĻŽā§Ÿ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āĻ•āĻžāϜāϏāĻŽā§‚āĻš", "library_created": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āϤ⧈āϰāĻŋ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇āσ {library}", "library_deleted": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "library_details": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϰ āĻŦāĻŋāĻŦāϰāĻŖ", + "library_folder_description": "āχāĻŽāĻĒā§‹āĻ°ā§āϟ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āĻāĻ•āϟāĻŋ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻ•āϰ⧁āύāĨ¤ āĻāχ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻāĻŦāĻ‚ āĻāϰ āϭ⧇āϤāϰ⧇āϰ āϏāĻŽāĻ¸ā§āϤ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻ›āĻŦāĻŋ āĻ“ āĻ­āĻŋāĻĄāĻŋāĻ“āϰ āϜāĻ¨ā§āϝ āĻ¸ā§āĻ•ā§āϝāĻžāύ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "library_remove_exclusion_pattern_prompt": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āĻāχ āĻāĻ•ā§āϏāĻ•ā§āϞ⧁āĻļāύ āĻĒā§āϝāĻžāϟāĻžāĻ°ā§āύāϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", + "library_remove_folder_prompt": "āφāĻĒāύāĻŋ āĻ•āĻŋ āύāĻŋāĻļā§āϚāĻŋāϤ āϝ⧇ āφāĻĒāύāĻŋ āĻāχ āχāĻŽāĻĒā§‹āĻ°ā§āϟ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰāϟāĻŋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āϚāĻžāύ?", "library_scanning": "āĻĒāĻ°ā§āϝāĻžāϝāĻŧāĻ•ā§āϰāĻŽāĻŋāĻ• āĻ¸ā§āĻ•ā§āϝāĻžāύāĻŋāĻ‚", "library_scanning_description": "āĻĒāĻ°ā§āϝāĻžāϝāĻŧāĻ•ā§āϰāĻŽāĻŋāĻ• āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύāĻŋāĻ‚ āĻ•āύāĻĢāĻŋāĻ—āĻžāϰ āĻ•āϰ⧁āύ", "library_scanning_enable_description": "āĻĒāĻ°ā§āϝāĻžāϝāĻŧāĻ•ā§āϰāĻŽāĻŋāĻ• āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύāĻŋāĻ‚ āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", "library_settings": "āĻŦāĻšāĻŋāϰāĻžāĻ—āϤ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ", "library_settings_description": "āĻŦāĻšāĻŋāϰāĻžāĻ—āϤ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", "library_tasks_description": "āύāϤ⧁āύ āĻāĻŦāĻ‚/āĻ…āĻĨāĻŦāĻž āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāĻŋāϤ āϏāĻŽā§āĻĒāĻĻ⧇āϰ āϜāĻ¨ā§āϝ āĻŦāĻšāĻŋāϰāĻžāĻ—āϤ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύ āĻ•āϰ⧁āύ", + "library_updated": "āφāĻĒāĻĄā§‡āϟāĻ•ā§ƒāϤ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāĨ¤", "library_watching_enable_description": "āĻĢāĻžāχāϞ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ⧇āϰ āϜāĻ¨ā§āϝ āĻŦāĻšāĻŋāϰāĻžāĻ—āϤ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋāϗ⧁āϞāĻŋ āĻĻ⧇āϖ⧁āύ", "library_watching_settings": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻĻ⧇āĻ–āĻž (āĻĒāϰ⧀āĻ•ā§āώāĻžāĻŽā§‚āϞāĻ•)", "library_watching_settings_description": "āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāĻŋāϤ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋāϰ āϜāĻ¨ā§āϝ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āύāϜāϰ āϰāĻžāϖ⧁āύ", @@ -125,9 +147,199 @@ "machine_learning_availability_checks_enabled": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻž āĻĒāϰ⧀āĻ•ā§āώāĻž āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", "machine_learning_availability_checks_interval": "āĻšā§‡āĻ• āĻŦā§āϝāĻŦāϧāĻžāύ", "machine_learning_availability_checks_interval_description": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻž āĻĒāϰ⧀āĻ•ā§āώāĻžāϗ⧁āϞāĻŋāϰ āĻŽāĻ§ā§āϝ⧇ āĻŦā§āϝāĻŦāϧāĻžāύ āĻŽāĻŋāϞāĻŋāϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡", + "machine_learning_availability_checks_timeout": "āĻ…āύ⧁āϰ⧋āϧ⧇āϰ āϏāĻŽā§ŸāϏ⧀āĻŽāĻž āĻļ⧇āώ", + "machine_learning_availability_checks_timeout_description": "āĻĒā§āϰāĻžāĻĒā§āϝāϤāĻžāϰ āĻĒāϰ⧀āĻ•ā§āώāĻžāϰ āϜāĻ¨ā§āϝ āĻŽāĻŋāϞāĻŋāϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡ āϏāĻŽā§ŸāϏ⧀āĻŽāĻžāĨ¤", "machine_learning_clip_model": "CLIP āĻŽāĻĄā§‡āϞ", "machine_learning_clip_model_description": "āĻāĻ–āĻžāύ⧇ āϤāĻžāϞāĻŋāĻ•āĻžāϭ⧁āĻ•ā§āϤ āĻāĻ•āϟāĻŋ CLIP āĻŽāĻĄā§‡āϞ⧇āϰ āύāĻžāĻŽāĨ¤ āĻŽāύ⧇ āϰāĻžāĻ–āĻŦ⧇āύ, āĻŽāĻĄā§‡āϞ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ⧇āϰ āĻĒāϰ āϏāĻŦ āĻ›āĻŦāĻŋāϰ āϜāĻ¨ā§āϝ āĻ…āĻŦāĻļā§āϝāχ ‘Smart Search’ āĻ•āĻžāϜāϟāĻŋ āφāĻŦāĻžāϰ āϚāĻžāϞāĻžāϤ⧇ āĻšāĻŦ⧇āĨ¤", "machine_learning_duplicate_detection": "āĻĒ⧁āύāϰāĻžāĻŦ⧃āĻ¤ā§āϤāĻŋ āϏāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ", - "machine_learning_duplicate_detection_enabled": "āĻĒ⧁āύāϰāĻžāĻŦ⧃āĻ¤ā§āϤāĻŋ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ" - } + "machine_learning_duplicate_detection_enabled": "āĻĒ⧁āύāϰāĻžāĻŦ⧃āĻ¤ā§āϤāĻŋ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ", + "machine_learning_duplicate_detection_enabled_description": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋ⧟ āĻĨāĻžāĻ•āϞ⧇āĻ“ āĻšā§āĻŦāĻšā§ āĻāĻ•āχ āϏāĻŽā§āĻĒāĻĻāϗ⧁āϞ⧋āϰ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟ āϏāϰāĻŋā§Ÿā§‡ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇āĨ¤", + "machine_learning_duplicate_detection_setting_description": "āϏāĻŽā§āĻ­āĻžāĻŦā§āϝ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟ āϖ⧁āρāĻœā§‡ āĻŦ⧇āϰ āĻ•āϰāϤ⧇ CLIP āĻāĻŽā§āĻŦ⧇āĻĄāĻŋāĻ‚ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύāĨ¤", + "machine_learning_enabled": "Machine Learning āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "machine_learning_enabled_description": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻ•āϞ⧇ āύāĻŋāĻšā§‡āϰ āϏ⧇āϟāĻŋāĻ‚āϏ āύāĻŋāĻ°ā§āĻŦāĻŋāĻļ⧇āώ⧇ āϏāĻŽāĻ¸ā§āϤ ML āĻŦ⧈āĻļāĻŋāĻˇā§āĻŸā§āϝ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "machine_learning_facial_recognition": "āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ", + "machine_learning_facial_recognition_description": "āĻ›āĻŦāĻŋāϤ⧇ āĻŽā§āĻ– āϏāύāĻžāĻ•ā§āϤ āĻ•āϰ⧁āύ, āϚāĻŋāύ⧁āύ āĻāĻŦāĻ‚ āĻ—ā§āϰ⧁āĻĒ āĻ•āϰ⧁āύāĨ¤", + "machine_learning_facial_recognition_model": "āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ āĻŽāĻĄā§‡āϞ", + "machine_learning_facial_recognition_model_description": "āĻŽāĻĄā§‡āϞāϗ⧁āϞāĻŋ āφāĻ•āĻžāϰ⧇āϰ āĻ…āϧāσāĻ•ā§āϰāĻŽ āĻ…āύ⧁āϝāĻžāϝāĻŧā§€ āϤāĻžāϞāĻŋāĻ•āĻžāϭ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇āĨ¤ āĻŦ⧜ āĻŽāĻĄā§‡āϞāϗ⧁āϞāĻŋ āϧ⧀āϰāĻ—āϤāĻŋāϰ āĻāĻŦāĻ‚ āĻŦ⧇āĻļāĻŋ āĻŽā§‡āĻŽāϰāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇, āϤāĻŦ⧇ āωāĻ¨ā§āύāϤ āĻĢāϞāĻžāĻĢāϞ āĻĒā§āϰāĻĻāĻžāύ āĻ•āϰ⧇āĨ¤ āĻŽāύ⧇ āϰāĻžāĻ–āĻŦ⧇āύ āϝ⧇ āĻāĻ•āϟāĻŋ āĻŽāĻĄā§‡āϞ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻžāϰ āĻĒāϰ āφāĻĒāύāĻžāϕ⧇ āϏāĻŽāĻ¸ā§āϤ āĻ›āĻŦāĻŋāϰ āϜāĻ¨ā§āϝ āĻĢ⧇āϏ āĻĄāĻŋāĻŸā§‡āĻ•āĻļāύ (Face Detection) āĻ•āĻžāϜāϟāĻŋ āĻĒ⧁āύāϰāĻžāϝāĻŧ āϚāĻžāϞāĻžāϤ⧇ āĻšāĻŦ⧇āĨ¤", + "machine_learning_facial_recognition_setting": "āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "machine_learning_facial_recognition_setting_description": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋ⧟ āĻĨāĻžāĻ•āϞ⧇, āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ⧇āϰ āϜāĻ¨ā§āϝ āĻ›āĻŦāĻŋāϗ⧁āϞ⧋ āĻāύāϕ⧋āĻĄ āĻ•āϰāĻž āĻšāĻŦ⧇ āύāĻž āĻāĻŦāĻ‚ āĻāĻ•ā§āϏāĻĒā§āϞ⧋āϰ āĻĒ⧇āĻœā§‡āϰ āĻĒāĻŋāĻĒāϞ (People) āϏ⧇āĻ•āĻļāύāϟāĻŋ āĻĒā§‚āĻ°ā§āĻŖ āĻšāĻŦ⧇ āύāĻžāĨ¤", + "machine_learning_max_detection_distance": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āĻĻā§‚āϰāĻ¤ā§āĻŦ", + "machine_learning_max_detection_distance_description": "āĻĻ⧁āϟāĻŋ āĻ›āĻŦāĻŋāϕ⧇ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟ āĻšāĻŋāϏ⧇āĻŦ⧇ āĻ—āĻŖā§āϝ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āϤāĻžāĻĻ⧇āϰ āĻŽāĻ§ā§āϝāĻ•āĻžāϰ āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻĻā§‚āϰāĻ¤ā§āĻŦ, āϝāĻžāϰ āĻĒāϰāĻŋāϏ⧀āĻŽāĻž ā§Ļ.ā§Ļā§Ļā§§-ā§Ļ.ā§§āĨ¤ āĻŽāĻžāύ āϝāϤ āĻŦ⧇āĻļāĻŋ āĻšāĻŦ⧇ āϤāϤ āĻŦ⧇āĻļāĻŋ āĻĄā§āĻĒā§āϞāĻŋāϕ⧇āϟ āĻļāύāĻžāĻ•ā§āϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāϤ⧇ āϭ⧁āϞ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāϪ⧇āϰ (false positives) āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻĨāĻžāĻ•āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "machine_learning_max_recognition_distance": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āϚāĻŋāĻšā§āύāĻŋāϤāĻ•āϰāĻŖ āĻĻā§‚āϰāĻ¤ā§āĻŦ", + "machine_learning_max_recognition_distance_description": "āĻĻ⧁āϟāĻŋ āĻŽā§āĻ–āϕ⧇ āĻāĻ•āχ āĻŦā§āϝāĻ•ā§āϤāĻŋ āĻšāĻŋāϏ⧇āĻŦ⧇ āĻ—āĻŖā§āϝ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āϤāĻžāĻĻ⧇āϰ āĻŽāĻ§ā§āϝāĻ•āĻžāϰ āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āĻĻā§‚āϰāĻ¤ā§āĻŦ, āϝāĻžāϰ āĻĒāϰāĻŋāϏ⧀āĻŽāĻž ā§Ļ-⧍āĨ¤ āĻāχ āĻŽāĻžāύ āĻ•āĻŽāĻžāϞ⧇ āĻĻā§â€™āϜāύ āĻ­āĻŋāĻ¨ā§āύ āĻŦā§āϝāĻ•ā§āϤāĻŋāϕ⧇ āĻāĻ•āχ āĻŦā§āϝāĻ•ā§āϤāĻŋ āĻšāĻŋāϏ⧇āĻŦ⧇ āϚāĻŋāĻšā§āύāĻŋāϤ āĻ•āϰāĻžāϰ āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻ•āĻŽā§‡, āφāϰ āĻŽāĻžāύ āĻŦāĻžā§œāĻžāϞ⧇ āĻāĻ•āχ āĻŦā§āϝāĻ•ā§āϤāĻŋāϕ⧇ āĻĻā§â€™āϜāύ āĻ­āĻŋāĻ¨ā§āύ āĻŦā§āϝāĻ•ā§āϤāĻŋ āĻšāĻŋāϏ⧇āĻŦ⧇ āϚāĻŋāĻšā§āύāĻŋāϤ āĻ•āϰāĻžāϰ āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻ•āĻŽā§‡āĨ¤ āĻŽāύ⧇ āϰāĻžāĻ–āĻŦ⧇āύ āϝ⧇, āĻĻā§â€™āϜāύ āĻŦā§āϝāĻ•ā§āϤāĻŋāϕ⧇ āĻāĻ•āĻ¤ā§āϰāĻŋāϤ āĻ•āϰāĻž (merge) āĻ…āĻĒ⧇āĻ•ā§āώāĻžāĻ•ā§ƒāϤ āϏāĻšāϜ āĻ•āĻŋāĻ¨ā§āϤ⧁ āĻāĻ•āϜāύāϕ⧇ āĻĻā§â€™āĻ­āĻžāϗ⧇ āĻ­āĻžāĻ— āĻ•āϰāĻž āĻ•āĻ āĻŋāύ, āϤāĻžāχ āϏāĻŽā§āĻ­āĻŦ āĻšāϞ⧇ āĻĨā§āϰ⧇āĻļāĻšā§‹āĻ˛ā§āĻĄ (threshold) āĻ•āĻŽ āϰāĻžāĻ–āĻžāχ āĻ­āĻžāϞ⧋āĨ¤", + "machine_learning_min_detection_score": "āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āĻ¸ā§āϕ⧋āϰ", + "machine_learning_min_detection_score_description": "āĻ›āĻŦāĻŋāϤ⧇ āĻŽā§āĻ– āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāĻ§ā§āϝ⧇ āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āĻ•āύāĻĢāĻŋāĻĄā§‡āĻ¨ā§āϏ āĻ¸ā§āϕ⧋āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻ•āĻŽ āĻšāĻŦ⧇ āϤāϤ āĻŦ⧇āĻļāĻŋ āĻŽā§āĻ– āĻļāύāĻžāĻ•ā§āϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāϤ⧇ āϭ⧁āϞ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāϪ⧇āϰ (false positives) āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻĨāĻžāĻ•āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "machine_learning_min_recognized_faces": "āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āĻ¸ā§āĻŦā§€āĻ•ā§ƒāϤ āĻŽā§āϖ⧇āϰ āϏāĻ‚āĻ–ā§āϝāĻž", + "machine_learning_min_recognized_faces_description": "āĻāĻ•āϜāύ āĻŦā§āϝāĻ•ā§āϤāĻŋ āĻšāĻŋāϏ⧇āĻŦ⧇ āϤ⧈āϰāĻŋ āĻšāĻ“āϝāĻŧāĻžāϰ āϜāĻ¨ā§āϝ āĻ¸ā§āĻŦā§€āĻ•ā§ƒāϤ āĻŽā§āϖ⧇āϰ āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āϏāĻ‚āĻ–ā§āϝāĻžāĨ¤ āĻāϟāĻŋ āĻŦāĻžāĻĄāĻŧāĻžāϞ⧇ āĻĢ⧇āϏāĻŋāϝāĻŧāĻžāϞ āϰāĻŋāĻ•āĻ—āύāĻŋāĻļāύ āφāϰāĻ“ āύāĻŋāϖ⧁āρāϤ āĻšāϝāĻŧ, āϤāĻŦ⧇ āĻāϤ⧇ āϕ⧋āύ⧋ āĻŽā§āĻ– āϕ⧋āύ⧋ āĻŦā§āϝāĻ•ā§āϤāĻŋāϰ āϏāĻžāĻĨ⧇ āϏāĻ‚āϝ⧁āĻ•ā§āϤ āύāĻž āĻšāĻ“āϝāĻŧāĻžāϰ āϏāĻŽā§āĻ­āĻžāĻŦāύāĻžāĻ“ āĻŦ⧃āĻĻā§āϧāĻŋ āĻĒāĻžāϝāĻŧāĨ¤", + "machine_learning_ocr": "OCR", + "machine_learning_ocr_description": "āĻ›āĻŦāĻŋāϤ⧇ āĻŸā§‡āĻ•ā§āϏāϟ (Text) āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāϤ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύāĨ¤", + "machine_learning_ocr_enabled": "OCR āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "machine_learning_ocr_enabled_description": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻ•āϞ⧇, āĻ›āĻŦāĻŋāϗ⧁āϞ⧋āϤ⧇ āĻŸā§‡āĻ•ā§āϏāϟ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āĻ•āϰāĻž āĻšāĻŦ⧇ āύāĻžāĨ¤", + "machine_learning_ocr_max_resolution": "āϏāĻ°ā§āĻŦā§‹āĻšā§āϚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ(Resolution)", + "machine_learning_ocr_max_resolution_description": "āĻāχ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ⧇āϰ āωāĻĒāϰ⧇āϰ āĻĒā§āϰāĻŋāĻ­āĻŋāωāϗ⧁āϞ⧋āϰ āĻ…ā§āϝāĻžāϏāĻĒ⧇āĻ•ā§āϟ āϰ⧇āĻļāĻŋāĻ“ (āφāĻ•āĻžāϰ āĻ“ āĻ…āύ⧁āĻĒāĻžāϤ) āĻ āĻŋāĻ• āϰ⧇āϖ⧇ āϰāĻŋāϏāĻžāχāϜ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤ āĻŽāĻžāύ āϝāϤ āĻŦ⧇āĻļāĻŋ āĻšāĻŦ⧇ āĻĢāϞāĻžāĻĢāϞ āϤāϤ āĻŦ⧇āĻļāĻŋ āύāĻŋāϖ⧁āρāϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāϟāĻŋ āĻĒā§āϰāϏ⧇āϏ āĻ•āϰāϤ⧇ āϏāĻŽā§Ÿ āĻŦ⧇āĻļāĻŋ āϞāĻžāĻ—āĻŦ⧇ āĻāĻŦāĻ‚ āĻŽā§‡āĻŽāϰāĻŋ āĻŦ⧇āĻļāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻŦ⧇āĨ¤", + "machine_learning_ocr_min_detection_score": "āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāĻŖ āĻ¸ā§āϕ⧋āϰ", + "machine_learning_ocr_min_detection_score_description": "āĻŸā§‡āĻ•ā§āϏāϟ āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻ¨ā§āϝ⧂āύāϤāĻŽ āĻ•āύāĻĢāĻŋāĻĄā§‡āĻ¨ā§āϏ āĻ¸ā§āϕ⧋āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻ•āĻŽ āĻšāĻŦ⧇ āϤāϤ āĻŦ⧇āĻļāĻŋ āĻŸā§‡āĻ•ā§āϏāϟ āĻļāύāĻžāĻ•ā§āϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāϤ⧇ āϭ⧁āϞ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāϪ⧇āϰ (false positives) āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻĨāĻžāĻ•āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "machine_learning_ocr_min_recognition_score": "āϏāĻ°ā§āĻŦāύāĻŋāĻŽā§āύ āϚāĻŋāĻšā§āύāĻŋāϤāĻ•āϰāĻŖ (Recognition)āĻ¸ā§āϕ⧋āϰ", + "machine_learning_ocr_min_score_recognition_description": "āĻļāύāĻžāĻ•ā§āϤāĻ•ā§ƒāϤ āĻŸā§‡āĻ•ā§āϏāϟ āϚāĻŋāĻšā§āύāĻŋāϤ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ ā§Ļ-ā§§ āĻāϰ āĻŽāĻ§ā§āϝ⧇ āĻ¨ā§āϝ⧂āύāϤāĻŽ āĻ•āύāĻĢāĻŋāĻĄā§‡āĻ¨ā§āϏ āĻ¸ā§āϕ⧋āϰāĨ¤ āĻŽāĻžāύ āϝāϤ āĻ•āĻŽ āĻšāĻŦ⧇ āϤāϤ āĻŦ⧇āĻļāĻŋ āĻŸā§‡āĻ•ā§āϏāϟ āϚāĻŋāĻšā§āύāĻŋāϤ āĻšāĻŦ⧇, āϤāĻŦ⧇ āĻāϤ⧇ āϭ⧁āϞ āĻļāύāĻžāĻ•ā§āϤāĻ•āϰāϪ⧇āϰ (false positives) āϏāĻŽā§āĻ­āĻžāĻŦāύāĻž āĻĨāĻžāĻ•āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "machine_learning_ocr_model": "OCR āĻŽāĻĄā§‡āϞ", + "machine_learning_ocr_model_description": "āϏāĻžāĻ°ā§āĻ­āĻžāϰ āĻŽāĻĄā§‡āϞāϗ⧁āϞ⧋ āĻŽā§‹āĻŦāĻžāχāϞ āĻŽāĻĄā§‡āϞ⧇āϰ āϤ⧁āϞāύāĻžā§Ÿ āĻŦ⧇āĻļāĻŋ āύāĻŋāĻ°ā§āϭ⧁āϞ, āϤāĻŦ⧇ āĻāϗ⧁āϞ⧋ āĻĒā§āϰāϏ⧇āϏ āĻ•āϰāϤ⧇ āϏāĻŽā§Ÿ āĻŦ⧇āĻļāĻŋ āϞāĻžāϗ⧇ āĻāĻŦāĻ‚ āĻŽā§‡āĻŽāϰāĻŋ āĻŦ⧇āĻļāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇āĨ¤", + "machine_learning_settings": "āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϏ⧇āϟāĻŋāĻ‚āϏ (Machine Learning Settings)", + "machine_learning_settings_description": "āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻŦ⧈āĻļāĻŋāĻˇā§āĻŸā§āϝ āĻāĻŦāĻ‚ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "machine_learning_smart_search": "āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āϏāĻžāĻ°ā§āϚ (Smart Search)", + "machine_learning_smart_search_description": "CLIP āĻāĻŽāĻŦ⧇āĻĄāĻŋāĻ‚ (embeddings) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻ›āĻŦāĻŋāϰ āĻŦāĻŋāώ⧟āĻŦāĻ¸ā§āϤ⧁ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻ•āϰ⧁āύ", + "machine_learning_smart_search_enabled": "āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āϏāĻžāĻ°ā§āϚ āϏāĻ•ā§āώāĻŽ āĻ•āϰ⧁āύ", + "machine_learning_smart_search_enabled_description": "āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ āĻĨāĻžāĻ•āϞ⧇, āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āϏāĻžāĻ°ā§āĻšā§‡āϰ āϜāĻ¨ā§āϝ āĻ›āĻŦāĻŋāϗ⧁āϞ⧋ āĻāύāϕ⧋āĻĄ (encode) āĻ•āϰāĻž āĻšāĻŦ⧇ āύāĻžāĨ¤", + "machine_learning_url_description": "āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇āϰ URLāĨ¤ āϝāĻĻāĻŋ āĻāϕ⧇āϰ āĻŦ⧇āĻļāĻŋ URL āĻĒā§āϰāĻĻāĻžāύ āĻ•āϰāĻž āĻšā§Ÿ, āϤāĻŦ⧇ āĻāĻ•āϟāĻŋ āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āϏāĻžā§œāĻž āύāĻž āĻĻ⧇āĻ“ā§ŸāĻž āĻĒāĻ°ā§āϝāĻ¨ā§āϤ āĻĒā§āϰāϤāĻŋāϟāĻŋ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇ āĻāĻ• āĻāĻ• āĻ•āϰ⧇ āĻšā§‡āĻˇā§āϟāĻž āĻ•āϰāĻž āĻšāĻŦ⧇ (āĻĒā§āϰāĻĨāĻŽ āĻĨ⧇āϕ⧇ āĻļ⧇āώ āĻ•ā§āϰāĻŽāĻžāύ⧁āϏāĻžāϰ⧇)āĨ¤ āϝ⧇ āϏāĻžāĻ°ā§āĻ­āĻžāϰāϗ⧁āϞ⧋ āϏāĻžā§œāĻž āĻĻ⧇āĻŦ⧇ āύāĻž, āϏ⧇āϗ⧁āϞ⧋ āĻĒ⧁āύāϰāĻžā§Ÿ āϏāϚāϞ āĻšāĻ“ā§ŸāĻž āĻĒāĻ°ā§āϝāĻ¨ā§āϤ āϏāĻžāĻŽā§ŸāĻŋāĻ•āĻ­āĻžāĻŦ⧇ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "maintenance_delete_backup": "āĻŦā§āϝāĻžāĻ•āφāĻĒ (Backup)āĻŽā§āϛ⧁āύ", + "maintenance_delete_backup_description": "āĻāχ āĻĢāĻžāχāϞāϟāĻŋ āϚāĻŋāϰāϤāϰ⧇ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇āĨ¤", + "maintenance_delete_error": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻŽā§āĻ›āϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšā§Ÿā§‡āϛ⧇āĨ¤", + "maintenance_restore_backup": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ(Restore) āĻ•āϰ⧁āύ", + "maintenance_restore_backup_description": "Immich āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāĻŦ⧇ āĻāĻŦāĻ‚ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻĨ⧇āϕ⧇ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤ āĻ•āĻžāĻ°ā§āϝāĻ•ā§āϰāĻŽ āϚāĻžāϞāĻŋā§Ÿā§‡ āϝāĻžāĻ“ā§ŸāĻžāϰ āφāϗ⧇ āĻāĻ•āϟāĻŋ āĻŦā§āϝāĻžāĻ•āφāĻĒ āϤ⧈āϰāĻŋ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "maintenance_restore_backup_different_version": "āĻāχ āĻŦā§āϝāĻžāĻ•āφāĻĒāϟāĻŋ Immich-āĻāϰ āĻāĻ•āϟāĻŋ āĻ­āĻŋāĻ¨ā§āύ āϏāĻ‚āĻ¸ā§āĻ•āϰāϪ⧇āϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ āϤ⧈āϰāĻŋ āĻ•āϰāĻž āĻšā§Ÿā§‡āĻ›āĻŋāϞ!", + "maintenance_restore_backup_unknown_version": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āϏāĻ‚āĻ¸ā§āĻ•āϰāĻŖ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŖ āĻ•āϰāĻž āϏāĻŽā§āĻ­āĻŦ āĻšāϝāĻŧāύāĻŋāĨ¤", + "maintenance_restore_database_backup": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻĒ⧁āύāϰ⧁āĻĻā§āϧāĻžāϰ āĻ•āϰ⧁āύ", + "maintenance_restore_database_backup_description": "āĻāĻ•āϟāĻŋ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻĢāĻžāχāϞ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧇ āĻĄā§‡āϟāĻžāĻŦ⧇āϏāϕ⧇ āĻĒā§‚āĻ°ā§āĻŦāĻŦāĻ°ā§āϤ⧀ āĻ…āĻŦāĻ¸ā§āĻĨāĻžāϝāĻŧ āĻĢāĻŋāϰāĻŋā§Ÿā§‡ āφāύ⧁āύāĨ¤", + "maintenance_settings": "āϰāĻ•ā§āώāĻŖāĻžāĻŦ⧇āĻ•ā§āώāĻŖ (Maintenance)", + "maintenance_settings_description": "Immich-āϕ⧇ āϰāĻ•ā§āώāĻŖāĻžāĻŦ⧇āĻ•ā§āώāĻŖ āĻŽā§‹āĻĄā§‡ (maintenance mode) āϰāĻžāϖ⧁āύāĨ¤", + "maintenance_start": "āϰāĻ•ā§āώāĻŖāĻžāĻŦ⧇āĻ•ā§āώāĻŖ āĻŽā§‹āĻĄā§‡ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰ⧁āύ", + "maintenance_start_error": "āϰāĻ•ā§āώāĻŖāĻžāĻŦ⧇āĻ•ā§āώāĻŖ āĻŽā§‹āĻĄ āϚāĻžāϞ⧁ āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšā§Ÿā§‡āϛ⧇āĨ¤", + "maintenance_upload_backup": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻĢāĻžāχāϞ āφāĻĒāϞ⧋āĻĄ āĻ•āϰ⧁āύ", + "maintenance_upload_backup_error": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āϝāĻžā§ŸāύāĻŋ, āĻāϟāĻŋ āĻ•āĻŋ āϕ⧋āύ⧋ .sql/.sql.gz āĻĢāĻžāχāϞ?", + "manage_concurrency": "āĻ•āύāĻ•āĻžāϰ⧇āĻ¨ā§āϏāĻŋ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ (Manage Concurrency)", + "manage_concurrency_description": "āϜāĻŦ āĻ•āύāĻ•āĻžāϰ⧇āĻ¨ā§āϏāĻŋ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰāϤ⧇ 'āϜāĻŦāϏ' (Jobs) āĻĒāĻžāϤāĻžāϝāĻŧ āϝāĻžāύāĨ¤", + "manage_log_settings": "āϞāĻ— āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "map_dark_style": "āĻĄāĻžāĻ°ā§āĻ• āĻ¸ā§āϟāĻžāχāϞ (Dark style)", + "map_enable_description": "āĻŽā§āϝāĻžāĻĒ āĻĢāĻŋāϚāĻžāϰāϗ⧁āϞ⧋ āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧁āύ (Enable map features)", + "map_gps_settings": "āĻŽā§āϝāĻžāĻĒ āĻāĻŦāĻ‚ āϜāĻŋāĻĒāĻŋāĻāϏ āϏ⧇āϟāĻŋāĻ‚āϏ (Map & GPS Settings)", + "map_gps_settings_description": "āĻŽā§āϝāĻžāĻĒ āĻāĻŦāĻ‚ āϜāĻŋāĻĒāĻŋāĻāϏ (āϰāĻŋāĻ­āĻžāĻ°ā§āϏ āϜāĻŋāĻ“āϕ⧋āĻĄāĻŋāĻ‚) āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ (Manage Map & GPS (Reverse Geocoding) Settings)", + "map_implications": "āĻŽā§āϝāĻžāĻĒ āĻĢāĻŋāϚāĻžāϰāϟāĻŋ āĻāĻ•āϟāĻŋ āĻāĻ•ā§āϏāϟāĻžāĻ°ā§āύāĻžāϞ āϟāĻžāχāϞ āϏāĻžāĻ°ā§āĻ­āĻŋāϏ⧇āϰ (tiles.immich.cloud) āĻ“āĻĒāϰ āύāĻŋāĻ°ā§āĻ­āϰ āĻ•āϰ⧇āĨ¤", + "map_light_style": "āϞāĻžāχāϟ āĻ¸ā§āϟāĻžāχāϞ (Light style)", + "map_manage_reverse_geocoding_settings": "āϰāĻŋāĻ­āĻžāĻ°ā§āϏ āϜāĻŋāĻ“āϕ⧋āĻĄāĻŋāĻ‚ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "map_reverse_geocoding": "āϰāĻŋāĻ­āĻžāĻ°ā§āϏ āϜāĻŋāĻ“āϕ⧋āĻĄāĻŋāĻ‚ (Reverse Geocoding)", + "map_reverse_geocoding_enable_description": "āϰāĻŋāĻ­āĻžāĻ°ā§āϏ āϜāĻŋāĻ“āϕ⧋āĻĄāĻŋāĻ‚ āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧁āύ (Enable reverse geocoding)", + "map_reverse_geocoding_settings": "āϰāĻŋāĻ­āĻžāĻ°ā§āϏ āϜāĻŋāĻ“āϕ⧋āĻĄāĻŋāĻ‚ āϏ⧇āϟāĻŋāĻ‚āϏ (Reverse Geocoding Settings)", + "map_settings": "āĻŽāĻžāύāϚāĻŋāĻ¤ā§āϰ (Map)", + "map_settings_description": "āĻŽāĻžāύāϚāĻŋāĻ¤ā§āϰ⧇āϰ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ (Manage map settings)", + "map_style_description": "āĻāĻ•āϟāĻŋ style.json āĻŽā§āϝāĻžāĻĒ āĻĨāĻŋāĻŽā§‡āϰ URL (URL to a style.json map theme)", + "memory_cleanup_job": "āĻŽā§‡āĻŽāϰāĻŋ āĻ•ā§āϞāĻŋāύāφāĻĒ (Memory cleanup)", + "memory_generate_job": "āĻ¸ā§āĻŽā§ƒāϤāĻŋ āϤ⧈āϰāĻŋ āĻ•āϰāĻž(Memory generation)", + "metadata_extraction_job": "āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āĻāĻ•ā§āϏāĻŸā§āĻ°ā§āϝāĻžāĻ•ā§āϟ āĻ•āϰ⧁āύ (Extract metadata)", + "metadata_extraction_job_description": "āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…ā§āϝāĻžāϏ⧇āϟ (Asset) āĻĨ⧇āϕ⧇ āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āϤāĻĨā§āϝ āĻāĻ•ā§āϏāĻŸā§āĻ°ā§āϝāĻžāĻ•ā§āϟ āĻ•āϰ⧁āύ, āϝ⧇āĻŽāύ: āϜāĻŋāĻĒāĻŋāĻāϏ (GPS), āĻšā§‡āĻšāĻžāϰāĻž (faces) āĻāĻŦāĻ‚ āϰ⧇āĻœā§‹āϞāĻŋāωāĻļāύ (resolution)āĨ¤", + "metadata_faces_import_setting": "āĻĢ⧇āϏ āχāĻŽā§āĻĒā§‹āĻ°ā§āϟ āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧁āύ (Enable face import)", + "metadata_faces_import_setting_description": "āĻ›āĻŦāĻŋāϰ EXIF āĻĄā§‡āϟāĻž āĻāĻŦāĻ‚ āϏāĻžāχāĻĄāĻ•āĻžāϰ (sidecar) āĻĢāĻžāχāϞ āĻĨ⧇āϕ⧇ āĻšā§‡āĻšāĻžāϰāĻž (faces) āχāĻŽā§āĻĒā§‹āĻ°ā§āϟ āĻ•āϰ⧁āύāĨ¤", + "metadata_settings": "āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āϏ⧇āϟāĻŋāĻ‚āϏ (Metadata Settings)", + "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_database_cleanup_setting": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻ•ā§āϞāĻŋāύāφāĻĒ āϟāĻžāĻ¸ā§āĻ•āϏāĻŽā§‚āĻš (Database cleanup tasks)", + "nightly_tasks_database_cleanup_setting_description": "āĻĄā§‡āϟāĻžāĻŦ⧇āϏ āĻĨ⧇āϕ⧇ āĻĒ⧁āϰ⧋āύ⧋ āĻāĻŦāĻ‚ āĻŽā§‡ā§ŸāĻžāĻĻā§‹āĻ¤ā§āϤ⧀āĻ°ā§āĻŖ āĻĄā§‡āϟāĻž āĻŽā§āϛ⧇ āĻĢ⧇āϞ⧁āύ", + "nightly_tasks_generate_memories_setting": "āĻŽā§‡āĻŽā§‹āϰāĻŋāϜ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ (Generate memories)", + "nightly_tasks_generate_memories_setting_description": "āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋ āĻĨ⧇āϕ⧇ āύāϤ⧁āύ āĻŽā§‡āĻŽā§‹āϰāĻŋāϜ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ", + "nightly_tasks_missing_thumbnails_setting": "āĻšāĻžāϰāĻŋāϝāĻŧ⧇ āϝāĻžāĻ“āϝāĻŧāĻž āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞāϗ⧁āϞ⧋ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ", + "nightly_tasks_missing_thumbnails_setting_description": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āύ⧇āχ āĻāĻŽāύ āĻĢāĻžāχāϞāϗ⧁āϞ⧋āϕ⧇ āĻ•āĻŋāωāϤ⧇ (Queue) āϝ⧋āĻ— āĻ•āϰ⧁āύ", + "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": "āϏāĻžāĻ°ā§āĻ­āĻžāϰ āϝāĻ–āύ āύāĻžāχāϟāϞāĻŋ āϟāĻžāĻ¸ā§āĻ• (nightly tasks) āϚāĻžāϞāĻžāύ⧋ āĻļ⧁āϰ⧁ āĻ•āϰ⧇ āϏ⧇āχ āϏāĻŽāϝāĻŧ", + "nightly_tasks_sync_quota_usage_setting": "āϕ⧋āϟāĻž āĻŦā§āϝāĻŦāĻšāĻžāϰ⧇āϰ āϤāĻĨā§āϝ āϏāĻŋāĻ™ā§āĻ• āĻ•āϰ⧁āύ (Sync quota usage)", + "nightly_tasks_sync_quota_usage_setting_description": "āĻŦāĻ°ā§āϤāĻŽāĻžāύ āĻŦā§āϝāĻŦāĻšāĻžāϰ⧇āϰ āĻ“āĻĒāϰ āĻ­āĻŋāĻ¤ā§āϤāĻŋ āĻ•āϰ⧇ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϕ⧋āϟāĻž āφāĻĒāĻĄā§‡āϟ āĻ•āϰ⧁āύāĨ¤", + "no_paths_added": "āϕ⧋āύ⧋ āĻĒāĻžāĻĨ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋ (No paths added)", + "no_pattern_added": "āϕ⧋āύ⧋ āĻĒā§āϝāĻžāϟāĻžāĻ°ā§āύ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧāύāĻŋ (No pattern added)", + "note_apply_storage_label_previous_assets": "āĻĻā§āϰāĻˇā§āϟāĻŦā§āϝ: āĻĒā§‚āĻ°ā§āĻŦ⧇ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋āϤ⧇ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧇āĻŦ⧇āϞ (Storage Label) āĻĒā§āϰāϝāĻŧā§‹āĻ— āĻ•āϰāϤ⧇ āύāĻŋāĻšā§‡āϰ āĻ•āĻŽāĻžāĻ¨ā§āĻĄāϟāĻŋ āϰāĻžāύ āĻ•āϰ⧁āĻ¨â€”", + "note_cannot_be_changed_later": "āϏāϤāĻ°ā§āĻ•āĻŦāĻžāĻ°ā§āϤāĻž: āĻāϟāĻŋ āĻĒāϰāĻŦāĻ°ā§āϤ⧀āϤ⧇ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻž āϝāĻžāĻŦ⧇ āύāĻž!", + "notification_email_from_address": "āĻĒā§āϰ⧇āϰāϕ⧇āϰ āĻ āĻŋāĻ•āĻžāύāĻž (From address)", + "notification_email_from_address_description": "āĻĒā§āϰ⧇āϰāϕ⧇āϰ āχāĻŽā§‡āϞ āĻ āĻŋāĻ•āĻžāύāĻž, āωāĻĻāĻžāĻšāϰāĻŖāĻ¸ā§āĻŦāϰ⧂āĻĒ: \"Immich Photo Server noreply@example.com\"āĨ¤ āύāĻŋāĻļā§āϚāĻŋāϤ āĻ•āϰ⧁āύ āϝ⧇ āφāĻĒāύāĻŋ āĻāĻŽāύ āĻāĻ•āϟāĻŋ āĻ āĻŋāĻ•āĻžāύāĻž āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāϛ⧇āύ āϝāĻž āĻĨ⧇āϕ⧇ āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ⧋āϰ āĻ…āύ⧁āĻŽāϤāĻŋ āφāĻĒāύāĻžāϰ āφāϛ⧇āĨ¤", + "notification_email_host_description": "āχāĻŽā§‡āϞ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇āϰ āĻšā§‹āĻ¸ā§āϟ (āϝ⧇āĻŽāύ: smtp.immich.app)", + "notification_email_ignore_certificate_errors": "āϏāĻžāĻ°ā§āϟāĻŋāĻĢāĻŋāϕ⧇āϟ āĻ¤ā§āϰ⧁āϟāĻŋāϗ⧁āϞ⧋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰ⧁āύ (Ignore certificate errors)", + "notification_email_ignore_certificate_errors_description": "TLS āϏāĻžāĻ°ā§āϟāĻŋāĻĢāĻŋāϕ⧇āϟ āĻ­ā§āϝāĻžāϞāĻŋāĻĄā§‡āĻļāύ āĻ¤ā§āϰ⧁āϟāĻŋāϗ⧁āϞ⧋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰ⧁āύ (āĻĒā§āϰāĻ¸ā§āϤāĻžāĻŦāĻŋāϤ āύāϝāĻŧ)", + "notification_email_password_description": "āχāĻŽā§‡āϞ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇ āĻ…āĻĨ⧇āĻ¨ā§āϟāĻŋāϕ⧇āĻļāύ āĻŦāĻž āϏāĻ¤ā§āϝāϤāĻž āϝāĻžāϚāĻžāĻ‡ā§Ÿā§‡āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ", + "notification_email_port_description": "āχāĻŽā§‡āϞ āϏāĻžāĻ°ā§āĻ­āĻžāϰ⧇āϰ āĻĒā§‹āĻ°ā§āϟ (āϝ⧇āĻŽāύ: ⧍ā§Ģ, ā§Ēā§Ŧā§Ģ, āĻ…āĻĨāĻŦāĻž ā§Ģā§Žā§­)", + "notification_email_secure": "SMTPS (āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āĻŽā§‡āχāϞ āĻŸā§āϰāĻžāĻ¨ā§āϏāĻĢāĻžāϰ āĻĒā§āϰ⧋āĻŸā§‹āĻ•āϞ āϏāĻŋāĻ•āĻŋāωāϰ)", + "notification_email_secure_description": "SMTPS (SMTP over TLS) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ", + "notification_email_sent_test_email_button": "āĻŸā§‡āĻ¸ā§āϟ āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ āĻāĻŦāĻ‚ āϏ⧇āĻ­ āĻ•āϰ⧁āύ", + "oauth_enable_description": "OAuth-āĻāϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡ āϞāĻ—āχāύ āĻ•āϰ⧁āύ", + "oauth_mobile_redirect_uri": "āĻŽā§‹āĻŦāĻžāχāϞ āϰāĻŋāĻĄāĻžāχāϰ⧇āĻ•ā§āϟ āχāωāφāϰāφāχ (URI)", + "oauth_mobile_redirect_uri_override": "āĻŽā§‹āĻŦāĻžāχāϞ āϰāĻŋāĻĄāĻžāχāϰ⧇āĻ•ā§āϟ āχāωāφāϰāφāχ (URI) āĻ“āĻ­āĻžāϰāϰāĻžāχāĻĄ", + "oauth_mobile_redirect_uri_override_description": "āϝāĻ–āύ OAuth āĻĒā§āϰ⧋āĻ­āĻžāχāĻĄāĻžāϰ āĻŽā§‹āĻŦāĻžāχāϞ āχāωāφāϰāφāχ (URI) āĻ…āύ⧁āĻŽāϤāĻŋ āĻĻā§‡ā§Ÿ āύāĻž, āϝ⧇āĻŽāύ ''{callback}'', āϤāĻ–āύ āĻāϟāĻŋ āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧁āύāĨ¤", + "oauth_role_claim": "āϰ⧋āϞ āĻ•ā§āϞ⧇āχāĻŽ (Role Claim)", + "oauth_role_claim_description": "āĻāχ āĻ•ā§āϞ⧇āχāĻŽāϟāĻŋāϰ āωāĻĒāĻ¸ā§āĻĨāĻŋāϤāĻŋāϰ āĻ“āĻĒāϰ āĻ­āĻŋāĻ¤ā§āϤāĻŋ āĻ•āϰ⧇ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āĻ…ā§āϝāĻžāĻ•ā§āϏ⧇āϏ āĻĒā§āϰāĻĻāĻžāύ āĻ•āϰ⧁āύāĨ¤ āĻ•ā§āϞ⧇āχāĻŽāϟāĻŋāϤ⧇ 'user' āĻ…āĻĨāĻŦāĻž 'admin' āϝ⧇āϕ⧋āύ⧋ āĻāĻ•āϟāĻŋ āĻĨāĻžāĻ•āϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "oauth_settings": "OAuth", + "oauth_settings_description": "OAuth āϞāĻ—āχāύ āϏ⧇āϟāĻŋāĻ‚āϏ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", + "oauth_settings_more_details": "āĻāχ āĻĢāĻŋāϚāĻžāϰ⧇āϰ āĻŦā§āϝāĻžāĻĒāĻžāϰ⧇ āφāϰāĻ“ āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āϜāĻžāύāϤ⧇, āĻĄāϕ⧁āĻŽā§‡āĻ¨ā§āϟāϏ āĻĻ⧇āϖ⧁āύāĨ¤", + "oauth_storage_label_claim": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧇āĻŦ⧇āϞ āĻ•ā§āϞ⧇āχāĻŽ (Storage label claim)", + "oauth_storage_label_claim_description": "āĻāχ āĻ•ā§āϞ⧇āχāĻŽ-āĻāϰ āĻ­ā§āϝāĻžāϞ⧁ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧇āĻŦ⧇āϞ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āϏ⧇āϟ āĻ•āϰ⧁āύāĨ¤", + "oauth_storage_quota_claim": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϕ⧋āϟāĻž āĻ•ā§āϞ⧇āχāĻŽ (Storage quota claim)", + "oauth_storage_quota_claim_description": "āĻāχ āĻ•ā§āϞ⧇āχāĻŽ-āĻāϰ āĻ­ā§āϝāĻžāϞ⧁ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϕ⧋āϟāĻž āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āϏ⧇āϟ āĻ•āϰ⧁āύāĨ¤", + "oauth_storage_quota_default": "āĻĄāĻŋāĻĢāĻ˛ā§āϟ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϕ⧋āϟāĻž (GiB)", + "oauth_storage_quota_default_description": "āĻ•ā§āϞ⧇āχāĻŽ āύāĻž āĻĻ⧇āĻ“ā§ŸāĻž āĻĨāĻžāĻ•āϞ⧇ āϝ⧇ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϕ⧋āϟāĻž (GiB-āϤ⧇) āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", + "oauth_timeout": "āϰāĻŋāϕ⧋āϝāĻŧ⧇āĻ¸ā§āϟ āϟāĻžāχāĻŽ-āφāωāϟ (Request Timeout)", + "oauth_timeout_description": "āĻŽāĻŋāϞāĻŋāϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡ āϰāĻŋāĻ•ā§‹ā§Ÿā§‡āĻ¸ā§āĻŸā§‡āϰ āϟāĻžāχāĻŽ-āφāωāϟ (Timeout for requests in milliseconds)", + "ocr_job_description": "āĻ›āĻŦāĻŋ āĻĨ⧇āϕ⧇ āĻŸā§‡āĻ•ā§āϏāϟ āĻļāύāĻžāĻ•ā§āϤ āĻ•āϰāϤ⧇ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻŦā§āϝāĻŦāĻšāĻžāϰ āĻ•āϰ⧁āύ", + "password_enable_description": "āχāĻŽā§‡āϞ āĻāĻŦāĻ‚ āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āĻĻāĻŋāϝāĻŧ⧇ āϞāĻ—āχāύ āĻ•āϰ⧁āύ", + "password_settings": "āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āϞāĻ—āχāύ (Password Login)", + "password_settings_description": "āĻĒāĻžāϏāĻ“āϝāĻŧāĻžāĻ°ā§āĻĄ āϞāĻ—āχāύ āϏ⧇āϟāĻŋāĻ‚āϏ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", + "paths_validated_successfully": "āϏāĻŦāϗ⧁āϞ⧋ āĻĒāĻžāĻĨ (path) āϏāĻĢāϞāĻ­āĻžāĻŦ⧇ āϝāĻžāϚāĻžāχ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", + "person_cleanup_job": "āĻĒāĻžāϰāϏāύ āĻ•ā§āϞāĻŋāύāφāĻĒ (Person Cleanup)", + "queue_details": "āĻ•āĻŋāω āĻĄāĻŋāĻŸā§‡āχāϞāϏ (Queue Details)", + "queues": "āϜāĻŦ āĻ•āĻŋāω (Job Queues)", + "queues_page_description": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϜāĻŦ āĻ•āĻŋāω (Job Queues) āĻĒ⧇āϜ", + "quota_size_gib": "āϕ⧋āϟāĻž āϏāĻžāχāϜ (GiB)", + "refreshing_all_libraries": "āϏāĻŦāϗ⧁āϞ⧋ āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āϰāĻŋāĻĢā§āϰ⧇āĻļ āĻ•āϰāĻž āĻšāĻšā§āϛ⧇", + "registration": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϰ⧇āϜāĻŋāĻ¸ā§āĻŸā§āϰ⧇āĻļāύ (Admin Registration)", + "registration_description": "āϝ⧇āĻšā§‡āϤ⧁ āφāĻĒāύāĻŋ āĻāχ āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽā§‡āϰ āĻĒā§āϰāĻĨāĻŽ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀, āϤāĻžāχ āφāĻĒāύāĻžāϕ⧇ āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ (Admin) āĻšāĻŋāϏ⧇āĻŦ⧇ āύāĻŋāϝ⧁āĻ•ā§āϤ āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤ āφāĻĒāύāĻŋ āϏāĻŽāĻ¸ā§āϤ āĻĒā§āϰāĻļāĻžāϏāύāĻŋāĻ• āĻ•āĻžāĻœā§‡āϰ āϜāĻ¨ā§āϝ āĻĻāĻžāϝāĻŧā§€ āĻĨāĻžāĻ•āĻŦ⧇āύ āĻāĻŦāĻ‚ āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰāĻž āφāĻĒāύāĻžāϰ āĻŽāĻžāĻ§ā§āϝāĻŽā§‡āχ āϤ⧈āϰāĻŋ āĻšāĻŦ⧇āĨ¤", + "remove_failed_jobs": "āĻŦā§āϝāĻ°ā§āĻĨ āĻšāĻ“āϝāĻŧāĻž āĻ•āĻžāϜāϗ⧁āϞ⧋ āĻŽā§āϛ⧇ āĻĢ⧇āϞ⧁āύ (Remove failed jobs)", + "require_password_change_on_login": "āĻĒā§āϰāĻĨāĻŽāĻŦāĻžāϰ āϞāĻ—āχāύ āĻ•āϰāĻžāϰ āϏāĻŽā§Ÿ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻĒāĻžāϏāĻ“ā§ŸāĻžāĻ°ā§āĻĄ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύ āĻ•āϰāĻž āĻŦāĻžāĻ§ā§āϝāϤāĻžāĻŽā§‚āϞāĻ• āĻ•āϰ⧁āύ", + "reset_settings_to_default": "āϏ⧇āϟāĻŋāĻ‚āϏ āϰāĻŋāϏ⧇āϟ āĻ•āϰ⧇ āĻĄāĻŋāĻĢāĻ˛ā§āϟ āĻ…āĻŦāĻ¸ā§āĻĨāĻžā§Ÿ āĻĢāĻŋāϰāĻŋā§Ÿā§‡ āφāύ⧁āύ (Reset settings to default)", + "reset_settings_to_recent_saved": "āϏāĻŽā§āĻĒā§āϰāϤāĻŋ āϏ⧇āĻ­ āĻ•āϰāĻž āϏ⧇āϟāĻŋāĻ‚āϏ⧇ āϰāĻŋāϏ⧇āϟ āĻ•āϰ⧁āύ (Reset settings to the recent saved settings)", + "scanning_library": "āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āĻ¸ā§āĻ•ā§āϝāĻžāύ āĻ•āϰāĻž āĻšāĻšā§āϛ⧇ (Scanning library)", + "search_jobs": "āϜāĻŦ āϏāĻžāĻ°ā§āϚ āĻ•āϰ⧁āύâ€Ļ", + "send_welcome_email": "āĻ¸ā§āĻŦāĻžāĻ—āϤ āχāĻŽā§‡āϞ āĻĒāĻžāĻ āĻžāύ", + "server_external_domain_settings": "āĻāĻ•ā§āϏāϟāĻžāĻ°ā§āύāĻžāϞ āĻĄā§‹āĻŽā§‡āχāύ (External Domain)", + "server_external_domain_settings_description": "āĻĒāĻžāĻŦāϞāĻŋāĻ• āĻļ⧇āϝāĻŧāĻžāϰāĻŋāĻ‚ āϞāĻŋāĻ™ā§āϕ⧇āϰ āϜāĻ¨ā§āϝ āĻĄā§‹āĻŽā§‡āχāύ (http(s):// āϏāĻš)", + "server_public_users": "āĻĒāĻžāĻŦāϞāĻŋāĻ• āχāωāϜāĻžāϰ (Public Users)", + "server_public_users_description": "āĻļ⧇āϝāĻŧāĻžāϰ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϞāĻŦāĻžāĻŽā§‡ āϕ⧋āύ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϕ⧇ āϝ⧋āĻ— āĻ•āϰāĻžāϰ āϏāĻŽāϝāĻŧ āϏāĻŽāĻ¸ā§āϤ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ (āύāĻžāĻŽ āĻāĻŦāĻ‚ āχāĻŽā§‡āϞ) āϤāĻžāϞāĻŋāĻ•āĻž āĻĻ⧇āĻ–āĻžāύ⧋ āĻšāϝāĻŧāĨ¤ āĻāϟāĻŋ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋāϝāĻŧ (Disabled) āĻ•āϰāĻž āĻšāϞ⧇, āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āϤāĻžāϞāĻŋāĻ•āĻž āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύāĻĻ⧇āϰ āϜāĻ¨ā§āϝ āωāĻĒāϞāĻŦā§āϧ āĻšāĻŦ⧇āĨ¤", + "server_settings": "āϏāĻžāĻ°ā§āĻ­āĻžāϰ āϏ⧇āϟāĻŋāĻ‚āϏ (Server Settings)", + "server_settings_description": "āϏāĻžāĻ°ā§āĻ­āĻžāϰ āϏ⧇āϟāĻŋāĻ‚āϏ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ (Manage server settings)", + "server_stats_page_description": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϏāĻžāĻ°ā§āĻ­āĻžāϰ āĻ¸ā§āĻŸā§āϝāĻžāϟāĻŋāĻ¸ā§āϟāĻŋāĻ•āϏ (Server Statistics) āĻĒ⧇āϜ", + "server_welcome_message": "āĻ¸ā§āĻŦāĻžāĻ—āϤ āĻŦāĻžāĻ°ā§āϤāĻž (Welcome message)", + "server_welcome_message_description": "āϞāĻ—āχāύ āĻĒ⧇āĻœā§‡ āĻĒā§āϰāĻĻāĻ°ā§āĻļāĻŋāϤ āĻāĻ•āϟāĻŋ āĻŦāĻžāĻ°ā§āϤāĻžāĨ¤", + "settings_page_description": "āĻ…ā§āϝāĻžāĻĄāĻŽāĻŋāύ āϏ⧇āϟāĻŋāĻ‚āϏ āĻĒ⧇āϜ", + "sidecar_job": "āϏāĻžāχāĻĄāĻ•āĻžāϰ āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž (Sidecar Metadata)", + "sidecar_job_description": "āĻĢāĻžāχāϞāϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻĨ⧇āϕ⧇ āϏāĻžāχāĻĄāĻ•āĻžāϰ āĻŽā§‡āϟāĻžāĻĄā§‡āϟāĻž āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻŦāĻž āϏāĻŋāĻ™ā§āĻ•ā§āϰ⧋āύāĻžāχāϜ āĻ•āϰ⧁āύ", + "slideshow_duration_description": "āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ›āĻŦāĻŋ āĻĻ⧇āĻ–āĻžāύ⧋āϰ āϏāĻŽā§ŸāĻ•āĻžāϞ (āϏ⧇āϕ⧇āĻ¨ā§āĻĄā§‡)", + "smart_search_job_description": "āĻ¸ā§āĻŽāĻžāĻ°ā§āϟ āϏāĻžāĻ°ā§āĻšā§‡āϰ āϏ⧁āĻŦāĻŋāϧāĻžāĻ°ā§āĻĨ⧇ āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋āϰ āĻ“āĻĒāϰ āĻŽā§‡āĻļāĻŋāύ āϞāĻžāĻ°ā§āύāĻŋāĻ‚ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰ⧁āύ", + "storage_template_date_time_description": "āĻ…ā§āϝāĻžāϏ⧇āϟ āϤ⧈āϰāĻŋāϰ āϏāĻŽā§ŸāĻ•āĻžāϞ (Timestamp) āϤāĻžāϰāĻŋāĻ– āĻ“ āϏāĻŽā§Ÿā§‡āϰ āϤāĻĨā§āϝ⧇āϰ āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšā§Ÿ", + "storage_template_date_time_sample": "āύāĻŽā§āύāĻž āϏāĻŽā§Ÿ {date}", + "storage_template_enable_description": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āχāĻžā§āϜāĻŋāύ āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧁āύ", + "storage_template_hash_verification_enabled": "āĻšā§āϝāĻžāĻļ āϭ⧇āϰāĻŋāĻĢāĻŋāϕ⧇āĻļāύ (Hash Verification) āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰāĻž āĻšā§Ÿā§‡āϛ⧇", + "storage_template_hash_verification_enabled_description": "āĻšā§āϝāĻžāĻļ āϭ⧇āϰāĻŋāĻĢāĻŋāϕ⧇āĻļāύ (Hash Verification) āϏāĻ•ā§āϰāĻŋ⧟ āĻ•āϰ⧇; āĻāϰ āĻĒā§āϰāĻ­āĻžāĻŦ āϏāĻŽā§āĻĒāĻ°ā§āϕ⧇ āύāĻŋāĻļā§āϚāĻŋāϤ āύāĻž āĻšā§Ÿā§‡ āĻāϟāĻŋ āύāĻŋāĻˇā§āĻ•ā§āϰāĻŋ⧟ āĻ•āϰāĻŦ⧇āύ āύāĻž", + "storage_template_migration": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āĻŽāĻžāχāĻ—ā§āϰ⧇āĻļāύ (Storage Template Migration)", + "storage_template_migration_description": "āĻĒā§‚āĻ°ā§āĻŦ⧇ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋āϤ⧇ āĻŦāĻ°ā§āϤāĻŽāĻžāύ {template} āĻĒā§āϰāϝāĻŧā§‹āĻ— āĻ•āϰ⧁āύ", + "storage_template_migration_info": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟāϟāĻŋ āϏāĻŽāĻ¸ā§āϤ āĻāĻ•ā§āϏāĻŸā§‡āύāĻļāύāϕ⧇ āϛ⧋āϟ āĻšāĻžāϤ⧇āϰ āĻ…āĻ•ā§āώāϰ⧇ (lowercase) āϰ⧂āĻĒāĻžāĻ¨ā§āϤāϰ āĻ•āϰāĻŦ⧇āĨ¤ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āĻŸā§‡āϰ āĻĒāϰāĻŋāĻŦāĻ°ā§āϤāύāϗ⧁āϞ⧋ āϕ⧇āĻŦāϞ āύāϤ⧁āύ āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋āϰ āĻ•ā§āώ⧇āĻ¤ā§āϰ⧇ āĻĒā§āϰāϝ⧋āĻœā§āϝ āĻšāĻŦ⧇āĨ¤ āĻĒā§‚āĻ°ā§āĻŦ⧇ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āϟāϗ⧁āϞ⧋āϤ⧇ āĻāχ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟāϟāĻŋ āĻ­ā§‚āϤāĻžāĻĒ⧇āĻ•ā§āώāĻ­āĻžāĻŦ⧇ (retroactively) āĻĒā§āϰāϝāĻŧā§‹āĻ— āĻ•āϰāϤ⧇ {job} āϰāĻžāύ āĻ•āϰ⧁āύāĨ¤", + "storage_template_migration_job": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āĻŽāĻžāχāĻ—ā§āϰ⧇āĻļāύ āϜāĻŦ", + "storage_template_more_details": "āĻāχ āĻĢāĻŋāϚāĻžāϰāϟāĻŋ āϏāĻŽā§āĻĒāĻ°ā§āϕ⧇ āφāϰāĻ“ āĻŦāĻŋāĻ¸ā§āϤāĻžāϰāĻŋāϤ āϜāĻžāύāϤ⧇, Storage Template āĻāĻŦāĻ‚ āĻāϰ āĻĒā§āϰāĻ­āĻžāĻŦāϗ⧁āϞ⧋ (implications) āĻĻ⧇āϖ⧁āύāĨ¤", + "storage_template_onboarding_description_v2": "āĻāϟāĻŋ āϏāĻ•ā§āϰāĻŋ⧟ āĻĨāĻžāĻ•āϞ⧇, āĻĢāĻŋāϚāĻžāϰāϟāĻŋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āύāĻŋāĻ°ā§āϧāĻžāϰāĻŋāϤ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āĻĢāĻžāχāϞāϗ⧁āϞ⧋āϕ⧇ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āĻ…āĻ°ā§āĻ—āĻžāύāĻžāχāϜ (Auto-organize) āĻ•āϰāĻŦ⧇āĨ¤ āφāϰāĻ“ āϤāĻĨā§āϝ⧇āϰ āϜāĻ¨ā§āϝ āĻ…āύ⧁āĻ—ā§āϰāĻš āĻ•āϰ⧇ āĻĄāϕ⧁āĻŽā§‡āĻ¨ā§āĻŸā§‡āĻļāύ āĻĻ⧇āϖ⧁āύāĨ¤", + "storage_template_path_length": "āφāύ⧁āĻŽāĻžāύāĻŋāĻ• āĻĒāĻžāĻĨ āϞ⧇āĻ¨ā§āĻĨ āϞāĻŋāĻŽāĻŋāϟ (Path length limit): {length, number}/{limit, number}", + "storage_template_settings": "āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āĻŸā§‡āĻŽāĻĒā§āϞ⧇āϟ (Storage Template)", + "storage_template_settings_description": "āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡āϰ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻ¸ā§āĻŸā§āϰāĻžāĻ•āϚāĻžāϰ āĻāĻŦāĻ‚ āĻĢāĻžāχāϞ āύ⧇āĻŽ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", + "storage_template_user_label": "{label} āĻšāϞ⧋ āĻŦā§āϝāĻŦāĻšāĻžāϰāĻ•āĻžāϰ⧀āϰ āĻ¸ā§āĻŸā§‹āϰ⧇āϜ āϞ⧇āĻŦ⧇āϞ (Storage Label)", + "theme_settings_description": "āχāĻŽāĻŋāϚ (Immich) āĻ“āϝāĻŧ⧇āĻŦ āχāĻ¨ā§āϟāĻžāϰāĻĢ⧇āϏ⧇āϰ āĻ•āĻžāĻ¸ā§āϟāĻŽāĻžāχāĻœā§‡āĻļāύ āĻŽā§āϝāĻžāύ⧇āϜ āĻ•āϰ⧁āύ", + "thumbnail_generation_job": "āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ (Generate Thumbnails)", + "thumbnail_generation_job_description": "āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…ā§āϝāĻžāϏ⧇āĻŸā§‡āϰ āϜāĻ¨ā§āϝ āĻŦ⧜, āϛ⧋āϟ āĻāĻŦāĻ‚ āĻŦā§āϞāĻžāϰ (āĻ…āĻ¸ā§āĻĒāĻˇā§āϟ) āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύ, āϏ⧇āχ āϏāĻžāĻĨ⧇ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻŦā§āϝāĻ•ā§āϤāĻŋāϰ āϜāĻ¨ā§āϝāĻ“ āĻĨāĻžāĻŽā§āĻŦāύ⧇āχāϞ āϤ⧈āϰāĻŋ āĻ•āϰ⧁āύāĨ¤", + "transcoding_acceleration_api": "āĻ…ā§āϝāĻžāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āϟ āĻāĻĒāĻŋāφāχ (Acceleration API)", + "transcoding_acceleration_api_description": "āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄāĻŋāĻ‚ (transcoding) āĻĻā§āϰ⧁āϤ āĻ•āϰāĻžāϰ āϜāĻ¨ā§āϝ āφāĻĒāύāĻžāϰ āĻĄāĻŋāĻ­āĻžāχāϏ⧇āϰ āϏāĻžāĻĨ⧇ āϝ⧇ API āχāĻ¨ā§āϟāĻžāϰāĻ…ā§āϝāĻžāĻ•ā§āϟ āĻ•āϰāĻŦ⧇āĨ¤ āĻāχ āϏ⧇āϟāĻŋāĻ‚āϏāϟāĻŋ 'āϏāĻžāĻ§ā§āϝāĻŽāϤ⧋' (best effort) āĻ•āĻžāϜ āĻ•āϰāĻŦ⧇: āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϞ⧇ āĻāϟāĻŋ āĻĒ⧁āύāϰāĻžā§Ÿ āϏāĻĢāϟāĻ“ā§Ÿā§āϝāĻžāϰ āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄāĻŋāĻ‚ā§Ÿā§‡ āĻĢāĻŋāϰ⧇ āφāϏāĻŦ⧇āĨ¤ āĻšāĻžāĻ°ā§āĻĄāĻ“ā§Ÿā§āϝāĻžāϰ⧇āϰ āĻ“āĻĒāϰ āĻ­āĻŋāĻ¤ā§āϤāĻŋ āĻ•āϰ⧇ VP9 āĻ•āĻžāϜ āĻ•āϰāϤ⧇āĻ“ āĻĒāĻžāϰ⧇, āφāĻŦāĻžāϰ āύāĻžāĻ“ āĻ•āϰāϤ⧇ āĻĒāĻžāϰ⧇āĨ¤", + "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU āĻĒā§āĻ°ā§Ÿā§‹āϜāύ)", + "transcoding_acceleration_qsv": "Quick Sync (ā§­āĻŽ āĻĒā§āϰāϜāĻ¨ā§āĻŽā§‡āϰ āχāύāĻŸā§‡āϞ CPU āĻŦāĻž āĻĒāϰāĻŦāĻ°ā§āϤ⧀ āĻ­āĻžāĻ°ā§āϏāύ āĻĒā§āĻ°ā§Ÿā§‹āϜāύ)", + "transcoding_acceleration_rkmpp": "RKMPP (āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ Rockchip SOC-āĻāϰ āϜāĻ¨ā§āϝ)", + "transcoding_acceleration_vaapi": "VA-API (āĻ­āĻŋāĻĄāĻŋāĻ“ āĻ…ā§āϝāĻžāĻ•ā§āϏāĻŋāϞāĻžāϰ⧇āĻļāύ āĻāĻĒāĻŋāφāχ)", + "transcoding_accepted_audio_codecs": "āĻ—ā§āϰāĻšāĻŖāϝ⧋āĻ—ā§āϝ āĻ…āĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϏāĻŽā§‚āĻš (Accepted audio codecs)", + "transcoding_accepted_audio_codecs_description": "āϕ⧋āύ āĻ…āĻĄāĻŋāĻ“ āϕ⧋āĻĄā§‡āĻ•āϗ⧁āϞ⧋ āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄ āĻ•āϰāĻžāϰ āĻĒā§āĻ°ā§Ÿā§‹āϜāύ āύ⧇āχ āϤāĻž āύāĻŋāĻ°ā§āĻŦāĻžāϚāύ āĻ•āϰ⧁āύāĨ¤ āĻāϟāĻŋ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āύāĻŋāĻ°ā§āĻĻāĻŋāĻˇā§āϟ āĻŸā§āϰāĻžāύāϏāϕ⧋āĻĄ āĻĒāϞāĻŋāϏāĻŋāϰ (transcode policies) āϜāĻ¨ā§āϝ āĻŦā§āϝāĻŦāĻšā§ƒāϤ āĻšā§ŸāĨ¤", + "transcoding_accepted_containers": "āĻ—ā§āϰāĻšāĻŖāϝ⧋āĻ—ā§āϝ āĻ•āĻ¨ā§āĻŸā§‡āχāύāĻžāϰāϏāĻŽā§‚āĻš (Accepted containers)" + }, + "yes": "āĻšā§āϝāĻžāρ", + "you_dont_have_any_shared_links": "āφāĻĒāύāĻžāϰ āϕ⧋āύ⧋ āĻļā§‡ā§ŸāĻžāϰ āĻ•āϰāĻž āϞāĻŋāĻ™ā§āĻ• āύ⧇āχ (You don't have any shared links)", + "your_wifi_name": "āφāĻĒāύāĻžāϰ āĻ“āϝāĻŧāĻžāχ-āĻĢāĻžāχ āĻāϰ āύāĻžāĻŽ (Your Wi-Fi name)", + "zero_to_clear_rating": "āĻ…ā§āϝāĻžāϏ⧇āϟ āϰ⧇āϟāĻŋāĻ‚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ ā§Ļ āϚāĻžāĻĒ⧁āύ", + "zoom_image": "āĻ›āĻŦāĻŋ āϜ⧁āĻŽ āĻ•āϰ⧁āύ (Zoom Image)", + "zoom_to_bounds": "āĻŦāĻžāωāĻ¨ā§āĻĄāϏ āĻ…āύ⧁āϝāĻžā§Ÿā§€ āϜ⧁āĻŽ āĻ•āϰ⧁āύ (Zoom to bounds)" } diff --git a/i18n/ca.json b/i18n/ca.json index 89fe1617cd..563d5f15c5 100644 --- a/i18n/ca.json +++ b/i18n/ca.json @@ -5,6 +5,7 @@ "acknowledge": "Base de coneixement", "action": "AcciÃŗ", "action_common_update": "Actualitzar", + "action_description": "Un conjunt d'accions a realitzar sobre els recursos filtrats", "actions": "Accions", "active": "Actiu", "active_count": "Activat: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Afegiu una ubicaciÃŗ", "add_a_name": "Afegir un nom", "add_a_title": "Afegir un títol", + "add_action": "Afegir acciÃŗ", + "add_action_description": "Feu clic per afegir una acciÃŗ a realitzar", + "add_assets": "Afegir recursos", "add_birthday": "Afegeix la data de naixement", "add_endpoint": "afegir endpoint", "add_exclusion_pattern": "Afegir un patrÃŗ d'exclusiÃŗ", + "add_filter": "Afegir filtre", + "add_filter_description": "Feu clic per afegir una condiciÃŗ de filtre", "add_location": "Afegir la ubicaciÃŗ", "add_more_users": "Afegir mÊs usuaris", "add_partner": "Afegir company/a", @@ -36,6 +42,7 @@ "add_to_shared_album": "Afegir a un àlbum compartit", "add_upload_to_stack": "Afegeix la càrrega a la pila", "add_url": "Afegir URL", + "add_workflow_step": "Afegeix un pas del flux de treball", "added_to_archive": "Afegir a l'arxiu", "added_to_favorites": "Afegit als preferits", "added_to_favorites_count": "{count, number} afegits als preferits", @@ -97,6 +104,8 @@ "image_preview_description": "Imatge de mida mitjana amb metadades eliminades, que s'utilitza quan es visualitza un sol recurs i per a l'aprenentatge automàtic", "image_preview_quality_description": "Vista prèvia de la qualitat de l'1 al 100. MÊs alt Ês millor, perÃ˛ produeix fitxers mÊs grans i pot reduir la capacitat de resposta de l'aplicaciÃŗ. Establir un valor baix pot afectar la qualitat de l'aprenentatge automàtic.", "image_preview_title": "Paràmetres de previsualitzaciÃŗ", + "image_progressive": "Progressiu", + "image_progressive_description": "Codifica les imatges JPEG progressivament per a una visualitzaciÃŗ amb càrrega gradual. AixÃ˛ no tÊ cap efecte sobre les imatges WebP.", "image_quality": "Qualitat", "image_resolution": "ResoluciÃŗ", "image_resolution_description": "Les resolucions mÊs altes poden conservar mÊs detalls perÃ˛ triguen mÊs a codificar-se, tenen mides de fitxer mÊs grans i poden reduir la capacitat de resposta de l'aplicaciÃŗ.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Activa la cerca intel¡ligent", "machine_learning_smart_search_enabled_description": "Si està desactivada, les imatges no es codificaran per la cerca intel¡ligent.", "machine_learning_url_description": "L'URL del servidor d'aprenentatge automàtic. Si es proporciona mÊs d'un URL, s'intentarà accedir a cada servidor en ordre fins que un d'ells respongui correctament.", + "maintenance_delete_backup": "Elimina la cÃ˛pia de seguretat", + "maintenance_delete_backup_description": "Aquest fitxer s'eliminarà de forma permanent.", + "maintenance_delete_error": "No s'ha pogut suprimir la cÃ˛pia de seguretat.", + "maintenance_restore_backup": "Restaura la cÃ˛pia de seguretat", + "maintenance_restore_backup_description": "Immich s'esborrarà i es restaurarà des de la cÃ˛pia de seguretat escollida. Es crearà una cÃ˛pia de seguretat abans de continuar.", + "maintenance_restore_backup_different_version": "Aquesta cÃ˛pia de seguretat s'ha creat amb una versiÃŗ diferent d'Immich!", + "maintenance_restore_backup_unknown_version": "No s'ha pogut determinar la versiÃŗ de la cÃ˛pia de seguretat.", + "maintenance_restore_database_backup": "Restaurar la cÃ˛pia de seguretat de la base de dades", + "maintenance_restore_database_backup_description": "Reverteix a un estat anterior de la base de dades mitjançant un fitxer de cÃ˛pia de seguretat", "maintenance_settings": "En manteniment", "maintenance_settings_description": "Posar Immich en mode de manteniment.", - "maintenance_start": "Iniciar el mode de manteniment", + "maintenance_start": "Canviar al mode de manteniment", "maintenance_start_error": "Error en iniciar el mode de manteniment.", + "maintenance_upload_backup": "Puja el fitxer de cÃ˛pia de seguretat de la base de dades", + "maintenance_upload_backup_error": "No s'ha pogut carregar la cÃ˛pia de seguretat, Ês un fitxer .sql/.sql.gz?", "manage_concurrency": "Gestiona la concurrència", "manage_concurrency_description": "Ves a la pàgina de tasques per gestionar la concurrència de tasques", "manage_log_settings": "Gestiona la configuraciÃŗ del registre", @@ -252,7 +272,7 @@ "oauth_auto_register": "Registre automàtic", "oauth_auto_register_description": "Registra nous usuaris automàticament desprÊs d'iniciar sessiÃŗ amb OAuth", "oauth_button_text": "Text del botÃŗ", - "oauth_client_secret_description": "Requerit si PKCE (Proof Key for Code Exchange) no està suportat pel proveïdor OAuth", + "oauth_client_secret_description": "Requerit per clients confidencials, o si PKCE (Proof Key for Code Exchange) no està suportat pel client pÃēblic.", "oauth_enable_description": "Iniciar sessiÃŗ amb OAuth", "oauth_mobile_redirect_uri": "URI de redirecciÃŗ mÃ˛bil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecciÃŗ mÃ˛bil", @@ -267,7 +287,7 @@ "oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada", "oauth_storage_quota_claim_description": "Estableix automàticament la quota d'emmagatzematge de l'usuari al valor d'aquest paràmetre.", "oauth_storage_quota_default": "Quota d'emmagatzematge predeterminada (GiB)", - "oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il¡limitada).", + "oauth_storage_quota_default_description": "Quota en GiB que s'utilitzarà quan no es proporcioni cap valor específic.", "oauth_timeout": "Solicitud caducada", "oauth_timeout_description": "Timeout per a sol¡licituds en mil¡lisegons", "ocr_job_description": "Fes servir machine learning per reconèixer text a les imatges", @@ -291,7 +311,7 @@ "search_jobs": "Cercar treballsâ€Ļ", "send_welcome_email": "Enviar correu electrÃ˛nic de benvinguda", "server_external_domain_settings": "Domini extern", - "server_external_domain_settings_description": "Domini per enllaços pÃēblics compartits, incloent http(s)://", + "server_external_domain_settings_description": "Domini utilitzat per a enllaços externs", "server_public_users": "Usuaris pÃēblics", "server_public_users_description": "Tots els usuaris (nom i correu electrÃ˛nic) apareixen a la llista a l'afegir un usuari als àlbums compartits. Si es desactiva, la llista nomÊs serà disponible pels usuaris administradors.", "server_settings": "ConfiguraciÃŗ del servidor", @@ -431,6 +451,9 @@ "admin_password": "Contrasenya de l'administrador", "administration": "AdministraciÃŗ", "advanced": "Avançat", + "advanced_settings_clear_image_cache": "Esborra la memÃ˛ria cau de les imatges", + "advanced_settings_clear_image_cache_error": "No s'ha pogut esborrar la memÃ˛ria cau de les imatges", + "advanced_settings_clear_image_cache_success": "S'ha esborrat correctament {size}", "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}", @@ -467,10 +490,12 @@ "album_remove_user": "Eliminar l'usuari?", "album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?", "album_search_not_found": "No s'ha trobat cap àlbum que coincideixi amb la teva cerca", + "album_selected": "Àlbum seleccionat", "album_share_no_users": "Sembla que has compartit aquest àlbum amb tots els usuaris o no tens cap usuari amb qui compartir-ho.", "album_summary": "Resum de l'àlbum", "album_updated": "Àlbum actualitzat", "album_updated_setting_description": "Rep una notificaciÃŗ per correu electrÃ˛nic quan un àlbum compartit tingui recursos nous", + "album_upload_assets": "Carrega recursos des del teu ordinador i afegeix-los a l'àlbum", "album_user_left": "Surt de {album}", "album_user_removed": "{user} eliminat", "album_viewer_appbar_delete_confirm": "Confirmes que vols suprimir aquest àlbum del teu compte?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Ordre de classificaciÃŗ inicial dels recursos al crear àlbums nous.", "albums_feature_description": "Col¡leccions d'actius que es poden compartir amb altres usuaris.", "albums_on_device_count": "Àlbums al dispositiu ({count})", + "albums_selected": "{count, plural, one {# àlbum seleccionat} other {# àlbums seleccionats}}", "all": "Tots", "all_albums": "Tots els àlbum", "all_people": "Tota la gent", + "all_photos": "Totes les fotografies", "all_videos": "Tots els vídeos", "allow_dark_mode": "Permet el tema fosc", "allow_edits": "Permet editar", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permet que l'usuari pÃēblic pugui carregar", "allowed": "Permès", "alt_text_qr_code": "Codi QR", + "always_keep": "Mantenir sempre", + "always_keep_photos_hint": "Allibera espai mantÊ totes les fotos en aquest dispositiu.", + "always_keep_videos_hint": "Allibera espai mantÊ tots els vídeos en aquest dispositiu.", "anti_clockwise": "En sentit antihorari", "api_key": "Clau API", "api_key_description": "Aquest valor nomÊs es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.", @@ -507,7 +537,7 @@ "app_bar_signout_dialog_content": "Estàs segur que vols tancar la sessiÃŗ?", "app_bar_signout_dialog_ok": "Sí", "app_bar_signout_dialog_title": "Tanca la sessiÃŗ", - "app_download_links": "App descarrega enllaços", + "app_download_links": "Enllaços de descàrrega de l'App", "app_settings": "ConfiguraciÃŗ de l'app", "app_stores": "Botiga App", "app_update_available": "ActualitzaciÃŗ App disponible", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {Arxivat #} other {Arxivats #}}", "are_these_the_same_person": "SÃŗn la mateixa persona?", "are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?", + "array_field_not_fully_supported": "Els camps de matriu requereixen ediciÃŗ JSON manual", "asset_action_delete_err_read_only": "No es poden esborrar el fitxer(s) de nomÊs lectura, ometent", "asset_action_share_err_offline": "No s'ha pogut obtenir el fitxer(s) sense connexiÃŗ, ometent", "asset_added_to_album": "Afegit a l'àlbum", "asset_adding_to_album": "Afegint a l'àlbumâ€Ļ", + "asset_created": "Recurs creat", "asset_description_updated": "La descripciÃŗ del recurs s'ha actualitzat", "asset_filename_is_offline": "L'element {filename} està fora de línia", "asset_has_unassigned_faces": "L'element tÊ cares no assignades", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Disseny", "asset_list_settings_subtitle": "ConfiguraciÃŗ del disseny de la graella de fotos", "asset_list_settings_title": "Graella de fotos", + "asset_not_found_on_device_android": "No s'ha trobat l'actiu al dispositiu", + "asset_not_found_on_device_ios": "No s'ha trobat l'element al dispositiu. Si utilitzes l'iCloud, pot ser que no s'hi pugui accedir perquè el fitxer guardat a l'iCloud Ês corrupte", + "asset_not_found_on_icloud": "No s'ha trobat l'element a l'iCloud. Pot ser que no s'hi pugui accedir perquè el fitxer guardat a l'iCloud Ês corrupte", "asset_offline": "Element fora de línia", "asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.", "asset_restored_successfully": "Element recuperat correctament", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Selecciona àlbums", "backup_album_selection_page_selection_info": "InformaciÃŗ de la selecciÃŗ", "backup_album_selection_page_total_assets": "Total d'elements Ãēnics", - "backup_albums_sync": "SincronitzaciÃŗ d'àlbums de cÃ˛pia de seguretat", + "backup_albums_sync": "SincronitzaciÃŗ de la CÃ˛pia de Seguretat d'Àlbums", "backup_all": "Tots", "backup_background_service_backup_failed_message": "No s'ha pogut copiar els elements. Tornant a intentarâ€Ļ", "backup_background_service_complete_notification": "Backup completat d'actius", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Les contrasenyes no coincideixen", "change_password_form_reenter_new_password": "Torna a introduir la nova contrasenya", "change_pin_code": "Canviar el codi PIN", + "change_trigger": "Canvia el desencadenant", + "change_trigger_prompt": "Esteu segur que voleu canviar el disparador? AixÃ˛ eliminarà totes les accions i filtres existents.", "change_your_password": "Canvia la teva contrasenya", "changed_visibility_successfully": "Visibilitat canviada amb èxit", "charging": "Carregant", @@ -722,6 +759,18 @@ "checksum": "Suma de control", "choose_matching_people_to_merge": "Trieu les persones que coincideixin per combinar-les", "city": "Ciutat", + "cleanup_confirm_description": "Immich ha trobat {count} recursos (creats abans del {date}) carregats adequadament al servidor. Eliminar les cÃ˛pies locals d'aquest dispositiu?", + "cleanup_confirm_prompt_title": "Eliminar d'aquest dispositiu?", + "cleanup_deleted_assets": "S'han mogut {count} recursos a la paperera del dispositiu", + "cleanup_deleting": "Movent a la paperera...", + "cleanup_found_assets": "S'han trobat {count} recursos amb cÃ˛pia", + "cleanup_found_assets_with_size": "S'han trobat {count} elements copiats ({size})", + "cleanup_icloud_shared_albums_excluded": "Els àlbums compartits d'iCloud s'exclouen de la cerca", + "cleanup_no_assets_found": "No s'han trobat recursos que coincideixin amb el criteri de sobre. Allibera Espai nomÊs pot esborrar elements que s'hagin copiat al servidor", + "cleanup_preview_title": "Recursos a eliminar ({count})", + "cleanup_step3_description": "Cerca fotos i vídeos que ja tinguin una cÃ˛pia al servidor amb la data de tall i mantÊ els filtres seleccionats.", + "cleanup_step4_summary": "{count} recursos (creats abans del {date}) esborrats del dispositiu local. Les fotografies estaran disponibles a l'aplicaciÃŗ Immich.", + "cleanup_trash_hint": "Per a reclamar l'espai completament, obre la galeria del dispositiu i buida la paperera", "clear": "Buida", "clear_all": "Neteja-ho tot", "clear_all_recent_searches": "Esborra totes les cerques recents", @@ -733,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "S'ha importat el certificat del client", "client_cert_invalid_msg": "Fitxer de certificat no vàlid o contrasenya incorrecta", + "client_cert_password_message": "Introdueix la contrasenya per a aquest certificat", + "client_cert_password_title": "Contrasenya del certificat", "client_cert_remove_msg": "S'ha eliminat el certificat del client", "client_cert_subtitle": "NomÊs admet el format PKCS12 (.p12, .pfx). La importaciÃŗ/eliminaciÃŗ de certificats nomÊs està disponible abans d'iniciar sessiÃŗ", "client_cert_title": "Certificat de client SSL", @@ -743,6 +794,11 @@ "color": "Color", "color_theme": "Tema de color", "command": "Ordre", + "command_palette_prompt": "Trobar ràpidament pàgines, accions o comandes", + "command_palette_to_close": "per a tancar", + "command_palette_to_navigate": "per a introduir", + "command_palette_to_select": "per a seleccionar", + "command_palette_to_show_all": "per a mostrar-ho tot", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", "comments_and_likes": "Comentaris i agradaments", @@ -787,6 +843,7 @@ "create_album": "Crear un àlbum", "create_album_page_untitled": "Sense títol", "create_api_key": "Crear clau API", + "create_first_workflow": "Crea el primer flux de treball", "create_library": "Crea una llibreria", "create_link": "Crear enllaç", "create_link_to_share": "Crear enllaç per compartir", @@ -801,17 +858,25 @@ "create_tag": "Crear etiqueta", "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_user": "Crea un usuari", + "create_workflow": "Crea un flux de treball", "created": "Creat", "created_at": "Creat", "creating_linked_albums": "Creant àlbums enllaçats...", "crop": "Retalla", + "crop_aspect_ratio_fixed": "Fixat", + "crop_aspect_ratio_free": "Lliure", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Coses", "current_device": "Dispositiu actual", "current_pin_code": "Codi PIN actual", "current_server_address": "Adreça actual del servidor", + "custom_date": "Data personalitzada", "custom_locale": "LocalitzaciÃŗ personalitzada", "custom_locale_description": "Format de dates i nÃēmeros segons la llengua i regiÃŗ", "custom_url": "URL personalitzada", + "cutoff_date_description": "MantÊ fotos des de l'Ãēltimâ€Ļ", + "cutoff_day": "{count, plural, one {dia} other {dies}}", + "cutoff_year": "{count, plural, one {any} other {anys}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Fosc", @@ -867,6 +932,7 @@ "deselect_all": "Deseleccionar Tots", "details": "Detalls", "direction": "DirecciÃŗ", + "disable": "Desactiva", "disabled": "Desactivat", "disallow_edits": "No permetre les edicions", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Vídeos incrustats", "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat", "download_notfound": "No s'ha trobat la descàrrega", + "download_original": "Descarregar original", "download_paused": "Descàrrega pausada", "download_settings": "Descarregar", "download_settings_description": "Gestioneu la configuraciÃŗ relacionada amb la descàrrega de recursos", @@ -901,19 +968,20 @@ "download_waiting_to_retry": "Esperant per tornar-ho a intentar", "downloading": "Baixant", "downloading_asset_filename": "Descarregant l'element {filename}", + "downloading_from_icloud": "Descarregant des d'iCloud", "downloading_media": "Descàrrega multimèdia", - "drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per carregar-los", + "drop_files_to_upload": "Deixeu els fitxers a qualsevol lloc per pujar-los", "duplicates": "Duplicats", - "duplicates_description": "Resol cada grup indicant quins, si n'hi ha, sÃŗn duplicats", - "duration": "DuraciÃŗ", + "duplicates_description": "Resol cada grup indicant, si n'hi ha, quins sÃŗn duplicats", + "duration": "Durada", "edit": "Editar", "edit_album": "Edita l'àlbum", "edit_avatar": "Edita l'avatar", - "edit_birthday": "Editar aniversari", + "edit_birthday": "Edita l'aniversari", "edit_date": "Edita la data", - "edit_date_and_time": "Edita data i hora", + "edit_date_and_time": "Edita la data i l'hora", "edit_date_and_time_action_prompt": "{count} dates i hores editades", - "edit_date_and_time_by_offset": "Canviar data mitjançant diferència", + "edit_date_and_time_by_offset": "Canvia la data mitjançant diferència", "edit_date_and_time_by_offset_interval": "Nou rang de dates: {from}-{to}", "edit_description": "Edita la descripciÃŗ", "edit_description_prompt": "Si us plau, selecciona una nova descripciÃŗ:", @@ -929,11 +997,22 @@ "edit_tag": "Editar etiqueta", "edit_title": "Edita títol", "edit_user": "Edita l'usuari", + "edit_workflow": "Edita el flux de treball", "editor": "Editor", "editor_close_without_save_prompt": "No es desaran els canvis", "editor_close_without_save_title": "Tancar l'editor?", - "editor_crop_tool_h2_aspect_ratios": "RelaciÃŗ d'aspecte", - "editor_crop_tool_h2_rotation": "RotaciÃŗ", + "editor_confirm_reset_all_changes": "Segur que vols reiniciar tots els canvis?", + "editor_discard_edits_confirm": "Descarta les modificacions", + "editor_discard_edits_prompt": "Tens modificacions sense desar. Estàs segur que les vols descartar?", + "editor_discard_edits_title": "Vols descartar les modificacions?", + "editor_edits_applied_error": "No s'han pogut aplicar les modificacions", + "editor_edits_applied_success": "Les modificacions s'han aplicat correctament", + "editor_flip_horizontal": "Capgira horitzontalment", + "editor_flip_vertical": "Capgira verticalment", + "editor_orientation": "OrientaciÃŗ", + "editor_reset_all_changes": "Reiniciar canvis", + "editor_rotate_left": "Rota 90Âē al contrari de les agulles", + "editor_rotate_right": "Rota 90Âē en el sentit de les agulles", "email": "Correu electrÃ˛nic", "email_notifications": "Correu electrÃ˛nic de notificacions", "empty_folder": "Aquesta carpeta Ês buida", @@ -952,11 +1031,14 @@ "error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenaciÃŗ dels àlbums", "error_delete_face": "Error esborrant cara de les cares reconegudes", "error_getting_places": "S'ha produït un error en obtenir els llocs", + "error_loading_albums": "Error en carregar àlbums", "error_loading_image": "Error carregant la imatge", "error_loading_partners": "No s'han pogut carregar les parelles: {error}", + "error_retrieving_asset_information": "Error en recuperar la informaciÃŗ de l'actiu", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error a l'etiquetar la cara - no s'han pogut obtenir les coordenades de l'àrea", "error_title": "Error - Quelcom ha anat malament", + "error_while_navigating": "Error en navegar fins a l'actiu", "errors": { "cannot_navigate_next_asset": "No es pot navegar a l'element segÃŧent", "cannot_navigate_previous_asset": "No es pot navegar a l'element anterior", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "No es pot completar l'inici de sessiÃŗ OAuth", "unable_to_connect": "No pot connectar", "unable_to_copy_to_clipboard": "No es pot copiar al porta-retalls, assegureu-vos que esteu accedint a la pàgina mitjançant https", + "unable_to_create": "No s'ha pogut crear el flux de treball", "unable_to_create_admin_account": "No es pot crear un compte d'administrador", "unable_to_create_api_key": "No es pot crear una clau d'API nova", "unable_to_create_library": "No es pot crear la llibreria", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "No es pot suprimir el patrÃŗ d'exclusiÃŗ", "unable_to_delete_shared_link": "No es pot suprimir l'enllaç compartit", "unable_to_delete_user": "No es pot eliminar l'usuari", + "unable_to_delete_workflow": "No es pot suprimir el flux de treball", "unable_to_download_files": "No es poden descarregar fitxers", "unable_to_edit_exclusion_pattern": "No es pot editar el patrÃŗ d'exclusiÃŗ", "unable_to_empty_trash": "No es pot buidar la paperera", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "No es pot escanejar la biblioteca", "unable_to_set_feature_photo": "No s'ha pogut configurar la foto destacada", "unable_to_set_profile_picture": "No es pot configurar la foto de perfil", + "unable_to_set_rating": "No s'ha pogut establir la valoraciÃŗ", "unable_to_submit_job": "No es pot enviar la tasca", "unable_to_trash_asset": "No es pot eliminar el recurs a la paperera", "unable_to_unlink_account": "No es pot desenllaçar el compte", @@ -1074,10 +1159,12 @@ "unable_to_update_settings": "No es pot actualitzar la configuraciÃŗ", "unable_to_update_timeline_display_status": "No es pot actualitzar l'estat de visualitzaciÃŗ de la cronologia", "unable_to_update_user": "No es pot actualitzar l'usuari", + "unable_to_update_workflow": "No es pot actualitzar el flux de treball", "unable_to_upload_file": "No es pot carregar el fitxer" }, + "errors_text": "Errors", "exclusion_pattern": "PatrÃŗ d'exclusiÃŗ", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "Afegeix descripciÃŗ...", "exif_bottom_sheet_description_error": "No s'ha pogut actualitzar la descripciÃŗ", "exif_bottom_sheet_details": "DETALLS", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONES", "exif_bottom_sheet_person_add_person": "Afegir nom", "exit_slideshow": "Surt de la presentaciÃŗ de diapositives", + "expand": "Ampliar-ho", "expand_all": "Ampliar-ho tot", "experimental_settings_new_asset_list_subtitle": "Treball en curs", "experimental_settings_new_asset_list_title": "Habilita la graella de fotos experimental", @@ -1120,14 +1208,17 @@ "features": "Característiques", "features_in_development": "Funcions en desenvolupament", "features_setting_description": "Administrar les funcions de l'aplicaciÃŗ", - "file_name": "Nom de l'arxiu", "file_name_or_extension": "Nom de l'arxiu o extensiÃŗ", + "file_name_text": "Nom del fitxer", + "file_name_with_value": "Nom del fitxer: {file_name}", "file_size": "Mida del fitxer", "filename": "Nom del fitxer", "filetype": "Tipus d'arxiu", "filter": "Filtrar", + "filter_description": "Condicions per filtrar els actius de destinaciÃŗ", "filter_people": "Filtra persones", "filter_places": "Filtrar per llocs", + "filters": "Filtres", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "first": "Primer", "fix_incorrect_match": "Corregiu la coincidència incorrecta", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "forgot_pin_code_question": "Has oblidat el teu PIN?", "forward": "Endavant", + "free_up_space": "Alliberar Espai", + "free_up_space_description": "Mou fotos i videos que ja tinguen cÃ˛pia al servidor a la paperera del teu dispositiu per alliberar espai. Les cÃ˛pies del servidor no es modificaran.", + "free_up_space_settings_subtitle": "Alliberar espai del dispositiu", "full_path": "Ruta completa: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Aquesta funciÃŗ carrega recursos externs de Google per funcionar.", "general": "General", "geolocation_instruction_location": "Fes click en un element amb coordinades GPS per utilitzar la seva ubicaciÃŗ o selecciona una ubicaciÃŗ des del mapa", "get_help": "Aconseguir ajuda", + "get_people_error": "S'ha produït un error en aconseguir persones", "get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi", "getting_started": "Començant", "go_back": "Torna", @@ -1175,6 +1270,7 @@ "hide_named_person": "Amaga la persona {name}", "hide_password": "Amaga la contrasenya", "hide_person": "Amaga la persona", + "hide_schema": "Amaga l'esquema", "hide_text_recognition": "Oculta el reconeixement de text", "hide_unnamed_people": "Amaga persones sense nom", "home_page_add_to_album_conflicts": "S'han afegit {added} elements a l'àlbum {album}. {failed} elements ja existeixen a l'àlbum.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "El processament s'ha executat {dateTime}", "items_count": "{count, plural, one {# element} other {# elements}}", "jobs": "Tasques", + "json_editor": "Editor JSON", + "json_error": "Error en el JSON", "keep": "Mantenir", + "keep_albums": "Conserva els àlbums", + "keep_albums_count": "Conservant {count} {count, plural, one {àlbum} other {àlbums}}", "keep_all": "Mantenir-ho tot", + "keep_description": "Tria què es conserva al dispositiu quan s'allibera espai.", + "keep_favorites": "Mantindre els preferits", + "keep_on_device": "MantÊn al dispositiu", + "keep_on_device_hint": "Selecciona els elements que vulguis conservar en aquest dispositiu", "keep_this_delete_others": "Conserveu-ho, suprimiu-ne els altres", + "keeping": "Mantenint: {items}", "kept_this_deleted_others": "S'ha conservat aquest element i s'han suprimit {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Dreceres de teclat", "language": "Idioma", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Habilita la reproducciÃŗ en bucle del vídeo en els detalls.", "main_branch_warning": "Esteu utilitzant una versiÃŗ en desenvolupament; Recomanem fer servir una versiÃŗ publicada!", "main_menu": "MenÃē principal", + "maintenance_action_restore": "Restaurant la base de dades", "maintenance_description": "Immich ha estat posat en mode de manteniment.", "maintenance_end": "Finalitzar el mode de manteniment", "maintenance_end_error": "Error al finalitzar el mode de manteniment.", "maintenance_logged_in_as": "Actualment la sessiÃŗ esta iniciada per {user}", + "maintenance_restore_from_backup": "Restaurar des d'una cÃ˛pia de seguretat", + "maintenance_restore_library": "Restaura la teva biblioteca", + "maintenance_restore_library_confirm": "Si aixÃ˛ sembla correcte, continua restaurant una cÃ˛pia de seguretat!", + "maintenance_restore_library_description": "Restaurant la cÃ˛pia de seguretat", + "maintenance_restore_library_folder_has_files": "{folder} contÊ {count} carpeta/es", + "maintenance_restore_library_folder_no_files": "A {folder} li falten fitxers!", + "maintenance_restore_library_folder_pass": "llegible i escrivible", + "maintenance_restore_library_folder_read_fail": "no llegible", + "maintenance_restore_library_folder_write_fail": "no escrivible", + "maintenance_restore_library_hint_missing_files": "Potser et falten fitxers importants", + "maintenance_restore_library_hint_regenerate_later": "Pots regenerar-los mÊs tard a la configuraciÃŗ", + "maintenance_restore_library_hint_storage_template_missing_files": "Fas servir una plantilla d'emmagatzematge? Potser et falten fitxers", + "maintenance_restore_library_loading": "S'estan carregant les comprovacions d'integritat i heurístiquesâ€Ļ", + "maintenance_task_backup": "Creant una cÃ˛pia de seguretat de la base de dades existentâ€Ļ", + "maintenance_task_migrations": "Executant migracions de bases de dadesâ€Ļ", + "maintenance_task_restore": "Restaurant la cÃ˛pia de seguretat escollidaâ€Ļ", + "maintenance_task_rollback": "La restauraciÃŗ ha fallat, s'està tornant al punt de restauraciÃŗâ€Ļ", "maintenance_title": "Temporalment inaccessible", "make": "Fabricant", "manage_geolocation": "Gestioneu la vostra ubicaciÃŗ", @@ -1408,19 +1531,24 @@ "minimize": "Minimitza", "minute": "Minut", "minutes": "Minuts", + "mirror_horizontal": "Horitzontal", + "mirror_vertical": "Vertical", "missing": "Restants", "mobile_app": "AplicaciÃŗ mÃ˛bil", "mobile_app_download_onboarding_note": "Descarregar la App de mÃ˛bil fent servir les seguents opcions", "model": "Model", "month": "Mes", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "MMMM a", "more": "MÊs", "move": "Moure", + "move_down": "Moure cap avall", "move_off_locked_folder": "Moure fora de la carpeta bloquejada", "move_to": "Moure a", + "move_to_device_trash": "Mou a la paperera del dispositiu", "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", + "move_up": "Puja", "moved_to_archive": "S'han mogut {count, plural, one {# asset} other {# assets}} a l'arxiu", "moved_to_library": "S'ha mogut {count, plural, one {# asset} other {# assets}} a la llibreria", "moved_to_trash": "S'ha mogut a la paperera", @@ -1430,6 +1558,7 @@ "my_albums": "Els meus àlbums", "name": "Nom", "name_or_nickname": "Nom o sobrenom", + "name_required": "El nom Ês obligatori", "navigate": "Navegar", "navigate_to_time": "Navegar a un punt en el temps", "network_requirement_photos_upload": "Fes servir dades mÃ˛bils per a cÃ˛pies de seguretat de fotos", @@ -1454,20 +1583,24 @@ "next": "SegÃŧent", "next_memory": "SegÃŧent record", "no": "No", + "no_actions_added": "Encara no s'han afegit accions", + "no_albums_found": "No s'han trobat àlbums", "no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos", "no_albums_with_name_yet": "Sembla que encara no tens cap àlbum amb aquest nom.", "no_albums_yet": "Sembla que encara no tens cap àlbum.", "no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos", - "no_assets_message": "FEU CLIC PER PUJAR LA VOSTRA PRIMERA FOTO", + "no_assets_message": "Fes clic per pujar la teva primera foto", "no_assets_to_show": "No hi ha elements per mostrar", "no_cast_devices_found": "No s'han trobat dispositius per transmetre", "no_checksum_local": "Cap checksum disponible - no s'han pogut carregar els recursos locals", "no_checksum_remote": "Cap checksum disponible - no s'ha pogut obtenir el recurs remot", + "no_configuration_needed": "No cal configuraciÃŗ", "no_devices": "No hi ha dispositius autoritzats", "no_duplicates_found": "No s'han trobat duplicats.", "no_exif_info_available": "No hi ha informaciÃŗ d'exif disponible", "no_explore_results_message": "Penja mÊs fotos per explorar la teva col¡lecciÃŗ.", "no_favorites_message": "Afegiu preferits per trobar les millors fotos i vídeos a l'instant", + "no_filters_added": "Encara no s'han afegit filtres", "no_libraries_message": "Creeu una llibreria externa per veure les vostres fotos i vídeos", "no_local_assets_found": "No s'ha trobat cap recurs local amb aquest checksum", "no_location_set": "No s'ha definit cap ubicaciÃŗ", @@ -1481,11 +1614,11 @@ "no_results_description": "Proveu un sinÃ˛nim o una paraula clau mÊs general", "no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa", "no_uploads_in_progress": "Cap pujada en progrÊs", + "none": "Cap", "not_allowed": "No permès", "not_available": "N/A", "not_in_any_album": "En cap àlbum", "not_selected": "No seleccionat", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: per aplicar l'etiqueta d'emmagatzematge als actius penjats anteriorment, executeu el", "notes": "Notes", "nothing_here_yet": "No hi ha res encara", "notification_permission_dialog_content": "Per activar les notificacions, aneu a ConfiguraciÃŗ i seleccioneu permet.", @@ -1515,6 +1648,7 @@ "online": "En línia", "only_favorites": "NomÊs preferits", "open": "Obrir", + "open_calendar": "Obrir el calendari", "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", @@ -1563,6 +1697,7 @@ "people": "Persones", "people_edits_count": "{count, plural, one {# persona editada} other {# persones editades}}", "people_feature_description": "Explorar fotos i vídeos agrupades per persona", + "people_selected": "{count, plural, one {# persona seleccionada} other {# persones seleccionades}}", "people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral", "permanent_deletion_warning": "Avís d'eliminaciÃŗ permanent", "permanent_deletion_warning_setting_description": "Mostrar un avís quan s'eliminin els elements permanentment", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# anys}} d'antiguitat", "person_birthdate": "Nascut a {date}", "person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}", + "person_recognized": "Persona reconeguda", + "person_selected": "Persona seleccionada", "photo_shared_all_users": "Sembla que has compartit les teves fotos amb tots els usuaris o no tens cap usuari amb qui compartir-les.", "photos": "Fotos", "photos_and_videos": "Fotos i vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos d'anys anteriors", + "photos_only": "NomÊs fotos", "pick_a_location": "Triar una ubicaciÃŗ", "pick_custom_range": "Rang personalitzat", "pick_date_range": "Seleccioni un rang de dates", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", "query_asset_id": "Consulta d'identificaciÃŗ d'actius", "queue_status": "En cua {count}/{total}", + "rate_asset": "Valorar Recurs", "rating": "ValoraciÃŗ", "rating_clear": "Esborrar valoraciÃŗ", "rating_count": "{count, plural, one {# estrella} other {# estrelles}}", "rating_description": "Mostrar la valoraciÃŗ EXIF al panell d'informaciÃŗ", + "rating_set": "ValoraciÃŗ establerta a {rating, plural, one {# estrella} other {# estrelles}}", "reaction_options": "Opcions de reacciÃŗ", "read_changelog": "Llegeix el registre de canvis", "readonly_mode_disabled": "Mode de nomÊs lectura desactivat", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova", "reassing_hint": "Assignar els elements seleccionats a una persona existent", "recent": "Recent", - "recent-albums": "Àlbums recents", + "recent_albums": "Àlbums recents", "recent_searches": "Cerques recents", "recently_added": "Afegit recentment", "recently_added_page_title": "Afegit recentment", @@ -1770,9 +1910,11 @@ "saved_settings": "ConfiguraciÃŗ guardada", "say_something": "Digues quelcom", "scaffold_body_error_occurred": "S'ha produït un error", + "scan": "Escaneja", "scan_all_libraries": "Escanejar totes les llibreries", "scan_library": "Escaneja", "scan_settings": "ConfiguraciÃŗ d'escaneig", + "scanning": "Escanejant", "scanning_for_album": "S'està buscant l'àlbum...", "search": "Cerca", "search_albums": "Buscar àlbums", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Selecciona tipus de multimèdia", "search_filter_ocr": "Buscar per OCR", "search_filter_people_title": "Selecciona persones", + "search_filter_star_rating": "ClassificaciÃŗ per estrelles", "search_for": "Cercar", "search_for_existing_person": "Busca una persona existent", "search_no_more_result": "No mÊs resultats", @@ -1836,17 +1979,23 @@ "second": "Segon", "see_all_people": "Veure totes les persones", "select": "Selecciona", + "select_album": "Seleccionar àlbum", "select_album_cover": "Seleccionar la portada de l'àlbum", + "select_albums": "Seleccionar àlbums", "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_count": "{count, plural, one {Selecciona #} other {Selecciona #}}", + "select_cutoff_date": "Seleccionar data de tall", "select_face": "Selecciona cara", "select_featured_photo": "Selecciona foto principal", "select_from_computer": "Seleccionar des de l'ordinador", "select_keep_all": "MantÊn tota la selecciÃŗ", "select_library_owner": "Selecciona el propietari de la bilbioteca", "select_new_face": "Selecciona nova cara", + "select_people": "Seleccionar persones", + "select_person": "Seleccionar persona", "select_person_to_tag": "Selecciona una persona per etiquetar", "select_photos": "Tria fotografies", "select_trash_all": "Envia la selecciÃŗ a la paperera", @@ -1982,6 +2131,7 @@ "show_password": "Mostra contrasenya", "show_person_options": "Mostra opcions de la persona", "show_progress_bar": "Mostra barra de progrÊs", + "show_schema": "Mostrar esquema", "show_search_options": "Mostra opcions de cerca", "show_shared_links": "Mostra els enllaços compartits", "show_slideshow_transition": "Mostra la transiciÃŗ de la presentaciÃŗ de diapositives", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Anar a carpetes", "skip_to_tags": "Anar a etiquetes", "slideshow": "Diapositives", + "slideshow_repeat": "Repeteix la presentaciÃŗ de diapositives", + "slideshow_repeat_description": "Torna al principi quan acaba la presentaciÃŗ de diapositives", "slideshow_settings": "ConfiguraciÃŗ de diapositives", "sort_albums_by": "Ordena àlbums per...", "sort_created": "Data de creaciÃŗ", @@ -2038,6 +2190,7 @@ "support": "Suport", "support_and_feedback": "Suport i comentaris", "support_third_party_description": "La vostra instal¡laciÃŗ immich la va empaquetar un tercer. Els problemes que experimenteu poden ser causats per aquest paquet així que, si us plau, plantegeu els poblemes amb ells en primer lloc mitjançant els enllaços segÃŧents.", + "supporter": "Contribuïdor", "swap_merge_direction": "Canvia la direcciÃŗ d'uniÃŗ", "sync": "Sincronitza", "sync_albums": "Sincronitzar àlbums", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Trieu la configuraciÃŗ del tema de l'aplicaciÃŗ", "theme_setting_three_stage_loading_subtitle": "La càrrega en tres etapes podria augmentar el rendiment de càrrega, perÃ˛ causa un consum de xarxa significativament mÊs alt", "theme_setting_three_stage_loading_title": "Activa la càrrega en tres etapes", + "then": "Aleshores", "they_will_be_merged_together": "Es combinaran", "third_party_resources": "Recursos de tercers", "time": "Temps", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Selecciona elements", "trash_page_title": "Paperera ({count})", "trashed_items_will_be_permanently_deleted_after": "Els elements que s'enviïn a la paperera s'eliminaran permanentment desprÊs de {days, plural, one {# dia} other {# dies}}.", + "trigger": "Disparador", + "trigger_asset_uploaded": "Mitjà Carregat", + "trigger_asset_uploaded_description": "Es dispara quan un nou mitjà es puge al servidor", + "trigger_description": "L'esdeveniment que inicia l'automatitzaciÃŗ", + "trigger_person_recognized": "Persona identificada", + "trigger_person_recognized_description": "Es dispara quan es detecta una persona", + "trigger_type": "Tipus de disparador", "troubleshoot": "SoluciÃŗ de problemes", "type": "Tipus", "unable_to_change_pin_code": "No es pot canviar el codi PIN", @@ -2123,6 +2284,7 @@ "unhide_person": "Mostra persona", "unknown": "Desconegut", "unknown_country": "País Desconegut", + "unknown_date": "Data desconeguda", "unknown_year": "Any desconegut", "unlimited": "Il¡limitat", "unlink_motion_video": "Desvincular vídeo en moviment", @@ -2139,17 +2301,19 @@ "unstack": "Desapila", "unstack_action_prompt": "{count} sense apilar", "unstacked_assets_count": "No apilat {count, plural, one {# recurs} other {# recursos}}", + "unsupported_field_type": "Tipus de camp no suportat", "untagged": "Sense etiqueta", + "untitled_workflow": "AutomatitzaciÃŗ sense títol", "up_next": "PrÃ˛xim", "update_location_action_prompt": "Actualitza la ubicaciÃŗ de {count} elements seleccionats amb:", "updated_at": "Actualitzat", "updated_password": "Contrasenya actualitzada", "upload": "Pujar", - "upload_action_prompt": "{count} a la cua per a pujar", "upload_concurrency": "Concurrència de pujades", "upload_details": "Detalls de la Pujada", "upload_dialog_info": "Vols fer cÃ˛pia de seguretat dels elements seleccionats al servidor?", "upload_dialog_title": "Puja elements", + "upload_error_with_count": "Error en la càrrega de {count, plural, one {# actiu} other {# actius}}", "upload_errors": "Càrrega completada amb {count, plural, one {# error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.", "upload_finished": "Pujada finalitzada", "upload_progress": "Restant {remaining, number} - Processat {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "Ús", "use_biometric": "Empra biometria", - "use_current_connection": "utilitzar la connexiÃŗ actual", + "use_current_connection": "Utilitza la connexiÃŗ actual", "use_custom_date_range": "Fes servir un rang de dates personalitzat", "user": "Usuari", "user_has_been_deleted": "Aquest usuari ha sigut eliminat.", @@ -2185,6 +2349,7 @@ "utilities": "Utilitats", "validate": "Valida", "validate_endpoint_error": "Per favor introdueix un URL vàlid", + "validation_error": "Error de validaciÃŗ", "variables": "Variables", "version": "VersiÃŗ", "version_announcement_closing": "El teu amic Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Reprodueix la miniatura quan el ratolí plana sobre l'element. Fins i tot quan estigui deshabilitat, la reproducciÃŗ s'iniciarà planant sobre el botÃŗ de reproducciÃŗ.", "videos": "Vídeos", "videos_count": "{count, plural, one {# vídeo} other {# vídeos}}", + "videos_only": "NomÊs videos", "view": "Veure", "view_album": "Veure l'àlbum", "view_all": "Veure tot", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "Fes servir com a element principal", "viewer_unstack": "Desapila", "visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}", + "visual": "Visual", + "visual_builder": "Constructor visual", "waiting": "Esperant", "waiting_count": "Esperant: {count}", "warning": "Avís", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Benvingut a immich", "width": "Amplada", "wifi_name": "Nom Wi-Fi", - "workflow": "Flux de treball", + "workflow_delete_prompt": "Segur que vols eliminar aquesta automatitzaciÃŗ?", + "workflow_deleted": "AutomatitzaciÃŗ eliminada", + "workflow_description": "DescripciÃŗ de l'automatitzaciÃŗ", + "workflow_info": "InformaciÃŗ de l'automatitzaciÃŗ", + "workflow_json": "JSON de l'automatitzaciÃŗ", + "workflow_json_help": "Edita la configuraciÃŗ de l'automatitzaciÃŗ en format JSON. Els canvis es sincronitzaran amb el constructor visual.", + "workflow_name": "Nom de l'automatitzaciÃŗ", + "workflow_navigation_prompt": "Segur que vols sortir sense desar els canvis?", + "workflow_summary": "Resum de l'automatitzaciÃŗ", + "workflow_update_success": "AutomatitzaciÃŗ actualitzada amb èxit", + "workflow_updated": "AutomatitzaciÃŗ actualitzada", + "workflows": "Automatitzacions", + "workflows_help_text": "Les automatitzacions realitzen accions automàticament sobre els teus mitjans basant-se en disparadors i filtres", "wrong_pin_code": "Codi PIN incorrecte", "year": "Any", "years_ago": "Fa {years, plural, one {# any} other {# anys}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tens cap enllaç compartit", "your_wifi_name": "Nom del teu Wi-Fi", + "zero_to_clear_rating": "prem 0 per a buidar la valoraciÃŗ", "zoom_image": "Ampliar Imatge", "zoom_to_bounds": "Amplia als límits" } diff --git a/i18n/cs.json b/i18n/cs.json index 2f684d4ac6..77da129f83 100644 --- a/i18n/cs.json +++ b/i18n/cs.json @@ -5,6 +5,7 @@ "acknowledge": "Rozumím", "action": "Akce", "action_common_update": "Aktualizovat", + "action_description": "Sada akcí, kterÊ se mají provÊst na filtrovanÃŊch poloÅžkÃĄch", "actions": "Akce", "active": "Aktivní", "active_count": "Aktivní: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Přidat polohu", "add_a_name": "Přidat jmÊno", "add_a_title": "Přidat nÃĄzev", + "add_action": "Přidat akci", + "add_action_description": "Kliknutím přidejte akci, kterou chcete provÊst", + "add_assets": "Přidat poloÅžky", "add_birthday": "Přidat datum narození", "add_endpoint": "Přidat koncovÃŊ bod", "add_exclusion_pattern": "Přidat vzor vyloučení", + "add_filter": "Přidat filtr", + "add_filter_description": "Kliknutím přidejte podmínku filtru", "add_location": "Přidat polohu", "add_more_users": "Přidat dalÅĄÃ­ uÅživatele", "add_partner": "Přidat partnera", @@ -36,6 +42,7 @@ "add_to_shared_album": "Přidat do sdílenÊho alba", "add_upload_to_stack": "Přidat nahranÊ do zÃĄsobníku", "add_url": "Přidat URL", + "add_workflow_step": "Přidat krok pracovního postupu", "added_to_archive": "PřidÃĄno do archivu", "added_to_favorites": "PřidÃĄno do oblíbenÃŊch", "added_to_favorites_count": "PřidÃĄno {count, number} do oblíbenÃŊch", @@ -97,6 +104,8 @@ "image_preview_description": "Středně velkÃŊ obrÃĄzek se zbavenÃŊmi metadaty, kterÃŊ se pouŞívÃĄ při prohlíŞení jednÊ poloÅžky a pro strojovÊ učení", "image_preview_quality_description": "Kvalita nÃĄhledu od 1 do 100. VyÅĄÅĄÃ­ je lepÅĄÃ­, ale vytvÃĄÅ™Ã­ větÅĄÃ­ soubory a můŞe sníŞit responzivitu aplikace. Nastavení nízkÊ hodnoty můŞe ovlivnit kvalitu strojovÊho učení.", "image_preview_title": "NÃĄhledy", + "image_progressive": "Progresivní", + "image_progressive_description": "KÃŗdujte JPEG obrÃĄzky progresivně pro postupnÊ načítÃĄní zobrazení. Na WebP obrÃĄzky to nemÃĄ ÅžÃĄdnÃŊ vliv.", "image_quality": "Kvalita", "image_resolution": "RozliÅĄení", "image_resolution_description": "VyÅĄÅĄÃ­ rozliÅĄení mohou zachovat více detailů, ale jejich kÃŗdovÃĄní trvÃĄ dÊle, mají větÅĄÃ­ velikost souboru a mohou sníŞit odezvu aplikace.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Povolit chytrÊ vyhledÃĄvÃĄní", "machine_learning_smart_search_enabled_description": "Pokud je vypnuto, obrÃĄzky nebudou kÃŗdovÃĄny pro inteligentní vyhledÃĄvÃĄní.", "machine_learning_url_description": "URL serveru strojovÊho učení. Pokud je zadÃĄno více URL adres, budou jednotlivÊ servery zkouÅĄeny postupně, dokud jeden z nich neodpoví ÃēspÄ›ÅĄně, a to v pořadí od prvního k poslednímu. Servery, kterÊ neodpoví, budou dočasně ignorovÃĄny, dokud nebudou opět online.", + "maintenance_delete_backup": "Smazat zÃĄlohu", + "maintenance_delete_backup_description": "Tento soubor bude trvale smazÃĄn.", + "maintenance_delete_error": "Nepodařilo se smazat zÃĄlohu.", + "maintenance_restore_backup": "Obnovit zÃĄlohu", + "maintenance_restore_backup_description": "Immich bude vymazÃĄn a obnoven z vybranÊ zÃĄlohy. Před pokračovÃĄním bude vytvořena zÃĄloha.", + "maintenance_restore_backup_different_version": "Tato zÃĄloha byla vytvořena pomocí jinÊ verze aplikace Immich!", + "maintenance_restore_backup_unknown_version": "Nelze určit verzi zÃĄlohy.", + "maintenance_restore_database_backup": "Obnovit zÃĄlohu databÃĄze", + "maintenance_restore_database_backup_description": "Obnovení předchozího stavu databÃĄze pomocí zÃĄloÅžního souboru", "maintenance_settings": "ÚdrÅžba", "maintenance_settings_description": "Přepnout Immich do reÅžimu ÃēdrÅžby.", - "maintenance_start": "ZahÃĄjit reÅžim ÃēdrÅžby", + "maintenance_start": "Přepnout do reÅžimu ÃēdrÅžby", "maintenance_start_error": "Nepodařilo se zahÃĄjit reÅžim ÃēdrÅžby.", + "maintenance_upload_backup": "NahrÃĄt zÃĄloÅžní soubor databÃĄze", + "maintenance_upload_backup_error": "Nelze nahrÃĄt zÃĄlohu, jednÃĄ se o soubor .sql/.sql.gz?", "manage_concurrency": "SprÃĄva souběŞnosti", "manage_concurrency_description": "Přejděte na strÃĄnku Ãēloh a spravujte souběŞnost Ãēloh", "manage_log_settings": "SprÃĄva nastavení protokolu", @@ -252,7 +272,7 @@ "oauth_auto_register": "AutomatickÃĄ registrace", "oauth_auto_register_description": "Automaticky registrovat novÊ uÅživatele po přihlÃĄÅĄení pomocí OAuth", "oauth_button_text": "Text tlačítka", - "oauth_client_secret_description": "VyÅžaduje se, pokud poskytovatel OAuth nepodporuje PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "VyÅžadovÃĄno pro důvěrnÊ klienty nebo pokud PKCE (Proof Key for Code Exchange) není podporovÃĄno pro veřejnÊ klienty.", "oauth_enable_description": "PřihlÃĄsit pomocí OAuth", "oauth_mobile_redirect_uri": "Mobilní přesměrovÃĄní URI", "oauth_mobile_redirect_uri_override": "Přepsat mobilní přesměrovÃĄní URI", @@ -291,7 +311,7 @@ "search_jobs": "Hledat Ãēlohyâ€Ļ", "send_welcome_email": "Odeslat uvítací e-mail", "server_external_domain_settings": "Externí domÊna", - "server_external_domain_settings_description": "DomÊna pro veřejně sdílenÊ odkazy, včetně http(s)://", + "server_external_domain_settings_description": "DomÊna pouŞívanÃĄ pro externí odkazy", "server_public_users": "Veřejní uÅživatelÊ", "server_public_users_description": "VÅĄichni uÅživatelÊ (jmÊno a e-mail) jsou uvedeni při přidÃĄvÃĄní uÅživatele do sdílenÃŊch alb. Pokud je tato funkce vypnuta, bude seznam uÅživatelů dostupnÃŊ pouze uÅživatelům z řad sprÃĄvců.", "server_settings": "Server", @@ -431,6 +451,9 @@ "admin_password": "Heslo sprÃĄvce", "administration": "Administrace", "advanced": "PokročilÊ", + "advanced_settings_clear_image_cache": "Vyčistit mezipaměÅĨ obrÃĄzků", + "advanced_settings_clear_image_cache_error": "Chyba při čiÅĄtění mezipaměti obrÃĄzků", + "advanced_settings_clear_image_cache_success": "ÚspÄ›ÅĄně vyčiÅĄtěno {size}", "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}", @@ -467,10 +490,12 @@ "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_selected": "Album vybrÃĄno", "album_share_no_users": "Zřejmě jste toto album sdíleli se vÅĄemi uÅživateli, nebo nemÃĄte ÅžÃĄdnÊho uÅživatele, se kterÃŊm byste ho mohli sdílet.", "album_summary": "Souhrn alba", "album_updated": "Album aktualizovÃĄno", "album_updated_setting_description": "DostÃĄvat e-mailovÃĄ oznÃĄmení o novÃŊch poloÅžkÃĄch sdílenÊho alba", + "album_upload_assets": "Nahrajte soubory z počítače a přidejte je do alba", "album_user_left": "Opustil {album}", "album_user_removed": "UÅživatel {user} odebrÃĄn", "album_viewer_appbar_delete_confirm": "Opravdu chcete toto album odstranit ze svÊho Ãēčtu?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# album vybrÃĄno} few {# alba vybrÃĄny} other {# alb vybrÃĄno}}", "all": "VÅĄe", "all_albums": "VÅĄechna alba", "all_people": "VÅĄichni lidÊ", + "all_photos": "VÅĄechny fotky", "all_videos": "VÅĄechna videa", "allow_dark_mode": "Povolit tmavÃŊ reÅžim", "allow_edits": "Povolit Ãēpravy", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Povolit veřejnosti nahrÃĄvat", "allowed": "Povoleno", "alt_text_qr_code": "ObrÃĄzek QR kÃŗdu", + "always_keep": "PokaÅždÊ ponechat", + "always_keep_photos_hint": "Uvolnění místa ponechÃĄ vÅĄechny fotky na tomto zařízení.", + "always_keep_videos_hint": "Uvolnění místa ponechÃĄ vÅĄechny videa na tomto zařízení.", "anti_clockwise": "Proti směru hodinovÃŊch ručiček", "api_key": "API klíč", "api_key_description": "Tato hodnota se zobrazí pouze jednou. Před zavřením okna ji nezapomeňte zkopírovat.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {ArchivovÃĄno #}}", "are_these_the_same_person": "JednÃĄ se o stejnou osobu?", "are_you_sure_to_do_this": "Opravdu to chcete udělat?", + "array_field_not_fully_supported": "Prvky pole vyÅžadují ruční Ãēpravy JSON", "asset_action_delete_err_read_only": "Nelze odstranit poloÅžky pouze pro čtení, přeskakuji", "asset_action_share_err_offline": "Nelze načíst offline poloÅžky, přeskakuji", "asset_added_to_album": "PřidÃĄno do alba", "asset_adding_to_album": "PřidÃĄvÃĄní do albaâ€Ļ", + "asset_created": "PoloÅžka vytvořena", "asset_description_updated": "Popis poloÅžky byl aktualizovÃĄn", "asset_filename_is_offline": "PoloÅžka {filename} je offline", "asset_has_unassigned_faces": "PoloÅžka mÃĄ nepřiřazenÊ obličeje", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "RozloÅžení", "asset_list_settings_subtitle": "Nastavení rozloÅžení mříŞky fotografií", "asset_list_settings_title": "MříŞka fotografií", + "asset_not_found_on_device_android": "PoloÅžka nebyla nalezena na zařízení", + "asset_not_found_on_device_ios": "PoloÅžka nebyla nalezena na zařízení. Pokud pouŞívÃĄte iCloud, poloÅžka můŞe bÃŊt nepřístupnÃĄ kvůli poÅĄkozenÊmu souboru uloÅženÊmu na iCloudu", + "asset_not_found_on_icloud": "PoloÅžka nebyla nalezena na iCloudu. PoloÅžka můŞe bÃŊt nepřístupnÃĄ kvůli poÅĄkozenÊmu souboru uloÅženÊmu na iCloudu", "asset_offline": "Offline poloÅžka", "asset_offline_description": "Toto externí poloÅžka se jiÅž na disku nenachÃĄzí. ObraÅĨte se na sprÃĄvce Immich a poÅžÃĄdejte o pomoc.", "asset_restored_successfully": "PoloÅžka ÃēspÄ›ÅĄně obnovena", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_reenter_new_password": "Znovu zadejte novÊ heslo", "change_pin_code": "Změnit PIN kÃŗd", + "change_trigger": "SpouÅĄtěč změny", + "change_trigger_prompt": "Opravdu chcete změnit spouÅĄtěč? Tím se odstraní vÅĄechny existující akce a filtry.", "change_your_password": "Změna vaÅĄeho hesla", "changed_visibility_successfully": "Změna viditelnosti proběhla ÃēspÄ›ÅĄně", "charging": "Nabíjení", @@ -722,6 +759,18 @@ "checksum": "Kontrolní součet", "choose_matching_people_to_merge": "Zvolte odpovídající osoby ke sloučení", "city": "Město", + "cleanup_confirm_description": "Immich naÅĄel {count} poloÅžek (vytvořenÃŊch před {date}), kterÊ jsou bezpečně zÃĄlohovÃĄny na serveru. Chcete odstranit místní kopie z tohoto zařízení?", + "cleanup_confirm_prompt_title": "Odstranit z tohoto zařízení?", + "cleanup_deleted_assets": "Přesunuto {count} poloÅžek do koÅĄe zařízení", + "cleanup_deleting": "Přesun do koÅĄe...", + "cleanup_found_assets": "Nalezeno {count} zÃĄlohovanÃŊch poloÅžek", + "cleanup_found_assets_with_size": "Nalezeno {count} zaloÅženo {size} poloÅžek", + "cleanup_icloud_shared_albums_excluded": "SdílenÃĄ iCloud alba jsou vyloučena z prohledÃĄvÃĄní", + "cleanup_no_assets_found": "Nebyly nalezeny ÅžÃĄdnÊ poloÅžky odpovídající vÃŊÅĄe uvedenÃŊm kritÊriím. Funkce Uvolnit místo můŞe odstranit pouze poloÅžky, kterÊ byly zÃĄlohovÃĄny na server", + "cleanup_preview_title": "PoloÅžky k odstranění ({count})", + "cleanup_step3_description": "Vyhledat zÃĄlohovanÊ poloÅžky odpovídající vaÅĄemu datu a zachovat nastavení.", + "cleanup_step4_summary": "{count} poloÅžek (vytvořenÃŊch před {date}) je zařazeno do fronty k odstranění ze zařízení. Fotky zůstanou přístupnÊ z aplikace Immich.", + "cleanup_trash_hint": "Pro ÃēplnÊ uvolnění ÃēloÅžnÊho prostoru otevřete aplikaci systÊmovÊ galerie a vyprÃĄzdněte koÅĄ", "clear": "Vymazat", "clear_all": "Vymazat vÅĄe", "clear_all_recent_searches": "Vymazat vÅĄechna nedÃĄvnÃĄ vyhledÃĄvÃĄní", @@ -733,6 +782,8 @@ "client_cert_import": "Importovat", "client_cert_import_success_msg": "KlientskÃŊ certifikÃĄt je importovÃĄn", "client_cert_invalid_msg": "NeplatnÃŊ soubor certifikÃĄtu nebo ÅĄpatnÊ heslo", + "client_cert_password_message": "Zadejte heslo pro tento certifikÃĄt", + "client_cert_password_title": "Heslo certifikÃĄtu", "client_cert_remove_msg": "KlientskÃŊ certifikÃĄt je odstraněn", "client_cert_subtitle": "Podporuje pouze formÃĄt PKCS12 (.p12, .pfx). Import/odstranění certifikÃĄtu je moÅžnÊ pouze před přihlÃĄÅĄením", "client_cert_title": "KlientskÃŊ SSL certifikÃĄt [EXPERIMENTÁLNÍ]", @@ -743,6 +794,11 @@ "color": "Barva", "color_theme": "BarevnÃŊ motiv", "command": "Příkaz", + "command_palette_prompt": "RychlÊ vyhledÃĄvÃĄní strÃĄnek, akcí nebo příkazů", + "command_palette_to_close": "zavřít", + "command_palette_to_navigate": "vstoupit", + "command_palette_to_select": "vybrat", + "command_palette_to_show_all": "zobrazit vÅĄe", "comment_deleted": "KomentÃĄÅ™ odstraněn", "comment_options": "MoÅžnosti komentÃĄÅ™e", "comments_and_likes": "KomentÃĄÅ™e a lajky", @@ -787,6 +843,7 @@ "create_album": "Vytvořit album", "create_album_page_untitled": "Bez nÃĄzvu", "create_api_key": "Vytvořit API klíč", + "create_first_workflow": "Vytvořte první pracovní postup", "create_library": "Vytvořit knihovnu", "create_link": "Vytvořit odkaz", "create_link_to_share": "Vytvořit odkaz pro sdílení", @@ -801,17 +858,25 @@ "create_tag": "Vytvořit značku", "create_tag_description": "Vytvoření novÊ značky. U vnořenÃŊch značek zadejte celou cestu ke značce včetně dopřednÃŊch lomítek.", "create_user": "Vytvořit uÅživatele", + "create_workflow": "Vytvořit pracovní postup", "created": "Vytvořeno", "created_at": "Vytvořeno", "creating_linked_albums": "VytvÃĄÅ™ení propojenÃŊch alb...", "crop": "Oříznout", + "crop_aspect_ratio_fixed": "PevnÃŊ", + "crop_aspect_ratio_free": "VolnÃŊ", + "crop_aspect_ratio_original": "Původní", "curated_object_page_title": "Věci", "current_device": "SoučasnÊ zařízení", "current_pin_code": "AktuÃĄlní PIN kÃŗd", "current_server_address": "AktuÃĄlní adresa serveru", + "custom_date": "Vlastní datum", "custom_locale": "Vlastní lokalizace", "custom_locale_description": "FormÃĄtovat datumy a čísla podle jazyka a oblasti", "custom_url": "Vlastní URL", + "cutoff_date_description": "Zanechat fotografie a videa z posledníchâ€Ļ", + "cutoff_day": "{count, plural, one {den} few {dny} other {dnů}}", + "cutoff_year": "{count, plural, one {rok} few {roky} other {let}}", "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "dark": "TmavÃŊ", @@ -867,6 +932,7 @@ "deselect_all": "ZruÅĄit vÃŊběr vÅĄech", "details": "Podrobnosti", "direction": "Směr", + "disable": "ZakÃĄzat", "disabled": "ZakÃĄzÃĄno", "disallow_edits": "ZakÃĄzat Ãēpravy", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "VloÅženÃĄ videa", "download_include_embedded_motion_videos_description": "Zahrnout videa vloÅženÃĄ do pohyblivÃŊch fotografií jako samostatnÃŊ soubor", "download_notfound": "StahovÃĄní nebylo nalezeno", + "download_original": "StÃĄhnout originÃĄl", "download_paused": "StahovÃĄní pozastaveno", "download_settings": "StahovÃĄní", "download_settings_description": "SprÃĄva nastavení souvisejících se stahovÃĄním", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "ČekÃĄní na opakovanÃŊ pokus", "downloading": "StahovÃĄní", "downloading_asset_filename": "StahovÃĄní poloÅžky {filename}", + "downloading_from_icloud": "StahovÃĄní z iCloudu", "downloading_media": "StahovÃĄní mÊdia", "drop_files_to_upload": "Pro nahrÃĄní sem přetÃĄhněte soubory", "duplicates": "Duplicity", @@ -929,11 +997,22 @@ "edit_tag": "Upravit značku", "edit_title": "Upravit nÃĄzev", "edit_user": "Upravit uÅživatele", + "edit_workflow": "Upravit pracovní postup", "editor": "Editor", "editor_close_without_save_prompt": "Změny nebudou uloÅženy", "editor_close_without_save_title": "Zavřít editor?", - "editor_crop_tool_h2_aspect_ratios": "Poměr stran", - "editor_crop_tool_h2_rotation": "Otočení", + "editor_confirm_reset_all_changes": "Opravdu chcete zruÅĄit vÅĄechny změny?", + "editor_discard_edits_confirm": "ZruÅĄit Ãēpravy", + "editor_discard_edits_prompt": "MÃĄte neuloÅženÊ Ãēpravy. Opravdu je chcete smazat?", + "editor_discard_edits_title": "ZruÅĄit Ãēpravy?", + "editor_edits_applied_error": "Nepodařilo se pouŞít Ãēpravy", + "editor_edits_applied_success": "Úpravy byly ÃēspÄ›ÅĄně provedeny", + "editor_flip_horizontal": "Otočit vodorovně", + "editor_flip_vertical": "Otočit svisle", + "editor_orientation": "Orientace", + "editor_reset_all_changes": "ZruÅĄit změny", + "editor_rotate_left": "Otočit o 90° doleva", + "editor_rotate_right": "Otočit o 90° doprava", "email": "E-mail", "email_notifications": "E-mailovÃĄ oznÃĄmení", "empty_folder": "Tato sloÅžka je prÃĄzdnÃĄ", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Nepodařilo se změnit pořadí alba", "error_delete_face": "Chyba při odstraňovÃĄní obličeje z poloÅžky", "error_getting_places": "Chyba při zjiÅĄÅĨovÃĄní míst", + "error_loading_albums": "Chyba načítaní alb", "error_loading_image": "Chyba při načítÃĄní obrÃĄzku", "error_loading_partners": "Chyba při načítÃĄní partnerů: {error}", + "error_retrieving_asset_information": "Chyba při získÃĄvÃĄní informací o poloÅžce", "error_saving_image": "Chyba: {error}", "error_tag_face_bounding_box": "Chyba při označovÃĄní obličeje - nelze získat souřadnice ohraničujícího rÃĄmečku", "error_title": "Chyba - Něco se pokazilo", + "error_while_navigating": "Chyba při načítÃĄní poloÅžky", "errors": { "cannot_navigate_next_asset": "Nelze přejít na dalÅĄÃ­ poloÅžku", "cannot_navigate_previous_asset": "Nelze přejít na předchozí poloÅžku", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "Nelze dokončit OAuth přihlÃĄÅĄení", "unable_to_connect": "Nelze se připojit", "unable_to_copy_to_clipboard": "Nelze zkopírovat do schrÃĄnky, ujistěte se, Åže na strÃĄnku přistupujete přes https", + "unable_to_create": "Nelze vytvořit pracovní postup", "unable_to_create_admin_account": "Nelze vytvořit Ãēčet sprÃĄvce", "unable_to_create_api_key": "Nelze vytvořit novÃŊ API klíč", "unable_to_create_library": "Nelze vytvořit knihovnu", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Nelze odstranit vzor vyloučení", "unable_to_delete_shared_link": "Nepodařilo se odstranit sdílenÃŊ odkaz", "unable_to_delete_user": "Nelze odstranit uÅživatele", + "unable_to_delete_workflow": "Nelze odstranit pracovní postup", "unable_to_download_files": "Nelze stÃĄhnout soubory", "unable_to_edit_exclusion_pattern": "Nelze upravit vzor vyloučení", "unable_to_empty_trash": "Nelze vyprÃĄzdnit koÅĄ", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "Nelze prohledat knihovnu", "unable_to_set_feature_photo": "Nelze nastavit hlavní fotografii", "unable_to_set_profile_picture": "Nelze nastavit profilovÃŊ obrÃĄzek", + "unable_to_set_rating": "Nelze nastavit hodnocení", "unable_to_submit_job": "Nelze odeslat Ãēlohu", "unable_to_trash_asset": "Nelze vyhodit poloÅžku do koÅĄe", "unable_to_unlink_account": "Nelze zruÅĄit propojení Ãēčtu", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Nelze aktualizovat nastavení", "unable_to_update_timeline_display_status": "Nelze aktualizovat stav zobrazení časovÊ osy", "unable_to_update_user": "Nelze aktualizovat uÅživatele", + "unable_to_update_workflow": "Nelze aktualizovat pracovní postup", "unable_to_upload_file": "Nepodařilo se nahrÃĄt soubor" }, + "errors_text": "Chyby", "exclusion_pattern": "Vzor vyloučení", "exif": "Exif", "exif_bottom_sheet_description": "Přidat popis...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "LIDÉ", "exif_bottom_sheet_person_add_person": "Přidat jmÊno", "exit_slideshow": "Ukončit prezentaci", + "expand": "Rozbalit", "expand_all": "Rozbalit vÅĄe", "experimental_settings_new_asset_list_subtitle": "ZpracovÃĄvÃĄm", "experimental_settings_new_asset_list_title": "Povolení experimentÃĄlní mříŞky fotografií", @@ -1120,14 +1208,17 @@ "features": "Funkce", "features_in_development": "Funkce ve vÃŊvoji", "features_setting_description": "SprÃĄva funkcí aplikace", - "file_name": "NÃĄzev souboru", "file_name_or_extension": "NÃĄzev nebo přípona souboru", + "file_name_text": "NÃĄzev souboru", + "file_name_with_value": "NÃĄzev souboru: {file_name}", "file_size": "Velikost souboru", "filename": "NÃĄzev souboru", "filetype": "Typ souboru", "filter": "Filtr", + "filter_description": "Podmínky pro filtrovÃĄní cílovÃŊch poloÅžek", "filter_people": "Filtrovat lidi", "filter_places": "Filtrovat místa", + "filters": "Filtry", "find_them_fast": "Najděte je rychle vyhledÃĄním jejich jmÊna", "first": "První", "fix_incorrect_match": "Opravit nesprÃĄvnou shodu", @@ -1137,12 +1228,16 @@ "folders_feature_description": "ProchÃĄzení zobrazení sloÅžek s fotografiemi a videi v souborovÊm systÊmu", "forgot_pin_code_question": "Zapomněli jste PIN?", "forward": "Dopředu", + "free_up_space": "Uvolnit místo", + "free_up_space_description": "Přesunout zÃĄlohovanÊ fotografie a videa do koÅĄe zařízení, abyste uvolnili místo. VaÅĄe kopie na serveru zůstanou v bezpečí.", + "free_up_space_settings_subtitle": "Uvolnit ÃēloÅžiÅĄtě zařízení", "full_path": "ÚplnÃĄ cesta: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Tato funkce načítÃĄ externí zdroje z Googlu, aby mohla fungovat.", "general": "ObecnÊ", "geolocation_instruction_location": "Klikněte na poloÅžku s GPS souřadnicemi, abyste mohli pouŞít její polohu, nebo vyberte polohu přímo z mapy", "get_help": "Získat pomoc", + "get_people_error": "Chyba při načítÃĄní lidí", "get_wifiname_error": "Nepodařilo se získat nÃĄzev Wi-Fi. Zkontrolujte, zda jste udělili potřebnÃĄ oprÃĄvnění a zda jste připojeni k Wi-Fi síti", "getting_started": "ZačínÃĄme", "go_back": "Přejít zpět", @@ -1175,6 +1270,7 @@ "hide_named_person": "SkrÃŊt osobu {name}", "hide_password": "SkrÃŊt heslo", "hide_person": "SkrÃŊt osobu", + "hide_schema": "SkrÃŊt schÊma", "hide_text_recognition": "SkrÃŊt rozpoznÃĄvÃĄní textu", "hide_unnamed_people": "SkrÃŊt nejmenovanÊ lidi", "home_page_add_to_album_conflicts": "PřidÃĄno {added} poloÅžek do alba {album}. {failed} poloÅžek je jiÅž v albu.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "ZpracovÃĄní spuÅĄtěno {dateTime}", "items_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžek}}", "jobs": "Úlohy", + "json_editor": "JSON editor", + "json_error": "Chyba JSON", "keep": "Ponechat", + "keep_albums": "Ponechat alba", + "keep_albums_count": "PonechÃĄní {count} {count, plural, one {alba} other {alb}}", "keep_all": "Ponechat vÅĄe", + "keep_description": "Vyberte co po uvolnění místa zůstane na vaÅĄem zařízení.", + "keep_favorites": "Zachovat oblíbenÊ", + "keep_on_device": "Ponechat na zařízení", + "keep_on_device_hint": "Vyberte poloÅžky kterÊ chcete zachovat na tomto zařízení", "keep_this_delete_others": "Ponechat tuto, odstranit ostatní", + "keeping": "Ponechat: {items}", "kept_this_deleted_others": "PonechÃĄna tato poloÅžka a {count, plural, one {odstraněna # poloÅžka} few {odstraněny # poloÅžky} other {odstraněno # poloÅžek}}", "keyboard_shortcuts": "KlÃĄvesovÊ zkratky", "language": "Jazyk", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Povolit automatickou smyčku videa v prohlíŞeči.", "main_branch_warning": "PouŞívÃĄte vÃŊvojovou verzi; důrazně doporučujeme pouŞívat verzi z vydÃĄní!", "main_menu": "Hlavní nabídka", + "maintenance_action_restore": "Obnovení databÃĄze", "maintenance_description": "Immich byl přepnut do reÅžimu ÃēdrÅžby.", "maintenance_end": "Ukončit reÅžim ÃēdrÅžby", "maintenance_end_error": "Nepodařilo se ukončit reÅžim ÃēdrÅžby.", "maintenance_logged_in_as": "AktuÃĄlně přihlÃĄÅĄen jako {user}", + "maintenance_restore_from_backup": "Obnovit ze zÃĄlohy", + "maintenance_restore_library": "Obnovte svou knihovnu", + "maintenance_restore_library_confirm": "Pokud vÅĄe vypadÃĄ sprÃĄvně, pokračujte v obnovení zÃĄlohy!", + "maintenance_restore_library_description": "Obnovení databÃĄze", + "maintenance_restore_library_folder_has_files": "{folder} obsahuje {count} sloÅžek", + "maintenance_restore_library_folder_no_files": "V sloÅžce {folder} chybí soubory!", + "maintenance_restore_library_folder_pass": "čitelnÊ a zapisovatelnÊ", + "maintenance_restore_library_folder_read_fail": "nečitelnÊ", + "maintenance_restore_library_folder_write_fail": "nezapisovatelnÊ", + "maintenance_restore_library_hint_missing_files": "Mohou vÃĄm chybět důleÅžitÊ soubory", + "maintenance_restore_library_hint_regenerate_later": "Tyto můŞete později obnovit v nastavení", + "maintenance_restore_library_hint_storage_template_missing_files": "PouŞívÃĄte ÅĄablonu ÃēloÅžiÅĄtě? Mohou vÃĄm chybět soubory", + "maintenance_restore_library_loading": "NačítÃĄní kontrol integrity a heuristikyâ€Ļ", + "maintenance_task_backup": "VytvÃĄÅ™ení zÃĄlohy existující databÃĄzeâ€Ļ", + "maintenance_task_migrations": "ProbíhÃĄ migrace databÃĄzeâ€Ļ", + "maintenance_task_restore": "Obnovení vybranÊ zÃĄlohyâ€Ļ", + "maintenance_task_rollback": "Obnova se nezdařila, nÃĄvrat k bodu obnoveníâ€Ļ", "maintenance_title": "Dočasně nedostupnÊ", "make": "VÃŊrobce", "manage_geolocation": "Spravovat polohu", @@ -1408,6 +1531,8 @@ "minimize": "Minimalizovat", "minute": "Minuta", "minutes": "Minut", + "mirror_horizontal": "Vodorovně", + "mirror_vertical": "Svisle", "missing": "Chybějící", "mobile_app": "Mobilní aplikace", "mobile_app_download_onboarding_note": "StÃĄhněte si doprovodnou mobilní aplikaci pomocí nÃĄsledujících moÅžností", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "LLLL y", "more": "Více", "move": "Přesunout", + "move_down": "Přesunout dolů", "move_off_locked_folder": "Přesunout z uzamčenÊ sloÅžky", "move_to": "Přesunout do", + "move_to_device_trash": "Přesunout do koÅĄe zařízení", "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", + "move_up": "Přesunout nahoru", "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", @@ -1430,6 +1558,7 @@ "my_albums": "Moje alba", "name": "JmÊno", "name_or_nickname": "JmÊno nebo přezdívka", + "name_required": "JmÊno je povinnÊ", "navigate": "Navigovat", "navigate_to_time": "Navigovat na čas", "network_requirement_photos_upload": "Pro zÃĄlohovÃĄní fotografií pouŞívat mobilní data", @@ -1454,20 +1583,24 @@ "next": "DalÅĄÃ­", "next_memory": "DalÅĄÃ­ vzpomínka", "no": "Ne", + "no_actions_added": "Zatím nebyly přidÃĄny ÅžÃĄdnÊ akce", + "no_albums_found": "ÅŊÃĄdnÃĄ alba nenalezena", "no_albums_message": "Vytvořte si album pro uspoÅ™ÃĄdÃĄní fotografií a videí", "no_albums_with_name_yet": "VypadÃĄ to, Åže zatím nemÃĄte ÅžÃĄdnÃĄ alba s tímto nÃĄzvem.", "no_albums_yet": "VypadÃĄ to, Åže jeÅĄtě nemÃĄte ÅžÃĄdnÃĄ alba.", "no_archived_assets_message": "Archivujte fotografie a videa a skryjte je ze zobrazení v sekci Fotky", - "no_assets_message": "KLIKNĚTE PRO NAHRÁNÍ PRVNÍ FOTOGRAFIE", + "no_assets_message": "Klikněte pro nahrÃĄní první fotografie", "no_assets_to_show": "ÅŊÃĄdnÊ poloÅžky k zobrazení", "no_cast_devices_found": "Nebyla nalezena ÅžÃĄdnÃĄ zařízení", "no_checksum_local": "Není k dispozici kontrolní součet - nelze načíst místní poloÅžky", "no_checksum_remote": "Není k dispozici kontrolní součet - nelze načíst vzdÃĄlenou poloÅžku", + "no_configuration_needed": "Není nutnÃĄ ÅžÃĄdnÃĄ konfigurace", "no_devices": "ÅŊÃĄdnÃĄ autorizovanÃĄ zařízení", "no_duplicates_found": "Nebyly nalezeny ÅžÃĄdnÊ duplicity.", "no_exif_info_available": "Exif není k dispozici", "no_explore_results_message": "Nahrajte dalÅĄÃ­ fotografie a prozkoumejte svou sbírku.", "no_favorites_message": "Přidejte si oblíbenÊ poloÅžky a rychle najděte svÊ nejlepÅĄÃ­ obrÃĄzky a videa", + "no_filters_added": "Zatím nebyly přidÃĄny ÅžÃĄdnÊ filtry", "no_libraries_message": "Vytvořte si externí knihovnu pro zobrazení fotografií a videí", "no_local_assets_found": "Nebyly nalezeny ÅžÃĄdnÊ místní poloÅžky s tímto kontrolním součtem", "no_location_set": "Není nastavena poloha", @@ -1481,11 +1614,11 @@ "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í", + "none": "ÅŊÃĄdnÊ", "not_allowed": "Nepovoleno", "not_available": "Není k dispozici", "not_in_any_album": "Bez alba", "not_selected": "Není vybrÃĄno", - "note_apply_storage_label_to_previously_uploaded assets": "Upozornění: Chcete-li pouŞít ÅĄtítek ÃēloÅžiÅĄtě na dříve nahranÊ poloÅžky, spusÅĨte příkaz", "notes": "PoznÃĄmky", "nothing_here_yet": "Zatím zde nic není", "notification_permission_dialog_content": "Chcete-li povolit oznÃĄmení, přejděte do nastavení a vyberte moÅžnost povolit.", @@ -1515,6 +1648,7 @@ "online": "Online", "only_favorites": "Pouze oblíbenÊ", "open": "Otevřít", + "open_calendar": "Otevřít kalendÃĄÅ™", "open_in_map_view": "Otevřít v zobrazení mapy", "open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_the_search_filters": "Otevřít vyhledÃĄvací filtry", @@ -1563,6 +1697,7 @@ "people": "LidÊ", "people_edits_count": "Upraveno {count, plural, one {# osoba} few {# osoby} other {# lidí}}", "people_feature_description": "ProchÃĄzení fotografií a videí seskupenÃŊch podle osob", + "people_selected": "{count, plural, one {# osoba vybrÃĄna} few {# osob vybrÃĄno} other {# lidí vybrÃĄno}}", "people_sidebar_description": "Zobrazit sekci LidÊ v postranním panelu", "permanent_deletion_warning": "Upozornění na trvalÊ smazÃĄní", "permanent_deletion_warning_setting_description": "Zobrazit varovÃĄní při trvalÊm odstranění poloÅžek", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, one {# rok} few {# roky} other {# let}}", "person_birthdate": "Narozen(a) {date}", "person_hidden": "{name}{hidden, select, true { (skryto)} other {}}", + "person_recognized": "Osoba rozpoznÃĄna", + "person_selected": "Osoba vybrÃĄna", "photo_shared_all_users": "VypadÃĄ to, Åže jste fotky sdíleli se vÅĄemi uÅživateli, nebo nemÃĄte ÅžÃĄdnÊho uÅživatele, se kterÃŊm byste je mohli sdílet.", "photos": "Fotky", "photos_and_videos": "Fotky a videa", "photos_count": "{count, plural, one {{count, number} fotka} few {{count, number} fotky} other {{count, number} fotek}}", "photos_from_previous_years": "Fotky z předchozích let", + "photos_only": "Pouze fotografie", "pick_a_location": "Vyberte polohu", "pick_custom_range": "Vlastní rozsah", "pick_date_range": "Vyberte rozsah dat", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "ProduktovÃŊ klíč serveru spravuje sprÃĄvce", "query_asset_id": "ID poloÅžky dotazu", "queue_status": "Ve frontě {count}/{total}", + "rate_asset": "Hodnotit poloÅžku", "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}}", "rating_description": "Zobrazit EXIF hodnocení v informačním panelu", + "rating_set": "Hodnocení nastaveno na {rating, plural, one {# hvězdičku} few {# hvězdičky} other {# hvězdiček}}", "reaction_options": "MoÅžnosti reakce", "read_changelog": "Přečtěte si seznam změn", "readonly_mode_disabled": "ReÅžim pouze pro čtení je deaktivovÃĄn", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {Přeřazena # poloÅžka} few {Přeřazeny # poloÅžky} other {Přeřazeno # poloÅžek}} na novou osobu", "reassing_hint": "Přiřazení vybranÃŊch poloÅžek existující osobě", "recent": "NedÃĄvnÊ", - "recent-albums": "NedÃĄvnÃĄ alba", + "recent_albums": "NedÃĄvnÃĄ alba", "recent_searches": "NedÃĄvnÃĄ vyhledÃĄvÃĄní", "recently_added": "NedÃĄvno přidanÊ", "recently_added_page_title": "NedÃĄvno přidanÊ", @@ -1770,9 +1910,11 @@ "saved_settings": "Nastavení uloÅženo", "say_something": "NapiÅĄte něco", "scaffold_body_error_occurred": "DoÅĄlo k chybě", + "scan": "Prohledat", "scan_all_libraries": "Prohledat vÅĄechny knihovny", "scan_library": "Prohledat", "scan_settings": "Nastavení prohledÃĄvÃĄní", + "scanning": "ProhlÃĄdÃĄvÃĄ se", "scanning_for_album": "ProhledÃĄvÃĄní alba...", "search": "Hledat", "search_albums": "VyhledÃĄvejte alba", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "VÃŊběr typu mÊdia", "search_filter_ocr": "Hledat pomocí OCR", "search_filter_people_title": "VÃŊběr lidí", + "search_filter_star_rating": "Hodnocení hvězdičkami", "search_for": "Vyhledat", "search_for_existing_person": "Vyhledat existující osobu", "search_no_more_result": "ÅŊÃĄdnÊ dalÅĄÃ­ vÃŊsledky", @@ -1836,17 +1979,23 @@ "second": "Sekunda", "see_all_people": "Zobrazit vÅĄechny lidi", "select": "Vybrat", + "select_album": "Vybrat album", "select_album_cover": "Vybrat obal alba", + "select_albums": "Vybrat alba", "select_all": "Vybrat vÅĄe", "select_all_duplicates": "Vybrat vÅĄechny duplicity", "select_all_in": "Vybrat vÅĄe ve skupině {group}", "select_avatar_color": "Vyberte barvu avatara", + "select_count": "{count, plural, one {Vybrat #} other {Vybrat #}}", + "select_cutoff_date": "Vybrat mezní datum", "select_face": "Vybrat obličej", "select_featured_photo": "Vybrat hlavní fotografii", "select_from_computer": "Vybrat z počítače", "select_keep_all": "Vybrat ponechat vÅĄe", "select_library_owner": "Vyberte vlastníka knihovny", "select_new_face": "VÃŊběr novÊho obličeje", + "select_people": "Vybrat lidi", + "select_person": "Vybrat osobu", "select_person_to_tag": "Vyberte osobu, kterou chcete označit", "select_photos": "Vybrat fotky", "select_trash_all": "Vybrat vyhodit vÅĄe", @@ -1982,6 +2131,7 @@ "show_password": "Zobrazit heslo", "show_person_options": "Zobrazit moÅžnosti osoby", "show_progress_bar": "Zobrazit ukazatel průběhu", + "show_schema": "Zobrazit schÊma", "show_search_options": "Zobrazit moÅžnosti vyhledÃĄvÃĄní", "show_shared_links": "Zobrazit sdílenÊ odkazy", "show_slideshow_transition": "Zobrazit přechod prezentace", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Přeskočit na sloÅžky", "skip_to_tags": "Přeskočit na značky", "slideshow": "Prezentace", + "slideshow_repeat": "Opakovat prezentaci", + "slideshow_repeat_description": "Po skončení prezentace se vrÃĄtit na zaÄÃĄtek", "slideshow_settings": "Nastavení prezentace", "sort_albums_by": "Seřadit alba podle...", "sort_created": "Datum vytvoření", @@ -2038,6 +2190,7 @@ "support": "Podpora", "support_and_feedback": "Podpora a zpětnÃĄ vazba", "support_third_party_description": "VaÅĄe Immich instalace byla připravena třetí stranou. ProblÊmy, kterÊ se u vÃĄs vyskytly, mohou bÃŊt způsobeny tímto balíčkem, proto se na ně obraÅĨte v první řadě pomocí níŞe uvedenÃŊch odkazů.", + "supporter": "Podporovatel", "swap_merge_direction": "ObrÃĄtit směr sloučení", "sync": "Synchronizovat", "sync_albums": "Synchronizovat alba", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Vyberte nastavení tÊmatu aplikace", "theme_setting_three_stage_loading_subtitle": "TřístupňovÊ načítÃĄní můŞe zvÃŊÅĄit vÃŊkonnost načítÃĄní, ale vede k vÃŊrazně vyÅĄÅĄÃ­mu zatíŞení sítě", "theme_setting_three_stage_loading_title": "Povolení třístupňovÊho načítÃĄní", + "then": "Pak", "they_will_be_merged_together": "Budou sloučeny dohromady", "third_party_resources": "Zdroje třetích stran", "time": "Čas", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Vybrat poloÅžky", "trash_page_title": "KoÅĄ ({count})", "trashed_items_will_be_permanently_deleted_after": "SmazanÊ poloÅžky budou trvale odstraněny po {days, plural, one {# dni} other {# dnech}}.", + "trigger": "SpouÅĄtěč", + "trigger_asset_uploaded": "PoloÅžka nahrÃĄna", + "trigger_asset_uploaded_description": "Spustí se při nahrÃĄní novÊho souboru", + "trigger_description": "UdÃĄlost, kterÃĄ spustí pracovní postup", + "trigger_person_recognized": "Osoba rozpoznÃĄna", + "trigger_person_recognized_description": "Spustí se, kdyÅž je objevena osoba", + "trigger_type": "Typ spouÅĄtěče", "troubleshoot": "Diagnostika", "type": "Typ", "unable_to_change_pin_code": "Nelze změnit PIN kÃŗd", @@ -2123,6 +2284,7 @@ "unhide_person": "ZruÅĄit skrytí osoby", "unknown": "NeznÃĄmÃŊ", "unknown_country": "NeznÃĄmÃĄ země", + "unknown_date": "NeznÃĄmÊ datum", "unknown_year": "NeznÃĄmÃŊ rok", "unlimited": "Neomezeně", "unlink_motion_video": "Odpojit pohyblivÊ video", @@ -2139,17 +2301,19 @@ "unstack": "ZruÅĄit seskupení", "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}}", + "unsupported_field_type": "NepodporovanÃŊ typ pole", "untagged": "Neoznačeno", + "untitled_workflow": "Pracovní postup bez nÃĄzvu", "up_next": "To je prozatím vÅĄe", "update_location_action_prompt": "Aktualizovat polohu {count} vybranÃŊch poloÅžek pomocí:", "updated_at": "AktualizovÃĄno", "updated_password": "Heslo aktualizovÃĄno", "upload": "NahrÃĄt", - "upload_action_prompt": "{count} ve frontě pro nahrÃĄní", "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_error_with_count": "Chyba při nahrÃĄvÃĄní {count, plural, one {# poloÅžky} other {# poloÅžek}}", "upload_errors": "NahrÃĄvÃĄní bylo dokončeno s {count, plural, one {# chybou} other {# chybami}}, obnovte strÃĄnku pro zobrazení novÃŊch poloÅžek.", "upload_finished": "NahrÃĄvÃĄní dokončeno", "upload_progress": "ZbÃŊvÃĄ {remaining, number} - ZpracovÃĄno {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "VyuÅžití", "use_biometric": "PouŞít biometrickÊ Ãēdaje", - "use_current_connection": "pouŞít aktuÃĄlní připojení", + "use_current_connection": "PouŞít aktuÃĄlní připojení", "use_custom_date_range": "PouŞít vlastní rozsah dat", "user": "UÅživatel", "user_has_been_deleted": "Tento uÅživatel byl smazÃĄn.", @@ -2185,6 +2349,7 @@ "utilities": "NÃĄstroje", "validate": "Ověřit", "validate_endpoint_error": "Zadejte platnÊ URL", + "validation_error": "Chyba ověření", "variables": "ProměnnÊ", "version": "Verze", "version_announcement_closing": "VÃĄÅĄ přítel Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "PřehrÃĄt miniaturu videa při najetí myÅĄÃ­ na poloÅžku. I kdyÅž je přehrÃĄvÃĄní vypnuto, lze jej spustit najetím na ikonu přehrÃĄvÃĄní.", "videos": "Videa", "videos_count": "{count, plural, one {# video} few {# videa} other {# videí}}", + "videos_only": "Pouze videa", "view": "Zobrazit", "view_album": "Zobrazit album", "view_all": "Zobrazit vÅĄe", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "PouŞít jako hlavní poloÅžku", "viewer_unstack": "ZruÅĄit zÃĄsobník", "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}", + "visual": "VizuÃĄlní", + "visual_builder": "VizuÃĄlní nÃĄvrhÃĄÅ™", "waiting": "Čekající", "waiting_count": "Čekající: {count}", "warning": "Upozornění", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Vítejte v Immichi", "width": "Šířka", "wifi_name": "NÃĄzev Wi-Fi", - "workflow": "Pracovní postup", + "workflow_delete_prompt": "Opravdu chcete tento pracovní postup smazat?", + "workflow_deleted": "Pracovní postup smazÃĄn", + "workflow_description": "Popis pracovního postupu", + "workflow_info": "Informace o pracovním postupu", + "workflow_json": "JSON pracovního postupu", + "workflow_json_help": "Upravte konfiguraci pracovního postupu ve formÃĄtu JSON. Změny se synchronizují s vizuÃĄlním nÃĄvrhÃĄÅ™em.", + "workflow_name": "NÃĄzev pracovního postupu", + "workflow_navigation_prompt": "Opravdu chcete odejít bez uloÅžení změn?", + "workflow_summary": "Shrnutí pracovního postupu", + "workflow_update_success": "Pracovní postup byl ÃēspÄ›ÅĄně aktualizovÃĄn", + "workflow_updated": "Pracovní postup aktualizovÃĄn", + "workflows": "Pracovní postupy", + "workflows_help_text": "Pracovní postupy automatizují akce tÃŊkající se vaÅĄich poloÅžek na zÃĄkladě spouÅĄtěčů a filtrů", "wrong_pin_code": "ChybnÃŊ PIN kÃŗd", "year": "Rok", "years_ago": "Před {years, plural, one {rokem} other {# lety}}", "yes": "Ano", "you_dont_have_any_shared_links": "NemÃĄte ÅžÃĄdnÊ sdílenÊ odkazy", "your_wifi_name": "NÃĄzev vaÅĄÃ­ Wi-Fi", + "zero_to_clear_rating": "stiskněte 0 pro vymazÃĄní hodnocení poloÅžky", "zoom_image": "ZvětÅĄit obrÃĄzek", "zoom_to_bounds": "PřiblíŞit na okraje" } diff --git a/i18n/cv.json b/i18n/cv.json index 0dde498d08..52008a176f 100644 --- a/i18n/cv.json +++ b/i18n/cv.json @@ -75,6 +75,7 @@ "map_settings": "ĐšĐ°Ņ€Ņ‚Ņ‚Ķ‘ ĕĐŊĐĩŅ€ĐģĐĩĐŊĕвĕ", "no_explore_results_message": "ĐĨĶ‘Đ˛Ķ‘Ņ€ ĐēĐžĐģĐģĐĩĐēŅ†Đ¸ĐŋĐĩ ĐēиĐģĐĩĐŊĐŧĐĩ҈ĐēĶ—ĐŊ ҁ͑ĐŊĶŗĐēĐĩҀ҇͗ĐēҁĐĩĐŧ ҋ҂ĐģĐ°Ņ€Đ°Ņ… Ņ‚Đ¸ĐšĶ—Ņ€.", "open_in_openstreetmap": "OpenStreetMap-Đŋа ҃ŌĢ", + "organize_your_library": "ĐĨĶ‘Đ˛Ķ‘ĐŊ Đ˛ŅƒĐģĐ°Đ˛Ķ‘ŅˆĐŊа ĐšĶ—Ņ€ĐēĐĩĐģĐĩ", "partner_sharing": "ĐŸĐ°Ņ€Ņ‚ĐŊĐĩŅ€ ĐŋаКĐģаĐŊĶ‘Đ˛Ķ—", "people": "ŌĒŅ‹ĐŊҁĐĩĐŧ", "photos": "ĐĄĶ‘ĐŊĶŗĐēĐĩҀ҇͗ĐēҁĐĩĐŧ", @@ -90,5 +91,6 @@ "sharing": "ПайĐģаĐŊи", "sharing_enter_password": "ĐšŅƒ ĐŋĐ¸Ņ‚ĐŊĐĩ ĐēŅƒŅ€Đŧа ĐŋĐ°Ņ€ĐžĐģҌ Đē͗Ҁ҂͗Ҁ.", "user_usage_stats": "Đ¨ŅƒŅ‚Đ° ŌĢҋҀĐŊи ŅƒŅĶ‘ ĐēŅƒŅ€ĐŧаĐģĐģи ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа", - "user_usage_stats_description": "Đ¨ŅƒŅ‚Đ° ŌĢҋҀĐŊи ŅƒŅĶ‘ ĐēŅƒŅ€ĐŧаĐģĐģи ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸ĐēĶ‘ĐŊа ĐŋĶ‘Ņ…Đ°ŅŅĐ¸" + "user_usage_stats_description": "Đ¨ŅƒŅ‚Đ° ŌĢҋҀĐŊи ŅƒŅĶ‘ ĐēŅƒŅ€ĐŧаĐģĐģи ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸ĐēĶ‘ĐŊа ĐŋĶ‘Ņ…Đ°ŅŅĐ¸", + "utilities": "ĐŸŅƒĐģĶ‘ŅˆĐ°ĐēаĐŊҁĐĩĐŧ" } diff --git a/i18n/da.json b/i18n/da.json index ce07a931b8..6981d6dae3 100644 --- a/i18n/da.json +++ b/i18n/da.json @@ -5,6 +5,7 @@ "acknowledge": "Accepter", "action": "Handling", "action_common_update": "Opdater", + "action_description": "Et sÃĻt handlinger, der skal udføres pÃĨ de filtrerede mediefiler", "actions": "Handlinger", "active": "Aktiv", "active_count": "Aktiv: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Tilføj en placering", "add_a_name": "Tilføj et navn", "add_a_title": "Tilføj en titel", + "add_action": "Tilføj handling", + "add_action_description": "Klik for at tilføje en handling, der skal udføres", + "add_assets": "Tilføj ressourcer", "add_birthday": "Tilføj en fødselsdag", "add_endpoint": "Tilføj endepunkt", "add_exclusion_pattern": "Tilføj udelukkelsesmønster", + "add_filter": "Tilføj filter", + "add_filter_description": "Klik for at tilføje en filterbetingelse", "add_location": "Tilføj placering", "add_more_users": "Tilføj flere brugere", "add_partner": "Tilføj partner", @@ -36,6 +42,7 @@ "add_to_shared_album": "Tilføj til delt album", "add_upload_to_stack": "Tilføj upload til stack", "add_url": "Tilføj URL", + "add_workflow_step": "Tilføj workflow-trin", "added_to_archive": "Tilføjet til arkiv", "added_to_favorites": "Tilføjet til favoritter", "added_to_favorites_count": "Tilføjede {count, number} til favoritter", @@ -97,6 +104,8 @@ "image_preview_description": "Mellemstørrelse billede med fjernet metadata, der bruges, nÃĨr du ser en enkelt mediefil og til machine learning", "image_preview_quality_description": "Kvalitet af forhÃĨndsvisning fra 1-100. Højere er bedre, men producerer større filer og kan reducere apprespons. Valg af en lav vÃĻrdi kan pÃĨvirke kvaliteten af maskin lÃĻring.", "image_preview_title": "Indstillinger for forhÃĨndsvisning", + "image_progressive": "Progressivt", + "image_progressive_description": "Indkod JPEG-billeder progressivt for gradvis indlÃĻsning. Dette har ingen effekt pÃĨ WebP-billeder.", "image_quality": "Kvalitet", "image_resolution": "Opløsning", "image_resolution_description": "Højere opløsning indeholder flere detaljer, men tager lÃĻngere tid at processerer, giver større filer og sÃĻnker svartiderne i applikationen.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Aktiver smart søgning", "machine_learning_smart_search_enabled_description": "Hvis deaktiveret, vil billeder ikke blive kodet til smart søgning.", "machine_learning_url_description": "URL’en for maskinlÃĻringsserveren. Hvis mere end Ên URL angives, vil hver server blive forsøgt Ên ad gangen, indtil en svarer succesfuldt, i rÃĻkkefølge fra første til sidste. Servere, der ikke svarer, vil midlertidigt blive ignoreret, indtil de kommer online igen.", + "maintenance_delete_backup": "Slet Backup", + "maintenance_delete_backup_description": "Denne fil vil blive slettet permanent.", + "maintenance_delete_error": "Sletning af backup fejlede.", + "maintenance_restore_backup": "Genskab backup", + "maintenance_restore_backup_description": "Immich bliver slettet og genskabt fra den valgte backup. Der vil blive taget en backup før du fortsÃĻtter.", + "maintenance_restore_backup_different_version": "Denne backup blev lavet med en anden version af Immich!", + "maintenance_restore_backup_unknown_version": "Kunne ikke bestemme versionen af backup'en.", + "maintenance_restore_database_backup": "Genskab databasebackup", + "maintenance_restore_database_backup_description": "Gendan en tidligere databasetilstand ved hjÃĻlp af en sikkerhedskopifil", "maintenance_settings": "Vedligeholdelse", "maintenance_settings_description": "SÃĻt Immich i vedligeholdelsestilstand.", - "maintenance_start": "Start vedligeholdelsestilstand", + "maintenance_start": "Skift til vedligeholdelsestilstand", "maintenance_start_error": "Vedligeholdelsestilstand kunne ikke startes.", + "maintenance_upload_backup": "Upload databasebackupfil", + "maintenance_upload_backup_error": "Kunne ikke uploade backup, er det en .sql/.sql.gz fil?", "manage_concurrency": "Administrer antallet af samtidige opgaver", "manage_concurrency_description": "Naviger til jobsiden for at administrere jobsamtidighed", "manage_log_settings": "Administrer logindstillinger", @@ -252,7 +272,7 @@ "oauth_auto_register": "AutoregistrÊr", "oauth_auto_register_description": "RegistrÊr automatisk nye brugere efter at have logget ind med OAuth", "oauth_button_text": "Knaptekst", - "oauth_client_secret_description": "PÃĨkrÃĻvet hvis PKCE (Proof Key for Code Exchange) ikke er supporteret af OAuth-udbyderen", + "oauth_client_secret_description": "PÃĨkrÃĻvet for en fortrolig klient eller hvis PKCE (Proof Key for Code Exchange) ikke understøttes for en offentlig klient.", "oauth_enable_description": "Log ind med OAuth", "oauth_mobile_redirect_uri": "Mobilomdiregerings-URL", "oauth_mobile_redirect_uri_override": "TilsidesÃĻttelse af mobil omdiregerings-URL", @@ -363,7 +383,7 @@ "transcoding_hardware_acceleration": "Hardwareacceleration", "transcoding_hardware_acceleration_description": "Eksperimentel: hurtigere transkodning men kan sÃĻnke kvaliteten ved samme bitrate", "transcoding_hardware_decoding": "Hardware-afkodning", - "transcoding_hardware_decoding_setting_description": "GÃĻlder kun NVENC, QSV og RKMPP. SlÃĨr ende-til-ende acceleration til i stedet for kun at accelerere indkodning. Virker mÃĨske ikke pÃĨ alle videoer.", + "transcoding_hardware_decoding_setting_description": "SlÃĨr ende‑til‑ende‑acceleration til i stedet for kun at accelerere indkodning. Virker muligvis ikke pÃĨ alle videoer.", "transcoding_max_b_frames": "Maksimum B-frames", "transcoding_max_b_frames_description": "Højere vÃĻrdier forbedrer kompressionseffektivitet, men kan gøre indkodning langsommere. Er mÃĨske ikke kompatibelt med hardware-acceleration pÃĨ ÃĻldre enheder. 0 slÃĨr B-frames fra, mens -1 sÃĻtter denne vÃĻrdi automatisk.", "transcoding_max_bitrate": "Maksimal bitrate", @@ -431,6 +451,9 @@ "admin_password": "Administratoradgangskode", "administration": "Administration", "advanced": "Avanceret", + "advanced_settings_clear_image_cache": "Ryd billedcache", + "advanced_settings_clear_image_cache_error": "Billedcachen kunne ikke ryddes", + "advanced_settings_clear_image_cache_success": "Ryddet {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Brug denne valgmulighed for at filtrere media under synkronisering baseret pÃĨ alternative kriterier. Prøv kun denne, hvis du har problemer med, at appen ikke opdager alle albums.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTEL] Brug alternativ enheds album synkroniserings filter", "advanced_settings_log_level_title": "Logniveau: {level}", @@ -447,9 +470,9 @@ "advanced_settings_tile_subtitle": "Avancerede brugerindstillinger", "advanced_settings_troubleshooting_subtitle": "SlÃĨ ekstra funktioner for fejlsøgning til", "advanced_settings_troubleshooting_title": "Fejlsøgning", - "age_months": "Alder {months, plural, one {# mÃĨned} other {# mÃĨneder}}", - "age_year_months": "Alder 1 ÃĨr, {months, plural, one {# mÃĨned} other {# mÃĨneder}}", - "age_years": "{years, plural, other {Alder #}}", + "age_months": "{months, plural, one {# mÃĨned} other {# mÃĨneder}} gammel", + "age_year_months": "1 ÃĨr, {months, plural, one {# mÃĨned} other {# mÃĨneder}} gammel", + "age_years": "{years, plural, other {# ÃĨr}}", "album": "Album", "album_added": "Album tilføjet", "album_added_notification_setting_description": "Modtag en emailnotifikation nÃĨr du bliver tilføjet til en delt album", @@ -467,10 +490,12 @@ "album_remove_user": "Fjern bruger?", "album_remove_user_confirmation": "Er du sikker pÃĨ at du vil fjerne {user}?", "album_search_not_found": "Ingen album fundet som matcher din søgning", + "album_selected": "Album valgt", "album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.", "album_summary": "Albumoversigt", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation nÃĨr et delt album fÃĨr nye mediefiler", + "album_upload_assets": "Upload filer fra din computer og tilføj dem til album", "album_user_left": "Forlod {album}", "album_user_removed": "Fjernede {user}", "album_viewer_appbar_delete_confirm": "Er du sikker pÃĨ, du vil slette dette album fra din bruger?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "GrundlÃĻggende sortering ved oprettelse af nyt album.", "albums_feature_description": "Samling af billeder der kan deles med andre brugere.", "albums_on_device_count": "Albummer pÃĨ enheden ({count})", + "albums_selected": "{count, plural, one {# album valgt} other {# valgte albummer}}", "all": "Alt", "all_albums": "Alle albummer", "all_people": "Alle personer", + "all_photos": "Alle billeder", "all_videos": "Alle videoer", "allow_dark_mode": "Tillad mørk tilstand", "allow_edits": "Tillad redigeringer", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Tillad offentlige brugere til at uploade", "allowed": "Tilladt", "alt_text_qr_code": "QR-kode billede", + "always_keep": "Opbevar altid", + "always_keep_photos_hint": "Frigør plads vil bevare alle billeder pÃĨ denne enhed.", + "always_keep_videos_hint": "Frigør plads vil bevare alle videoer pÃĨ denne enhed.", "anti_clockwise": "Mod uret", "api_key": "API-nøgle", "api_key_description": "Denne vÃĻrdi vises kun Ên gang. Venligst kopiÊr den før du lukker vinduet.", @@ -507,7 +537,7 @@ "app_bar_signout_dialog_content": "Er du sikker pÃĨ, du vil logge ud?", "app_bar_signout_dialog_ok": "Ja", "app_bar_signout_dialog_title": "Log ud", - "app_download_links": "App Download Links", + "app_download_links": "Links til app download", "app_settings": "Appindstillinger", "app_stores": "App Butikker", "app_update_available": "App opdatering er tilgÃĻngelig", @@ -515,19 +545,21 @@ "apply_count": "Brug ({count, number})", "archive": "Arkiv", "archive_action_prompt": "{count} føjet til arkiv", - "archive_or_unarchive_photo": "ArkivÊr eller dearkivÊr billede", + "archive_or_unarchive_photo": "ArkivÊr eller fjern billede fra arkiv", "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet", "archive_page_title": "ArkivÊr ({count})", - "archive_size": "Arkiv størelse", + "archive_size": "Arkivstørrelse", "archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)", "archived": "Arkiveret", - "archived_count": "{count, plural, other {Arkiveret #}}", + "archived_count": "{count, plural, other {# arkiveret}}", "are_these_the_same_person": "Er disse den samme person?", "are_you_sure_to_do_this": "Er du sikker pÃĨ, at du vil gøre det her?", + "array_field_not_fully_supported": "Arrayfelter krÃĻver manuel JSON-redigering", "asset_action_delete_err_read_only": "Kan ikke slette kun lÃĻselige elementer. Springer over", "asset_action_share_err_offline": "Kan ikke hente offline element(er). Springer over", "asset_added_to_album": "Tilføjet til album", "asset_adding_to_album": "Tilføjer til albumâ€Ļ", + "asset_created": "Mediefil oprettet", "asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret", "asset_filename_is_offline": "Mediefil {filename} er offline", "asset_has_unassigned_faces": "Aktivet har ikke-tildelte ansigter", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Udseende", "asset_list_settings_subtitle": "Indstillinger for billedgitterlayout", "asset_list_settings_title": "Billedgitter", + "asset_not_found_on_device_android": "Kan ikke finde elementet pÃĨ enheden", + "asset_not_found_on_device_ios": "Mediet blev ikke fundet pÃĨ enheden. Hvis du bruger iCloud, kan mediet vÃĻre utilgÃĻngeligt, hvis en fejlagtig fil ligger pÃĨ iCloud", + "asset_not_found_on_icloud": "Mediet blev ikke fundet pÃĨ iCloud. Det kan vÃĻre utilgÃĻngeligt, hvis det er en fejlagtig fil, der ligger pÃĨ iCloud", "asset_offline": "Mediefil offline", "asset_offline_description": "Denne eksterne mediefil kan ikke lÃĻngere findes pÃĨ drevet. Kontakt venligst din Immich-administrator for hjÃĻlp.", "asset_restored_successfully": "Elementet blev gendannet succesfuldt", @@ -659,7 +694,7 @@ "biometric_no_options": "Ingen biometrisk adgangskontrol tilgÃĻngelig", "biometric_not_available": "Biometrisk adgangskontrol er ikke tilgÃĻngelig pÃĨ denne enhed", "birthdate_saved": "Fødselsdatoen blev gemt", - "birthdate_set_description": "Fødselsdato bruges til at beregne alderen pÃĨ denne person pÃĨ tidspunktet for et billede.", + "birthdate_set_description": "Fødselsdato bruges til at beregne denne persons alder pÃĨ det tidspunkt, et billede er taget.", "blurred_background": "Sløret baggrund", "bugs_and_feature_requests": "Fejl & forbedringsønsker", "build": "Byg", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Kodeord er ikke ens", "change_password_form_reenter_new_password": "Gentag nyt kodeord", "change_pin_code": "Skift PIN kode", + "change_trigger": "Skift udløser", + "change_trigger_prompt": "Er du sikker pÃĨ, at du vil ÃĻndre udløseren? Dette vil fjerne alle eksisterende handlinger og filtre.", "change_your_password": "Skift dit kodeord", "changed_visibility_successfully": "Synlighed blev ÃĻndret", "charging": "Lader", @@ -722,6 +759,18 @@ "checksum": "Checksum", "choose_matching_people_to_merge": "VÃĻlg matchende personer til sammenfletning", "city": "By", + "cleanup_confirm_description": "Immich fandt {count} assets (oprettet før {date}) sikkert sikkerhedskopieret til serveren. Fjern de lokale kopier fra denne enhed?", + "cleanup_confirm_prompt_title": "Fjern fra denne enhed?", + "cleanup_deleted_assets": "Flyttede {count} filer til enhedens skraldespand", + "cleanup_deleting": "Flytter til skraldespand...", + "cleanup_found_assets": "Fandt {count} sikkerhedskopierede filer", + "cleanup_found_assets_with_size": "Fundet {count} sikkerhedskopierede objekter ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud delte albummer er udelukket fra scanningen", + "cleanup_no_assets_found": "Ingen elementer matcher kriterierne ovenfor. Frigiv plads kan kun fjerne elementer, der er sikkerhedskopieret til serveren", + "cleanup_preview_title": "Filer at fjerne ({count})", + "cleanup_step3_description": "Skan efter sikkerhedskopierede elementer, som matcher dine dato- og indstillingsvalg.", + "cleanup_step4_summary": "{count, plural, one {element} other {elementer}} oprettet før {date} stÃĨr til at blive fjernet fra denne enhed. Billeder vil stadig vÃĻre tilgÃĻngelige i Immich‑appen.", + "cleanup_trash_hint": "For at genvinde lagringsplads helt, skal du ÃĨbne din indbyggede galleriapp og tømme papirkurven", "clear": "Ryd", "clear_all": "Ryd alle", "clear_all_recent_searches": "Ryd alle seneste søgninger", @@ -733,6 +782,8 @@ "client_cert_import": "Importer", "client_cert_import_success_msg": "Klient certifikat er importeret", "client_cert_invalid_msg": "Invalid certifikat fil eller forkert adgangskode", + "client_cert_password_message": "Skriv kodeord til dette certifikat", + "client_cert_password_title": "Kodeord til certifikat", "client_cert_remove_msg": "Klient certifikat er fjernet", "client_cert_subtitle": "Supportere kun PKCS12 (.p12, .pfx) format. Certifikat importering/fjernelse er kun tilgÃĻngeligt før login", "client_cert_title": "SSL Klient Certifikat [EKSPERIMENTAL]", @@ -787,6 +838,7 @@ "create_album": "Opret album", "create_album_page_untitled": "Uden titel", "create_api_key": "Opret API nøgle", + "create_first_workflow": "Opret første workflow", "create_library": "Opret bibliotek", "create_link": "Opret link", "create_link_to_share": "Opret link for at dele", @@ -801,17 +853,25 @@ "create_tag": "Opret tag", "create_tag_description": "Opret et nyt tag. For indlejrede tags skal du indtaste den fulde sti til tagget inklusive skrÃĨstreger.", "create_user": "Opret bruger", + "create_workflow": "Opret workflow", "created": "Oprettet", "created_at": "Oprettet", "creating_linked_albums": "Opretter sammenkÃĻdede albums...", "crop": "BeskÃĻr", + "crop_aspect_ratio_fixed": "Fikset", + "crop_aspect_ratio_free": "Gratis", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Ting", "current_device": "NuvÃĻrende enhed", "current_pin_code": "NuvÃĻrende PIN kode", "current_server_address": "NuvÃĻrende serveraddresse", + "custom_date": "Brugerdefineret dato", "custom_locale": "Brugerdefineret lokale", "custom_locale_description": "FormatÊr datoer og tal baseret pÃĨ sproget og regionen", "custom_url": "Tilpasset URL", + "cutoff_date_description": "Behold fotos fra den sidsteâ€Ļ", + "cutoff_day": "{count, plural, one {dag} other {dage}}", + "cutoff_year": "{count, plural, one {ÃĨr} other {ÃĨr}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Mørk", @@ -865,8 +925,9 @@ "description_input_hint_text": "Tilføj en beskrivelse...", "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer", "deselect_all": "AfmarkÊr alt", - "details": "DETALJER", + "details": "Detaljer", "direction": "Retning", + "disable": "Deaktiver", "disabled": "Deaktiveret", "disallow_edits": "DeaktivÊr redigeringer", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Indlejrede videoer", "download_include_embedded_motion_videos_description": "Inkluder videoer indlejret i levende billeder som en separat fil", "download_notfound": "Download ikke fundet", + "download_original": "Download original", "download_paused": "Download pauset", "download_settings": "Download", "download_settings_description": "Administrer indstillinger relateret til mediefil-downloads", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Afventer at prøve igen", "downloading": "Downloader", "downloading_asset_filename": "Downloader mediefil {filename}", + "downloading_from_icloud": "Downloading fra iCloud", "downloading_media": "Download medier", "drop_files_to_upload": "Slip filer hvor som helst for at uploade dem", "duplicates": "Duplikater", @@ -929,11 +992,22 @@ "edit_tag": "Rediger tag", "edit_title": "RedigÊr titel", "edit_user": "RedigÊr bruger", + "edit_workflow": "Rediger workflow", "editor": "Redaktør", "editor_close_without_save_prompt": "Ændringerne vil ikke blive gemt", "editor_close_without_save_title": "Luk editor?", - "editor_crop_tool_h2_aspect_ratios": "Størrelsesforhold", - "editor_crop_tool_h2_rotation": "Rotere", + "editor_confirm_reset_all_changes": "Er du sikker pÃĨ, at du vil nulstille alle ÃĻndringer?", + "editor_discard_edits_confirm": "KassÊr redigeringer", + "editor_discard_edits_prompt": "Du har ikke‑gemte redigeringer. Er du sikker pÃĨ, at du vil kassere dem?", + "editor_discard_edits_title": "KassÊr ÃĻndringer?", + "editor_edits_applied_error": "Kunne ikke gemme redigeringer", + "editor_edits_applied_success": "Redigeringer gemt", + "editor_flip_horizontal": "Vend horisontalt", + "editor_flip_vertical": "Flip vertikal", + "editor_orientation": "Orientering", + "editor_reset_all_changes": "Nulstil ÃĻndringer", + "editor_rotate_left": "RotÊr 90° mod uret", + "editor_rotate_right": "RotÊr 90° med uret", "email": "E-mail", "email_notifications": "Email notifikationer", "empty_folder": "Denne mappe er tom", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Ændring af sorteringsrÃĻkkefølgen mislykkedes", "error_delete_face": "Fejl ved sletning af ansigt fra mediefil", "error_getting_places": "Fejl ved hentning af steder", + "error_loading_albums": "Fejl ved indlÃĻsning af album", "error_loading_image": "Fejl ved indlÃĻsning af billede", "error_loading_partners": "Fejl ved indlÃĻsning af partnere: {error}", + "error_retrieving_asset_information": "Fejl ved hentning af objekt-data", "error_saving_image": "Fejl: {error}", "error_tag_face_bounding_box": "Fejl ved tagging af ansigt - kan ikke finde koordinator for afgrÃĻnsningskasse", "error_title": "Fejl - Noget gik galt", + "error_while_navigating": "Fejl ved navigering til objekt", "errors": { "cannot_navigate_next_asset": "Kan ikke navigere til nÃĻste mediefil", "cannot_navigate_previous_asset": "Kan ikke navigere til forrige mediefil", @@ -1001,7 +1078,7 @@ "unable_to_add_comment": "Ikke i stand til at tilføje kommentar", "unable_to_add_exclusion_pattern": "Kunne ikke tilføje udelukkelsesmønster", "unable_to_add_partners": "Ikke i stand til at tilføje partnere", - "unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv", + "unable_to_add_remove_archive": "Kan ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv", "unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter", "unable_to_archive_unarchive": "Ude af stand til at {archived, select, true {arkivere} other {fjerne fra arkiv}}", "unable_to_change_album_user_role": "Ikke i stand til at ÃĻndre albumbrugerens rolle", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "Kan ikke fuldføre OAuth-login", "unable_to_connect": "Kan ikke oprette forbindelse", "unable_to_copy_to_clipboard": "Kan ikke kopiere til udklipsholder, sørg for at du tilgÃĨr siden gennem https", + "unable_to_create": "Kan ikke oprette workflow", "unable_to_create_admin_account": "Kan ikke oprette en administratorkonto", "unable_to_create_api_key": "Kunne ikke oprette ny API-nøgle", "unable_to_create_library": "Ikke i stand til at oprette bibliotek", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Kunne ikke slette udelukkelsesmønster", "unable_to_delete_shared_link": "Kunne ikke slette delt link", "unable_to_delete_user": "Ikke i stand til at slette bruger", + "unable_to_delete_workflow": "Kan ikke slette workflow", "unable_to_download_files": "Kan ikke downloade filer", "unable_to_edit_exclusion_pattern": "Kunne ikke redigere udelukkelsesmønster", "unable_to_empty_trash": "Ikke i stand til at tømme papirkurv", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "Ikke i stand til at skanne bibliotek", "unable_to_set_feature_photo": "Det var ikke muligt at indstille et fremhÃĻvet billede", "unable_to_set_profile_picture": "Ikke i stand til at sÃĻtte profilbillede", + "unable_to_set_rating": "Ikke i stand til at angive vurdering", "unable_to_submit_job": "Ikke i stand til at indsende opgave", "unable_to_trash_asset": "Kunne ikke slette medie", "unable_to_unlink_account": "Ikke i stand til at frakoble konto", @@ -1074,8 +1154,10 @@ "unable_to_update_settings": "Ikke i stand til at opdatere indstillinger", "unable_to_update_timeline_display_status": "Kunne ikke opdate status for tidslinjevisning", "unable_to_update_user": "Ikke i stand til at opdatere bruger", + "unable_to_update_workflow": "Kan ikke opdatere workflow", "unable_to_upload_file": "Filen kunne ikke uploades" }, + "errors_text": "Fejl", "exclusion_pattern": "Udelukkelsesmønster", "exif": "Exif", "exif_bottom_sheet_description": "Tilføj beskrivelse...", @@ -1120,14 +1202,17 @@ "features": "Funktioner", "features_in_development": "Funktioner under udvikling", "features_setting_description": "Administrer app-funktioner", - "file_name": "Filnavn", "file_name_or_extension": "Filnavn eller filtype", + "file_name_text": "Filnavn", + "file_name_with_value": "Filnavn: {file_name}", "file_size": "Fil størrelse", "filename": "Filnavn", "filetype": "Filtype", "filter": "Filter", + "filter_description": "Betingelser for filtrering af valgte mediefiler", "filter_people": "FiltrÊr personer", "filter_places": "Filtrer steder", + "filters": "Filtre", "find_them_fast": "Find dem hurtigt med søgning via navn", "first": "Første", "fix_incorrect_match": "Fix forkert match", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Gennemse mappevisningen efter fotos og videoer pÃĨ filsystemet", "forgot_pin_code_question": "Har du glemt PIN-koden?", "forward": "Fremad", + "free_up_space": "Frigør plads", + "free_up_space_description": "Flyt sikkerhedskopierede fotos og videoer til din enheds skraldespand for at frigøre plads. Dine kopier pÃĨ serveren forbliver sikre.", + "free_up_space_settings_subtitle": "Frigør enhedslagerplads", "full_path": "Fuld sti: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funktion indlÃĻser eksterne ressourcer fra Google for at virke.", "general": "Generel", "geolocation_instruction_location": "Klik pÃĨ et objekt med GPS-koordinater for at bruge dettes position, eller vÃĻlg position direkte pÃĨ kortet", "get_help": "FÃĨ hjÃĻlp", + "get_people_error": "Fejl ved indhentning af personer", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navn. Sørg for, at du har givet de nødvendige tilladelser og er forbundet til et Wi-Fi-netvÃĻrk", "getting_started": "Kom godt i gang", "go_back": "GÃĨ tilbage", @@ -1175,13 +1264,14 @@ "hide_named_person": "Skjul person {name}", "hide_password": "Skjul adgangskode", "hide_person": "Skjul person", + "hide_schema": "Skjul skema", "hide_text_recognition": "Skjul tekstgenkendelse", "hide_unnamed_people": "Skjul unavngivne personer", "home_page_add_to_album_conflicts": "Tilføjede {added} elementer til album {album}. {failed} elementer er allerede i albummet.", "home_page_add_to_album_err_local": "Kan endnu ikke tilføje lokale elementer til album. Springer over", "home_page_add_to_album_success": "Tilføjede {added} elementer til album {album}.", "home_page_album_err_partner": "Kan endnu ikke tilføje partners elementer til album. Springer over", - "home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu.. Springer over", + "home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu. Springer over", "home_page_archive_err_partner": "Kan endnu ikke arkivere partners elementer. Springer over", "home_page_building_timeline": "Bygger tidslinjen", "home_page_delete_err_partner": "Kan endnu ikke slette partners elementer. Springer over", @@ -1223,7 +1313,7 @@ "in_archive": "I arkiv", "in_year": "I {year}", "in_year_selector": "I", - "include_archived": "Inkluder arkiveret", + "include_archived": "Inkluder arkiverede", "include_shared_albums": "InkludÊr delte albummer", "include_shared_partner_assets": "InkludÊr delte partnermedier", "individual_share": "Individuel andel", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Behandlingen kørte {dateTime}", "items_count": "{count, plural, one {# element} other {# elementer}}", "jobs": "Opgaver", + "json_editor": "JSON editor", + "json_error": "JSON fejl", "keep": "Behold", + "keep_albums": "Behold albums", + "keep_albums_count": "Beholder {count} {count, plural, one {album} other {albums}}", "keep_all": "Behold alle", + "keep_description": "VÃĻlg hvad der skal forblive pÃĨ din enhed efter oprydning af plads.", + "keep_favorites": "Behold favoritter", + "keep_on_device": "Behold pÃĨ enheden", + "keep_on_device_hint": "VÃĻlg elementer, der skal beholdes pÃĨ denne enhed", "keep_this_delete_others": "Behold dette, slet andre", + "keeping": "Beholder: {items}", "kept_this_deleted_others": "Beholdt denne mediefil og slettede {count, plural, one {# aktiv} other {# aktiver}}", "keyboard_shortcuts": "Tastaturgenveje", "language": "Sprog", @@ -1343,10 +1442,28 @@ "loop_videos_description": "AktivÊr for at genafspille videoer automatisk i detaljeret visning.", "main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!", "main_menu": "Hovedmenu", + "maintenance_action_restore": "Genopretter database", "maintenance_description": "Immich er blevet sat i vedligeholdelsestilstand.", "maintenance_end": "Afslut vedligeholdelsestilstand", "maintenance_end_error": "Vedligeholdelsestilstand kunne ikke afsluttes.", "maintenance_logged_in_as": "Aktuelt logget ind som {user}", + "maintenance_restore_from_backup": "Genskab fra sikkerhedskopi", + "maintenance_restore_library": "Genskab dit bibliotek", + "maintenance_restore_library_confirm": "Hvis dette ser korrekt ud, sÃĨ fortsÃĻt for at genoprette fra en sikkerhedskopi!", + "maintenance_restore_library_description": "Genopretter database", + "maintenance_restore_library_folder_has_files": "{folder} har {count, plural, one {mappe} other {mapper}}", + "maintenance_restore_library_folder_no_files": "{folder} mangler filer!", + "maintenance_restore_library_folder_pass": "lÃĻs- og skrivbar", + "maintenance_restore_library_folder_read_fail": "ikke lÃĻsbar", + "maintenance_restore_library_folder_write_fail": "ikke skrivbar", + "maintenance_restore_library_hint_missing_files": "Du mangler mÃĨske vigtige filer", + "maintenance_restore_library_hint_regenerate_later": "Du kan genindstille disse senere, i indstillinger", + "maintenance_restore_library_hint_storage_template_missing_files": "Bruger du en lagringsskabelon? Du mangler mÃĨske nogle filer", + "maintenance_restore_library_loading": "IndlÃĻser integritetskontroller og heuristikker â€Ļ", + "maintenance_task_backup": "Laver en backup af den eksisterende database â€Ļ", + "maintenance_task_migrations": "Kører migration af databaseâ€Ļ", + "maintenance_task_restore": "Genskaber den valgte backupâ€Ļ", + "maintenance_task_rollback": "Genoprettelse slog fejl, ruller tilbage til genoprettelsespunktâ€Ļ", "maintenance_title": "Midlertidigt UtilgÃĻngelig", "make": "Producent", "manage_geolocation": "Administrer placering", @@ -1379,7 +1496,7 @@ "map_settings_date_range_option_year": "Sidste ÃĨr", "map_settings_date_range_option_years": "Sidste {years} ÃĨr", "map_settings_dialog_title": "Kortindstillinger", - "map_settings_include_show_archived": "Inkluder arkiveret", + "map_settings_include_show_archived": "Inkluder arkiverede", "map_settings_include_show_partners": "Inkluder partnere", "map_settings_only_show_favorites": "Vis kun favoritter", "map_settings_theme_settings": "Korttema", @@ -1408,6 +1525,8 @@ "minimize": "MinimÊr", "minute": "Minut", "minutes": "Minutter", + "mirror_horizontal": "Horisontalt", + "mirror_vertical": "Vertikal", "missing": "Mangler", "mobile_app": "Mobil App", "mobile_app_download_onboarding_note": "Hent den tilhørende mobilapp via en af følgende muligheder", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM ÃĨ", "more": "Mere", "move": "Flyt", + "move_down": "Flyt ned", "move_off_locked_folder": "Flyt ud af lÃĨst mappe", "move_to": "Flyt til", + "move_to_device_trash": "Flyt til enheds skraldespand", "move_to_lock_folder_action_prompt": "{count} føjet til den lÃĨste mappe", "move_to_locked_folder": "Flyt til lÃĨst mappe", "move_to_locked_folder_confirmation": "Disse billeder og videoer vil blive fjernet fra alle albums, og vil kun vÃĻre synlig fra den lÃĨste mappe", + "move_up": "Flyt op", "moved_to_archive": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til arkivet", "moved_to_library": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til biblioteket", "moved_to_trash": "Flyttet til papirkurv", @@ -1430,6 +1552,7 @@ "my_albums": "Mine albummer", "name": "Navn", "name_or_nickname": "Navn eller kaldenavn", + "name_required": "Navn er pÃĨkrÃĻvet", "navigate": "Naviger", "navigate_to_time": "Naviger til tid", "network_requirement_photos_upload": "Benyt mobildatanettet for at sikkerhedskopiere dine fotos", @@ -1454,20 +1577,24 @@ "next": "NÃĻste", "next_memory": "NÃĻste minde", "no": "Nej", + "no_actions_added": "Ingen handlinger tilføjet endnu", + "no_albums_found": "Ingen album fundet", "no_albums_message": "Opret et album for at organisere dine billeder og videoer", "no_albums_with_name_yet": "Det ser ud til, at du ikke har noget album med dette navn endnu.", "no_albums_yet": "Det ser ud til, at du ikke har nogen album endnu.", "no_archived_assets_message": "ArkivÊr billeder og videoer for at gemme dem vÃĻk fra din billedoversigt", - "no_assets_message": "KLIK FOR AT UPLOADE DIT FØRSTE BILLEDE", + "no_assets_message": "Klik for at uploade dit første foto", "no_assets_to_show": "Ingen elementer at vise", "no_cast_devices_found": "Ingen Cast-enheder fundet", "no_checksum_local": "Ingen checksum tilgÃĻngelig – kan ikke hente lokale objekter", "no_checksum_remote": "Ingen checksum tilgÃĻngelig – kan ikke hente eksterne objekter", + "no_configuration_needed": "Ingen konfiguration nødvendig", "no_devices": "Ingen godkendte enheder", "no_duplicates_found": "Ingen duplikater fundet.", "no_exif_info_available": "Ingen tilgÃĻngelig exif information", "no_explore_results_message": "Upload flere billeder for at udforske din samling.", "no_favorites_message": "Tilføj favoritter for hurtigt at finde dine bedst billeder og videoer", + "no_filters_added": "Ingen filtre tilføjet endnu", "no_libraries_message": "Opret et eksternt bibliotek for at se dine billeder og videoer", "no_local_assets_found": "Ingen lokale objekter fundet med denne checksum", "no_location_set": "Ingen placering sat", @@ -1481,11 +1608,11 @@ "no_results_description": "Prøv et synonym eller et mere generelt søgeord", "no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netvÃĻrk", "no_uploads_in_progress": "Ingen upload i gang", + "none": "Ingen", "not_allowed": "Ikke tilladt", "not_available": "ikke tilgÃĻngelig", "not_in_any_album": "Ikke i noget album", "not_selected": "Ikke valgt", - "note_apply_storage_label_to_previously_uploaded assets": "BemÃĻrk: For at anvende LagringsmÃĻrkat pÃĨ tidligere uploadede medier, kør", "notes": "Noter", "nothing_here_yet": "Intet her endnu", "notification_permission_dialog_content": "GÃĨ til indstillinger for at slÃĨ notifikationer til.", @@ -1531,9 +1658,9 @@ "owned": "Egne", "owner": "Ejer", "page": "Side", - "partner": "Partnerpartner", + "partner": "Partner", "partner_can_access": "{partner} kan tilgÃĨ", - "partner_can_access_assets": "Alle dine billeder og videoer, bortset fra dem i Arkivet og Slettet", + "partner_can_access_assets": "Alle dine billeder og videoer, bortset fra dem i Arkiv og Slettet", "partner_can_access_location": "Stedet, hvor dine billeder blev taget", "partner_list_user_photos": "{user}s billeder", "partner_list_view_all": "Se alle", @@ -1563,6 +1690,7 @@ "people": "Personer", "people_edits_count": "Redigeret {count, plural, one {# person} other {# people}}", "people_feature_description": "Gennemse billeder og videoer grupperet efter personer", + "people_selected": "{count, plural, one {# person vagt} other {# personer valgt}}", "people_sidebar_description": "Vis et link til Personer i sidepanelet", "permanent_deletion_warning": "Advarsel om permanent sletning", "permanent_deletion_warning_setting_description": "Vis en advarsel, nÃĨr medier slettes permanent", @@ -1580,24 +1708,27 @@ "permission_onboarding_permission_denied": "Tilladelse afvist. For at bruge Immich, skal der gives tilladelse til at se billeder og videoer i indstillinger.", "permission_onboarding_permission_granted": "Tilladelse givet! Du er nu klar.", "permission_onboarding_permission_limited": "Tilladelse begrÃĻnset. For at lade Immich lave sikkerhedskopi og styre hele dit galleri, skal der gives tilladelse til billeder og videoer i indstillinger.", - "permission_onboarding_request": "Immich krÃĻver tilliadelse til at se dine billeder og videoer.", - "person": "Personperson", + "permission_onboarding_request": "Immich krÃĻver tilladelse til at se dine billeder og videoer.", + "person": "Person", "person_age_months": "{months, plural, one {# month} other {# months}} gammel", "person_age_year_months": "1 ÃĨr, {months, plural, one {# month} other {# months}} gammel", "person_age_years": "{years, plural, other {# years}} gammel", "person_birthdate": "Født den {date}", "person_hidden": "{name}{hidden, select, true { (skjult)} other {}}", + "person_recognized": "Person genkendt", + "person_selected": "Person valgt", "photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller ogsÃĨ har du ikke nogen bruger at dele med.", "photos": "Billeder", "photos_and_videos": "Billeder og videoer", "photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}", "photos_from_previous_years": "Billeder fra tidligere ÃĨr", + "photos_only": "Kun fotos", "pick_a_location": "VÃĻlg et sted", - "pick_custom_range": "Brugerdefineret periode", + "pick_custom_range": "Brugerdefineret interval", "pick_date_range": "VÃĻlg et datointerval", - "pin_code_changed_successfully": "Ændring af PIN kode vellykket", - "pin_code_reset_successfully": "Nulstilling af PIN kode vellykket", - "pin_code_setup_successfully": "OpsÃĻtning af PIN kode vellykket", + "pin_code_changed_successfully": "Ændring af PIN kode lykkedes", + "pin_code_reset_successfully": "Nulstilling af PIN kode lykkedes", + "pin_code_setup_successfully": "OpsÃĻtning af PIN kode var vellykket", "pin_verification": "PIN kode verifikation", "place": "Sted", "places": "Steder", @@ -1611,7 +1742,7 @@ "play_transcoded_video": "Afspil transkodet video", "please_auth_to_access": "Log venligst ind for at tilgÃĨ", "port": "Port", - "preferences_settings_subtitle": "Administrer app-prÃĻferencer", + "preferences_settings_subtitle": "Administrer appens indstillinger", "preferences_settings_title": "PrÃĻferencer", "preparing": "Forberedelse", "preset": "Forudindstilling", @@ -1652,7 +1783,7 @@ "purchase_license_subtitle": "Køb Immich for at understøtte den fortsatte udvikling af tjenesten", "purchase_lifetime_description": "Livsvarigt køb", "purchase_option_title": "KØBSMULIGHEDER", - "purchase_panel_info_1": "At bygge Immich tager meget tid og krÃĻfter, og vi har fuldtidsingeniører, der arbejder pÃĨ det for at gøre det sÃĨ godt, som vi overhovedet kan. Vores mission er, at open source-software og etisk forretningspraksis bliver en bÃĻredygtig indtÃĻgtskilde for udviklere og at skabe et privatlivsrespekterende økosystem med reelle alternativer til udnyttende cloud-tjenester.", + "purchase_panel_info_1": "At bygge Immich tager meget tid og krÃĻfter, og vi har fuldtidsudviklere, der arbejder pÃĨ det for at gøre det sÃĨ godt, som vi overhovedet kan. Vores mission er, at open source-software og etisk forretningspraksis bliver en bÃĻredygtig indtÃĻgtskilde for udviklere og at skabe et privatlivsrespekterende økosystem med reelle alternativer til udnyttende cloud-tjenester.", "purchase_panel_info_2": "Da vi er forpligtet til ikke at tilføje betalingsvÃĻgge, vil dette køb ikke give dig yderligere funktioner i Immich. Vi er afhÃĻngige af, at brugere som dig støtter Immichs løbende udvikling.", "purchase_panel_title": "Støt projektet", "purchase_per_server": "Pr. server", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren", "query_asset_id": "Forespørgsels Asset ID", "queue_status": "Kø {count}/{total}", + "rate_asset": "Vurder filer", "rating": "Stjernebedømmelse", "rating_clear": "Nulstil vurdering", "rating_count": "{count, plural, one {# stjerne} other {# stjerner}}", "rating_description": "Vis EXIF-klassificeringen i infopanelet", + "rating_set": "Vurdering sat til {rating, plural, one {# stjerne} other {# stjerner}}", "reaction_options": "Reaktionsindstillinger", "read_changelog": "LÃĻs ÃĻndringslog", "readonly_mode_disabled": "Skrivebeskyttet tilstand deaktiveret", @@ -1681,12 +1814,12 @@ "reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person", "reassing_hint": "Tildel valgte mediefiler til en eksisterende person", "recent": "For nylig", - "recent-albums": "Seneste albums", + "recent_albums": "Seneste albums", "recent_searches": "Seneste søgninger", "recently_added": "Senest tilføjet", "recently_added_page_title": "Nyligt tilføjet", - "recently_taken": "For nylig taget", - "recently_taken_page_title": "For nylig taget", + "recently_taken": "Taget for nylig", + "recently_taken_page_title": "Taget For nylig", "refresh": "OpdatÊr", "refresh_encoded_videos": "Opdater kodede videoer", "refresh_faces": "Opdater ansigter", @@ -1738,8 +1871,8 @@ "reset_password": "Nulstil adgangskode", "reset_people_visibility": "Nulstil personsynlighed", "reset_pin_code": "Nulstil PIN kode", - "reset_pin_code_description": "Hvis du har glemt din PIN-kode, kan du kontakte serveradministratoren for at fÃĨ den stillet tilbage", - "reset_pin_code_success": "PIN-koden er stillet tilbage", + "reset_pin_code_description": "Hvis du har glemt din PIN-kode, kan du kontakte serveradministratoren for at fÃĨ den nulstillet", + "reset_pin_code_success": "PIN-koden er Nulstillet", "reset_pin_code_with_password": "Du kan altid nulstille din PIN-kode med dit password", "reset_sqlite": "Reset SQLite Databasen", "reset_sqlite_confirmation": "Er du sikker pÃĨ, at du vil nulstille SQLite databasen? Du er nødt til at logge ud og ind igen for at gensynkronisere dine data", @@ -1770,9 +1903,11 @@ "saved_settings": "Gemte indstillinger", "say_something": "Skriv noget", "scaffold_body_error_occurred": "Der opstod en fejl", + "scan": "Skan", "scan_all_libraries": "Skan alle biblioteker", "scan_library": "Skan", "scan_settings": "Skanningsindstillinger", + "scanning": "Skanner", "scanning_for_album": "Skanner efter albummer...", "search": "Søg", "search_albums": "Søg i albummer", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "VÃĻlg medietype", "search_filter_ocr": "Søg via OCR", "search_filter_people_title": "VÃĻlg personer", + "search_filter_star_rating": "Stjerne Vurdering", "search_for": "Søg efter", "search_for_existing_person": "Søg efter eksisterende person", "search_no_more_result": "Ikke flere resultater", @@ -1836,17 +1972,23 @@ "second": "Sekund", "see_all_people": "Se alle personer", "select": "VÃĻlg", + "select_album": "VÃĻlg album", "select_album_cover": "VÃĻlg albumcover", + "select_albums": "VÃĻlg albummer", "select_all": "VÃĻlg alle", "select_all_duplicates": "VÃĻlg alle dubletter", "select_all_in": "VÃĻlg alt i {group}", "select_avatar_color": "VÃĻlg avatarfarve", + "select_count": "{count, plural, one {VÃĻlg #} other {VÃĻlg #}}", + "select_cutoff_date": "VÃĻlg stop-dato", "select_face": "VÃĻlg ansigt", "select_featured_photo": "VÃĻlg forsidebillede", "select_from_computer": "VÃĻlg fra computer", "select_keep_all": "VÃĻlg gem alle", "select_library_owner": "VÃĻlg biblioteksejer", "select_new_face": "VÃĻlg nyt ansigt", + "select_people": "VÃĻlg personer", + "select_person": "VÃĻlg person", "select_person_to_tag": "VÃĻlg en person at tagge", "select_photos": "VÃĻlg billeder", "select_trash_all": "VÃĻlg smid alle ud", @@ -1902,7 +2044,7 @@ "settings": "Indstillinger", "settings_require_restart": "Genstart venligst Immich for at anvende denne ÃĻndring", "settings_saved": "Indstillinger er gemt", - "setup_pin_code": "SÃĻt in PIN kode", + "setup_pin_code": "Indstil en PIN kode", "share": "Del", "share_action_prompt": "Delte {count} objekter", "share_add_photos": "Tilføj billeder", @@ -1938,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} ÃĨr", "shared_link_edit_password_hint": "Indtast kodeordet", "shared_link_edit_submit_button": "Opdater link", - "shared_link_error_server_url_fetch": "Kan ikke finde server URL", + "shared_link_error_server_url_fetch": "Kan ikke hente server URL", "shared_link_expires_day": "Udløber om {count} dag", "shared_link_expires_days": "Udløber om {count} dage", "shared_link_expires_hour": "Udløber om {count} time", @@ -1982,6 +2124,7 @@ "show_password": "Vis adgangskode", "show_person_options": "Vis personindstillinger", "show_progress_bar": "Vis statuslinje", + "show_schema": "Vis skema", "show_search_options": "Vis søgeindstillinger", "show_shared_links": "Vis delte links", "show_slideshow_transition": "Vis overgang til diasshow", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Spring til mapper", "skip_to_tags": "Spring til tags", "slideshow": "Diasshow", + "slideshow_repeat": "Gentag diasshow", + "slideshow_repeat_description": "Hop tilbage til begyndelsen nÃĨr diasshow stopper", "slideshow_settings": "Diasshowindstillinger", "sort_albums_by": "SortÊr albummer efter...", "sort_created": "Dato oprettet", @@ -2022,7 +2167,7 @@ "start_date_before_end_date": "Startdato skal ligge før slutdato", "state": "Stat", "status": "Status", - "stop_casting": "Stop casting", + "stop_casting": "Stop med at caste", "stop_motion_photo": "Stopmotionbillede", "stop_photo_sharing": "Stop med at dele dine billeder?", "stop_photo_sharing_description": "{partner} vil ikke lÃĻngere kunne tilgÃĨ dine billeder.", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "VÃĻlg appens temaindstilling", "theme_setting_three_stage_loading_subtitle": "Tre-trins indlÃĻsning kan øge ydeevnen, men kan ligeledes føre til højere netvÃĻrksbelastning", "theme_setting_three_stage_loading_title": "SlÃĨ tre-trins indlÃĻsning til", + "then": "Siden", "they_will_be_merged_together": "De vil blive slÃĨet sammen", "third_party_resources": "Tredjepartsressourcer", "time": "Tid", @@ -2109,12 +2255,19 @@ "trash_page_select_assets_btn": "VÃĻlg elementer", "trash_page_title": "Papirkurv ({count})", "trashed_items_will_be_permanently_deleted_after": "Mediefiler i papirkurven vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", + "trigger": "Udløser", + "trigger_asset_uploaded": "Mediefil uploaded", + "trigger_asset_uploaded_description": "Udløses, nÃĨr et nyt asset bliver uploaded", + "trigger_description": "En begivenhed, der starter en arbejdsgang", + "trigger_person_recognized": "Peron genkendt", + "trigger_person_recognized_description": "Udløses, nÃĨr en person er detekteret", + "trigger_type": "Udløsertype", "troubleshoot": "Fejlfinding", "type": "Type", "unable_to_change_pin_code": "Kunne ikke ÃĻndre PIN kode", "unable_to_check_version": "Kan ikke tjekke app- eller serverversion", "unable_to_setup_pin_code": "Kunne ikke sÃĻtte PIN kode", - "unarchive": "Af AkivÊr", + "unarchive": "Fjern fra arkiv", "unarchive_action_prompt": "{count} slettet fra Arkiv", "unarchived_count": "{count, plural, other {Uarkiveret #}}", "undo": "Fortryd", @@ -2123,6 +2276,7 @@ "unhide_person": "Stop med at skjule person", "unknown": "Ukendt", "unknown_country": "Ukendt land", + "unknown_date": "Ukendt dato", "unknown_year": "Ukendt ÃĨr", "unlimited": "UbegrÃĻnset", "unlink_motion_video": "Fjern link til bevÃĻgelsesvideo", @@ -2139,17 +2293,19 @@ "unstack": "Fjern fra stak", "unstack_action_prompt": "{count} ustakket", "unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}", + "unsupported_field_type": "Ikke-understøttet felttype", "untagged": "UmÃĻrket", + "untitled_workflow": "Unavngivet arbejdsgang", "up_next": "NÃĻste", "update_location_action_prompt": "Opdater lokationen for {count} valgte objekter med:", "updated_at": "Opdateret", "updated_password": "Opdaterede adgangskode", "upload": "Upload", - "upload_action_prompt": "{count} i kø til upload", "upload_concurrency": "Upload samtidighed", "upload_details": "Upload detaljer", "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_title": "Upload element", + "upload_error_with_count": "Upload-fejl for {count, plural, one {# objekt} other {# objekter}}", "upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.", "upload_finished": "Upload fuldført", "upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "Forbrug", "use_biometric": "Brug biometrisk", - "use_current_connection": "brug nuvÃĻrende forbindelse", + "use_current_connection": "Brug nuvÃĻrende forbindelse", "use_custom_date_range": "Brug tilpasset datointerval i stedet", "user": "Bruger", "user_has_been_deleted": "Denne bruger er slettet.", @@ -2181,10 +2337,11 @@ "user_usage_stats_description": "Vis konto anvendelsesstatistik", "username": "Brugernavn", "users": "Brugere", - "users_added_to_album_count": "Føjet {count, plural, one {# bruker} other {# brukere}} til albummet", + "users_added_to_album_count": "Tilføjet {count, plural, one {# bruker} other {# brukere}} til albummet", "utilities": "VÃĻrktøjer", "validate": "ValidÊr", "validate_endpoint_error": "Indtast en gyldig URL", + "validation_error": "Validerings fejl", "variables": "Variabler", "version": "Version", "version_announcement_closing": "Din ven, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Afspil miniaturevisning for videoer nÃĨr musemarkøren holdes over elementet. Selv nÃĨr det er deaktiveret, kan afspilning startes ved at holde musen over afspilningsikonet.", "videos": "Videoer", "videos_count": "{count, plural, one {# Video} other {# Videoer}}", + "videos_only": "Kun videoer", "view": "Se", "view_album": "Se album", "view_all": "Se alle", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Brug som hovedelement", "viewer_unstack": "Fjern fra stak", "visibility_changed": "Synlighed ÃĻndret for {count, plural, one {# person} other {# personer}}", + "visual": "Visuel", + "visual_builder": "Visuel builder", "waiting": "Venter", "waiting_count": "Venter: {count}", "warning": "Advarsel", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Velkommen til Immich", "width": "Bredde", "wifi_name": "Wi-Fi navn", - "workflow": "Arbejdsproces", + "workflow_delete_prompt": "Er du sikker pÃĨ, at du vil slette denne arbejdsgang?", + "workflow_deleted": "Arbejdsgang slettet", + "workflow_description": "Arbejdsgangsbeskrivelse", + "workflow_info": "Information om arbejdsgang", + "workflow_json": "Arbejdsgang JSON", + "workflow_json_help": "Rediger arbejdsgangskonfiguration i JSON-format. Ændringer vil synkroniseres til den visuelle opbygger.", + "workflow_name": "Navn pÃĨ arbejdsgang", + "workflow_navigation_prompt": "Er du sikker pÃĨ, at du vil forlade uden at gemme dine ÃĻndringer?", + "workflow_summary": "Arbejdsgangsoversigt", + "workflow_update_success": "Arbejdsgang opdateret korrekt", + "workflow_updated": "Arbejdsgang opdateret", + "workflows": "Arbejdsgange", + "workflows_help_text": "Arbejdsgange automatiserer handlinger pÃĨ dine filer baseret pÃĨ udløsere og filtre", "wrong_pin_code": "Forkert PIN kode", "year": "År", "years_ago": "{years, plural, one {# ÃĨr} other {# ÃĨr}} siden", "yes": "Ja", "you_dont_have_any_shared_links": "Du har ikke nogen delte links", "your_wifi_name": "Dit Wi-Fi navn", + "zero_to_clear_rating": "Tryk pÃĨ 0 for at fjerne fil vurderingen", "zoom_image": "Zoom billede", "zoom_to_bounds": "Zoom til grÃĻnserne" } diff --git a/i18n/de.json b/i18n/de.json index 94cfbba01f..b32ac57aba 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -2,9 +2,10 @@ "about": "Über Immich", "account": "Konto", "account_settings": "Kontoeinstellungen", - "acknowledge": "Bestätigen", + "acknowledge": "Verstanden", "action": "Aktion", "action_common_update": "Aktualisieren", + "action_description": "Eine Reihe von Aktionen, die an den gefilterten Assets ausgefÃŧhrt werden sollen", "actions": "Aktionen", "active": "Aktiv", "active_count": "Aktive:{count}", @@ -15,9 +16,14 @@ "add_a_location": "Standort hinzufÃŧgen", "add_a_name": "Name hinzufÃŧgen", "add_a_title": "Titel hinzufÃŧgen", + "add_action": "Aktion hinzufÃŧgen", + "add_action_description": "Klicken um eine Aktion hinzuzufÃŧgen", + "add_assets": "Assets hinzufÃŧgen", "add_birthday": "Geburtsdatum hinzufÃŧgen", "add_endpoint": "Endpunkt hinzufÃŧgen", "add_exclusion_pattern": "Ausschlussmuster hinzufÃŧgen", + "add_filter": "Filter hinzufÃŧgen", + "add_filter_description": "Klicken um eine Filterbedingung hinzuzufÃŧgen", "add_location": "Standort hinzufÃŧgen", "add_more_users": "Weitere Nutzer hinzufÃŧgen", "add_partner": "Partner hinzufÃŧgen", @@ -36,6 +42,7 @@ "add_to_shared_album": "Zu geteiltem Album hinzufÃŧgen", "add_upload_to_stack": "Upload zum Stapel hinzufÃŧgen", "add_url": "URL hinzufÃŧgen", + "add_workflow_step": "Workflow-Schritt hinzufÃŧgen", "added_to_archive": "Zum Archiv hinzugefÃŧgt", "added_to_favorites": "Zu Favoriten hinzugefÃŧgt", "added_to_favorites_count": "{count, number} zu Favoriten hinzugefÃŧgt", @@ -97,6 +104,8 @@ "image_preview_description": "Mittelgroßes Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und fÃŧr maschinelles Lernen verwendet wird", "image_preview_quality_description": "Vorschauqualität von 1-100. Ein hÃļherer Wert ist besser, erzeugt dadurch aber grÃļßere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Die Einstellung eines niedrigen Wertes kann dafÃŧr aber die Qualität des maschinellen Lernens beeinträchtigen.", "image_preview_title": "Vorschaueinstellungen", + "image_progressive": "Fortschrittlich", + "image_progressive_description": "JPEG-Bilder werden schrittweise kodiert, um ein stufenweises Laden zu ermÃļglichen. Dies hat keine Auswirkungen auf WebP-Bilder.", "image_quality": "Qualität", "image_resolution": "AuflÃļsung", "image_resolution_description": "HÃļhere AuflÃļsungen kÃļnnen mehr Details erhalten, benÃļtigen aber mehr Zeit fÃŧr die Kodierung, haben grÃļßere DateigrÃļßen und kÃļnnen die Reaktionsfähigkeit von Anwendungen beeinträchtigen.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Intelligente Suche aktivieren", "machine_learning_smart_search_enabled_description": "Ist diese Option deaktiviert, werden die Bilder nicht fÃŧr die intelligente Suche verwendet.", "machine_learning_url_description": "Die URL des Servers fÃŧr maschinelles Lernen. Wenn mehr als eine URL angegeben wird, wird jeder Server einzeln ausprobiert, bis einer erfolgreich antwortet, und zwar in der Reihenfolge vom ersten bis zum letzten. Server die nicht antworten werden temporär ignoriert, bis sie wieder verfÃŧgbar sind.", + "maintenance_delete_backup": "Backup lÃļschen", + "maintenance_delete_backup_description": "Diese Datei wird irreversibel gelÃļscht.", + "maintenance_delete_error": "Die LÃļschung der Sicherungskopie ist fehlgeschlagen.", + "maintenance_restore_backup": "Sicherungskopie wiederherstellen", + "maintenance_restore_backup_description": "Immich wird zurÃŧckgesetzt und von der ausgewählten Sicherungskopie wiederhergestellt. Ein Backup wird erstellt, bevor es weitergeht.", + "maintenance_restore_backup_different_version": "Diese Sicherungskopie wurde mit einer anderen Version von Immich erstellt!", + "maintenance_restore_backup_unknown_version": "Konnte Version der Sicherungskopie nicht erkennen.", + "maintenance_restore_database_backup": "Stelle Datenbankbackup wieder her", + "maintenance_restore_database_backup_description": "ZurÃŧckrollen zu einem vorherigen Datenbankzustand mit einem Backup", "maintenance_settings": "Wartung", "maintenance_settings_description": "Immich in den Wartungsmodus versetzen.", - "maintenance_start": "Wartungsmodus starten", + "maintenance_start": "In Wartungsmodus umschalten", "maintenance_start_error": "Wartungsmodus konnte nicht gestartet werden.", + "maintenance_upload_backup": "Lade Datenbankbackup hoch", + "maintenance_upload_backup_error": "Konnte Backup nicht hochladen. Ist es eine .sql/.sql.gz Datei?", "manage_concurrency": "Gleichzeitige AusfÃŧhrungen verwalten", "manage_concurrency_description": "Navigieren Sie zur Job-Seite, um die Job-Parallelität zu verwalten", "manage_log_settings": "Log-Einstellungen verwalten", @@ -222,7 +242,7 @@ "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_start_time_setting_description": "Die Zeit, zu welcher 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", @@ -252,7 +272,7 @@ "oauth_auto_register": "Automatische Registrierung", "oauth_auto_register_description": "Automatische Registrierung neuer Benutzer nach der OAuth-Anmeldung", "oauth_button_text": "Button-Text", - "oauth_client_secret_description": "Erforderlich wenn PKCE (Proof Key for Code Exchange) nicht vom OAuth- Anbieter unterstÃŧtzt wird", + "oauth_client_secret_description": "Erforderlich fÃŧr Confidential Clients oder wenn PKCE (Proof Key for Code Exchange) nicht fÃŧr Public Clients unterstÃŧtzt wird.", "oauth_enable_description": "Anmeldung mit OAuth", "oauth_mobile_redirect_uri": "Mobile Umleitungs-URI", "oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI Ãŧberschreiben", @@ -431,6 +451,9 @@ "admin_password": "Administrator Passwort", "administration": "Verwaltung", "advanced": "Erweitert", + "advanced_settings_clear_image_cache": "LÃļsche Bildercache", + "advanced_settings_clear_image_cache_error": "LÃļschung des Bildercaches misslungen", + "advanced_settings_clear_image_cache_success": "Erfolgreich gelÃļscht {size}", "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}", @@ -467,10 +490,12 @@ "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_selected": "Album ausgewählt", "album_share_no_users": "Es sieht so aus, als hättest du dieses Album mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", "album_summary": "Album Zusammenfassung", "album_updated": "Album aktualisiert", "album_updated_setting_description": "Erhalte eine E-Mail-Benachrichtigung, wenn ein freigegebenes Album neue Dateien enthält", + "album_upload_assets": "Assets vom Computer hochladen und zu Album hinzufÃŧgen", "album_user_left": "{album} verlassen", "album_user_removed": "{user} entfernt", "album_viewer_appbar_delete_confirm": "Bist du sicher, dass du dieses Album aus deinem Konto lÃļschen mÃļchtest?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# Album ausgewählt} other {# Alben ausgewählt}}", "all": "Alle", "all_albums": "Alle Alben", "all_people": "Alle Personen", + "all_photos": "Alle Fotos", "all_videos": "Alle Videos", "allow_dark_mode": "Dunkel-Modus erlauben", "allow_edits": "Bearbeiten erlauben", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Erlaube Ãļffentlichen Benutzern, hochzuladen", "allowed": "Erlaubt", "alt_text_qr_code": "QR-Code Bild", + "always_keep": "Immer behalten", + "always_keep_photos_hint": "Speicherfreigabe wird alle Fotos auf dem Gerät behalten.", + "always_keep_videos_hint": "Speicherfreigabe wird alle Videos auf dem Gerät behalten.", "anti_clockwise": "Gegen den Uhrzeigersinn", "api_key": "API-SchlÃŧssel", "api_key_description": "Dieser Wert wird nur einmal angezeigt. Bitte kopiere ihn, bevor du das Fenster schließt.", @@ -507,7 +537,7 @@ "app_bar_signout_dialog_content": "Bist du dir sicher, dass du dich abmelden mÃļchtest?", "app_bar_signout_dialog_ok": "Ja", "app_bar_signout_dialog_title": "Abmelden", - "app_download_links": "App Download Links", + "app_download_links": "App Download-Links", "app_settings": "App-Einstellungen", "app_stores": "App Stores", "app_update_available": "App Update verfÃŧgbar", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {# archiviert}}", "are_these_the_same_person": "Ist das dieselbe Person?", "are_you_sure_to_do_this": "Bist du sicher, dass du das tun willst?", + "array_field_not_fully_supported": "Array-Felder erfordern manuelle JSON-Bearbeitung", "asset_action_delete_err_read_only": "SchreibgeschÃŧtzte Inhalte kÃļnnen nicht gelÃļscht werden, Ãŧberspringen", "asset_action_share_err_offline": "Die Offline-Inhalte konnten nicht gelesen werden, Ãŧberspringen", "asset_added_to_album": "Zum Album hinzugefÃŧgt", "asset_adding_to_album": "HinzufÃŧgen zum Albumâ€Ļ", + "asset_created": "Datei erstellt", "asset_description_updated": "Die Beschreibung der Datei wurde aktualisiert", "asset_filename_is_offline": "Datei {filename} ist offline", "asset_has_unassigned_faces": "Datei hat nicht zugewiesene Gesichter", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Einstellungen fÃŧr das Fotogitter-Layout", "asset_list_settings_title": "Fotogitter", + "asset_not_found_on_device_android": "Datei auf Gerät nicht gefunden", + "asset_not_found_on_device_ios": "Datei auf Gerät nicht gefunden. Wenn Du iCloud verwendest, kann die Datei mÃļglicherweise aufgrund schlechter Dateispeicherung von iCloud nicht auffindbar sein", + "asset_not_found_on_icloud": "Datei in iCloud nicht gefunden. Die Datei kann mÃļglicherweise aufgrund schlechter Dateispeicherung in iCloud nicht auffindbar sein", "asset_offline": "Datei offline", "asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.", "asset_restored_successfully": "Datei erfolgreich wiederhergestellt", @@ -575,14 +610,14 @@ "assets_were_part_of_album_count": "{count, plural, one {# Datei ist} other {# Dateien sind}} bereits im Album vorhanden", "assets_were_part_of_albums_count": "{count, plural, one {Datei war} other {Dateien waren}} bereits in den Alben", "authorized_devices": "Verwendete Geräte", - "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal Ãŧber ein bestimmtes WLAN, wenn es verfÃŧgbar ist, und verwenden Sie andere VerbindungsmÃļglichkeiten anderswo", + "automatic_endpoint_switching_subtitle": "Verbinden Sie sich lokal Ãŧber ein bestimmtes WiFi, wenn es verfÃŧgbar ist, und verwenden Sie andere VerbindungsmÃļglichkeiten", "automatic_endpoint_switching_title": "Automatische URL-Umschaltung", "autoplay_slideshow": "Automatische Diashow", "back": "ZurÃŧck", "back_close_deselect": "ZurÃŧck, Schließen oder Abwählen", "background_backup_running_error": "Sicherung läuft im Hintergrund. Manuelle Sicherung kann nicht gestartet werden", "background_location_permission": "Hintergrund Standortfreigabe", - "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu kÃļnnen, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WLAN-Netzwerks ermitteln kann", + "background_location_permission_content": "Um im Hintergrund zwischen den Netzwerken wechseln zu kÃļnnen, muss Immich *immer* Zugriff auf den genauen Standort haben, damit die App den Namen des WiFi-Netzwerks ermitteln kann", "background_options": "Hintergrund Optionen", "backup": "Sicherung", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({count})", @@ -617,7 +652,7 @@ "backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert", "backup_controller_page_background_turn_off": "Hintergrundservice ausschalten", "backup_controller_page_background_turn_on": "Hintergrundservice einschalten", - "backup_controller_page_background_wifi": "Nur im WLAN", + "backup_controller_page_background_wifi": "Nur im WiFi", "backup_controller_page_backup": "Sicherung", "backup_controller_page_backup_selected": "Ausgewählt: ", "backup_controller_page_backup_sub": "Gesicherte Fotos und Videos", @@ -711,17 +746,31 @@ "change_password_form_password_mismatch": "PasswÃļrter stimmen nicht Ãŧberein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", "change_pin_code": "PIN-Code ändern", + "change_trigger": "AuslÃļser ändern", + "change_trigger_prompt": "Bist du sicher, dass du den AuslÃļser ändern willst? Dies entfernt alle bestehenden Aktionen und Filter.", "change_your_password": "Ändere dein Passwort", "changed_visibility_successfully": "Die Sichtbarkeit wurde erfolgreich geändert", "charging": "Aufladen", "charging_requirement_mobile_backup": "Backup im Hintergrund erfordert Aufladen des Geräts", "check_corrupt_asset_backup": "Auf beschädigte Asset-Backups ÃŧberprÃŧfen", "check_corrupt_asset_backup_button": "ÜberprÃŧfung durchfÃŧhren", - "check_corrupt_asset_backup_description": "FÃŧhre diese PrÃŧfung nur mit aktivierten WLAN durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.", + "check_corrupt_asset_backup_description": "FÃŧhre diese PrÃŧfung nur mit aktivierten WiFi durch, nachdem alle Dateien gesichert worden sind. Dieser Vorgang kann ein paar Minuten dauern.", "check_logs": "Logs prÃŧfen", "checksum": "PrÃŧfsumme", "choose_matching_people_to_merge": "Wähle passende Personen zum ZusammenfÃŧhren", "city": "Stadt", + "cleanup_confirm_description": "Immich hat {count} Dateien (vor dem {date} erstellt) sicher auf dem Server gefunden. Sollen die lokalen Kopien von diesem Gerät gelÃļscht werden?", + "cleanup_confirm_prompt_title": "Von diesem Gerät entfernen?", + "cleanup_deleted_assets": "{count} Dateien in den lokalen Papierkorb verschoben", + "cleanup_deleting": "In den Papierkorb verschiebenâ€Ļ", + "cleanup_found_assets": "{count} hochgeladene Dateien gefunden", + "cleanup_found_assets_with_size": "{count} gesicherte Dateien gefunden ({size})", + "cleanup_icloud_shared_albums_excluded": "Geteilte Alben aus iCloud sind vom Scan ausgeschlossen", + "cleanup_no_assets_found": "Keine passenden Assets gefunden. Speicherbereinigung kann nur auf Assets angewendet werden, die bereits auf den Server gesichert wurden", + "cleanup_preview_title": "Zu lÃļschende Assets ({count})", + "cleanup_step3_description": "Nach gesicherten Mediendateien scannen, die mit den Filterkriterien und gespeicherten Einstellungen Ãŧbereinstimmen.", + "cleanup_step4_summary": "{count} Assets, die vor dem {date} erstellt wurden, warten auf LÃļschung von Ihrem Gerät. Die Photos werden auch weiterhin Ãŧber die Immich-App verfÃŧgbar sein.", + "cleanup_trash_hint": "Um den Speicher vollständig freizugeben, Ãļffnen Sie die Galerie-App und leeren Sie den Papierkorb", "clear": "Leeren", "clear_all": "Alles leeren", "clear_all_recent_searches": "Alle letzten Suchvorgänge lÃļschen", @@ -733,6 +782,8 @@ "client_cert_import": "Importieren", "client_cert_import_success_msg": "Client Zertifikat wurde importiert", "client_cert_invalid_msg": "UngÃŧltige Zertifikatsdatei oder falsches Passwort", + "client_cert_password_message": "Passwort fÃŧr dieses Zertifikat angeben", + "client_cert_password_title": "Passwort des Zertifikats", "client_cert_remove_msg": "Client Zertifikat wurde entfernt", "client_cert_subtitle": "UnterstÃŧtzt nur das PKCS12 (.p12, .pfx) Format. Zertifikatsimporte oder -entfernungen sind nur vor dem Login mÃļglich", "client_cert_title": "SSL-Client-Zertifikat [Experimentell]", @@ -787,6 +838,7 @@ "create_album": "Album erstellen", "create_album_page_untitled": "Unbenannt", "create_api_key": "API Key erstellen", + "create_first_workflow": "Ersten Workflow erstellen", "create_library": "Bibliothek erstellen", "create_link": "Link erstellen", "create_link_to_share": "Link zum Teilen erstellen", @@ -801,17 +853,25 @@ "create_tag": "Tag erstellen", "create_tag_description": "Erstelle einen neuen Tag. FÃŧr verschachtelte Tags, gib den gesamten Pfad inklusive Schrägstrich an.", "create_user": "Nutzer erstellen", + "create_workflow": "Workflow erstellen", "created": "Erstellt", "created_at": "Erstellt", "creating_linked_albums": "Erstelle verknÃŧpfte Alben...", "crop": "Zuschneiden", + "crop_aspect_ratio_fixed": "Fixiert", + "crop_aspect_ratio_free": "Frei", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Dinge", "current_device": "Aktuelles Gerät", "current_pin_code": "Aktueller PIN-Code", "current_server_address": "Aktuelle Serveradresse", + "custom_date": "Benutzerdefiniertes Datum", "custom_locale": "Benutzerdefinierte Sprache", "custom_locale_description": "Datumsangaben und Zahlen je nach Sprache und Land formatieren", "custom_url": "Benutzerdefinierte URL", + "cutoff_date_description": "Behalte Fotos der letztenâ€Ļ", + "cutoff_day": "{count, plural, one {Tag} other {Tage}}", + "cutoff_year": "{count, plural, one {Jahr} other {Jahre}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Dunkel", @@ -867,6 +927,7 @@ "deselect_all": "Alle abwählen", "details": "Details", "direction": "Richtung", + "disable": "Deaktivieren", "disabled": "Deaktiviert", "disallow_edits": "Bearbeitungen verbieten", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Eingebettete Videos", "download_include_embedded_motion_videos_description": "Videos, die in Bewegungsfotos eingebettet sind, als separate Datei einfÃŧgen", "download_notfound": "Download nicht gefunden", + "download_original": "Original herunterladen", "download_paused": "Download pausiert", "download_settings": "Download", "download_settings_description": "Einstellungen fÃŧr das Herunterladen von Dateien verwalten", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Warte auf erneuten Versuch", "downloading": "Herunterladen", "downloading_asset_filename": "Datei {filename} wird heruntergeladen", + "downloading_from_icloud": "von iCloud herunterladen", "downloading_media": "Medien werden heruntergeladen", "drop_files_to_upload": "Lade Dateien hoch, indem du sie hierhin ziehst", "duplicates": "Duplikate", @@ -929,11 +992,22 @@ "edit_tag": "Tag bearbeiten", "edit_title": "Titel bearbeiten", "edit_user": "Nutzer bearbeiten", - "editor": "Bearbeiter", + "edit_workflow": "Workflow bearbeiten", + "editor": "Bearbeiten", "editor_close_without_save_prompt": "Die Änderungen werden nicht gespeichert", "editor_close_without_save_title": "Editor schließen?", - "editor_crop_tool_h2_aspect_ratios": "Seitenverhältnisse", - "editor_crop_tool_h2_rotation": "Drehung", + "editor_confirm_reset_all_changes": "Alle Änderungen zurÃŧcksetzen?", + "editor_discard_edits_confirm": "Änderungen verwerfen", + "editor_discard_edits_prompt": "Es liegen ungespeicherte Änderungen vorhanden. Sicher, dass diese verworfen werden sollen?", + "editor_discard_edits_title": "Änderungen verwerfen?", + "editor_edits_applied_error": "Änderungen konnten nicht angewendet werden", + "editor_edits_applied_success": "Änderungen erfolgreich angewendet", + "editor_flip_horizontal": "Horizontal spiegeln", + "editor_flip_vertical": "Vertikal spiegeln", + "editor_orientation": "Ausrichtung", + "editor_reset_all_changes": "Änderungen zurÃŧcksetzen", + "editor_rotate_left": "Um 90° gegen den Uhrzeigersinn drehen", + "editor_rotate_right": "Um 90° im Uhrzeigersinn drehen", "email": "E-Mail", "email_notifications": "E-Mail Benachrichtigungen", "empty_folder": "Dieser Ordner ist leer", @@ -945,18 +1019,21 @@ "enabled": "Aktiviert", "end_date": "Enddatum", "enqueued": "Eingereiht", - "enter_wifi_name": "WLAN-Name eingeben", + "enter_wifi_name": "WiFi-Name eingeben", "enter_your_pin_code": "PIN-Code eingeben", "enter_your_pin_code_subtitle": "Gib deinen PIN-Code ein, um auf den gesperrten Ordner zuzugreifen", "error": "Fehler", "error_change_sort_album": "Ändern der Anzeigereihenfolge fehlgeschlagen", "error_delete_face": "Fehler beim LÃļschen des Gesichts", "error_getting_places": "Fehler beim Abrufen der Orte", + "error_loading_albums": "Fehler beim Laden der Alben", "error_loading_image": "Fehler beim Laden des Bildes", "error_loading_partners": "Fehler beim Laden der Partner: {error}", + "error_retrieving_asset_information": "Fehler beim Abruf der Dateiinformationen", "error_saving_image": "Fehler: {error}", "error_tag_face_bounding_box": "Fehler beim Markieren des Gesichts - Begrenzungen kÃļnnen nicht abgerufen werden", "error_title": "Fehler - Etwas ist schief gelaufen", + "error_while_navigating": "Fehler beim Navigieren zur Datei", "errors": { "cannot_navigate_next_asset": "Kann nicht zur nächsten Datei navigieren", "cannot_navigate_previous_asset": "Kann nicht zur vorherigen Datei navigieren", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "OAuth-Anmeldung konnte nicht abgeschlossen werden", "unable_to_connect": "Verbindung konnte nicht hergestellt werden", "unable_to_copy_to_clipboard": "Konnte nicht in die Zwischenablage kopieren, stelle sicher, dass du per https auf die Seite zugreifst", + "unable_to_create": "Workflow konnte nicht erstellt werden", "unable_to_create_admin_account": "Administratorkonto konnte nicht erstellt werden", "unable_to_create_api_key": "Es konnte kein API-SchlÃŧssel erstellt werden", "unable_to_create_library": "Bibliothek konnte nicht erstellt werden", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Ausschlussmuster konnte nicht gelÃļscht werden", "unable_to_delete_shared_link": "Geteilter Link kann nicht gelÃļscht werden", "unable_to_delete_user": "Nutzer konnte nicht gelÃļscht werden", + "unable_to_delete_workflow": "Workflow konnte nicht gelÃļscht werden", "unable_to_download_files": "Dateien konnten nicht heruntergeladen werden", "unable_to_edit_exclusion_pattern": "Ausschlussmuster konnte nicht bearbeitet werden", "unable_to_empty_trash": "Papierkorb konnte nicht geleert werden", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "Bibliothek konnte nicht gescannt werden", "unable_to_set_feature_photo": "Hauptfoto konnte nicht festgelegt werden", "unable_to_set_profile_picture": "Profilbild konnte nicht gesetzt werden", + "unable_to_set_rating": "Bewertung konnte nicht gespeichert werden", "unable_to_submit_job": "Aufgabe konnte nicht eingereicht werden", "unable_to_trash_asset": "Objekte konnten nicht gelÃļscht werden", "unable_to_unlink_account": "Die VerknÃŧpfung des Kontos kann nicht aufgehoben werden", @@ -1074,8 +1154,10 @@ "unable_to_update_settings": "Die Einstellungen konnten nicht aktualisiert werden", "unable_to_update_timeline_display_status": "Status der Zeitleistenanzeige konnte nicht aktualisiert werden", "unable_to_update_user": "Der Nutzer konnte nicht aktualisiert werden", + "unable_to_update_workflow": "Workflow konnte nicht aktualisiert werden", "unable_to_upload_file": "Datei konnte nicht hochgeladen werden" }, + "errors_text": "Fehler", "exclusion_pattern": "Ausschlussmuster", "exif": "EXIF", "exif_bottom_sheet_description": "Beschreibung hinzufÃŧgen...", @@ -1120,14 +1202,17 @@ "features": "Funktionen", "features_in_development": "Feature in Entwicklung", "features_setting_description": "Funktionen der App verwalten", - "file_name": "Dateiname", "file_name_or_extension": "Dateiname oder -erweiterung", + "file_name_text": "Dateiname", + "file_name_with_value": "Dateiname: {file_name}", "file_size": "DateigrÃļße", "filename": "Dateiname", "filetype": "Dateityp", "filter": "Filter", + "filter_description": "Bedingungen zur Filterung der betreffenden Dateien", "filter_people": "Personen filtern", "filter_places": "Orte filtern", + "filters": "Filter", "find_them_fast": "Finde sie schneller mit der Suche nach Namen", "first": "Erste", "fix_incorrect_match": "Fehlerhafte Übereinstimmung beheben", @@ -1137,13 +1222,17 @@ "folders_feature_description": "Durchsuchen der Ordneransicht fÃŧr Fotos und Videos im Dateisystem", "forgot_pin_code_question": "PIN-Code vergessen?", "forward": "Vorwärts", + "free_up_space": "Speicherplatz freigeben", + "free_up_space_description": "Bewege Fotos und Videos, die bereits gesichert wurden, in den Papierkorb auf deinem Gerät. Die Kopie auf dem Server bleibt unberÃŧhrt.", + "free_up_space_settings_subtitle": "Gerätespeicher freigeben", "full_path": "Vollständiger Pfad: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Diese Funktion lädt externe Quellen von Google, um zu funktionieren.", "general": "Allgemein", "geolocation_instruction_location": "Klicke auf eine Datei mit GPS Koordinaten um diesen Standort zu verwenden oder wähle einen Standort direkt auf der Karte", "get_help": "Hilfe erhalten", - "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", + "get_people_error": "Fehler beim Laden der Personen", + "get_wifiname_error": "WiFi-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WiFi-Netzwerk verbunden bist", "getting_started": "Erste Schritte", "go_back": "ZurÃŧck", "go_to_folder": "Gehe zu Ordner", @@ -1175,6 +1264,7 @@ "hide_named_person": "Person {name} verbergen", "hide_password": "Passwort verbergen", "hide_person": "Person verbergen", + "hide_schema": "Schema ausblenden", "hide_text_recognition": "Texterkennung verbergen", "hide_unnamed_people": "Unbenannte Personen verbergen", "home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefÃŧgt. {failed} Elemente sind bereits vorhanden.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Prozess läuft {dateTime}", "items_count": "{count, plural, one {# Eintrag} other {# Einträge}}", "jobs": "Aufgaben", + "json_editor": "JSON-Editor", + "json_error": "JSON-Fehler", "keep": "Behalten", + "keep_albums": "Alben behalten", + "keep_albums_count": "Behalte {count} {count, plural, one {album} other {albums}}", "keep_all": "Alle behalten", + "keep_description": "Wähle aus, was beim Speicher freigeben auf dem Gerät behalten werden soll.", + "keep_favorites": "Favoriten behalten", + "keep_on_device": "Auf Gerät behalten", + "keep_on_device_hint": "Wähle die Elemente, die auf dem Gerät bleiben sollen", "keep_this_delete_others": "Dieses behalten, andere lÃļschen", + "keeping": "Behalte: {items}", "kept_this_deleted_others": "Diese Datei behalten und {count, plural, one {# Datei} other {# Dateien}} gelÃļscht", "keyboard_shortcuts": "TastenkÃŧrzel", "language": "Sprache", @@ -1297,7 +1396,7 @@ "local_network_sheet_info": "Die App stellt Ãŧber diese URL eine Verbindung zum Server her, wenn sie das angegebene WLAN-Netzwerk verwendet", "location": "Standort", "location_permission": "Standort Genehmigung", - "location_permission_content": "Um die automatische Umschaltfunktion nutzen zu kÃļnnen, benÃļtigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WLAN-Netzwerks ermitteln kann", + "location_permission_content": "Um die automatische Umschaltfunktion nutzen zu kÃļnnen, benÃļtigt Immich genaue Standortberechtigung, damit es den Namen des aktuellen WiFi-Netzwerks ermitteln kann", "location_picker_choose_on_map": "Auf der Karte auswählen", "location_picker_latitude_error": "GÃŧltigen Breitengrad eingeben", "location_picker_latitude_hint": "Breitengrad eingeben", @@ -1343,10 +1442,28 @@ "loop_videos_description": "Aktiviere diese Option, um eine automatische Videoschleife in der Detailansicht zu erstellen.", "main_branch_warning": "Du benutzt eine Entwicklungsversion. Wir empfehlen dringend, eine Release-Version zu verwenden!", "main_menu": "HauptmenÃŧ", + "maintenance_action_restore": "Datenbank wird wiederhergestellt", "maintenance_description": "Immich wurde in den Wartungsmodus versetzt.", "maintenance_end": "Wartungsmodus beenden", "maintenance_end_error": "Wartungsmodus konnte nicht beendet werden.", "maintenance_logged_in_as": "Aktuell angemeldet als {user}", + "maintenance_restore_from_backup": "Von Datenbank wiederherstellen", + "maintenance_restore_library": "Deine Bibliothek wiederherstellen", + "maintenance_restore_library_confirm": "Wenn das korrekt aussieht, mache weiter mit der Wiederherstellung des Backups!", + "maintenance_restore_library_description": "Datenbank wird wiederhergestellt", + "maintenance_restore_library_folder_has_files": "{folder} hat {count} Ordner", + "maintenance_restore_library_folder_no_files": "{folder} fehlen Dateien!", + "maintenance_restore_library_folder_pass": "lesbar und schreibbar", + "maintenance_restore_library_folder_read_fail": "nicht lesbar", + "maintenance_restore_library_folder_write_fail": "nicht schreibbar", + "maintenance_restore_library_hint_missing_files": "Es kÃļnnten dir wichtige Dateien fehlen", + "maintenance_restore_library_hint_regenerate_later": "Sie kÃļnnen diese später in den Einstellungen erneut generieren", + "maintenance_restore_library_hint_storage_template_missing_files": "Speichervorlage verwendet? Es kÃļnnten wichtige Dateien fehlen", + "maintenance_restore_library_loading": "Lade IntegritätsprÃŧfungen und Heuristikenâ€Ļ", + "maintenance_task_backup": "Erstelle ein Backup der vorhandenen Datenbankâ€Ļ", + "maintenance_task_migrations": "Datenbankmigrationen laufenâ€Ļ", + "maintenance_task_restore": "Ausgewählte Sicherungskopie wird wiederhergestelltâ€Ļ", + "maintenance_task_rollback": "Wiederherstellen scheiterte, zurÃŧck zu Wiederherstellungspunktâ€Ļ", "maintenance_title": "VorrÃŧbergehend nicht verfÃŧgbar", "make": "Marke", "manage_geolocation": "Standort verwalten", @@ -1408,6 +1525,8 @@ "minimize": "Minimieren", "minute": "Minute", "minutes": "Minuten", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertikal", "missing": "Fehlende", "mobile_app": "Mobile App", "mobile_app_download_onboarding_note": "Herunterladen der mobilen Begleiter-App Ãŧber einen der folgenden MÃļglichkeiten", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mehr", "move": "Verschieben", + "move_down": "Nach unten", "move_off_locked_folder": "Aus dem gesperrten Ordner verschieben", "move_to": "Verschieben nach", + "move_to_device_trash": "In Papierkorb 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", + "move_up": "Nach oben", "moved_to_archive": "{count, plural, one {# Datei} other {# Dateien}} archiviert", "moved_to_library": "{count, plural, one {# Datei} other {# Dateien}} in die Bibliothek verschoben", "moved_to_trash": "In den Papierkorb verschoben", @@ -1430,6 +1552,7 @@ "my_albums": "Meine Alben", "name": "Name", "name_or_nickname": "Name oder Nickname", + "name_required": "Name ist erforderlich", "navigate": "Navigation", "navigate_to_time": "Navigiere zu Zeit", "network_requirement_photos_upload": "Mobile Daten verwenden, um Fotos zu sichern", @@ -1454,20 +1577,24 @@ "next": "Weiter", "next_memory": "Nächste Erinnerung", "no": "Nein", + "no_actions_added": "Noch keine Aktionen hinzugefÃŧgt", + "no_albums_found": "Keine Alben gefunden", "no_albums_message": "Erstelle ein Album, um deine Fotos und Videos zu organisieren", "no_albums_with_name_yet": "Es sieht so aus, als hättest du noch keine Alben mit diesem Namen.", "no_albums_yet": "Es sieht so aus, als hättest du noch keine Alben.", "no_archived_assets_message": "Archiviere Fotos und Videos, um sie aus deiner Fotoansicht zu entfernen", - "no_assets_message": "KLICKE, UM DEIN ERSTES FOTO HOCHZULADEN", + "no_assets_message": "Klicke, um dein erstes Foto hochzuladen", "no_assets_to_show": "Keine Vorschau vorhanden", "no_cast_devices_found": "Keine Geräte zum Übertragen gefunden", "no_checksum_local": "PrÃŧfsumme nicht verfÃŧgbar - kann lokale Datei/en nicht laden", "no_checksum_remote": "PrÃŧfsumme nicht verfÃŧgbar - kann entfernte Datei/en nicht laden", + "no_configuration_needed": "Keine Konfiguration benÃļtigt", "no_devices": "Keine verwendeten Geräte", "no_duplicates_found": "Es wurden keine Duplikate gefunden.", "no_exif_info_available": "Keine EXIF-Informationen vorhanden", "no_explore_results_message": "Lade weitere Fotos hoch, um deine Sammlung zu erkunden.", "no_favorites_message": "FÃŧge Favoriten hinzu, um deine besten Bilder und Videos schnell zu finden", + "no_filters_added": "Noch keine Filter hinzugefÃŧgt", "no_libraries_message": "Eine externe Bibliothek erstellen, um deine Fotos und Videos anzusehen", "no_local_assets_found": "Keine lokale Datei mit dieser PrÃŧfsumme gefunden", "no_location_set": "Kein Standort festgelegt", @@ -1481,11 +1608,11 @@ "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", + "none": "Keine", "not_allowed": "Nicht erlaubt", "not_available": "N/A", "not_in_any_album": "In keinem Album", "not_selected": "Nicht ausgewählt", - "note_apply_storage_label_to_previously_uploaded assets": "Hinweis: Um eine Speicherpfadbezeichnung anzuwenden, starte den", "notes": "Notizen", "nothing_here_yet": "Noch nichts hier", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\".", @@ -1563,6 +1690,7 @@ "people": "Personen", "people_edits_count": "{count, plural, one {# Person} other {# Personen}} bearbeitet", "people_feature_description": "Fotos und Videos nach Personen gruppiert durchsuchen", + "people_selected": "{count, plural, one {# Person ausgewählt} other {# Personen ausgewählt}}", "people_sidebar_description": "Eine VerknÃŧpfung zu Personen in der Seitenleiste anzeigen", "permanent_deletion_warning": "Warnung vor endgÃŧltiger LÃļschung", "permanent_deletion_warning_setting_description": "Anzeige einer Warnung beim endgÃŧltigen LÃļschen von Objekten", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, one {# Jahr} other {# Jahre}} alt", "person_birthdate": "Geboren am {date}", "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}", + "person_recognized": "Person erkannt", + "person_selected": "Person ausgewählt", "photo_shared_all_users": "Es sieht so aus, als hättest du deine Fotos mit allen Benutzern geteilt oder du hast keine Benutzer, mit denen du teilen kannst.", "photos": "Fotos", "photos_and_videos": "Fotos & Videos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos von vorherigen Jahren", + "photos_only": "Nur Fotos", "pick_a_location": "Wähle einen Ort", "pick_custom_range": "Benutzerdefinierter Zeitraum", "pick_date_range": "Wähle einen Zeitraum", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Der Server-ProduktschlÃŧssel wird durch den Administrator verwaltet", "query_asset_id": "Datei-ID abfragen", "queue_status": "Warteschlange {count}/{total}", + "rate_asset": "Datei bewerten", "rating": "Bewertung", "rating_clear": "Bewertung lÃļschen", "rating_count": "{count, plural, one {# Stern} other {# Sterne}}", "rating_description": "Stellt die EXIF-Bewertung im Informationsbereich dar", + "rating_set": "Mit {rating, plural, one {# Stern} other {# Sternen}} bewertet", "reaction_options": "ReaktionsmÃļglichkeiten", "read_changelog": "Changelog lesen", "readonly_mode_disabled": "SchreibgeschÃŧtzter Modus deaktiviert", @@ -1680,8 +1813,8 @@ "reassigned_assets_to_existing_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} {name, select, null {einer vorhandenen Person} other {{name}}} zugewiesen", "reassigned_assets_to_new_person": "{count, plural, one {# Datei wurde} other {# Dateien wurden}} einer neuen Person zugewiesen", "reassing_hint": "Markierte Dateien einer vorhandenen Person zuweisen", - "recent": "Neuste", - "recent-albums": "Neuste Alben", + "recent": "Neueste", + "recent_albums": "Neueste Alben", "recent_searches": "Letzte Suchen", "recently_added": "KÃŧrzlich hinzugefÃŧgt", "recently_added_page_title": "Zuletzt hinzugefÃŧgt", @@ -1770,14 +1903,16 @@ "saved_settings": "Einstellungen gespeichert", "say_something": "Etwas sagen", "scaffold_body_error_occurred": "Ein Fehler ist aufgetreten", + "scan": "Scannen", "scan_all_libraries": "Alle Bibliotheken scannen", "scan_library": "Scannen", "scan_settings": "Scan-Einstellungen", + "scanning": "Scanne", "scanning_for_album": "Nach Alben scannen...", "search": "Suche", "search_albums": "Album suchen", "search_by_context": "Suche nach Kontext", - "search_by_description": "Nach Beschreibung suchen", + "search_by_description": "Suche nach Beschreibung", "search_by_description_example": "Wandern in Sapa", "search_by_filename": "Suche nach Dateiname oder -erweiterung", "search_by_filename_example": "z.B. IMG_1234.JPG oder PNG", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Medientyp auswählen", "search_filter_ocr": "Suche per OCR", "search_filter_people_title": "Personen auswählen", + "search_filter_star_rating": "Sternebewertung", "search_for": "Suche nach", "search_for_existing_person": "Suche nach vorhandener Person", "search_no_more_result": "Keine weiteren Ergebnisse", @@ -1828,7 +1964,7 @@ "search_state": "Suche nach Bundesland / Provinz...", "search_suggestion_list_smart_search_hint_1": "Intelligente Suche ist standardmäßig aktiviert; um nach Metadaten zu suchen, folgenden Syntax benutzen: ", "search_suggestion_list_smart_search_hint_2": "m:dein-suchbegriff", - "search_tags": "Sache nach Tags...", + "search_tags": "Suche nach Tags...", "search_timezone": "Suche nach Zeitzone...", "search_type": "Suche nach Typ", "search_your_photos": "Durchsuche deine Fotos", @@ -1836,17 +1972,23 @@ "second": "Sekunde", "see_all_people": "Alle Personen anzeigen", "select": "Auswählen", + "select_album": "Album auswählen", "select_album_cover": "Album-Cover auswählen", + "select_albums": "Alben auswählen", "select_all": "Alles auswählen", "select_all_duplicates": "Alle Duplikate auswählen", "select_all_in": "Alle in {group} auswählen", "select_avatar_color": "Avatar-Farbe auswählen", + "select_count": "{count, plural, one {Wähle #} other {Wähle #}}", + "select_cutoff_date": "Stichtag auswählen", "select_face": "Gesicht auswählen", "select_featured_photo": "Anzeigebild auswählen", "select_from_computer": "Vom Computer auswählen", "select_keep_all": "Alle behalten", "select_library_owner": "Bibliotheksbesitzer auswählen", "select_new_face": "Neues Gesicht auswählen", + "select_people": "Personen auswählen", + "select_person": "Person auswählen", "select_person_to_tag": "Wählen Sie eine Person zum Markieren aus", "select_photos": "Fotos auswählen", "select_trash_all": "Alle lÃļschen", @@ -1982,6 +2124,7 @@ "show_password": "Passwort anzeigen", "show_person_options": "Personen-Optionen anzeigen", "show_progress_bar": "Fortschrittsbalken anzeigen", + "show_schema": "Schema anzeigen", "show_search_options": "Suchoptionen anzeigen", "show_shared_links": "Zeige geteilte Links", "show_slideshow_transition": "Slideshow-Übergang anzeigen", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Springe zu Ordnern", "skip_to_tags": "Springe zu Tags", "slideshow": "Diashow", + "slideshow_repeat": "Slideshow wiederholen", + "slideshow_repeat_description": "Wenn Slideshow beendet, zum Anfang zurÃŧckkehren", "slideshow_settings": "Diashow-Einstellungen", "sort_albums_by": "Alben sortieren nach...", "sort_created": "Erstellungsdatum", @@ -2007,7 +2152,7 @@ "sort_newest": "Neuestes Foto", "sort_oldest": "Ältestes Foto", "sort_people_by_similarity": "Personen nach Ähnlichkeit sortieren", - "sort_recent": "Neustes Foto", + "sort_recent": "Neuestes Foto", "sort_title": "Titel", "source": "Quellcode", "stack": "Stapel", @@ -2065,7 +2210,7 @@ "theme_setting_asset_list_storage_indicator_title": "Fortschrittsbalken der Sicherung auf dem Vorschaubild", "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({count})", "theme_setting_colorful_interface_subtitle": "Primärfarbe auf App-Hintergrund anwenden.", - "theme_setting_colorful_interface_title": "Farbige UI-Oberfläche", + "theme_setting_colorful_interface_title": "Farbige Benutzeroberfläche", "theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters", "theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters", "theme_setting_primary_color_subtitle": "Farbauswahl fÃŧr primäre Aktionen und Akzente.", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App", "theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhÃļht allerdings den Datenverbrauch deutlich", "theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren", + "then": "Dann", "they_will_be_merged_together": "Sie werden zusammengefÃŧhrt", "third_party_resources": "Drittanbieter-Quellen", "time": "Zeit", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Elemente auswählen", "trash_page_title": "Papierkorb ({count})", "trashed_items_will_be_permanently_deleted_after": "Objekte im Papierkorb werden nach {days, plural, one {# Tag} other {# Tagen}} endgÃŧltig gelÃļscht.", + "trigger": "AuslÃļser", + "trigger_asset_uploaded": "Datei hochgeladen", + "trigger_asset_uploaded_description": "LÃļst aus, wenn eine neue Datei hochgeladen wurde", + "trigger_description": "Ein Ereignis, das den Workflow startet", + "trigger_person_recognized": "Person erkannt", + "trigger_person_recognized_description": "LÃļst aus, wenn eine Person erkannt wird", + "trigger_type": "AuslÃļser-Typ", "troubleshoot": "Fehler beheben", "type": "Typ", "unable_to_change_pin_code": "PIN-Code konnte nicht geändert werden", @@ -2123,6 +2276,7 @@ "unhide_person": "Person einblenden", "unknown": "Unbekannt", "unknown_country": "Unbekanntes Land", + "unknown_date": "Unbekanntes Datum", "unknown_year": "Unbekanntes Jahr", "unlimited": "Unlimitiert", "unlink_motion_video": "VerknÃŧpfung zum Bewegungsvideo aufheben", @@ -2139,17 +2293,19 @@ "unstack": "Entstapeln", "unstack_action_prompt": "{count} entstapelt", "unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt", + "unsupported_field_type": "Nicht unterstÃŧtzter Feldtyp", "untagged": "Ohne Tag", + "untitled_workflow": "Unbenannter Workflow", "up_next": "Weiter", "update_location_action_prompt": "Aktualsiere den Ort von {count} ausgewählten Dateien mit:", "updated_at": "Aktualisiert", "updated_password": "Passwort aktualisiert", "upload": "Hochladen", - "upload_action_prompt": "{count} in der Warteschlange fÃŧr Upload", "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_error_with_count": "Uploadfehler fÃŧr {count, plural, one {# asset} other {# assets}}", "upload_errors": "Hochladen mit {count, plural, one {# Fehler} other {# Fehlern}} abgeschlossen, aktualisiere die Seite, um neu hochgeladene Dateien zu sehen.", "upload_finished": "Upload fertig", "upload_progress": "{remaining, number} verbleibend - {processed, number}/{total, number} verarbeitet", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "Verwendung", "use_biometric": "Biometrie verwenden", - "use_current_connection": "aktuelle Verbindung verwenden", + "use_current_connection": "Aktuelle Verbindung verwenden", "use_custom_date_range": "Stattdessen einen benutzerdefinierten Datumsbereich verwenden", "user": "Nutzer", "user_has_been_deleted": "Dieser Benutzer wurde gelÃļscht.", @@ -2185,6 +2341,7 @@ "utilities": "Werkzeuge", "validate": "Validieren", "validate_endpoint_error": "Bitte gib eine gÃŧltige URL ein", + "validation_error": "Validierungsfehler", "variables": "Variablen", "version": "Version", "version_announcement_closing": "Dein Freund, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Spiele die Miniaturansicht des Videos ab, wenn sich die Maus Ãŧber dem Element befindet. Auch wenn die Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem du mit der Maus Ãŧber das Wiedergabesymbol fährst.", "videos": "Videos", "videos_count": "{count, plural, one {# Video} other {# Videos}}", + "videos_only": "Nur Videos", "view": "Ansicht", "view_album": "Album anzeigen", "view_all": "Alles anzeigen", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "An Stapelanfang", "viewer_unstack": "Stapel aufheben", "visibility_changed": "Sichtbarkeit fÃŧr {count, plural, one {# Person} other {# Personen}} geändert", + "visual": "Visuell", + "visual_builder": "Visueller Editor", "waiting": "Wartend", "waiting_count": "In Warteschlage: {count}", "warning": "Warnung", @@ -2223,14 +2383,27 @@ "welcome": "Willkommen", "welcome_to_immich": "Willkommen bei Immich", "width": "Breite", - "wifi_name": "WLAN-Name", - "workflow": "Workflow", + "wifi_name": "WiFi-Name", + "workflow_delete_prompt": "Bist du sicher, dass du diesen Workflow lÃļschen willst?", + "workflow_deleted": "Workflow gelÃļscht", + "workflow_description": "Workflow-Beschreibung", + "workflow_info": "Workflow-Info", + "workflow_json": "Workflow JSON", + "workflow_json_help": "Workflow-Konfiguration im JSON-Editor bearbeiten. Änderungen werden mit dem visuellen Editor synchronisiert.", + "workflow_name": "Workflow-Name", + "workflow_navigation_prompt": "Bist du sicher, dass du den Editor ohne zu speichern verlassen willst?", + "workflow_summary": "Workflow-Zusammenfassung", + "workflow_update_success": "Workflow erfolgreich aktualisiert", + "workflow_updated": "Workflow aktualisiert", + "workflows": "Workflows", + "workflows_help_text": "Workflows automatisieren Aktionen auf deinen Dateien, basierend auf AuslÃļsern und Filtern", "wrong_pin_code": "PIN-Code falsch", "year": "Jahr", "years_ago": "Vor {years, plural, one {einem Jahr} other {# Jahren}}", "yes": "Ja", "you_dont_have_any_shared_links": "Du hast keine geteilten Links", - "your_wifi_name": "Dein WLAN-Name", + "your_wifi_name": "Dein WiFi-Name", + "zero_to_clear_rating": "drÃŧcke 0 um die Dateibewertung zurÃŧckzusetzen", "zoom_image": "Bild vergrÃļßern", "zoom_to_bounds": "Auf Grenzen zoomen" } diff --git a/i18n/de_CH.json b/i18n/de_CH.json index de10aee010..1925b4b8a4 100644 --- a/i18n/de_CH.json +++ b/i18n/de_CH.json @@ -1,38 +1,59 @@ { + "about": "Über", "account": "Konto", "account_settings": "Konto Istelligä", "acknowledge": "Bestätige", + "action": "Aktion", "action_common_update": "Update", + "action_description": "Es paar Aktione, wo a de gfilterete Assets usgfÃŧhrt wärde sÃļlled", + "actions": "Aktione", "active": "Aktiv", + "active_count": "Aktivi: {count}", "activity": "Aktivität", + "activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}", "add": "HinzuefÃŧegä", "add_a_description": "Beschriibig hinzuefÃŧege", "add_a_location": "Standort hinzuefÃŧege", "add_a_name": "Name hinzuefÃŧege", "add_a_title": "Titel hinzuefÃŧege", + "add_action": "Aktion hinzuefÃŧege", + "add_action_description": "Aklicke um en Aktion dure zfÃŧehre", + "add_assets": "Assets hinzufÃŧege", "add_birthday": "Geburtstag hinzuefÃŧege", + "add_endpoint": "Endpunkt hinzuefÃŧge", + "add_exclusion_pattern": "Uuschlussmuster hinzuefÃŧege", + "add_filter": "Filter hinzuefÃŧge", + "add_filter_description": "Klicke, um e Filterbedingig hinzuezfÃŧege", "add_location": "Standort hinzuefÃŧege", "add_more_users": "Meh Benutzer hinzuefÃŧege", + "add_partner": "Partner hinzuefÃŧege", "add_path": "Pfad hinzuefÃŧege", "add_photos": "FÃļteli hinzuefÃŧege", - "add_to": "Zu ... hinzuefÃŧege", + "add_tag": "Tag hinzuefÃŧege", + "add_to": "HinzuefÃŧege zu â€Ļ", "add_to_album": "Zum Album hinzuefÃŧege", + "add_to_album_bottom_sheet_added": "Zu {album} hinzuegfÃŧegt", + "add_to_album_bottom_sheet_already_exists": "Scho in {album}", "add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nÃļd chÃļne im Album hinzuegfÃŧegt werde", + "add_to_album_toggle": "Uuswahl umschalte fÃŧr {album}", "add_to_albums": "Zu Albe hinzuefÃŧege", + "add_to_albums_count": "Zu Albe hinzuefÃŧege ({count})", "add_to_bottom_bar": "HinzuefÃŧege zu", "add_to_shared_album": "Zum teilte Album hinzuefÃŧege", "add_upload_to_stack": "Upload zum Stack hinzuefÃŧege", "add_url": "URL hinzuefÃŧege", + "add_workflow_step": "Workflow-Schritt hinzuefÃŧege", "added_to_archive": "Is Archiv verschobe", "added_to_favorites": "Zu dine Favoritä hinzuegfÃŧegt", + "added_to_favorites_count": "{count, number} zu Favorite hinzuegfÃŧegt", "admin": { - "add_exclusion_pattern_description": "FÃŧeg Usnahm-Patterne dezue. Globbing mit *, ** und ? wird unterstÃŧtzt. Wänn du alli Dateie i jedem Ordner mit em Name ÂĢRawÂģ ignoriere wetsch, nimm \"**/Raw/**\". FÃŧr alli Dateie, wo uf ÂĢ.tifÂģ änded, nimm \"**/*.tif.\" Wänn du en absolute Pfad ignoriere wetsch, nimm \"/path/to/ignore/**\".", + "add_exclusion_pattern_description": "Uusschlussmuster hinzuefÃŧge. Platzhalter, wie *, **, und ? wärded understÃŧtzt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.", "admin_user": "Admin Benutzer", - "asset_offline_description": "S externi Bibliothek-Asset isch uf em Dateträger nÃŧmme gfunde worde und isch in Papierkorb verschobe worde. Falls d Datei innerhalb vo de Bibliothek verschobe worde isch, lueg i dinere Timeline nach em neu passende Asset. Zum s Asset wiederherstelle, stell bitte sicher, dass dä Pfad wo une aageh isch fÃŧr Immich zugänglich isch, und scan d Bibliothek bitte nomal.", + "asset_offline_description": "Die Datei vonere externe Bibliothek isch nÃŧmme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, ÃŧberprÃŧf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.", "authentication_settings": "Authentifizierigs Iistellige", "authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte", "authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.", - "authentication_settings_reenable": "Zum Wider-aktiviere bruuchsch en Server-Command.", + "authentication_settings_reenable": "Bruuch ein Server-Befehl zum reaktiviere.", "background_task_job": "Hintergrund Ufgabä", "backup_database": "Datenbank-Dump aalege", "backup_database_enable_description": "Datenbank-Dumps aktiviere", @@ -51,6 +72,33 @@ "confirm_delete_library": "Bisch sicher, dass du d Bibliothek {library} wotsch lÃļsche?", "confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lÃļsche? Das lÃļscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nÃļd rÃŧckgängig gmacht werde. D Dateie bliibed uf em Dateträger.", "confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe", - "confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glÃļscht." + "confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glÃļscht.", + "confirm_user_password_reset": "Bisch sicher, dass du s Passwort fÃŧr {user} mÃļchtisch zruggsetze?", + "confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} mÃļchtisch zruggsetze?", + "copy_config_to_clipboard_description": "Kopiere die aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage", + "create_job": "Uufgabe erstelle", + "cron_expression": "Cron-Ziitagabe", + "cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bÃŧtet z. B. der Crontab Guru", + "cron_expression_presets": "Vorlage fÃŧr Cron-Uusdruck", + "disable_login": "Login deaktiviere", + "duplicate_detection_job_description": "Die Uufgab fÃŧehrt s maschinelle Lärne fÃŧr jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi", + "exclusion_pattern_description": "Mit 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.", + "export_config_as_json_description": "Lade die aktuelle Systemkonfiguration als JSON-Datei herunter", + "external_libraries_page_description": "Externe Bibliotheksseite fÃŧr Administratoren", + "face_detection": "Gsichtserkennig", + "face_detection_description": "Diese Aufgabe erfasst Gesichter in Dateien mittels maschinellen Lernens. Bei Videos wird nur die Miniaturansicht verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „ZurÃŧcksetzen“ setzt zusätzlich alle Gesichter zurÃŧck. „Fehlende“ stellt nur nicht verarbeitete Dateien in die Warteschlange. Erfasste Gesichter werden zur Gesichtsidentifizierung in die Warteschlange gestellt, um sie in bestehende oder neue Personen zu gruppieren.", + "facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserfassung die erfassten Gesichter zu Personen. „ZurÃŧcksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.", + "failed_job_command": "Befehl {command} ist fÃŧr Aufgabe {job} fehlgeschlagen", + "force_delete_user_warning": "WARNUNG: Diese Aktion lÃļscht sofort den Benutzer und all seine Dateien. Dies kann nicht rÃŧckgängig gemacht werden und die Dateien kÃļnnen nicht wiederhergestellt werden.", + "image_format": "Format", + "image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.", + "image_fullsize_description": "HochauflÃļsendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird", + "image_fullsize_enabled": "HochauflÃļsende Vorschaubilder aktivieren", + "image_fullsize_enabled_description": "Generiere hochauflÃļsende Vorschaubilder in OriginalauflÃļsung fÃŧr nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.", + "image_fullsize_quality_description": "Qualität der hochauflÃļsenden Vorschaubilder von 1-100. HÃļher ist besser, erzeugt aber grÃļssere Dateien.", + "image_fullsize_title": "HochauflÃļsende Vorschaueinstellungen", + "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen", + "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage fÃŧr die Bildverarbeitung, sofern diese zur VerfÃŧgung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.", + "image_prefer_wide_gamut": "Breites Spektrum bevorzugen" } } diff --git a/i18n/el.json b/i18n/el.json index 43a56916da..b1a868023e 100644 --- a/i18n/el.json +++ b/i18n/el.json @@ -5,6 +5,7 @@ "acknowledge": "ΈÎģιβι ÎŗÎŊĪŽĪƒÎˇ", "action": "ΕÎŊÎ­ĪÎŗÎĩΚι", "action_common_update": "ΕÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ", + "action_description": "ΕÎŊÎ­ĪÎŗÎĩΚÎĩĪ‚ Ī€ÎŋĪ… ÎĩĪ†ÎąĪÎŧΌÎļÎŋÎŊĪ„ÎąÎš ĪƒĪ„Îą Ī†ÎšÎģĪ„ĪÎąĪÎšĪƒÎŧέÎŊÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", "actions": "ΕÎŊÎ­ĪÎŗÎĩΚÎĩĪ‚", "active": "ΕÎŊÎĩĪÎŗÎŦ", "active_count": "ΕÎŊÎĩĪÎŗÎŦ: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", "add_a_name": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎĩÎŊĪŒĪ‚ ÎŋÎŊΌÎŧÎąĪ„ÎŋĪ‚", "add_a_title": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ Ī„Î¯Ī„ÎģÎŋĪ…", + "add_action": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎĩÎŊÎ­ĪÎŗÎĩÎšÎąĪ‚", + "add_action_description": "ΚÎŦÎŊĪ„Îĩ ÎēÎģΚÎē ÎŗÎšÎą ÎŊÎą ΀΁ÎŋĪƒÎ¸Î­ĪƒÎĩĪ„Îĩ ÎĩÎŊÎ­ĪÎŗÎĩΚι", + "add_assets": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "add_birthday": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎŗÎĩÎŊÎĩθÎģÎ¯Ī‰ÎŊ", "add_endpoint": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ Ī„ÎĩÎģΚÎēÎŋĪ ĪƒÎˇÎŧÎĩίÎŋĪ…", "add_exclusion_pattern": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎŧÎŋĪ„Î¯Î˛ÎŋĪ… ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ", + "add_filter": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ Ī†Î¯Îģ΄΁ÎŋĪ…", + "add_filter_description": "ΚÎŦÎŊĪ„Îĩ ÎēÎģΚÎē ÎŗÎšÎą ÎŊÎą ΀΁ÎŋĪƒÎ¸Î­ĪƒÎĩĪ„Îĩ ĪƒĪ…ÎŊθΎÎēΡ Ī†Î¯Îģ΄΁ÎŋĪ…", "add_location": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", "add_more_users": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎĩĪ€ÎšĪ€ÎģέÎŋÎŊ Ī‡ĪÎˇĪƒĪ„ĪŽÎŊ", "add_partner": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ĪƒĪ…ÎŊÎĩĪÎŗÎŦĪ„Îˇ", @@ -36,6 +42,7 @@ "add_to_shared_album": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ΃Îĩ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "add_upload_to_stack": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎąĪĪ‡ÎĩίÎŋĪ… ĪƒĪ„ÎˇÎŊ Îŋ΅΁ÎŦ", "add_url": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ÎŖĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", + "add_workflow_step": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ βΎÎŧÎąĪ„ÎŋĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "added_to_archive": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ ĪƒĪ„Îŋ ÎąĪĪ‡ÎĩίÎŋ", "added_to_favorites": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ ĪƒĪ„Îą ÎąÎŗÎąĪ€ÎˇÎŧέÎŊÎą", "added_to_favorites_count": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎąÎŊ {count, number} ĪƒĪ„Îą ÎąÎŗÎąĪ€ÎˇÎŧέÎŊÎą", @@ -43,7 +50,7 @@ "add_exclusion_pattern_description": "Î ĪÎŋĪƒÎ¸Î­ĪƒĪ„Îĩ ÎŧÎŋĪ„Î¯Î˛Îą ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ. ÎĨĪ€ÎŋĪƒĪ„ÎˇĪÎ¯ÎļÎĩĪ„ÎąÎš Ρ ÎĩĪ€ÎšÎģÎŋÎŗÎŽ Ī€ÎŋÎģÎģĪŽÎŊ ÎŧÎĩ *, **, ÎēιΚ ?. Για ÎŊÎą ÎąÎŗÎŊÎŋΡθÎŋĪÎŊ ΌÎģÎą Ī„Îą ÎąĪĪ‡ÎĩÎ¯Îą ΃Îĩ έÎŊÎąÎŊ ΆÎŦÎēÎĩÎģÎŋ ÎŧÎĩ Ī„Îŋ ΌÎŊÎŋÎŧÎą \"Raw\", Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ \"**/Raw/**\". Για ÎŊÎą ÎąÎŗÎŊÎŋΡθÎŋĪÎŊ ΌÎģÎą Ī„Îą ÎąĪĪ‡ÎĩÎ¯Îą ÎŧÎĩ ÎēÎąĪ„ÎŦÎģΡΞΡ \".tif\", Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ \"**/*.tif\". Για ÎŊÎą ÎąÎŗÎŊÎŋΡθÎĩί ÎŧÎ¯Îą ÎąĪ€ĪŒÎģĪ…Ī„Îˇ Î´ÎšÎąÎ´ĪÎŋÎŧÎŽ, Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ \"/path/to/ignore/**\".", "admin_user": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽĪ‚", "asset_offline_description": "Î‘Ī…Ī„ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēÎŽĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚ δÎĩ Î˛ĪÎ¯ĪƒÎēÎĩĪ„ÎąÎš Ī€ÎģέÎŋÎŊ ĪƒĪ„Îŋ Î´Î¯ĪƒÎēÎŋ ÎēιΚ Î­Ī‡ÎĩΚ ÎŧÎĩĪ„ÎąĪ†ÎĩĪÎ¸Îĩί ĪƒĪ„Îą ÎąĪ€ÎŋĪĪÎ¯ÎŧÎŧÎąĪ„Îą. ΕÎŦÎŊ Ī„Îŋ ÎąĪĪ‡ÎĩίÎŋ Î­Ī‡ÎĩΚ ÎŧÎĩĪ„ÎąÎēΚÎŊΡθÎĩί ÎĩÎŊĪ„ĪŒĪ‚ Ī„ÎˇĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚, ÎĩÎģÎ­ÎŗÎžĪ„Îĩ Ī„Îŋ ·΁ÎŋÎŊÎŋÎģĪŒÎŗÎšÎŋ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ĪƒÎąĪ‚ ÎŗÎšÎą Ī„Îŋ ÎŊέÎŋ ÎąÎŊĪ„Î¯ĪƒĪ„ÎŋÎšĪ‡Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ. Για ÎŊÎą ÎĩĪ€ÎąÎŊÎąĪ†Î­ĪÎĩĪ„Îĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ, βÎĩÎ˛ÎąÎšĪ‰Î¸ÎĩÎ¯Ī„Îĩ ĪŒĪ„Îš Ī„Îŋ Ī€ÎąĪÎąÎēÎŦ΄Ή ÎŧÎŋÎŊÎŋĪ€ÎŦĪ„Îš ÎąĪĪ‡ÎĩίÎŋĪ… ÎĩίÎŊιΚ ΀΁ÎŋĪƒÎ˛ÎŦĪƒÎšÎŧÎŋ ÎąĪ€ĪŒ Ī„Îŋ Immich ÎēιΚ ĪƒÎąĪĪŽĪƒĪ„Îĩ Ī„Îˇ βΚβÎģΚÎŋθΎÎēΡ.", - "authentication_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ΕÎģÎ­ÎŗĪ‡ÎŋĪ… Î¤ÎąĪ…Ī„ĪŒĪ„ÎˇĪ„ÎąĪ‚", + "authentication_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… Ī„ÎąĪ…Ī„ĪŒĪ„ÎˇĪ„ÎąĪ‚", "authentication_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ÎēĪ‰Î´ÎšÎēÎŋĪ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚, OAuth ÎēιΚ ÎŦÎģÎģΉÎŊ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… Ī„ÎąĪ…Ī„ĪŒĪ„ÎˇĪ„ÎąĪ‚", "authentication_settings_disable_all": "Î•Î¯ĪƒĪ„Îĩ βέβιΚÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒÎĩĪ„Îĩ ΌÎģÎĩĪ‚ Ī„ÎšĪ‚ ÎŧÎĩÎ¸ĪŒÎ´ÎŋĪ…Ī‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚; Η ĪƒĪÎŊδÎĩĪƒÎˇ θι ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡθÎĩί Ī€ÎģÎŽĪĪ‰Ī‚.", "authentication_settings_reenable": "Για ÎĩĪ€ÎąÎŊÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ, Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ ÎŧÎ¯Îą ΕÎŊĪ„ÎŋÎģÎŽ ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽ.", @@ -97,6 +104,8 @@ "image_preview_description": "ΜÎĩĪƒÎąÎ¯ÎŋĪ… ÎŧÎĩÎŗÎ­Î¸ÎŋĪ…Ī‚ ÎĩΚÎēΌÎŊÎĩĪ‚, Ī‡Ī‰ĪÎ¯Ī‚ ÎŧÎĩĪ„ÎąÎ´ÎĩδÎŋÎŧέÎŊÎą, ÎŋΚ ÎŋĪ€ÎŋίÎĩĪ‚ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎŋĪÎŊĪ„ÎąÎš ĪƒĪ„ÎˇÎŊ ΀΁ÎŋβÎŋÎģÎŽ ÎĩÎŊĪŒĪ‚ ÎąÎŊĪ„ÎšÎēÎĩΚÎŧέÎŊÎŋĪ… ÎēιΚ ÎŗÎšÎą ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽ ÎŧÎŦÎ¸ÎˇĪƒÎˇ", "image_preview_quality_description": "ΠÎŋÎšĪŒĪ„ÎˇĪ„Îą ΀΁ÎŋÎĩĪ€ÎšĪƒÎēĪŒĪ€ÎˇĪƒÎˇĪ‚ ÎąĪ€ĪŒ 1 Î­Ī‰Ī‚ 100. ÎŒĪƒÎŋ ÎŧÎĩÎŗÎąÎģĪĪ„ÎĩĪÎˇ Ī„ÎšÎŧÎŽ Ī„ĪŒĪƒÎŋ ÎēÎąÎģĪĪ„ÎĩĪÎˇ Ρ Ī€ÎŋÎšĪŒĪ„ÎˇĪ„Îą, ÎąÎģÎģÎŦ Ī€ÎąĪÎŦÎŗÎŋÎŊĪ„ÎąÎš ÎŧÎĩÎŗÎąÎģĪĪ„ÎĩĪÎą ÎąĪĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… ÎĩÎŊÎ´Î­Ī‡ÎĩĪ„ÎąÎš ÎŊÎą ÎŧÎĩÎšĪŽĪƒÎŋĪ…ÎŊ Ī„ÎˇÎŊ Ī„ÎąĪ‡ĪĪ„ÎˇĪ„Îą ÎąĪ€ĪŒÎēĪÎšĪƒÎˇĪ‚ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚. Οι Ī‡ÎąÎŧΡÎģÎ­Ī‚ Ī„ÎšÎŧÎ­Ī‚ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎĩĪ€ÎˇĪÎĩÎŦ΃ÎŋĪ…ÎŊ Ī„Îˇ Ī€ÎŋÎšĪŒĪ„ÎˇĪ„Îą Ī„ÎˇĪ‚ ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ÎŧÎŦÎ¸ÎˇĪƒÎˇĪ‚.", "image_preview_title": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ Î ĪÎŋÎĩĪ€ÎšĪƒÎēĪŒĪ€ÎšĪƒÎˇĪ‚", + "image_progressive": "Î ĪÎŋÎŋδÎĩĪ…Ī„ÎšÎēΌ", + "image_progressive_description": "Î ĪÎŋÎŋδÎĩĪ…Ī„ÎšÎēÎŽ ÎēĪ‰Î´ÎšÎēÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩΚÎēΌÎŊΉÎŊ JPEG ÎŗÎšÎą ĪƒĪ„ÎąÎ´ÎšÎąÎēÎŽ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ΀΁ÎŋβÎŋÎģÎŽ. ΔÎĩÎŊ ÎĩĪ€ÎˇĪÎĩÎŦÎļÎĩΚ Ī„ÎšĪ‚ ÎĩΚÎēΌÎŊÎĩĪ‚ WebP.", "image_quality": "ΠÎŋÎšĪŒĪ„ÎˇĪ„Îą", "image_resolution": "ΑÎŊÎŦÎģĪ…ĪƒÎˇ", "image_resolution_description": "ÎĨĪˆÎˇÎģĪŒĪ„Îĩ΁ÎĩĪ‚ ÎąÎŊÎąÎģĪĪƒÎĩÎšĪ‚ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą Î´ÎšÎąĪ„ÎˇĪÎŽĪƒÎŋĪ…ÎŊ Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ ÎģÎĩ΀΄ÎŋÎŧÎ­ĪÎĩΚÎĩĪ‚, ÎąÎģÎģÎŦ ·΁ÎĩΚÎŦÎļÎŋÎŊĪ„ÎąÎš Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁Îŋ Ī‡ĪĪŒÎŊÎŋ ÎŊÎą ÎēĪ‰Î´ÎšÎēÎŋĪ€ÎŋΚΡθÎŋĪÎŊ, Î­Ī‡ÎŋĪ…ÎŊ ÎŧÎĩÎŗÎąÎģĪĪ„ÎĩĪÎą ÎŧÎĩÎŗÎ­Î¸Îˇ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ ÎēιΚ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ÎŧÎĩÎšĪŽĪƒÎŋĪ…ÎŊ Ī„ÎˇÎŊ ÎąĪ€ĪŒÎēĪÎšĪƒÎˇ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚.", @@ -109,7 +118,7 @@ "job_concurrency": "Î¤ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊΡ ÎĩÎēĪ„Î­ÎģÎĩĪƒÎˇ {job}", "job_created": "Î•ĪÎŗÎąĪƒÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "job_not_concurrency_safe": "Î‘Ī…Ī„ÎŽ Ρ ÎĩĪÎŗÎąĪƒÎ¯Îą δÎĩÎŊ ÎĩίÎŊιΚ ÎąĪƒĪ†ÎąÎģÎŽĪ‚ ÎŗÎšÎą Ī„ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊΡ ÎĩÎēĪ„Î­ÎģÎĩĪƒÎˇ.", - "job_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ Î•ĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "job_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "job_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊÎˇĪ‚ ÎĩÎēĪ„Î­ÎģÎĩĪƒÎˇĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "jobs_delayed": "{jobCount, plural, one {# ÎēÎąÎ¸Ī…ĪƒĪ„Î­ĪÎˇĪƒÎĩ} other {# ÎēÎąÎ¸Ī…ĪƒĪ„Î­ĪÎˇĪƒÎąÎŊ}}", "jobs_failed": "{jobCount, plural, one {# ÎąĪ€Î­Ī„Ī…Ī‡Îĩ} other {# ÎąĪ€Î­Ī„Ī…Ī‡ÎąÎŊ}}", @@ -123,7 +132,7 @@ "library_scanning": "ΠÎĩĪÎšÎŋδΚÎēÎŽ ÎŖÎŦĪĪ‰ĪƒÎˇ", "library_scanning_description": "ÎĄĪÎ¸ÎŧÎšĪƒÎˇ Ī€ÎĩĪÎšÎŋδΚÎēÎŽĪ‚ ΃ÎŦĪĪ‰ĪƒÎˇĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", "library_scanning_enable_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Ī€ÎĩĪÎšÎŋδΚÎēÎŽĪ‚ ΃ÎŦĪĪ‰ĪƒÎˇĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", - "library_settings": "Î•ÎžĪ‰Ī„ÎĩĪÎšÎēÎŽ ΒιβÎģΚÎŋθΎÎēΡ", + "library_settings": "Î•ÎžĪ‰Ī„ÎĩĪÎšÎēÎŽ βΚβÎģΚÎŋθΎÎēΡ", "library_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēÎŽĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", "library_tasks_description": "ÎŖÎŦĪĪ‰ĪƒÎˇ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēĪŽÎŊ βΚβÎģΚÎŋθΡÎēĪŽÎŊ ÎŗÎšÎą ÎŊέι ÎŽ/ÎēιΚ ÎąÎģÎģÎąÎŗÎŧέÎŊÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", "library_updated": "ΕÎŊΡÎŧÎĩ΁ΉÎŧέÎŊΡ βΚβÎģΚÎŋθΎÎēΡ", @@ -132,7 +141,7 @@ "library_watching_settings_description": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„Îˇ Ī€ÎąĪÎąÎēÎŋÎģÎŋĪÎ¸ÎˇĪƒÎˇ ÎŗÎšÎą ΄΁ÎŋĪ€ÎŋĪ€ÎŋΚΡÎŧέÎŊÎą ÎąĪĪ‡ÎĩÎ¯Îą", "logging_enable_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎēÎąĪ„ÎąÎŗĪÎąĪ†ÎŽĪ‚ ĪƒĪ…ÎŧβÎŦÎŊ΄ΉÎŊ", "logging_level_description": "ΤÎŋ ÎĩĪ€Î¯Ī€ÎĩδÎŋ ÎēÎąĪ„ÎąÎŗĪÎąĪ†ÎŽĪ‚ ĪƒĪ…ÎŧβÎŦÎŊ΄ΉÎŊ Ī€ÎŋĪ… θι ÎĩĪ†ÎąĪÎŧÎŋĪƒĪ„Îĩί, ĪŒĪ„ÎąÎŊ ÎąĪ…Ī„ÎŽ ÎĩίÎŊιΚ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ.", - "logging_settings": "ÎšÎąĪ„ÎąÎŗĪÎąĪ†ÎŽ ÎŖĪ…ÎŧβÎŦÎŊ΄ΉÎŊ", + "logging_settings": "ÎšÎąĪ„ÎąÎŗĪÎąĪ†ÎŽ ĪƒĪ…ÎŧβÎŦÎŊ΄ΉÎŊ", "machine_learning_availability_checks": "ΈÎģÎĩÎŗĪ‡ÎŋΚ δΚιθÎĩĪƒÎšÎŧĪŒĪ„ÎˇĪ„ÎąĪ‚", "machine_learning_availability_checks_description": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„ÎŋĪ‚ ÎąÎŊÎ¯Ī‡ÎŊÎĩĪ…ĪƒÎˇ ÎēιΚ ΀΁ÎŋĪ„Î¯ÎŧÎˇĪƒÎˇ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΉÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ĪŽÎŊ ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ÎŧÎŦÎ¸ÎˇĪƒÎˇĪ‚", "machine_learning_availability_checks_enabled": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩÎģÎ­ÎŗĪ‡Ī‰ÎŊ δΚιθÎĩĪƒÎšÎŧĪŒĪ„ÎˇĪ„ÎąĪ‚", @@ -174,17 +183,28 @@ "machine_learning_ocr_min_score_recognition_description": "ΕÎģÎŦĪ‡ÎšĪƒĪ„ÎŋĪ‚ βιθÎŧĪŒĪ‚ ÎĩÎŧĪ€ÎšĪƒĪ„ÎŋĪƒĪÎŊÎˇĪ‚ ÎŗÎšÎą Ī„ÎˇÎŊ ÎąÎŊÎąÎŗÎŊĪŽĪÎšĪƒÎˇ ÎąÎŊÎšĪ‡ÎŊÎĩĪ…ÎŧέÎŊÎŋĪ… ÎēÎĩΚÎŧέÎŊÎŋĪ… ÎąĪ€ĪŒ 0 Î­Ī‰Ī‚ 1. ΧιÎŧΡÎģĪŒĪ„Îĩ΁ÎĩĪ‚ Ī„ÎšÎŧÎ­Ī‚ θι ÎąÎŊÎąÎŗÎŊĪ‰ĪÎ¯ÎļÎŋĪ…ÎŊ Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁Îŋ ÎēÎĩίÎŧÎĩÎŊÎŋ, ÎąÎģÎģÎŦ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎŋÎ´ÎˇÎŗÎŽĪƒÎŋĪ…ÎŊ ΃Îĩ ΈÎĩĪ…Î´ĪŽĪ‚ θÎĩĪ„ÎšÎēÎŦ ÎąĪ€ÎŋĪ„ÎĩÎģÎ­ĪƒÎŧÎąĪ„Îą.", "machine_learning_ocr_model": "ΜÎŋÎŊĪ„Î­ÎģÎŋ OCR", "machine_learning_ocr_model_description": "Τι ÎŧÎŋÎŊĪ„Î­ÎģÎą δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎĩίÎŊιΚ Ī€ÎšÎŋ ÎąÎēĪÎšÎ˛ÎŽ ÎąĪ€ĪŒ Ī„Îą ÎŧÎŋÎŊĪ„Î­ÎģÎą ΄ΉÎŊ ÎēΚÎŊÎˇĪ„ĪŽÎŊ, ÎąÎģÎģÎŦ ·΁ÎĩΚÎŦÎļÎŋÎŊĪ„ÎąÎš Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁Îŋ Ī‡ĪĪŒÎŊÎŋ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ÎēιΚ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎŋĪÎŊ Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„ÎĩĪÎˇ ÎŧÎŊÎŽÎŧΡ.", - "machine_learning_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎœÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ΜÎŦÎ¸ÎˇĪƒÎˇĪ‚", + "machine_learning_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ÎŧÎŦÎ¸ÎˇĪƒÎˇĪ‚", "machine_learning_settings_description": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„ÎšĪ‚ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎĩĪ‚ ÎēιΚ Ī„ÎšĪ‚ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ÎŧÎŦÎ¸ÎˇĪƒÎˇĪ‚", "machine_learning_smart_search": "ÎˆÎžĪ…Ī€ÎŊΡ ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ", "machine_learning_smart_search_description": "ΑÎŊÎąÎļÎˇĪ„ÎŽĪƒĪ„Îĩ ÎĩΚÎēΌÎŊÎĩĪ‚ ĪƒÎˇÎŧÎąĪƒÎšÎŋÎģÎŋÎŗÎšÎēÎŦ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšĪŽÎŊĪ„ÎąĪ‚ ÎĩÎŊĪƒĪ‰ÎŧÎąĪ„ĪŽĪƒÎĩÎšĪ‚ CLIP", "machine_learning_smart_search_enabled": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Î­ÎžĪ…Ī€ÎŊÎˇĪ‚ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇĪ‚", "machine_learning_smart_search_enabled_description": "ΑÎŊ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡθÎĩί, ÎŋΚ ÎĩΚÎēΌÎŊÎĩĪ‚ δÎĩÎŊ θι ÎēĪ‰Î´ÎšÎēÎŋĪ€ÎŋΚÎŋĪÎŊĪ„ÎąÎš ÎŗÎšÎą Î­ÎžĪ…Ī€ÎŊΡ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ.", "machine_learning_url_description": "Η δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ URL Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎŧÎˇĪ‡ÎąÎŊΚÎēÎŽĪ‚ ÎŧÎŦÎ¸ÎˇĪƒÎˇĪ‚. ΑÎŊ δÎŋθÎŋĪÎŊ Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ ÎąĪ€ĪŒ ÎŧÎ¯Îą δΚÎĩĪ…Î¸ĪÎŊ΃ÎĩÎšĪ‚ URL, ÎēÎŦθÎĩ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ θι δÎŋÎēΚÎŧÎŦÎļÎĩĪ„ÎąÎš δΚιδÎŋĪ‡ÎšÎēÎŦ ÎŧÎ­Ī‡ĪÎš ÎŊÎą ÎąÎŊĪ„ÎąĪ€ÎŋÎēĪÎšÎ¸Îĩί έÎŊÎąĪ‚ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą, ÎŧÎĩ Ī„Îˇ ΃ÎĩÎšĪÎŦ ÎąĪ€ĪŒ Ī„ÎˇÎŊ Ī€ĪĪŽĪ„Îˇ Î­Ī‰Ī‚ Ī„ÎˇÎŊ Ī„ÎĩÎģÎĩĪ…Ī„ÎąÎ¯Îą. Οι δΚιÎēÎŋÎŧÎšĪƒĪ„Î­Ī‚ Ī€ÎŋĪ… δÎĩÎŊ ÎąÎŊĪ„ÎąĪ€ÎŋÎēĪÎ¯ÎŊÎŋÎŊĪ„ÎąÎš θι ÎąÎŗÎŊÎŋÎŋĪÎŊĪ„ÎąÎš ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŦ ÎŧÎ­Ī‡ĪÎš ÎŊÎą ÎĩĪ€ÎąÎŊέÎģθÎŋĪ…ÎŊ ΃Îĩ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą.", + "maintenance_delete_backup": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", + "maintenance_delete_backup_description": "Î‘Ī…Ī„ĪŒ Ī„Îŋ ÎąĪĪ‡ÎĩίÎŋ θι Î´ÎšÎąÎŗĪÎąĪ†Îĩί ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎēιΚ Ī‡Ī‰ĪÎ¯Ī‚ Î´Ī…ÎŊÎąĪ„ĪŒĪ„ÎˇĪ„Îą ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦĪ‚.", + "maintenance_delete_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ Ī„ÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚.", + "maintenance_restore_backup": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", + "maintenance_restore_backup_description": "ΤÎŋ Immich θι Î´ÎšÎąÎŗĪÎąĪ†Îĩί Ī€ÎģÎŽĪĪ‰Ī‚ ÎēιΚ θι ÎĩĪ€ÎąÎŊÎąĪ†ÎĩĪÎ¸Îĩί ÎąĪ€ĪŒ Ī„Îŋ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋ ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îŋ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚. Θι δΡÎŧΚÎŋĪ…ĪÎŗÎˇÎ¸Îĩί ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îŋ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ Ī€ĪÎšÎŊ Ī„Îˇ ĪƒĪ…ÎŊÎ­Ī‡ÎĩΚι.", + "maintenance_restore_backup_different_version": "Î‘Ī…Ī„ĪŒ Ī„Îŋ ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îŋ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ δΡÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ ÎŧÎĩ Î´ÎšÎąĪ†Îŋ΁ÎĩĪ„ÎšÎēÎŽ έÎēδÎŋĪƒÎˇ Ī„ÎŋĪ… Immich!", + "maintenance_restore_backup_unknown_version": "ΔÎĩÎŊ ÎŽĪ„ÎąÎŊ Î´Ī…ÎŊÎąĪ„ĪŒĪ‚ Îŋ ΀΁ÎŋĪƒÎ´ÎšÎŋĪÎšĪƒÎŧĪŒĪ‚ Ī„ÎˇĪ‚ έÎēδÎŋĪƒÎˇĪ‚ Ī„ÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚.", + "maintenance_restore_database_backup": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ Ī„ÎˇĪ‚ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ", + "maintenance_restore_database_backup_description": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ Ī„ÎˇĪ‚ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ ΃Îĩ ΀΁ÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊΡ ÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšĪŽÎŊĪ„ÎąĪ‚ ÎąĪĪ‡ÎĩίÎŋ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", "maintenance_settings": "ÎŖĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇ", "maintenance_settings_description": "Î˜Î­ĪƒĪ„Îĩ Ī„Îŋ Immich ΃Îĩ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚.", - "maintenance_start": "ΈÎŊÎąĪÎžÎˇ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚", + "maintenance_start": "ΑÎģÎģÎąÎŗÎŽ ΃Îĩ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚", "maintenance_start_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą έÎŊÎąĪÎžÎˇĪ‚ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚.", + "maintenance_upload_backup": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎąĪĪ‡ÎĩίÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ", + "maintenance_upload_backup_error": "ΔÎĩÎŊ ÎŽĪ„ÎąÎŊ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ Ī„ÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚, ÎĩίÎŊιΚ ÎąĪĪ‡ÎĩίÎŋ .sql/.sql.gz;", "manage_concurrency": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊΡ ÎĩÎēĪ„Î­ÎģÎĩĪƒÎˇĪ‚", "manage_concurrency_description": "ΜÎĩĪ„ÎąÎ˛ÎĩÎ¯Ī„Îĩ ĪƒĪ„Îˇ ΃ÎĩÎģÎ¯Î´Îą ÎĩĪÎŗÎąĪƒÎšĪŽÎŊ ÎŗÎšÎą ÎŊÎą Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„ÎˇÎŊ Ī„ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊΡ ÎĩÎēĪ„Î­ÎģÎĩĪƒÎˇ ÎĩĪÎŗÎąĪƒÎšĪŽÎŊ", "manage_log_settings": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ ÎąĪĪ‡ÎĩίÎŋĪ… ÎēÎąĪ„ÎąÎŗĪÎąĪ†ÎŽĪ‚", @@ -252,7 +272,7 @@ "oauth_auto_register": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„Îˇ ÎēÎąĪ„ÎąĪ‡ĪŽĪÎˇĪƒÎˇ", "oauth_auto_register_description": "Î‘Ī…Ī„ĪŒÎŧÎąĪ„Îˇ ÎēÎąĪ„ÎąĪ‡ĪŽĪÎˇĪƒÎˇ ÎŊέÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ ÎąĪ†ÎŋĪ ĪƒĪ…ÎŊδÎĩθÎĩί ÎŧÎĩ OAuth", "oauth_button_text": "ΚÎĩίÎŧÎĩÎŊÎŋ ÎēÎŋĪ…ÎŧĪ€ÎšÎŋĪ", - "oauth_client_secret_description": "ÎĨĪ€Îŋ·΁ÎĩĪ‰Ī„ÎšÎēΌ ÎĩÎąÎŊ PKCE (Proof Key for Code Exchange) δÎĩÎŊ Ī…Ī€ÎŋĪƒĪ„ÎˇĪÎ¯ÎļÎĩĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎŋÎŊ OAuth Ī€ÎŦ΁Îŋ·Îŋ", + "oauth_client_secret_description": "Î‘Ī€ÎąÎšĪ„ÎĩÎ¯Ī„ÎąÎš ÎŗÎšÎą έÎŧĪ€ÎšĪƒĪ„Îŋ Ī€ĪĪŒÎŗĪÎąÎŧÎŧÎą Ī€ÎĩÎģÎŦĪ„Îˇ ÎŽ ÎąÎŊ δÎĩÎŊ Ī…Ī€ÎŋĪƒĪ„ÎˇĪÎ¯ÎļÎĩĪ„ÎąÎš PKCE (Proof Key for Code Exchange) ΃Îĩ δΡÎŧĪŒĪƒÎšÎŋ Ī€ĪĪŒÎŗĪÎąÎŧÎŧÎą Ī€ÎĩÎģÎŦĪ„Îˇ.", "oauth_enable_description": "ÎŖĪÎŊδÎĩĪƒÎˇ ÎŧÎĩ OAuth", "oauth_mobile_redirect_uri": "URI ΑÎŊÎąÎēÎąĪ„ÎĩĪÎ¸Ī…ÎŊĪƒÎˇĪ‚ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ Ī„ÎˇÎģÎ­Ī†Ī‰ÎŊÎą", "oauth_mobile_redirect_uri_override": "Î ĪÎŋĪƒĪ€Î­ÎģÎąĪƒÎˇ URI ÎąÎŊÎąÎēÎąĪ„ÎĩĪÎ¸Ī…ÎŊĪƒÎˇĪ‚ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ Ī„ÎˇÎģÎ­Ī†Ī‰ÎŊÎą", @@ -294,7 +314,7 @@ "server_external_domain_settings_description": "ΔιÎĩĪÎ¸Ī…ÎŊĪƒÎˇ Ī„ÎŋÎŧέι ÎŗÎšÎą δΡÎŧĪŒĪƒÎšÎŋĪ…Ī‚ ÎēÎŋΚÎŊÎŋĪĪ‚ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…Ī‚, Ī€ÎĩĪÎšÎģÎąÎŧβιÎŊÎŋÎŧέÎŊÎŋĪ… Ī„ÎŋĪ… http(s)://", "server_public_users": "ΔηÎŧĪŒĪƒÎšÎŋΚ Î§ĪÎŽĪƒĪ„ÎĩĪ‚", "server_public_users_description": "ΌÎģÎŋΚ ÎŋΚ Ī‡ĪÎŽĪƒĪ„ÎĩĪ‚ (ΌÎŊÎŋÎŧÎą ÎēιΚ email) ÎĩÎŧĪ†ÎąÎŊίÎļÎŋÎŊĪ„ÎąÎš ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ΀΁ÎŋĪƒÎ¸ÎŽÎēΡ ÎĩÎŊĪŒĪ‚ Ī‡ĪÎŽĪƒĪ„Îˇ ΃Îĩ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ. ÎŒĪ„ÎąÎŊ ÎąĪ…Ī„ÎŽ Ρ ÎĩĪ€ÎšÎģÎŋÎŗÎŽ ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ, Ρ ÎģÎ¯ĪƒĪ„Îą Ī‡ĪÎˇĪƒĪ„ĪŽÎŊ θι ÎĩίÎŊιΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΡ ÎŧΌÎŊÎŋ ĪƒĪ„ÎŋĪ…Ī‚ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„Î­Ī‚.", - "server_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽ", + "server_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_stats_page_description": "ÎŖÎĩÎģÎ¯Î´Îą ĪƒĪ„ÎąĪ„ÎšĪƒĪ„ÎšÎēĪŽÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "server_welcome_message": "ΜήÎŊĪ…ÎŧÎą ÎēÎąÎģĪ‰ĪƒÎŋĪÎ¯ĪƒÎŧÎąĪ„ÎŋĪ‚", @@ -316,7 +336,7 @@ "storage_template_more_details": "Για Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ ÎģÎĩ΀΄ÎŋÎŧÎ­ĪÎĩΚÎĩĪ‚ ĪƒĪ‡ÎĩĪ„ÎšÎēÎŦ ÎŧÎĩ ÎąĪ…Ī„ÎŽÎŊ Ī„Îˇ Î´Ī…ÎŊÎąĪ„ĪŒĪ„ÎˇĪ„Îą, ÎąÎŊÎąĪ„ĪÎ­ÎžĪ„Îĩ ĪƒĪ„Îŋ Î ĪĪŒĪ„Ī…Ī€Îŋ Î‘Ī€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ ÎēιΚ ĪƒĪ„ÎšĪ‚ ĪƒĪ…ÎŊÎ­Ī€ÎĩÎšÎ­Ī‚ Ī„ÎŋĪ…", "storage_template_onboarding_description_v2": "ÎŒĪ„ÎąÎŊ ÎĩίÎŊιΚ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ, ÎąĪ…Ī„ÎŽ Ρ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą θι ÎŋĪÎŗÎąÎŊĪŽÎŊÎĩΚ ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îą Ī„Îą ÎąĪĪ‡ÎĩÎ¯Îą ÎŧÎĩ βÎŦĪƒÎˇ έÎŊÎą Ī€ĪĪŒĪ„Ī…Ī€Îŋ Ī€ÎŋĪ… ÎŋĪÎ¯ÎļÎĩĪ„ÎąÎš ÎąĪ€ĪŒ Ī„Îŋ Ī‡ĪÎŽĪƒĪ„Îˇ. Για Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ Ī€ÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚, Ī€ÎąĪÎąÎēÎąÎģĪŽ ÎąÎŊÎąĪ„ĪÎ­ÎžĪ„Îĩ ĪƒĪ„ÎšĪ‚ ÎŋÎ´ÎˇÎŗÎ¯ÎĩĪ‚ Ī‡ĪÎŽĪƒÎˇĪ‚.", "storage_template_path_length": "ÎŒĪÎšÎŋ ÎŧÎŽÎēÎŋĪ…Ī‚ Î´ÎšÎąÎ´ĪÎŋÎŧÎŽĪ‚: {length, number}/{limit, number}, ÎēÎąĪ„ÎŦ ΀΁ÎŋĪƒÎ­ÎŗÎŗÎšĪƒÎˇ", - "storage_template_settings": "Î ĪĪŒĪ„Ī…Ī€Îŋ Î‘Ī€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚", + "storage_template_settings": "Î ĪĪŒĪ„Ī…Ī€Îŋ ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚", "storage_template_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎˇĪ‚ δÎŋÎŧÎŽĪ‚ Ī†ÎąÎēέÎģÎŋĪ… ÎēιΚ Ī„ÎŋĪ… ÎŋÎŊΌÎŧÎąĪ„ÎŋĪ‚, Ī„ÎŋĪ… ÎąÎŊÎĩÎ˛ÎąĪƒÎŧέÎŊÎŋĪ… ÎąĪĪ‡ÎĩίÎŋĪ…", "storage_template_user_label": "{label} ÎĩίÎŊιΚ Ρ Î•Ī„ÎšÎēÎ­Ī„Îą Î‘Ī€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ", "system_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎŖĪ…ĪƒĪ„ÎŽÎŧÎąĪ„ÎŋĪ‚", @@ -331,8 +351,8 @@ "template_settings": "Î ĪĪŒĪ„Ī…Ī€Îą ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩΉÎŊ", "template_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ΀΁ÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊΉÎŊ ΀΁ÎŋĪ„ĪĪ€Ī‰ÎŊ ÎŗÎšÎą ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚", "theme_custom_css_settings": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊÎŋ CSS", - "theme_custom_css_settings_description": "Τι Cascading Style Sheets(CSS) ÎĩĪ€ÎšĪ„ĪÎ­Ī€ÎĩΚ Ī„ÎˇÎŊ ΀΁ÎŋĪƒÎąĪÎŧÎŋÎŗÎŽ Ī„ÎŋĪ… ĪƒĪ‡ÎĩÎ´ÎšÎąĪƒÎŧÎŋĪ Ī„ÎŋĪ… Immich.", - "theme_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ΘέÎŧÎąĪ„ÎŋĪ‚", + "theme_custom_css_settings_description": "Τι Cascading Style Sheets ÎĩĪ€ÎšĪ„ĪÎ­Ī€ÎĩΚ Ī„ÎˇÎŊ ΀΁ÎŋĪƒÎąĪÎŧÎŋÎŗÎŽ Ī„ÎŋĪ… ĪƒĪ‡ÎĩÎ´ÎšÎąĪƒÎŧÎŋĪ Ī„ÎŋĪ… Immich.", + "theme_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ θέÎŧÎąĪ„ÎŋĪ‚", "theme_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎˇĪ‚ ΀΁ÎŋĪƒÎąĪÎŧÎŋÎŗÎŽĪ‚ Ī„ÎŋĪ… ÎšĪƒĪ„ĪŒĪ„ÎŋĪ€ÎŋĪ… Ī„ÎŋĪ… Immich", "thumbnail_generation_job": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ΜιÎē΁ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", "thumbnail_generation_job_description": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŧÎĩÎŗÎŦÎģΉÎŊ, ÎŧΚÎēĪĪŽÎŊ ÎēιΚ θÎŋÎģĪŽÎŊ ÎŧΚÎē΁ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ÎŗÎšÎą ÎēÎŦθÎĩ ÎąĪĪ‡ÎĩίÎŋ, ÎēÎąÎ¸ĪŽĪ‚ ÎēιΚ ÎŧΚÎē΁ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ÎŗÎšÎą ÎēÎŦθÎĩ ÎŦĪ„ÎŋÎŧÎŋ", @@ -399,7 +419,7 @@ "trash_enabled_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎšĪŽÎŊ ΚÎŦδÎŋĪ… Î‘Ī€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "trash_number_of_days": "Î‘ĪÎšÎ¸ÎŧĪŒĪ‚ ΡÎŧÎĩĪĪŽÎŊ", "trash_number_of_days_description": "Î‘ĪÎšÎ¸ÎŧĪŒĪ‚ ΡÎŧÎĩĪĪŽÎŊ Ī€ÎąĪÎąÎŧÎŋÎŊÎŽĪ‚ ΄ΉÎŊ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ, Ī€ĪÎšÎŊ ÎąĪ€ĪŒ Ī„ÎˇÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŽ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ Ī„ÎŋĪ…Ī‚", - "trash_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ΚÎŦδÎŋĪ… Î‘Ī€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", + "trash_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "trash_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸Î¯ĪƒÎĩΉÎŊ ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "unlink_all_oauth_accounts": "Î‘Ī€ÎŋĪƒĪÎŊδÎĩĪƒÎˇ ΌÎģΉÎŊ ΄ΉÎŊ ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧĪŽÎŊ OAuth", "unlink_all_oauth_accounts_description": "ΜηÎŊ ΞÎĩ·ÎŦ΃ÎĩĪ„Îĩ ÎŊÎą ÎąĪ€ÎŋĪƒĪ…ÎŊÎ´Î­ĪƒÎĩĪ„Îĩ ΌÎģÎŋĪ…Ī‚ Ī„ÎŋĪ…Ī‚ ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋĪĪ‚ OAuth Ī€ĪÎšÎŊ ÎŧÎĩĪ„ÎąÎ˛ÎĩÎ¯Ī„Îĩ ΃Îĩ ÎŊέÎŋ Ī€ÎŦ΁Îŋ·Îŋ.", @@ -422,7 +442,7 @@ "users_page_description": "ÎŖÎĩÎģÎ¯Î´Îą Ī‡ĪÎˇĪƒĪ„ĪŽÎŊ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "version_check_enabled_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… έÎēδÎŋĪƒÎˇĪ‚", "version_check_implications": "Η ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ÎĩÎģÎ­ÎŗĪ‡ÎŋĪ… έÎēδÎŋĪƒÎˇĪ‚, ÎĩÎžÎąĪĪ„ÎŦĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŋδΚÎēÎŽ ÎĩĪ€ÎšÎēÎŋΚÎŊΉÎŊÎ¯Îą ÎŧÎĩ Ī„Îŋ github.com", - "version_check_settings": "ΈÎģÎĩÎŗĪ‡ÎŋĪ‚ ΈÎēδÎŋĪƒÎˇĪ‚", + "version_check_settings": "ΈÎģÎĩÎŗĪ‡ÎŋĪ‚ ÎĩÎēδÎŋĪƒÎˇĪ‚", "version_check_settings_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ/ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ Ī„ÎˇĪ‚ ÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚ ÎŗÎšÎą ÎŊέι έÎēδÎŋĪƒÎˇ", "video_conversion_job": "ΜÎĩĪ„ÎąĪ„ĪÎŋĪ€ÎŽ Î˛Î¯ÎŊĪ„ÎĩÎŋ", "video_conversion_job_description": "ΜÎĩĪ„ÎąĪ„ĪÎŋĪ€ÎŽ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎŗÎšÎą ÎŧÎĩÎŗÎąÎģĪĪ„ÎĩĪÎˇ ĪƒĪ…ÎŧÎ˛ÎąĪ„ĪŒĪ„ÎˇĪ„Îą ÎŧÎĩ ΀΁ÎŋÎŗĪÎŦÎŧÎŧÎąĪ„Îą Ī€ÎĩĪÎšÎŽÎŗÎˇĪƒÎˇĪ‚ ÎēιΚ ĪƒĪ…ĪƒÎēÎĩĪ…Î­Ī‚" @@ -431,6 +451,9 @@ "admin_password": "ÎšĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚ Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "administration": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ", "advanced": "Για ΀΁ÎŋĪ‡Ī‰ĪÎˇÎŧέÎŊÎŋĪ…Ī‚", + "advanced_settings_clear_image_cache": "ÎšÎąÎ¸ÎąĪÎšĪƒÎŧĪŒĪ‚ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎĩΚÎēΌÎŊΉÎŊ", + "advanced_settings_clear_image_cache_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ÎēÎąÎ¸ÎąĪÎšĪƒÎŧÎŋĪ ΀΁ÎŋĪƒĪ‰ĪÎšÎŊÎŽĪ‚ ÎŧÎŊÎŽÎŧÎˇĪ‚ ÎĩΚÎēΌÎŊΉÎŊ", + "advanced_settings_clear_image_cache_success": "Î•Ī€ÎšĪ„Ī…Ī‡ÎŽĪ‚ ÎĩÎēÎēιθÎŦĪÎšĪƒÎˇ {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Î§ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„ÎˇÎŊ ÎĩĪ€ÎšÎģÎŋÎŗÎŽ ÎŗÎšÎą ÎŊÎą Ī†ÎšÎģ΄΁ÎŦ΁ÎĩĪ„Îĩ Ī„Îą ÎŧÎ­ĪƒÎą ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇĪ‚ ÎēÎąĪ„ÎŦ Ī„ÎŋÎŊ ĪƒĪ…ÎŗĪ‡ĪÎŋÎŊÎšĪƒÎŧΌ ÎŧÎĩ βÎŦĪƒÎˇ ÎĩÎŊÎąÎģÎģÎąÎēĪ„ÎšÎēÎŦ ÎēĪÎšĪ„ÎŽĪÎšÎą. ΔÎŋÎēΚÎŧÎŦĪƒĪ„Îĩ ÎąĪ…Ī„ÎŽ Ī„Îˇ Î´Ī…ÎŊÎąĪ„ĪŒĪ„ÎˇĪ„Îą ÎŧΌÎŊÎŋ ÎąÎŊ Î­Ī‡ÎĩĪ„Îĩ ΀΁ÎŋβÎģÎŽÎŧÎąĪ„Îą ÎŧÎĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ Ī€ÎŋĪ… ÎĩÎŊĪ„ÎŋĪ€Î¯ÎļÎĩΚ ΌÎģÎą Ī„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ.", "advanced_settings_enable_alternate_media_filter_title": "[ΠΕΙΡΑΜΑΤΙΚΟ] Î§ĪÎŽĪƒÎˇ ÎĩÎŊÎąÎģÎģÎąÎēĪ„ÎšÎēÎŋĪ Ī†Î¯Îģ΄΁ÎŋĪ… ĪƒĪ…ÎŗĪ‡ĪÎŋÎŊÎšĪƒÎŧÎŋĪ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚", "advanced_settings_log_level_title": "Î•Ī€Î¯Ī€ÎĩδÎŋ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚: {level}", @@ -467,10 +490,12 @@ "album_remove_user": "Î”ÎšÎąÎŗĪÎąĪ†ÎŽ Ī‡ĪÎŽĪƒĪ„Îˇ;", "album_remove_user_confirmation": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎąĪ†ÎąÎšĪÎ­ĪƒÎĩĪ„Îĩ Ī„ÎŋÎŊ/Ī„ÎˇÎŊ {user};", "album_search_not_found": "ΔÎĩ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ Ī€ÎŋĪ… ÎŊÎą Ī„ÎąÎšĪÎšÎŦÎļÎŋĪ…ÎŊ ÎŧÎĩ Ī„ÎˇÎŊ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎŽ ĪƒÎąĪ‚", + "album_selected": "ΆÎģÎŧĪ€ÎŋĪ…Îŧ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋ", "album_share_no_users": "ÎĻÎąÎ¯ÎŊÎĩĪ„ÎąÎš ĪŒĪ„Îš Î­Ī‡ÎĩĪ„Îĩ ÎēÎŋΚÎŊÎŋĪ€ÎŋÎšÎŽĪƒÎĩΚ ÎąĪ…Ī„ĪŒ Ī„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ΃Îĩ ΌÎģÎŋĪ…Ī‚ Ī„ÎŋĪ…Ī‚ Ī‡ĪÎŽĪƒĪ„ÎĩĪ‚ ÎŽ δÎĩÎŊ Î­Ī‡ÎĩĪ„Îĩ Ī‡ĪÎŽĪƒĪ„ÎĩĪ‚ ÎŗÎšÎą ÎŊÎą Ī„Îŋ ÎēÎŋΚÎŊÎŋĪ€ÎŋÎšÎŽĪƒÎĩĪ„Îĩ.", "album_summary": "ΠÎĩĪÎ¯ÎģÎˇĪˆÎˇ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "album_updated": "ΤÎŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ, ÎĩÎŊΡÎŧÎĩĪĪŽÎ¸ÎˇÎēÎĩ", "album_updated_setting_description": "ΛÎŦβÎĩĪ„Îĩ ÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎŧÎ­ĪƒĪ‰ email ĪŒĪ„ÎąÎŊ έÎŊÎą ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ Î­Ī‡ÎĩΚ ÎŊέι ÎąĪĪ‡ÎĩÎ¯Îą", + "album_upload_assets": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎąĪ€ĪŒ Ī„ÎŋÎŊ Ī…Ī€ÎŋÎģÎŋÎŗÎšĪƒĪ„ÎŽ ĪƒÎąĪ‚ ÎēιΚ ΀΁ÎŋĪƒÎ¸ÎŽÎēΡ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "album_user_left": "Î‘Ī€ÎŋĪ‡Ī‰ĪÎŽĪƒÎąĪ„Îĩ ÎąĪ€ĪŒ Ī„Îŋ {album}", "album_user_removed": "Î‘Ī†ÎąÎšĪÎ­Î¸ÎˇÎēÎĩ Îŋ/Ρ {user}", "album_viewer_appbar_delete_confirm": "Î•Î¯ĪƒĪ„Îĩ βέβιΚÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎąĪ€ĪŒ Ī„ÎŋÎŊ ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧΌ ĪƒÎąĪ‚;", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Î‘ĪĪ‡ÎšÎēÎŽ Ī„ÎąÎžÎšÎŊΌÎŧÎˇĪƒÎˇ ÎēÎąĪ„ÎŦ Ī„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊÎ­Ī‰ÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ.", "albums_feature_description": "ÎŖĪ…ÎģÎģÎŋÎŗÎ­Ī‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ Ī€ÎŋĪ… ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ÎēÎŋΚÎŊÎŋĪ€ÎŋΚΡθÎŋĪÎŊ ΃Îĩ ÎŦÎģÎģÎŋĪ…Ī‚ Ī‡ĪÎŽĪƒĪ„ÎĩĪ‚.", "albums_on_device_count": "ΆÎģÎŧĪ€ÎŋĪ…Îŧ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ ({count})", + "albums_selected": "{count, plural, one {# ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎĩ} other {# ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎąÎŊ}}", "all": "ΌÎģÎą", "all_albums": "ΌÎģÎą Ī„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "all_people": "ΌÎģÎą Ī„Îą ÎŦĪ„ÎŋÎŧÎą", + "all_photos": "ΌÎģÎĩĪ‚ ÎŋΚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "all_videos": "ΌÎģÎą Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ", "allow_dark_mode": "Î•Ī€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ Ī„Îˇ ΃ÎēÎŋĪ„ÎĩΚÎŊÎŽ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą", "allow_edits": "Î•Ī€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ Ī„ÎšĪ‚ ΄΁ÎŋĪ€ÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Î•Ī€ÎšĪ„ĪÎ­ĪˆĪ„Îĩ ĪƒĪ„ÎŋÎŊ δΡÎŧĪŒĪƒÎšÎŋ Ī‡ĪÎŽĪƒĪ„Îˇ ÎŊÎą ÎąÎŊÎĩβÎŦ΃ÎĩΚ", "allowed": "Î•Ī€ÎšĪ„ĪÎĩĪ€ĪŒÎŧÎĩÎŊÎŋ", "alt_text_qr_code": "ΕιÎēΌÎŊÎą ÎēĪ‰Î´ÎšÎēÎŋĪ QR", + "always_keep": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ Ī€ÎŦÎŊĪ„Îą", + "always_keep_photos_hint": "Η ÂĢÎ‘Ī€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ…Âģ θι ÎēĪÎąĪ„ÎŽĪƒÎĩΚ ΌÎģÎĩĪ‚ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ΃Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ.", + "always_keep_videos_hint": "Η ÂĢÎ‘Ī€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ…Âģ θι ÎēĪÎąĪ„ÎŽĪƒÎĩΚ ΌÎģÎą Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ ΃Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ.", "anti_clockwise": "ΑÎŊĪ„Î¯Î¸ÎĩĪ„Îą ÎŧÎĩ Ī„Îˇ ΆÎŋ΁ÎŦ Ī„ÎŋĪ… ΁ÎŋÎģÎŋÎŗÎšÎŋĪ", "api_key": "ΚÎģÎĩΚδί API", "api_key_description": "Î‘Ī…Ī„ÎŽ Ρ Ī„ÎšÎŧÎŽ θι ÎĩÎŧĪ†ÎąÎŊÎšĪƒĪ„Îĩί ÎŧΌÎŊÎŋ ÎŧÎ¯Îą ΆÎŋ΁ÎŦ. Î ÎąĪÎąÎēÎąÎģĪŽ βÎĩÎ˛ÎąÎšĪ‰Î¸ÎĩÎ¯Ī„Îĩ ĪŒĪ„Îš Ī„ÎˇÎŊ Î­Ī‡ÎĩĪ„Îĩ ÎąÎŊĪ„ÎšÎŗĪÎŦΈÎĩΚ Ī€ĪÎšÎŊ ÎēÎģÎĩÎ¯ĪƒÎĩĪ„Îĩ Ī„Îŋ Ī€ÎąĪÎŦÎ¸Ī…ĪÎŋ.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {Î‘ĪĪ‡ÎĩΚÎŋθÎĩĪ„ÎŽÎ¸ÎˇÎēÎąÎŊ #}}", "are_these_the_same_person": "ΕίÎŊιΚ Ī„Îŋ ίδΚÎŋ ÎŦĪ„ÎŋÎŧÎŋ;", "are_you_sure_to_do_this": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Ī„Îŋ ÎēÎŦÎŊÎĩĪ„Îĩ ÎąĪ…Ī„ĪŒ;", + "array_field_not_fully_supported": "Τι Ī€ÎĩÎ´Î¯Îą Ī€Î¯ÎŊÎąÎēÎą ÎąĪ€ÎąÎšĪ„ÎŋĪÎŊ ·ÎĩÎšĪÎŋÎēίÎŊÎˇĪ„Îˇ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą JSON", "asset_action_delete_err_read_only": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎŧΌÎŊÎŋ ÎŗÎšÎą ÎąÎŊÎŦÎŗÎŊĪ‰ĪƒÎˇ, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "asset_action_share_err_offline": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚, Ī€ÎąĪÎąÎģÎĩÎ¯Ī€ÎĩĪ„ÎąÎš", "asset_added_to_album": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎĩ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "asset_adding_to_album": "Î ĪÎŋĪƒĪ„Î¯Î¸ÎĩĪ„ÎąÎš ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧâ€Ļ", + "asset_created": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ δΡÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "asset_description_updated": "Η Ī€ÎĩĪÎšÎŗĪÎąĪ†ÎŽ Ī„ÎŋĪ… ÎąÎŊĪ„ÎšÎēÎĩΚÎŧέÎŊÎŋĪ… Î­Ī‡ÎĩΚ ÎĩÎŊΡÎŧÎĩĪĪ‰Î¸Îĩί", "asset_filename_is_offline": "ΤÎŋ ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎŋ {filename} ÎĩίÎŊιΚ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "asset_has_unassigned_faces": "ΤÎŋ ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎŋ Î­Ī‡ÎĩΚ ÎŧΡ ÎąÎŊÎąĪ„ÎĩθÎĩΚÎŧέÎŊÎą Ī€ĪĪŒĪƒĪ‰Ī€Îą", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "ΔιÎŦĪ„ÎąÎžÎˇ", "asset_list_settings_subtitle": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ δΚÎŦĪ„ÎąÎžÎˇĪ‚ Ī€ÎģÎ­ÎŗÎŧÎąĪ„ÎŋĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", "asset_list_settings_title": "ΠÎģÎ­ÎŗÎŧÎą ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", + "asset_not_found_on_device_android": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ δÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎĩ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", + "asset_not_found_on_device_ios": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ δÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎĩ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ. ΑÎŊ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎĩÎ¯Ī„Îĩ iCloud, ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎŧΡÎŊ ÎĩίÎŊιΚ ΀΁ÎŋĪƒÎ˛ÎŦĪƒÎšÎŧÎŋ ÎģĪŒÎŗĪ‰ ΀΁ÎŋβÎģΡÎŧÎąĪ„ÎšÎēÎŋĪ ÎąĪĪ‡ÎĩίÎŋĪ… Ī€ÎŋĪ… ÎĩίÎŊιΚ ÎąĪ€ÎŋθΡÎēÎĩĪ…ÎŧέÎŊÎŋ ĪƒĪ„Îŋ iCloud", + "asset_not_found_on_icloud": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ δÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎĩ ĪƒĪ„Îŋ iCloud. ÎœĪ€Îŋ΁Îĩί ÎŊÎą ÎŧΡÎŊ ÎĩίÎŊιΚ ΀΁ÎŋĪƒÎ˛ÎŦĪƒÎšÎŧÎŋ ÎģĪŒÎŗĪ‰ ΀΁ÎŋβÎģΡÎŧÎąĪ„ÎšÎēÎŋĪ ÎąĪĪ‡ÎĩίÎŋĪ… Ī€ÎŋĪ… ÎĩίÎŊιΚ ÎąĪ€ÎŋθΡÎēÎĩĪ…ÎŧέÎŊÎŋ ĪƒĪ„Îŋ iCloud", "asset_offline": "ΑÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎŋ ÎĩÎēĪ„ĪŒĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "asset_offline_description": "Î‘Ī…Ī„ĪŒ Ī„Îŋ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēΌ ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎŋ δÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎĩ Ī€ÎģέÎŋÎŊ ĪƒĪ„ÎŋÎŊ Î´Î¯ĪƒÎēÎŋ. Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩĪ€ÎšÎēÎŋΚÎŊΉÎŊÎŽĪƒĪ„Îĩ ÎŧÎĩ Ī„ÎŋÎŊ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ Ī„ÎŋĪ… Immich ÎŗÎšÎą βÎŋΎθÎĩΚι.", "asset_restored_successfully": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎąĪ€ÎŋÎēÎąĪ„ÎąĪƒĪ„ÎŦθΡÎēÎĩ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "backup_album_selection_page_selection_info": "ΠÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚ ÎĩĪ€ÎšÎģÎŋÎŗÎŽĪ‚", "backup_album_selection_page_total_assets": "ÎŖĪ…ÎŊÎŋÎģΚÎēÎŦ ÎŧÎŋÎŊιδΚÎēÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", - "backup_albums_sync": "ÎŖĪ…ÎŗĪ‡ĪÎŋÎŊÎšĪƒÎŧĪŒĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", + "backup_albums_sync": "ÎŖĪ…ÎŗĪ‡ĪÎŋÎŊÎšĪƒÎŧĪŒĪ‚ ΑÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ Î‘ĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ΆÎģÎŧĪ€ÎŋĪ…Îŧ", "backup_all": "ΌÎģÎą", "backup_background_service_backup_failed_message": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚. Î•Ī€ÎąÎŊÎŦÎģÎˇĪˆÎˇâ€Ļ", "backup_background_service_complete_notification": "ΟÎģÎŋÎēÎģÎŽĪĪ‰ĪƒÎˇ ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Οι ÎēĪ‰Î´ÎšÎēÎŋί δÎĩÎŊ Ī„ÎąÎšĪÎšÎŦÎļÎŋĪ…ÎŊ", "change_password_form_reenter_new_password": "Î•Ī€ÎąÎŊÎĩÎšĪƒÎąÎŗĪ‰ÎŗÎŽ ΝέÎŋĪ… ÎšĪ‰Î´ÎšÎēÎŋĪ", "change_pin_code": "ΑÎģÎģÎąÎŗÎŽ ÎēĪ‰Î´ÎšÎēÎŋĪ PIN", + "change_trigger": "ΑÎģÎģÎąÎŗÎŽ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎˇĪ„ÎŽ", + "change_trigger_prompt": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎąÎģÎģÎŦΞÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎˇĪ„ÎŽ; Î‘Ī…Ī„ĪŒ θι Î´ÎšÎąÎŗĪÎŦΈÎĩΚ ΌÎģÎĩĪ‚ Ī„ÎšĪ‚ Ī…Ī€ÎŦ΁·ÎŋĪ…ĪƒÎĩĪ‚ ÎĩÎŊÎ­ĪÎŗÎĩΚÎĩĪ‚ ÎēιΚ Ī†Î¯ÎģĪ„ĪÎą.", "change_your_password": "ΑÎģÎģÎŦÎžĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ ĪƒÎąĪ‚", "changed_visibility_successfully": "Η ΀΁ÎŋβÎŋÎģÎŽ, ÎŦÎģÎģιΞÎĩ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", "charging": "ÎĻĪŒĪĪ„ÎšĪƒÎˇ", @@ -722,6 +759,18 @@ "checksum": "ΈÎģÎĩÎŗĪ‡ÎŋĪ‚ ÎąÎēÎĩĪÎąÎšĪŒĪ„ÎˇĪ„ÎąĪ‚", "choose_matching_people_to_merge": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī„Îą ÎąÎŊĪ„Î¯ĪƒĪ„ÎŋÎšĪ‡Îą ÎŦĪ„ÎŋÎŧÎą ÎŗÎšÎą ĪƒĪ…ÎŗĪ‡ĪŽÎŊÎĩĪ…ĪƒÎˇ", "city": "Î ĪŒÎģΡ", + "cleanup_confirm_description": "ΤÎŋ Immich ÎĩÎŊĪ„ĪŒĪ€ÎšĪƒÎĩ {count} ÎąĪĪ‡ÎĩÎ¯Îą (δΡÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎąÎŊ Ī€ĪÎšÎŊ ÎąĪ€ĪŒ {date}) Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąĪƒĪ†ÎąÎģĪŽĪ‚ ÎąÎŊĪ„ÎšÎŗĪÎąĪ†Îĩί ĪƒĪ„ÎŋÎŊ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ. Να Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ Ī„Îą Ī„ÎŋĪ€ÎšÎēÎŦ ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îą ÎąĪ€ĪŒ ÎąĪ…Ī„ÎŽ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ;", + "cleanup_confirm_prompt_title": "Να Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ ÎąĪ€ĪŒ ÎąĪ…Ī„ÎŽÎŊ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ;", + "cleanup_deleted_assets": "ΜÎĩĪ„ÎąĪ†Î­ĪÎ¸ÎˇÎēÎąÎŊ {count} ÎąĪĪ‡ÎĩÎ¯Îą ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ Ī„ÎˇĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚", + "cleanup_deleting": "ΜÎĩĪ„ÎąĪ†Îŋ΁ÎŦ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋâ€Ļ", + "cleanup_found_assets": "Î’ĪÎ­Î¸ÎˇÎēÎąÎŊ {count} ÎąĪĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąÎŊĪ„ÎšÎŗĪÎąĪ†Îĩί ÎąĪƒĪ†ÎąÎģĪŽĪ‚", + "cleanup_found_assets_with_size": "Î’ĪÎ­Î¸ÎˇÎēÎąÎŊ {count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ({size})", + "cleanup_icloud_shared_albums_excluded": "Τι ΚÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„Îą ΆÎģÎŧĪ€ÎŋĪ…Îŧ iCloud ÎĩÎžÎąÎšĪÎŋĪÎŊĪ„ÎąÎš ÎąĪ€ĪŒ Ī„Îˇ ΃ÎŦĪĪ‰ĪƒÎˇ", + "cleanup_no_assets_found": "ΔÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… ÎŊÎą Ī„ÎąÎšĪÎšÎŦÎļÎŋĪ…ÎŊ ÎŧÎĩ Ī„Îą Ī€ÎąĪÎąĪ€ÎŦÎŊΉ ÎēĪÎšĪ„ÎŽĪÎšÎą. Η ÂĢÎ‘Ī€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ…Âģ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩΚ ÎŧΌÎŊÎŋ Ī„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąÎŊĪ„ÎšÎŗĪÎąĪ†Îĩί ÎąĪƒĪ†ÎąÎģĪŽĪ‚ ĪƒĪ„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", + "cleanup_preview_title": "ÎŖĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ΀΁ÎŋĪ‚ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ({count})", + "cleanup_step3_description": "ÎŖÎŦĪĪ‰ĪƒÎˇ ÎŗÎšÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąÎŊĪ„ÎšÎŗĪÎąĪ†Îĩί ÎąĪƒĪ†ÎąÎģĪŽĪ‚ ĪƒĪÎŧΆΉÎŊÎą ÎŧÎĩ Ī„ÎˇÎŊ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎēιΚ Ī„ÎšĪ‚ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ Î´ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇĪ‚.", + "cleanup_step4_summary": "{count} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą (δΡÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎąÎŊ Ī€ĪÎšÎŊ ÎąĪ€ĪŒ {date}) ΀΁ÎŋĪ‚ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ÎąĪ€ĪŒ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ ĪƒÎąĪ‚. Οι ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ θι Ī€ÎąĪÎąÎŧÎĩίÎŊÎŋĪ…ÎŊ ΀΁ÎŋĪƒÎ˛ÎŦĪƒÎšÎŧÎĩĪ‚ ÎąĪ€ĪŒ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ Immich.", + "cleanup_trash_hint": "Για Ī„ÎˇÎŊ Ī€ÎģÎŽĪÎˇ ÎąĪ€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī„ÎŋĪ… Ī‡ĪŽĪÎŋĪ… ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚, ÎąÎŊÎŋÎ¯ÎžĪ„Îĩ Ī„ÎˇÎŊ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ Ī„ÎŋĪ… ĪƒĪ…ĪƒĪ„ÎŽÎŧÎąĪ„ĪŒĪ‚ ĪƒÎąĪ‚ ÎēιΚ ιδÎĩΚÎŦĪƒĪ„Îĩ Ī„ÎŋÎŊ ÎēÎŦδÎŋ", "clear": "ΕÎēÎēιθÎŦĪÎšĪƒÎˇ", "clear_all": "ΕÎēÎēιθÎŦĪÎšĪƒÎˇ ΌÎģΉÎŊ", "clear_all_recent_searches": "ΕÎēÎēιθÎŦĪÎšĪƒÎˇ ΌÎģΉÎŊ ΄ΉÎŊ Ī€ĪĪŒĪƒĪ†ÎąĪ„Ī‰ÎŊ ÎąÎŊÎąÎļÎˇĪ„ÎŽĪƒÎĩΉÎŊ", @@ -733,6 +782,8 @@ "client_cert_import": "Î•ÎšĪƒÎąÎŗĪ‰ÎŗÎŽ", "client_cert_import_success_msg": "ΤÎŋ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēΌ Ī€ÎĩÎģÎŦĪ„Îˇ ÎĩÎšĪƒÎŦÎŗÎĩĪ„ÎąÎš", "client_cert_invalid_msg": "Μη Î­ÎŗÎē΅΁Îŋ ÎąĪĪ‡ÎĩίÎŋ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēÎŋĪ ÎŽ ÎģÎŦθÎŋĪ‚ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚", + "client_cert_password_message": "Î•ÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚ ÎŗÎšÎą ÎąĪ…Ī„ĪŒ Ī„Îŋ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēΌ", + "client_cert_password_title": "ÎšĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēÎŋĪ", "client_cert_remove_msg": "ΤÎŋ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēΌ Ī€ÎĩÎģÎŦĪ„Îˇ ÎēÎąĪ„ÎąĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "client_cert_subtitle": "ÎĨĪ€ÎŋĪƒĪ„ÎˇĪÎ¯ÎļÎĩΚ ÎŧΌÎŊÎŋ Ī„Îˇ ÎŧÎŋĪĪ†ÎŽ PKCS12 (.p12, .pfx). Η ÎĩÎšĪƒÎąÎŗĪ‰ÎŗÎŽ/ÎąĪ†ÎąÎ¯ĪÎĩĪƒÎˇ Ī€ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēÎŋĪ ÎĩίÎŊιΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΡ ÎŧΌÎŊÎŋ Ī€ĪÎšÎŊ ÎąĪ€ĪŒ Ī„Îˇ ĪƒĪÎŊδÎĩĪƒÎˇ", "client_cert_title": "Î ÎšĪƒĪ„ÎŋĪ€ÎŋÎšÎˇĪ„ÎšÎēΌ SSL Ī€ÎĩÎģÎŦĪ„Îˇ [ΠΕΙΡΑΜΑΤΙΚΟ]", @@ -787,6 +838,7 @@ "create_album": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "create_album_page_untitled": "Î§Ī‰ĪÎ¯Ī‚ Ī„Î¯Ī„ÎģÎŋ", "create_api_key": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎēÎģÎĩΚδΚÎŋĪ API", + "create_first_workflow": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽĪƒĪ„Îĩ Ī„ÎˇÎŊ Ī€ĪĪŽĪ„Îˇ ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "create_library": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ΒιβÎģΚÎŋθΎÎēÎˇĪ‚", "create_link": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "create_link_to_share": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ… ÎŗÎšÎą δΚιÎŧÎŋÎšĪÎąĪƒÎŧΌ", @@ -801,17 +853,25 @@ "create_tag": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎĩĪ„ÎšÎēÎ­Ī„ÎąĪ‚", "create_tag_description": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎŊÎ­ÎąĪ‚ ÎĩĪ„ÎšÎēÎ­Ī„ÎąĪ‚. Για Ī„ÎšĪ‚ έÎŊθÎĩĪ„ÎĩĪ‚ ÎĩĪ„ÎšÎēÎ­Ī„ÎĩĪ‚, Ī€ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎšĪƒÎŦÎŗÎĩĪ„Îĩ Ī„Îˇ Ī€ÎģÎŽĪÎˇ Î´ÎšÎąÎ´ĪÎŋÎŧÎŽ Ī„ÎˇĪ‚, ĪƒĪ…ÎŧĪ€ÎĩĪÎšÎģÎąÎŧβιÎŊÎŋÎŧέÎŊΉÎŊ ΄ΉÎŊ ÎēÎŦθÎĩ΄ΉÎŊ Î´ÎšÎąĪ‡Ī‰ĪÎšĪƒĪ„ÎšÎēĪŽÎŊ.", "create_user": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą Ī‡ĪÎŽĪƒĪ„Îˇ", + "create_workflow": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "created": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "created_at": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽÎ¸ÎˇÎēÎĩ", "creating_linked_albums": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊΉÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ...", "crop": "Î‘Ī€ÎŋÎēÎŋĪ€ÎŽ", + "crop_aspect_ratio_fixed": "ΔιÎŋĪÎ¸ĪŽÎ¸ÎˇÎēÎĩ", + "crop_aspect_ratio_free": "ΕÎģÎĩĪÎ¸Îĩ΁Îŋ", + "crop_aspect_ratio_original": "Î‘Ī…Î¸ÎĩÎŊĪ„ÎšÎēΌ", "curated_object_page_title": "Î ĪÎŦÎŗÎŧÎąĪ„Îą", "current_device": "Î¤ĪÎ­Ī‡ÎŋĪ…ĪƒÎą ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", "current_pin_code": "Î¤ĪÎ­Ī‡Ī‰ÎŊ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ PIN", "current_server_address": "Î¤ĪÎ­Ī‡ÎŋĪ…ĪƒÎą δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", + "custom_date": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊΡ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą", "custom_locale": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊΡ ΤÎŋĪ€ÎšÎēÎŽ ÎĄĪÎ¸ÎŧÎšĪƒÎˇ", "custom_locale_description": "ΜÎŋ΁ΆÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎšĪ‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊίÎĩĪ‚ ÎēιΚ Ī„ÎŋĪ…Ī‚ ÎąĪÎšÎ¸ÎŧÎŋĪĪ‚, ĪƒĪÎŧΆΉÎŊÎą ÎŧÎĩ Ī„Îˇ ÎŗÎģĪŽĪƒĪƒÎą ÎēιΚ Ī„ÎˇÎŊ Ī€ÎĩĪÎšÎŋĪ‡ÎŽ", "custom_url": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊΡ δΚÎĩĪÎ¸Ī…ÎŊĪƒÎˇ URL", + "cutoff_date_description": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ ÎąĪ€ĪŒ Ī„ÎšĪ‚ Ī„ÎĩÎģÎĩĪ…Ī„ÎąÎ¯ÎĩĪ‚â€Ļ", + "cutoff_day": "{count, plural, one {ΡÎŧÎ­ĪÎą} other {ΡÎŧÎ­ĪÎĩĪ‚}}", + "cutoff_year": "{count, plural, one {Î­Ī„ÎŋĪ‚} other {Î­Ī„Îˇ}}", "daily_title_text_date": "Ε, MMM dd", "daily_title_text_date_year": "Ε, MMM dd, yyyy", "dark": "ÎŖÎēÎŋĪĪÎŋ", @@ -867,6 +927,7 @@ "deselect_all": "ΑÎēĪĪĪ‰ĪƒÎˇ ΌÎģΉÎŊ ΄ΉÎŊ ÎĩĪ€ÎšÎģÎŋÎŗĪŽÎŊ", "details": "ΛÎĩ΀΄ÎŋÎŧÎ­ĪÎĩΚÎĩĪ‚", "direction": "ÎšÎąĪ„ÎĩĪÎ¸Ī…ÎŊĪƒÎˇ", + "disable": "Î‘Ī€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", "disabled": "Î‘Ī€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊÎŋ", "disallow_edits": "Î‘Ī€ÎąÎŗĪŒĪÎĩĪ…ĪƒÎˇ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎšĪŽÎŊ", "discord": "ΠÎģÎąĪ„Ī†ĪŒĪÎŧÎą Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "ΕÎŊĪƒĪ‰ÎŧÎąĪ„Ī‰ÎŧέÎŊÎą Î˛Î¯ÎŊĪ„ÎĩÎŋ", "download_include_embedded_motion_videos_description": "ÎŖĪ…ÎŧĪ€ÎĩĪÎšÎģÎŦβÎĩĪ„Îĩ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… ÎĩίÎŊιΚ ÎĩÎŊĪƒĪ‰ÎŧÎąĪ„Ī‰ÎŧέÎŊÎą ΃Îĩ ÎēΚÎŊÎŋĪÎŧÎĩÎŊÎĩĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ Ή΂ ΞÎĩĪ‡Ī‰ĪÎšĪƒĪ„ĪŒ ÎąĪĪ‡ÎĩίÎŋ", "download_notfound": "ΤÎŋ ÎąĪĪ‡ÎĩίÎŋ δÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎĩ", + "download_original": "Î›ÎŽĪˆÎˇ Ī€ĪĪ‰Ī„ĪŒĪ„Ī…Ī€ÎŋĪ…", "download_paused": "Η ÎģÎŽĪˆÎˇ δΚιÎēĪŒĪ€ÎˇÎēÎĩ", "download_settings": "Î›ÎŽĪˆÎˇ", "download_settings_description": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ Ī€ÎŋĪ… ĪƒĪ‡ÎĩĪ„Î¯ÎļÎŋÎŊĪ„ÎąÎš ÎŧÎĩ Ī„Îˇ ÎģÎŽĪˆÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "ΑÎŊÎąÎŧÎŋÎŊÎŽ ÎŗÎšÎą ÎĩĪ€ÎąÎŊÎŦÎģÎˇĪˆÎˇ", "downloading": "ΓίÎŊÎĩĪ„ÎąÎš ÎģÎŽĪˆÎˇ", "downloading_asset_filename": "Î›ÎŽĪˆÎˇ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ… {filename}", + "downloading_from_icloud": "Î›ÎŽĪˆÎˇ ÎąĪ€ĪŒ Ī„Îŋ iCloud", "downloading_media": "Î›ÎŽĪˆÎˇ Ī€ÎŋÎģĪ…ÎŧÎ­ĪƒĪ‰ÎŊ", "drop_files_to_upload": "ÎŖĪĪÎĩĪ„Îĩ ÎąĪĪ‡ÎĩÎ¯Îą ÎĩÎ´ĪŽ ÎŗÎšÎą ÎŊÎą Ī„Îą ÎąÎŊÎĩβÎŦ΃ÎĩĪ„Îĩ", "duplicates": "Î”ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą", @@ -929,11 +992,22 @@ "edit_tag": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ÎĩĪ„ÎšÎēÎ­Ī„ÎąĪ‚", "edit_title": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą Î¤Î¯Ī„ÎģÎŋĪ…", "edit_user": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą Ī‡ĪÎŽĪƒĪ„Îˇ", + "edit_workflow": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "editor": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒĪ„ÎŽĪ‚", "editor_close_without_save_prompt": "Î‘Ī…Ī„Î­Ī‚ ÎŋΚ ÎąÎģÎģÎąÎŗÎ­Ī‚ δÎĩÎŊ θι ÎąĪ€ÎŋθΡÎēÎĩĪ…Ī„ÎŋĪÎŊ", "editor_close_without_save_title": "ΚÎģÎĩÎ¯ĪƒÎšÎŧÎŋ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒĪ„ÎŽ;", - "editor_crop_tool_h2_aspect_ratios": "ΑÎŊÎąÎģÎŋÎŗÎ¯ÎĩĪ‚ Î´ÎšÎąĪƒĪ„ÎŦ΃ÎĩΉÎŊ", - "editor_crop_tool_h2_rotation": "ΠÎĩĪÎšĪƒĪ„ĪÎŋĪ†ÎŽ", + "editor_confirm_reset_all_changes": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą ÎĩĪ€ÎąÎŊÎąĪ†Î­ĪÎĩĪ„Îĩ ΌÎģÎĩĪ‚ Ī„ÎšĪ‚ ÎąÎģÎģÎąÎŗÎ­Ī‚;", + "editor_discard_edits_confirm": "Î‘Ī€ĪŒĪĪÎšĪˆÎˇ ÎąÎģÎģÎąÎŗĪŽÎŊ", + "editor_discard_edits_prompt": "ÎˆĪ‡ÎĩĪ„Îĩ ÎŧΡ ÎąĪ€ÎŋθΡÎēÎĩĪ…ÎŧέÎŊÎĩĪ‚ ÎąÎģÎģÎąÎŗÎ­Ī‚. Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Ī„ÎšĪ‚ ÎąĪ€ÎŋĪĪÎ¯ĪˆÎĩĪ„Îĩ;", + "editor_discard_edits_title": "Î‘Ī€ĪŒĪĪÎšĪˆÎˇ ÎąÎģÎģÎąÎŗĪŽÎŊ;", + "editor_edits_applied_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚ ÎąÎģÎģÎąÎŗĪŽÎŊ", + "editor_edits_applied_success": "Οι ÎąÎģÎģÎąÎŗÎ­Ī‚ ÎĩĪ†ÎąĪÎŧĪŒĪƒĪ„ÎˇÎēÎąÎŊ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", + "editor_flip_horizontal": "ÎŸĪÎšÎļΌÎŊĪ„ÎšÎą ÎąÎŊÎąĪƒĪ„ĪÎŋĪ†ÎŽ", + "editor_flip_vertical": "ΚÎŦθÎĩĪ„Îˇ ÎąÎŊÎąĪƒĪ„ĪÎŋĪ†ÎŽ", + "editor_orientation": "Î ĪÎŋĪƒÎąÎŊÎąĪ„ÎŋÎģÎšĪƒÎŧĪŒĪ‚", + "editor_reset_all_changes": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ÎąÎģÎģÎąÎŗĪŽÎŊ", + "editor_rotate_left": "ΠÎĩĪÎšĪƒĪ„ĪÎŋĪ†ÎŽ 90° ÎąĪÎšĪƒĪ„ÎĩĪĪŒĪƒĪ„ĪÎŋĪ†Îą", + "editor_rotate_right": "ΠÎĩĪÎšĪƒĪ„ĪÎŋĪ†ÎŽ 90° δÎĩÎžÎšĪŒĪƒĪ„ĪÎŋĪ†Îą", "email": "Email", "email_notifications": "ΕιδÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚ email", "empty_folder": "Î‘Ī…Ī„ĪŒĪ‚ Îŋ ΆÎŦÎēÎĩÎģÎŋĪ‚ ÎĩίÎŊιΚ ÎēÎĩÎŊĪŒĪ‚", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Î‘Ī€Î­Ī„Ī…Ī‡Îĩ Ρ ÎąÎģÎģÎąÎŗÎŽ ΃ÎĩÎšĪÎŦĪ‚ Ī„ÎŋĪ… ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "error_delete_face": "ÎŖĪ†ÎŦÎģÎŧÎą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ… ÎąĪ€ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "error_getting_places": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎšĪŽÎŊ", + "error_loading_albums": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„Îˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ ΄ΉÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "error_loading_image": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„Îˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ Ī„ÎˇĪ‚ ÎĩΚÎēΌÎŊÎąĪ‚", "error_loading_partners": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„Îˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ ĪƒĪ…ÎŊÎĩĪÎŗÎąĪ„ĪŽÎŊ: {error}", + "error_retrieving_asset_information": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ Ī€ÎģÎˇĪÎŋΆÎŋĪÎšĪŽÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…", "error_saving_image": "ÎŖĪ†ÎŦÎģÎŧÎą: {error}", "error_tag_face_bounding_box": "ÎŖĪ†ÎŦÎģÎŧÎą ÎĩĪ€ÎšĪƒÎŽÎŧÎąÎŊĪƒÎˇĪ‚ ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ… - δÎĩÎŊ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ÎģÎˇĪ†Î¸ÎŋĪÎŊ ÎŋΚ ĪƒĪ…ÎŊĪ„ÎĩĪ„ÎąÎŗÎŧέÎŊÎĩĪ‚ Ī„ÎŋĪ… Ī€ÎģÎąÎšĪƒÎ¯ÎŋĪ… ÎŋĪÎšÎŋÎ¸Î­Ī„ÎˇĪƒÎˇĪ‚", "error_title": "ÎŖĪ†ÎŦÎģÎŧÎą - ΚÎŦĪ„Îš Ī€ÎŽÎŗÎĩ ĪƒĪ„ĪÎąÎ˛ÎŦ", + "error_while_navigating": "ÎŖĪ†ÎŦÎģÎŧÎą ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ Ī€ÎģÎŋÎŽÎŗÎˇĪƒÎˇ ĪƒĪ„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "errors": { "cannot_navigate_next_asset": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ Ī€ÎģÎŋÎŽÎŗÎˇĪƒÎˇ ĪƒĪ„Îŋ ÎĩĪ€ĪŒÎŧÎĩÎŊÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "cannot_navigate_previous_asset": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ Ī€ÎģÎŋÎŽÎŗÎˇĪƒÎˇ ĪƒĪ„Îŋ ΀΁ÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŋÎģÎŋÎēÎģÎŽĪĪ‰ĪƒÎˇĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚ ÎŧÎ­ĪƒĪ‰ OAuth", "unable_to_connect": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "unable_to_copy_to_clipboard": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎąĪ†ÎŽĪ‚ ĪƒĪ„Îŋ Ī€ĪĪŒĪ‡ÎĩÎšĪÎŋ, βÎĩÎ˛ÎąÎšĪ‰Î¸ÎĩÎ¯Ī„Îĩ ĪŒĪ„Îš Î­Ī‡ÎĩĪ„Îĩ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇ ĪƒĪ„Îˇ ΃ÎĩÎģÎ¯Î´Îą ÎŧÎ­ĪƒĪ‰ https", + "unable_to_create": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "unable_to_create_admin_account": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋĪ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "unable_to_create_api_key": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ÎĩÎŊĪŒĪ‚ ÎŊέÎŋĪ… ÎēÎģÎĩΚδΚÎŋĪ API", "unable_to_create_library": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą δΡÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ÎŧÎŋĪ„Î¯Î˛ÎŋĪ… ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ", "unable_to_delete_shared_link": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋĪ… ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", "unable_to_delete_user": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ Ī‡ĪÎŽĪƒĪ„Îˇ", + "unable_to_delete_workflow": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "unable_to_download_files": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎģÎŽĪˆÎˇĪ‚ ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ", "unable_to_edit_exclusion_pattern": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ÎŧÎŋĪ„Î¯Î˛ÎŋĪ… ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ", "unable_to_empty_trash": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ιδÎĩΚÎŦ΃ÎŧÎąĪ„ÎŋĪ‚ Ī„ÎŋĪ… ÎēÎŦδÎŋĪ… ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ΃ÎŦĪĪ‰ĪƒÎˇĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", "unable_to_set_feature_photo": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŋĪÎšĪƒÎŧÎŋĪ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎąĪ‚ Ī‡ÎąĪÎąÎēĪ„ÎˇĪÎšĪƒĪ„ÎšÎēÎŋĪ", "unable_to_set_profile_picture": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŋĪÎšĪƒÎŧÎŋĪ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎąĪ‚ ΀΁ÎŋĪ†Î¯Îģ", + "unable_to_set_rating": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŋĪÎšĪƒÎŧÎŋĪ βιθÎŧÎŋÎģÎŋÎŗÎ¯ÎąĪ‚", "unable_to_submit_job": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą Ī…Ī€ÎŋβÎŋÎģÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "unable_to_trash_asset": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŧÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇĪ‚ Ī„ÎŋĪ… ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ… ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", "unable_to_unlink_account": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąĪ€ÎŋĪƒĪÎŊδÎĩĪƒÎˇĪ‚ Ī„ÎŋĪ… ÎģÎŋÎŗÎąĪÎšÎąĪƒÎŧÎŋĪ", @@ -1074,8 +1154,10 @@ "unable_to_update_settings": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎŊÎąÎŊÎ­Ī‰ĪƒÎˇĪ‚ ΄ΉÎŊ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩΉÎŊ", "unable_to_update_timeline_display_status": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇĪ‚ ÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇĪ‚ Ī„ÎˇĪ‚ ΀΁ÎŋβÎŋÎģÎŽĪ‚ ·΁ÎŋÎŊÎŋÎģÎŋÎŗÎ¯ÎąĪ‚", "unable_to_update_user": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇĪ‚ Ī„ÎŋĪ… Ī‡ĪÎŽĪƒĪ„Îˇ", + "unable_to_update_workflow": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "unable_to_upload_file": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ÎąĪĪ‡ÎĩίÎŋĪ…" }, + "errors_text": "ÎŖĪ†ÎŦÎģÎŧÎąĪ„Îą", "exclusion_pattern": "ΜÎŋĪ„Î¯Î˛Îŋ ÎąĪ€ÎŋÎēÎģÎĩÎšĪƒÎŧÎŋĪ", "exif": "ΜÎĩĪ„ÎąÎ´ÎĩδÎŋÎŧέÎŊÎą Exif", "exif_bottom_sheet_description": "Î ĪÎŋĪƒÎ¸ÎŽÎēΡ ΠÎĩĪÎšÎŗĪÎąĪ†ÎŽĪ‚...", @@ -1120,14 +1202,17 @@ "features": "Î§ÎąĪÎąÎēĪ„ÎˇĪÎšĪƒĪ„ÎšÎēÎŦ", "features_in_development": "ΛÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎĩĪ‚ Ī…Ī€ĪŒ ΑÎŊÎŦĪ€Ī„Ī…ÎžÎˇ", "features_setting_description": "Î”ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„Îą Ī‡ÎąĪÎąÎēĪ„ÎˇĪÎšĪƒĪ„ÎšÎēÎŦ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚", - "file_name": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ…", "file_name_or_extension": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ… ÎŽ ÎĩĪ€Î­ÎēĪ„ÎąĪƒÎˇ", + "file_name_text": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ…", + "file_name_with_value": "ΌÎŊÎŋÎŧÎą ÎąĪĪ‡ÎĩίÎŋĪ…: {file_name}", "file_size": "ÎœÎ­ÎŗÎĩθÎŋĪ‚ ÎąĪĪ‡ÎĩίÎŋĪ…", "filename": "ΟÎŊÎŋÎŧÎąĪƒÎ¯Îą ÎąĪĪ‡ÎĩίÎŋĪ…", "filetype": "Î¤ĪĪ€ÎŋĪ‚ ÎąĪĪ‡ÎĩίÎŋĪ…", "filter": "ÎĻίÎģ΄΁Îŋ", + "filter_description": "ÎŖĪ…ÎŊθΎÎēÎĩĪ‚ ÎŗÎšÎą Ī†ÎšÎģ΄΁ÎŦĪÎšĪƒÎŧÎą ΄ΉÎŊ ĪƒĪ„Îŋ·ÎĩĪ…ÎŧέÎŊΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", "filter_people": "ÎĻΚÎģ΄΁ÎŦĪÎšĪƒÎŧÎą ÎąĪ„ĪŒÎŧΉÎŊ", "filter_places": "ÎĻΚÎģ΄΁ÎŦĪÎšĪƒÎŧÎą Ī„ÎŋĪ€ÎŋθÎĩĪƒÎšĪŽÎŊ", + "filters": "ÎĻίÎģĪ„ĪÎą", "find_them_fast": "Î’ĪÎĩÎ¯Ī„Îĩ Ī„ÎŋĪ…Ī‚ ÎŗĪÎŽÎŗÎŋĪÎą ÎŧÎĩ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ ÎēÎąĪ„ÎŦ ΌÎŊÎŋÎŧÎą", "first": "Î‘ĪĪ‡ÎšÎēÎŦ", "fix_incorrect_match": "Î”ÎšĪŒĪÎ¸Ī‰ĪƒÎˇ ÎģÎąÎŊÎ¸ÎąĪƒÎŧέÎŊÎˇĪ‚ ÎąÎŊĪ„ÎšĪƒĪ„ÎŋÎ¯Ī‡ÎšĪƒÎˇĪ‚", @@ -1137,12 +1222,16 @@ "folders_feature_description": "ΠÎĩĪÎšÎŽÎŗÎˇĪƒÎˇ ĪƒĪ„ÎˇÎŊ ΀΁ÎŋβÎŋÎģÎŽ Ī†ÎąÎēέÎģÎŋĪ… ÎŗÎšÎą Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ ĪƒĪ„Îŋ ĪƒĪĪƒĪ„ÎˇÎŧÎą ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ", "forgot_pin_code_question": "ΞÎĩ·ÎŦĪƒÎąĪ„Îĩ Ī„Îŋ PIN;", "forward": "Î ĪÎŋĪ‚ Ī„Îą ÎĩÎŧĪ€ĪĪŒĪ‚", + "free_up_space": "Î‘Ī€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ…", + "free_up_space_description": "ΜÎĩĪ„ÎąÎēΚÎŊÎŽĪƒĪ„Îĩ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎąÎŊĪ„ÎšÎŗĪÎąĪ†Îĩί ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ Ī„ÎˇĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚ ĪƒÎąĪ‚ ÎŗÎšÎą ÎŊÎą ÎąĪ€ÎĩÎģÎĩĪ…Î¸ÎĩĪĪŽĪƒÎĩĪ„Îĩ Ī‡ĪŽĪÎŋ. Τι ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†ÎŦ ĪƒÎąĪ‚ ĪƒĪ„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ, Ī€ÎąĪÎąÎŧέÎŊÎŋĪ…ÎŊ ÎąĪƒĪ†ÎąÎģÎŽ.", + "free_up_space_settings_subtitle": "Î‘Ī€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ… ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", "full_path": "ΠÎģÎŽĪÎˇĪ‚ Î´ÎšÎąÎ´ĪÎŋÎŧÎŽ: {path}", "gcast_enabled": "ΜÎĩĪ„ÎŦδÎŋĪƒÎˇ Ī€ÎĩĪÎšÎĩ·ÎŋÎŧέÎŊÎŋĪ… Google Cast", "gcast_enabled_description": "Î‘Ī…Ī„ĪŒ Ī„Îŋ Ī‡ÎąĪÎąÎēĪ„ÎˇĪÎšĪƒĪ„ÎšÎēΌ ΆÎŋĪĪ„ĪŽÎŊÎĩΚ ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēÎŋĪĪ‚ Ī€ĪŒĪÎŋĪ…Ī‚ ÎąĪ€ĪŒ Ī„Îˇ Google ÎŗÎšÎą ÎŊÎą ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎŽĪƒÎĩΚ.", "general": "ΓÎĩÎŊΚÎēÎŦ", "geolocation_instruction_location": "ΚÎŦÎŊÎĩ ÎēÎģΚÎē ΃Îĩ έÎŊÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎŧÎĩ ĪƒĪ…ÎŊĪ„ÎĩĪ„ÎąÎŗÎŧέÎŊÎĩĪ‚ GPS ÎŗÎšÎą ÎŊÎą Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚ Ī„ÎˇÎŊ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą Ī„ÎŋĪ…, ÎŽ ÎĩĪ€Î¯ÎģÎĩΞÎĩ ÎąĪ€ÎĩĪ…Î¸ÎĩÎ¯ÎąĪ‚ ÎŧΚι Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą ÎąĪ€ĪŒ Ī„ÎŋÎŊ ·ÎŦĪĪ„Îˇ", "get_help": "Î–ÎˇĪ„ÎŽĪƒĪ„Îĩ βÎŋΎθÎĩΚι", + "get_people_error": "ÎŖĪ†ÎŦÎģÎŧÎą ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇĪ‚ Ī‡ĪÎˇĪƒĪ„ĪŽÎŊ", "get_wifiname_error": "ΔÎĩÎŊ ÎŽĪ„ÎąÎŊ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎģÎŽĪˆÎˇ Ī„ÎŋĪ… ÎŋÎŊΌÎŧÎąĪ„ÎŋĪ‚ Wi-Fi. ΒÎĩÎ˛ÎąÎšĪ‰Î¸ÎĩÎ¯Ī„Îĩ ĪŒĪ„Îš Î­Ī‡ÎĩĪ„Îĩ Î´ĪŽĪƒÎĩΚ Ī„ÎšĪ‚ ÎąĪ€ÎąĪÎąÎ¯Ī„ÎˇĪ„ÎĩĪ‚ ÎŦδÎĩΚÎĩĪ‚ ÎēιΚ ĪŒĪ„Îš ÎĩÎ¯ĪƒĪ„Îĩ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎŋΚ ΃Îĩ δίÎē΄΅Îŋ Wi-Fi", "getting_started": "ΞÎĩÎēΚÎŊĪŽÎŊĪ„ÎąĪ‚", "go_back": "Î ÎˇÎŗÎąÎ¯ÎŊÎĩĪ„Îĩ Ī€Î¯ĪƒĪ‰", @@ -1175,6 +1264,7 @@ "hide_named_person": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ Ī„ÎŋĪ… ÎąĪ„ĪŒÎŧÎŋĪ… {name}", "hide_password": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ ÎēĪ‰Î´ÎšÎēÎŋĪ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚", "hide_person": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ ÎąĪ„ĪŒÎŧÎŋĪ…", + "hide_schema": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ ĪƒĪ‡ÎŽÎŧÎąĪ„ÎŋĪ‚", "hide_text_recognition": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ ÎąÎŊÎąÎŗÎŊĪŽĪÎšĪƒÎˇĪ‚ ÎēÎĩΚÎŧέÎŊÎŋĪ…", "hide_unnamed_people": "Î‘Ī€ĪŒÎēĪĪ…ĪˆÎˇ ÎąĪ„ĪŒÎŧΉÎŊ Ī‡Ī‰ĪÎ¯Ī‚ ΌÎŊÎŋÎŧÎą", "home_page_add_to_album_conflicts": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎąÎŊ {added} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ {album}. {failed} ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ ΎδΡ ĪƒĪ„Îŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Η ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą ÎĩÎēĪ„ÎĩÎģÎ­ĪƒĪ„ÎˇÎēÎĩ ĪƒĪ„ÎšĪ‚ {dateTime}", "items_count": "{count, plural, one {# ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎŋ} other {# ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą}}", "jobs": "Î•ĪÎŗÎąĪƒÎ¯ÎĩĪ‚", + "json_editor": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒĪ„ÎŽĪ‚ JSON", + "json_error": "ÎŖĪ†ÎŦÎģÎŧÎą JSON", "keep": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ", + "keep_albums": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ΄ΉÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", + "keep_albums_count": "Î”ÎšÎąĪ„ÎˇĪÎŋĪÎŊĪ„ÎąÎš {count} {count, plural, one {ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ} other {ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ}}", "keep_all": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ΌÎģΉÎŊ", + "keep_description": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī„Îš θι Ī€ÎąĪÎąÎŧÎĩίÎŊÎĩΚ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ ĪƒÎąĪ‚ ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ÎąĪ€ÎĩÎģÎĩĪ…Î¸Î­ĪĪ‰ĪƒÎˇ Ī‡ĪŽĪÎŋĪ….", + "keep_favorites": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ÎąÎŗÎąĪ€ÎˇÎŧέÎŊΉÎŊ", + "keep_on_device": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ĪƒĪ„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", + "keep_on_device_hint": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī„Îą ÎąÎŊĪ„ÎšÎēÎĩίÎŧÎĩÎŊÎą Ī€ÎŋĪ… θι ÎēĪÎąĪ„ÎŽĪƒÎĩĪ„Îĩ ΃Îĩ ÎąĪ…Ī„ÎŽÎŊ Ī„Îˇ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽ", "keep_this_delete_others": "Î”ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ÎąĪ…Ī„ÎŋĪ, Î´ÎšÎąÎŗĪÎąĪ†ÎŽ Ī…Ī€ÎŋÎģÎŋÎ¯Ī€Ī‰ÎŊ", + "keeping": "Î”ÎšÎąĪ„ÎˇĪÎŋĪÎŊĪ„ÎąÎš: {items}", "kept_this_deleted_others": "Î”ÎšÎąĪ„ÎˇĪÎŽÎ¸ÎˇÎēÎĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎēιΚ Î´ÎšÎąÎŗĪÎŦĪ†ÎˇÎēÎĩ/ÎēÎąÎŊ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}}", "keyboard_shortcuts": "ÎŖĪ…ÎŊĪ„ÎŋÎŧÎĩĪĪƒÎĩÎšĪ‚ Ī€ÎģΡÎē΄΁ÎŋÎģÎŋÎŗÎ¯ÎŋĪ…", "language": "ΓÎģĪŽĪƒĪƒÎą", @@ -1343,10 +1442,28 @@ "loop_videos_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„ÎˇÎŊ ÎąĪ…Ī„ĪŒÎŧÎąĪ„Îˇ ÎĩĪ€ÎąÎŊÎŦÎģÎˇĪˆÎˇ ÎĩÎŊĪŒĪ‚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ĪƒĪ„Îŋ Ī€ĪĪŒÎŗĪÎąÎŧÎŧÎą ΀΁ÎŋβÎŋÎģÎŽĪ‚ ÎģÎĩ΀΄ÎŋÎŧÎĩ΁ÎĩÎšĪŽÎŊ.", "main_branch_warning": "Î§ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋΚÎĩÎ¯Ī„Îĩ ÎŧΚι έÎēδÎŋĪƒÎˇ ΃Îĩ ÎąÎŊÎŦĪ€Ī„Ī…ÎžÎˇÎ‡ ĪƒĪ…ÎŊÎšĪƒĪ„ÎŋĪÎŧÎĩ ÎąÎŊÎĩĪ€ÎšĪ†ĪÎģÎąÎēĪ„Îą Ī„Îˇ Ī‡ĪÎŽĪƒÎˇ ÎŧÎšÎąĪ‚ Ī„ÎĩÎģΚÎēÎŽĪ‚ έÎēδÎŋĪƒÎˇĪ‚!", "main_menu": "ÎšĪĪÎšÎŋ ÎŧÎĩÎŊÎŋĪ", + "maintenance_action_restore": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ", "maintenance_description": "ΤÎŋ Immich Î­Ī‡ÎĩΚ Ī„ÎĩθÎĩί ΃Îĩ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚.", "maintenance_end": "ΤÎĩ΁ÎŧÎąĪ„ÎšĪƒÎŧĪŒĪ‚ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚", "maintenance_end_error": "Î‘Ī€ÎŋĪ„Ī…Ī‡Î¯Îą Ī„Îĩ΁ÎŧÎąĪ„ÎšĪƒÎŧÎŋĪ Ī„ÎˇĪ‚ ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯ÎąĪ‚ ĪƒĪ…ÎŊĪ„ÎŽĪÎˇĪƒÎˇĪ‚.", "maintenance_logged_in_as": "Î‘Ī…Ī„ÎŽÎŊ Ī„Îˇ ĪƒĪ„ÎšÎŗÎŧÎŽ ÎĩÎ¯ĪƒĪ„Îĩ ĪƒĪ…ÎŊδÎĩδÎĩÎŧέÎŊÎŋĪ‚ Ή΂ {user}", + "maintenance_restore_from_backup": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ÎąĪ€ĪŒ ÎąÎŊĪ„Î¯ÎŗĪÎąĪ†Îŋ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚", + "maintenance_restore_library": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ Ī„ÎˇĪ‚ βΚβÎģΚÎŋθΎÎēÎˇĪ‚ ĪƒÎąĪ‚", + "maintenance_restore_library_confirm": "ΑÎŊ ΌÎģÎą Ī†ÎąÎ¯ÎŊÎŋÎŊĪ„ÎąÎš ĪƒĪ‰ĪƒĪ„ÎŦ, ΀΁ÎŋĪ‡Ī‰ĪÎŽĪƒĪ„Îĩ ĪƒĪ„ÎˇÎŊ ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦ Ī„ÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚!", + "maintenance_restore_library_description": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊ", + "maintenance_restore_library_folder_has_files": "{folder} Î­Ī‡ÎĩΚ {count} ΆÎŦÎēÎĩÎģÎŋ(ÎŋĪ…Ī‚)", + "maintenance_restore_library_folder_no_files": "ÎŖĪ„Îŋ ΆÎŦÎēÎĩÎģÎŋ {folder} ÎģÎĩÎ¯Ī€ÎŋĪ…ÎŊ ÎąĪĪ‡ÎĩÎ¯Îą!", + "maintenance_restore_library_folder_pass": "ÎąÎŊÎąÎŗÎŊĪŽĪƒÎšÎŧÎŋ ÎēιΚ ÎĩÎŗÎŗĪÎŦĪˆÎšÎŧÎŋ", + "maintenance_restore_library_folder_read_fail": "ÎŧΡ ÎąÎŊÎąÎŗÎŊĪŽĪƒÎšÎŧÎŋ", + "maintenance_restore_library_folder_write_fail": "ÎŧΡ ÎĩÎŗÎŗĪÎŦĪˆÎšÎŧÎŋ", + "maintenance_restore_library_hint_missing_files": "ÎœĪ€Îŋ΁Îĩί ÎŊÎą ÎģÎĩÎ¯Ī€ÎŋĪ…ÎŊ ĪƒÎˇÎŧÎąÎŊĪ„ÎšÎēÎŦ ÎąĪĪ‡ÎĩÎ¯Îą", + "maintenance_restore_library_hint_regenerate_later": "ÎœĪ€Îŋ΁ÎĩÎ¯Ī„Îĩ ÎŊÎą Ī„Îą ÎĩĪ€ÎąÎŊιδΡÎŧΚÎŋĪ…ĪÎŗÎŽĪƒÎĩĪ„Îĩ ÎąĪÎŗĪŒĪ„ÎĩĪÎą ĪƒĪ„ÎšĪ‚ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚", + "maintenance_restore_library_hint_storage_template_missing_files": "Î§ĪÎŽĪƒÎˇ Ī€ĪĪŒĪ„Ī…Ī€ÎŋĪ… ÎąĪ€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚; ÎœĪ€Îŋ΁Îĩί ÎŊÎą ÎģÎĩÎ¯Ī€ÎŋĪ…ÎŊ ÎąĪĪ‡ÎĩÎ¯Îą", + "maintenance_restore_library_loading": "ÎĻĪŒĪĪ„Ī‰ĪƒÎˇ ÎĩÎģÎ­ÎŗĪ‡Ī‰ÎŊ ÎąÎēÎĩĪÎąÎšĪŒĪ„ÎˇĪ„ÎąĪ‚ ÎēιΚ Î­ÎžĪ…Ī€ÎŊΉÎŊ ÎĩÎģÎ­ÎŗĪ‡Ī‰ÎŊâ€Ļ", + "maintenance_task_backup": "ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ Ī„ÎˇĪ‚ Ī…Ī€ÎŦ΁·ÎŋĪ…ĪƒÎąĪ‚ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊâ€Ļ", + "maintenance_task_migrations": "ΕÎēĪ„Î­ÎģÎĩĪƒÎˇ ÎŧÎĩĪ„ÎąĪ„ĪÎŋĪ€ĪŽÎŊ/ÎĩÎŊΡÎŧÎĩĪĪŽĪƒÎĩΉÎŊ βÎŦĪƒÎˇĪ‚ δÎĩδÎŋÎŧέÎŊΉÎŊâ€Ļ", + "maintenance_task_restore": "Î•Ī€ÎąÎŊÎąĪ†Îŋ΁ÎŦ Ī„ÎŋĪ… ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎŋĪ… ÎąÎŊĪ„ÎšÎŗĪÎŦΆÎŋĪ… ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚â€Ļ", + "maintenance_task_rollback": "Η ÎĩĪ€ÎąÎŊÎąĪ†Îŋ΁ÎŦ ÎąĪ€Î­Ī„Ī…Ī‡Îĩ, ÎĩĪ€ÎšĪƒĪ„ĪÎŋĪ†ÎŽ ĪƒĪ„ÎˇÎŊ ΀΁ÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊΡ ÎēÎąĪ„ÎŦĪƒĪ„ÎąĪƒÎˇâ€Ļ", "maintenance_title": "Î ĪÎŋĪƒĪ‰ĪÎšÎŊÎŦ ÎŧΡ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ", "make": "ÎšÎąĪ„ÎąĪƒÎēÎĩĪ…ÎąĪƒĪ„ÎŽĪ‚", "manage_geolocation": "Î”ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚", @@ -1408,6 +1525,8 @@ "minimize": "ΕÎģÎąĪ‡ÎšĪƒĪ„ÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", "minute": "ΛÎĩĪ€Ī„ĪŒ", "minutes": "ΛÎĩ΀΄ÎŦ", + "mirror_horizontal": "ÎŸĪÎšÎļΌÎŊĪ„ÎšÎą", + "mirror_vertical": "ΚÎŦθÎĩĪ„Îą", "missing": "ÎŒĪƒÎą ΛÎĩÎ¯Ī€ÎŋĪ…ÎŊ", "mobile_app": "Î•Ī†ÎąĪÎŧÎŋÎŗÎŽ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ", "mobile_app_download_onboarding_note": "ÎšÎąĪ„Î­Î˛ÎąĪƒÎĩ Ī„ÎˇÎŊ ĪƒĪ…ÎŊÎŋδÎĩĪ…Ī„ÎšÎēÎŽ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽ ÎŗÎšÎą ÎēΚÎŊÎˇĪ„ÎŦ Ī‡ĪÎˇĪƒÎšÎŧÎŋĪ€ÎŋÎšĪŽÎŊĪ„ÎąĪ‚ Ī„ÎšĪ‚ Ī€ÎąĪÎąÎēÎŦ΄Ή ÎĩĪ€ÎšÎģÎŋÎŗÎ­Ī‚", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "ΜΜΜΜ y", "more": "ΠÎĩĪÎšĪƒĪƒĪŒĪ„ÎĩĪÎą", "move": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ", + "move_down": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ ΀΁ÎŋĪ‚ Ī„Îą ÎēÎŦ΄Ή", "move_off_locked_folder": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ Î­ÎžĪ‰ ÎąĪ€ĪŒ Ī„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", "move_to": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ ΃Îĩ", + "move_to_device_trash": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ Ī„ÎˇĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…ÎŽĪ‚", "move_to_lock_folder_action_prompt": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎąÎŊ {count} ĪƒĪ„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", "move_to_locked_folder": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ ΃Îĩ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", "move_to_locked_folder_confirmation": "Î‘Ī…Ī„Î­Ī‚ ÎŋΚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩÎŋ θι ÎąĪ†ÎąÎšĪÎĩθÎŋĪÎŊ ÎąĪ€ĪŒ ΌÎģÎą Ī„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎēιΚ θι ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ΀΁ÎŋβÎģΡθÎŋĪÎŊ ÎŧΌÎŊÎŋ ÎąĪ€ĪŒ Ī„ÎŋÎŊ ÎēÎģÎĩÎšÎ´Ī‰ÎŧέÎŊÎŋ ΆÎŦÎēÎĩÎģÎŋ", + "move_up": "ΜÎĩĪ„ÎąÎēίÎŊÎˇĪƒÎˇ ΀΁ÎŋĪ‚ Ī„Îą Ī€ÎŦÎŊΉ", "moved_to_archive": "ΜÎĩĪ„ÎąÎēΚÎŊΎθΡÎēÎąÎŊ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}} ĪƒĪ„Îŋ ÎąĪĪ‡ÎĩίÎŋ", "moved_to_library": "ΜÎĩĪ„ÎąÎēΚÎŊΎθΡÎēÎĩ/ÎąÎŊ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}} ĪƒĪ„Îˇ βΚβÎģΚÎŋθΎÎēΡ", "moved_to_trash": "ΜÎĩĪ„ÎąÎēΚÎŊΎθΡÎēÎĩ ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ", @@ -1430,6 +1552,7 @@ "my_albums": "Τι ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎŧÎŋĪ…", "name": "ΌÎŊÎŋÎŧÎą", "name_or_nickname": "ΌÎŊÎŋÎŧÎą ÎŽ ΈÎĩĪ…Î´ĪŽÎŊĪ…ÎŧÎŋ", + "name_required": "Î‘Ī€ÎąÎšĪ„ÎĩÎ¯Ī„ÎąÎš ΌÎŊÎŋÎŧÎą", "navigate": "ΠÎģÎŋÎˇÎŗÎˇÎ¸ÎĩÎ¯Ī„Îĩ", "navigate_to_time": "ΠÎģÎŋÎˇÎŗÎˇÎ¸ÎĩÎ¯Ī„Îĩ ĪƒĪ„Îŋ Î§ĪĪŒÎŊÎŋ", "network_requirement_photos_upload": "Î§ĪÎŽĪƒÎˇ δÎĩδÎŋÎŧέÎŊΉÎŊ ÎēΚÎŊÎˇĪ„ÎŽĪ‚ Ī„ÎˇÎģÎĩΆΉÎŊÎ¯ÎąĪ‚ ÎŗÎšÎą Ī„Îˇ δΡÎŧΚÎŋĪ…ĪÎŗÎ¯Îą ÎąÎŊĪ„ÎšÎŗĪÎŦΆΉÎŊ ÎąĪƒĪ†ÎąÎģÎĩÎ¯ÎąĪ‚ ΄ΉÎŊ ΆΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", @@ -1454,20 +1577,24 @@ "next": "Î•Ī€ĪŒÎŧÎĩÎŊÎŋ", "next_memory": "Î•Ī€ĪŒÎŧÎĩÎŊΡ ÎąÎŊÎŦÎŧÎŊÎˇĪƒÎˇ", "no": "ÎŒĪ‡Îš", + "no_actions_added": "ΔÎĩÎŊ Î­Ī‡ÎŋĪ…ÎŊ ΀΁ÎŋĪƒĪ„ÎĩθÎĩί ÎąÎēΌÎŧÎą ÎĩÎŊÎ­ĪÎŗÎĩΚÎĩĪ‚", + "no_albums_found": "ΔÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "no_albums_message": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽĪƒĪ„Îĩ έÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎŗÎšÎą ÎŊÎą ÎŋĪÎŗÎąÎŊĪŽĪƒÎĩĪ„Îĩ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩΌ ĪƒÎąĪ‚", "no_albums_with_name_yet": "ÎĻÎąÎ¯ÎŊÎĩĪ„ÎąÎš ĪŒĪ„Îš δÎĩÎŊ Î­Ī‡ÎĩĪ„Îĩ ÎēÎąÎŊέÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎŧÎĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ ΌÎŊÎŋÎŧÎą ÎąÎēΌÎŧÎą.", "no_albums_yet": "ÎĻÎąÎ¯ÎŊÎĩĪ„ÎąÎš ĪŒĪ„Îš δÎĩÎŊ Î­Ī‡ÎĩĪ„Îĩ ÎēÎąÎŊέÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎąÎēΌÎŧÎą.", "no_archived_assets_message": "Î‘ĪĪ‡ÎĩΚÎŋθÎĩĪ„ÎŽĪƒĪ„Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎŗÎšÎą ÎŊÎą Ī„Îą ÎąĪ€ÎŋÎēĪĪĪˆÎĩĪ„Îĩ ÎąĪ€ĪŒ Ī„ÎˇÎŊ Î ĪÎŋβÎŋÎģÎŽ ÎĻΉ΄ÎŋÎŗĪÎąĪ†ÎšĪŽÎŊ", - "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ Î‘ÎÎ•Î’Î‘ÎŖÎ•Î¤Î• ΤΗΝ ΠΡΩΤΗ ÎŖÎ‘ÎŖ ÎĻΩΤΟΓΡΑÎĻΙΑ", + "no_assets_message": "ΚÎģΚÎēÎŦ΁ÎĩĪ„Îĩ ÎŗÎšÎą ÎŊÎą ÎąÎŊÎĩβÎŦ΃ÎĩĪ„Îĩ Ī„ÎˇÎŊ Ī€ĪĪŽĪ„Îˇ ĪƒÎąĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą", "no_assets_to_show": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ΀΁ÎŋĪ‚ ÎĩÎŧΆÎŦÎŊÎšĪƒÎˇ", "no_cast_devices_found": "ΔÎĩ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ ĪƒĪ…ĪƒÎēÎĩĪ…Î­Ī‚ ÎŧÎĩĪ„ÎŦδÎŋĪƒÎˇĪ‚", "no_checksum_local": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎĩΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ checksum ÎŗÎšÎą έÎģÎĩÎŗĪ‡Îŋ ÎąÎēÎĩĪÎąÎšĪŒĪ„ÎˇĪ„ÎąĪ‚ – δÎĩÎŊ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ÎąÎŊÎąÎēĪ„ÎˇÎ¸ÎŋĪÎŊ Ī„Îą Ī„ÎŋĪ€ÎšÎēÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", "no_checksum_remote": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎĩΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ checksum ÎŗÎšÎą έÎģÎĩÎŗĪ‡Îŋ ÎąÎēÎĩĪÎąÎšĪŒĪ„ÎˇĪ„ÎąĪ‚ – δÎĩÎŊ ÎŧĪ€Îŋ΁ÎŋĪÎŊ ÎŊÎą ÎąÎŊÎąÎēĪ„ÎˇÎ¸ÎŋĪÎŊ Ī„Îą ÎąĪ€ÎŋÎŧÎąÎēĪĪ…ĪƒÎŧέÎŊÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", + "no_configuration_needed": "ΔÎĩÎŊ ÎąĪ€ÎąÎšĪ„ÎĩÎ¯Ī„ÎąÎš ĪĪÎ¸ÎŧÎšĪƒÎˇ", "no_devices": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ ÎĩΞÎŋĪ…ĪƒÎšÎŋδÎŋĪ„ÎˇÎŧέÎŊÎĩĪ‚ ĪƒĪ…ĪƒÎēÎĩĪ…Î­Ī‚", "no_duplicates_found": "ΔÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Îą.", "no_exif_info_available": "ΚαÎŧÎ¯Îą Ī€ÎģÎˇĪÎŋΆÎŋĪÎ¯Îą exif Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΡ", "no_explore_results_message": "ΑÎŊÎĩβÎŦĪƒĪ„Îĩ Ī€ÎĩĪÎšĪƒĪƒĪŒĪ„Îĩ΁ÎĩĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎŗÎšÎą ÎŊÎą Ī€ÎĩĪÎšÎˇÎŗÎˇÎ¸ÎĩÎ¯Ī„Îĩ ĪƒĪ„Îˇ ĪƒĪ…ÎģÎģÎŋÎŗÎŽ ĪƒÎąĪ‚.", "no_favorites_message": "Î ĪÎŋĪƒÎ¸Î­ĪƒĪ„Îĩ ÎąÎŗÎąĪ€ÎˇÎŧέÎŊÎą ÎŗÎšÎą ÎŊÎą Î˛ĪÎĩÎ¯Ī„Îĩ ÎŗĪÎŽÎŗÎŋĪÎą Ī„ÎšĪ‚ ÎēÎąÎģĪĪ„Îĩ΁ÎĩĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩΌ ĪƒÎąĪ‚", + "no_filters_added": "ΔÎĩÎŊ Î­Ī‡ÎŋĪ…ÎŊ ΀΁ÎŋĪƒĪ„ÎĩθÎĩί ÎąÎēΌÎŧÎą Ī†Î¯ÎģĪ„ĪÎą", "no_libraries_message": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽĪƒĪ„Îĩ ÎŧΚι ÎĩÎžĪ‰Ī„ÎĩĪÎšÎēÎŽ βΚβÎģΚÎŋθΎÎēΡ ÎŗÎšÎą ÎŊÎą ΀΁ÎŋβÎŦÎģÎĩĪ„Îĩ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Ī„Îą Î˛Î¯ÎŊĪ„ÎĩΌ ĪƒÎąĪ‚", "no_local_assets_found": "ΔÎĩÎŊ Î˛ĪÎ­Î¸ÎˇÎēÎąÎŊ Ī„ÎŋĪ€ÎšÎēÎŦ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩ ÎąĪ…Ī„ĪŒ Ī„Îŋ checksum", "no_location_set": "Η Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą δÎĩÎŊ Î­Ī‡ÎĩΚ ÎŋĪÎšĪƒĪ„Îĩί", @@ -1481,11 +1608,11 @@ "no_results_description": "ΔÎŋÎēΚÎŧÎŦĪƒĪ„Îĩ έÎŊÎą ĪƒĪ…ÎŊĪŽÎŊĪ…ÎŧÎŋ ÎŽ Ī€ÎšÎŋ ÎŗÎĩÎŊΚÎēÎŽ ÎģέΞΡ-ÎēÎģÎĩΚδί", "no_shared_albums_message": "ΔηÎŧΚÎŋĪ…ĪÎŗÎŽĪƒĪ„Îĩ έÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎŗÎšÎą ÎŊÎą ÎŧÎŋÎšĪÎŦÎļÎĩĪƒĪ„Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎŧÎĩ ÎŦĪ„ÎŋÎŧÎą ĪƒĪ„Îŋ δίÎēĪ„Ī…ĪŒ ĪƒÎąĪ‚", "no_uploads_in_progress": "ΚαÎŧÎ¯Îą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ΃Îĩ ÎĩΞέÎģΚΞΡ", + "none": "ΚαÎŊέÎŊÎą", "not_allowed": "ΔÎĩÎŊ ÎĩĪ€ÎšĪ„ĪÎ­Ī€ÎĩĪ„ÎąÎš", "not_available": "Μ/Δ (Μη Î”ÎšÎąÎ¸Î­ĪƒÎšÎŧÎŋ)", "not_in_any_album": "ÎŖÎĩ ÎēÎąÎŊέÎŊÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "not_selected": "ΔÎĩÎŊ ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎĩ", - "note_apply_storage_label_to_previously_uploaded assets": "ÎŖÎˇÎŧÎĩÎ¯Ī‰ĪƒÎˇ: Για ÎŊÎą ÎĩĪ†ÎąĪÎŧΌ΃ÎĩĪ„Îĩ Ī„ÎˇÎŊ Î•Ī„ÎšÎēÎ­Ī„Îą Î‘Ī€ÎŋθΎÎēÎĩĪ…ĪƒÎˇĪ‚ ΃Îĩ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î­Ī‡ÎŋĪ…ÎŊ ÎŧÎĩĪ„ÎąĪ†ÎŋĪĪ„Ī‰Î¸Îĩί ΀΁ÎŋÎˇÎŗÎŋĪ…ÎŧέÎŊΉ΂, ÎĩÎēĪ„ÎĩÎģÎ­ĪƒĪ„Îĩ Ī„Îŋ", "notes": "ÎŖÎˇÎŧÎĩÎšĪŽĪƒÎĩÎšĪ‚", "nothing_here_yet": "Î¤Î¯Ī€ÎŋĪ„Îą ÎĩÎ´ĪŽ ÎąÎēΌÎŧÎą", "notification_permission_dialog_content": "Για ÎŊÎą ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒÎĩĪ„Îĩ Ī„ÎšĪ‚ ÎĩΚδÎŋĪ€ÎŋÎšÎŽĪƒÎĩÎšĪ‚, ÎŧÎĩĪ„ÎąÎ˛ÎĩÎ¯Ī„Îĩ ĪƒĪ„ÎšĪ‚ ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎēιΚ ÎĩĪ€ÎšÎģÎ­ÎžĪ„Îĩ ÎŊÎą ÎĩĪ€ÎšĪ„ĪÎ­Ī€ÎĩĪ„ÎąÎš.", @@ -1563,6 +1690,7 @@ "people": "Î†Ī„ÎŋÎŧÎą", "people_edits_count": "ÎˆÎŗÎšÎŊÎĩ ÎĩĪ€ÎĩΞÎĩĪÎŗÎąĪƒÎ¯Îą {count, plural, one {# ÎąĪ„ĪŒÎŧÎŋĪ…} other {# ÎąĪ„ĪŒÎŧΉÎŊ}}", "people_feature_description": "ΠÎĩĪÎšÎŽÎŗÎˇĪƒÎˇ ΃Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ÎēιΚ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎŋÎŧιδÎŋĪ€ÎŋΚΡÎŧέÎŊÎą ÎąÎŊÎŦ ÎŦĪ„ÎŋÎŧÎŋ", + "people_selected": "{count, plural, one {# ÎŦĪ„ÎŋÎŧÎŋ ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎĩ} other {# ÎŦĪ„ÎŋÎŧÎą ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎąÎŊ}}", "people_sidebar_description": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ Î‘Ī„ĪŒÎŧΉÎŊ ĪƒĪ„ÎˇÎŊ Ī€ÎģÎąĪŠÎŊÎŽ ÎŗĪÎąÎŧÎŧÎŽ", "permanent_deletion_warning": "Î ĪÎŋÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ ÎŋĪÎšĪƒĪ„ÎšÎēÎŽĪ‚ Î´ÎšÎąÎŗĪÎąĪ†ÎŽĪ‚", "permanent_deletion_warning_setting_description": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ΀΁ÎŋÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇĪ‚ ÎēÎąĪ„ÎŦ Ī„ÎˇÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŽ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# Ī‡ĪĪŒÎŊΚι}} Ī€ÎąÎģΚÎŦ", "person_birthdate": "ΓÎĩÎŊÎŊΡθÎĩÎ¯Ī‚ ĪƒĪ„ÎšĪ‚ {date}", "person_hidden": "{name}{hidden, select, true { (ÎēĪĪ…Ī†ĪŒ)} other {}}", + "person_recognized": "Î†Ī„ÎŋÎŧÎŋ ÎąÎŊÎąÎŗÎŊĪ‰ĪÎ¯ĪƒĪ„ÎˇÎēÎĩ", + "person_selected": "Î†Ī„ÎŋÎŧÎŋ ÎĩĪ€ÎšÎģÎ­Ī‡Î¸ÎˇÎēÎĩ", "photo_shared_all_users": "ÎĻÎąÎ¯ÎŊÎĩĪ„ÎąÎš ĪŒĪ„Îš ÎŧÎŋÎšĪÎąĪƒĪ„ÎŽÎēÎąĪ„Îĩ Ī„ÎšĪ‚ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ĪƒÎąĪ‚ ÎŧÎĩ ΌÎģÎŋĪ…Ī‚ Ī„ÎŋĪ…Ī‚ Ī‡ĪÎŽĪƒĪ„ÎĩĪ‚ ÎŽ δÎĩÎŊ Î­Ī‡ÎĩĪ„Îĩ ÎēÎąÎŊέÎŊÎąÎŊ Ī‡ĪÎŽĪƒĪ„Îˇ ÎŗÎšÎą ÎēÎŋΚÎŊÎŽ Ī‡ĪÎŽĪƒÎˇ.", "photos": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "photos_and_videos": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ & ΒίÎŊĪ„ÎĩÎŋ", "photos_count": "{count, plural, one {{count, number} ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą} other {{count, number} ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚}}", "photos_from_previous_years": "ÎĻΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚ ΀΁ÎŋÎˇÎŗÎŋĪÎŧÎĩÎŊΉÎŊ ÎĩĪ„ĪŽÎŊ", + "photos_only": "ÎœĪŒÎŊÎŋ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "pick_a_location": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎŧΚι Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯Îą", "pick_custom_range": "Î ĪÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊÎŋ ÎĩĪĪÎŋĪ‚", "pick_date_range": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎĩĪĪÎŋĪ‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎšĪŽÎŊ", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Η Î´ÎšÎąĪ‡ÎĩÎ¯ĪÎšĪƒÎˇ Ī„ÎŋĪ… ÎēÎģÎĩΚδΚÎŋĪ ΀΁ÎŋΊΌÎŊĪ„ÎŋĪ‚ Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ ÎŗÎ¯ÎŊÎĩĪ„ÎąÎš ÎąĪ€ĪŒ Ī„ÎŋÎŊ Î´ÎšÎąĪ‡ÎĩÎšĪÎšĪƒĪ„ÎŽ", "query_asset_id": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ ID ÎŖĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…", "queue_status": "ΤÎŋĪ€ÎŋÎ¸Î­Ī„ÎˇĪƒÎˇ ĪƒĪ„Îˇ Îŋ΅΁ÎŦ {count} ÎąĪ€ĪŒ {total}", + "rate_asset": "ΒαθÎŧÎŋÎģÎŋÎŗÎŽĪƒĪ„Îĩ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "rating": "ΑξιÎŋÎģĪŒÎŗÎˇĪƒÎˇ ÎŧÎĩ ÎąĪƒĪ„Î­ĪÎšÎą", "rating_clear": "ΕÎēÎēιθÎŦĪÎšĪƒÎˇ ιΞΚÎŋÎģĪŒÎŗÎˇĪƒÎˇĪ‚", "rating_count": "{count, plural, one {# ÎąĪƒĪ„Î­ĪÎš} other {# ÎąĪƒĪ„Î­ĪÎšÎą}}", "rating_description": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ Ī„ÎˇĪ‚ ιΞΚÎŋÎģĪŒÎŗÎˇĪƒÎˇĪ‚ EXIF ĪƒĪ„ÎŋÎŊ Ī€Î¯ÎŊÎąÎēÎą Ī€ÎģÎˇĪÎŋΆÎŋĪÎšĪŽÎŊ", + "rating_set": "Η βιθÎŧÎŋÎģÎŋÎŗÎ¯Îą ÎŋĪÎ¯ĪƒĪ„ÎˇÎēÎĩ ΃Îĩ {rating, plural, one {# ÎąĪƒĪ„Î­ĪÎš} other {# ÎąĪƒĪ„Î­ĪÎšÎą}}", "reaction_options": "Î•Ī€ÎšÎģÎŋÎŗÎ­Ī‚ ÎąÎŊĪ„Î¯Î´ĪÎąĪƒÎˇĪ‚", "read_changelog": "ΔιαβÎŦĪƒĪ„Îĩ Ī„Îŋ Î‘ĪĪ‡ÎĩίÎŋ ÎšÎąĪ„ÎąÎŗĪÎąĪ†ÎŽĪ‚ ΑÎģÎģÎąÎŗĪŽÎŊ", "readonly_mode_disabled": "Η ÎģÎĩÎšĪ„ÎŋĪ…ĪÎŗÎ¯Îą ÎŧΌÎŊÎŋ-ÎŗÎšÎą-ÎąÎŊÎŦÎŗÎŊĪ‰ĪƒÎˇ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΎθΡÎēÎĩ", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "Η ÎąÎŊÎŦθÎĩĪƒÎˇ {count, plural, one {# ÎąĪĪ‡ÎĩίÎŋĪ…} other {# ÎąĪĪ‡ÎĩÎ¯Ī‰ÎŊ}} ΃Îĩ ÎŊέÎŋ ÎŦĪ„ÎŋÎŧÎŋ", "reassing_hint": "ΑÎŊÎŦθÎĩĪƒÎˇ ΄ΉÎŊ ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊΉÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ ΃Îĩ Ī…Ī€ÎŦ΁·ÎŋÎŊ ÎŦĪ„ÎŋÎŧÎŋ", "recent": "Î ĪĪŒĪƒĪ†ÎąĪ„Îą", - "recent-albums": "Î ĪĪŒĪƒĪ†ÎąĪ„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", + "recent_albums": "Î ĪĪŒĪƒĪ†ÎąĪ„Îą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "recent_searches": "Î ĪĪŒĪƒĪ†ÎąĪ„ÎĩĪ‚ ÎąÎŊÎąÎļÎˇĪ„ÎŽĪƒÎĩÎšĪ‚", "recently_added": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎąÎŊ Ī€ĪĪŒĪƒĪ†ÎąĪ„Îą", "recently_added_page_title": "Î ĪÎŋĪƒĪ„Î­Î¸ÎˇÎēÎąÎŊ Î ĪĪŒĪƒĪ†ÎąĪ„Îą", @@ -1770,9 +1903,11 @@ "saved_settings": "Î‘Ī€ÎŋθΡÎēÎĩĪ…ÎŧέÎŊÎĩĪ‚ ĪĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚", "say_something": "ΠÎĩÎ¯Ī„Îĩ ÎēÎŦĪ„Îš", "scaffold_body_error_occurred": "Î ÎąĪÎŋĪ…ĪƒÎšÎŦĪƒĪ„ÎˇÎēÎĩ ĪƒĪ†ÎŦÎģÎŧÎą", + "scan": "ÎŖÎŦĪĪ‰ĪƒÎˇ", "scan_all_libraries": "ÎŖÎŦĪĪ‰ĪƒÎˇ ΌÎģΉÎŊ ΄ΉÎŊ ΒιβÎģΚÎŋθΡÎēĪŽÎŊ", "scan_library": "ÎŖÎŦĪĪ‰ĪƒÎˇ", "scan_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ ÎŖÎŦĪĪ‰ĪƒÎˇĪ‚", + "scanning": "ÎŖÎąĪĪŽÎŊÎĩĪ„ÎąÎš", "scanning_for_album": "ÎŖÎŦĪĪ‰ĪƒÎˇ ÎŗÎšÎą ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ...", "search": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ", "search_albums": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī„ĪĪ€Îŋ ÎŧÎ­ĪƒÎŋĪ…", "search_filter_ocr": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ ÎēÎąĪ„ÎŦ OCR", "search_filter_people_title": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎŦĪ„ÎŋÎŧÎą", + "search_filter_star_rating": "ΒαθÎŧÎŋÎģÎŋÎŗÎ¯Îą ÎŧÎĩ ÎąĪƒĪ„Î­ĪÎšÎą", "search_for": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ ÎŗÎšÎą", "search_for_existing_person": "ΑÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇ Ī…Ī€ÎŦ΁·ÎŋÎŊĪ„ÎŋĪ‚ ÎąĪ„ĪŒÎŧÎŋĪ…", "search_no_more_result": "ΔÎĩÎŊ Ī…Ī€ÎŦ΁·ÎŋĪ…ÎŊ ÎŦÎģÎģÎą ÎąĪ€ÎŋĪ„ÎĩÎģÎ­ĪƒÎŧÎąĪ„Îą", @@ -1836,17 +1972,23 @@ "second": "ΔÎĩĪ…Ī„Îĩ΁ΌÎģÎĩ΀΄Îŋ", "see_all_people": "Î ĪÎŋβÎŋÎģÎŽ ΌÎģΉÎŊ ΄ΉÎŊ ÎąĪ„ĪŒÎŧΉÎŊ", "select": "Î•Ī€ÎšÎģÎŋÎŗÎŽ", + "select_album": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "select_album_cover": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎĩÎžĪŽĪ†Ī…ÎģÎģÎŋ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", + "select_albums": "Î•Ī€ÎšÎģÎŋÎŗÎŽ Ī€ÎŋÎģÎģĪŽÎŊ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ", "select_all": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ΌÎģΉÎŊ", "select_all_duplicates": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ΌÎģΉÎŊ ΄ΉÎŊ Î´ÎšĪ€ÎģĪŒĪ„Ī…Ī€Ī‰ÎŊ", "select_all_in": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ΌÎģΉÎŊ ĪƒĪ„Îŋ {group}", "select_avatar_color": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī‡ĪĪŽÎŧÎą avatar", + "select_count": "{count, plural, one {Î•Ī€Î¯ÎģÎĩΞÎĩ #} other {Î•Ī€Î¯ÎģÎĩΞÎĩ #}}", + "select_cutoff_date": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ÎēÎŋĪ€ÎŽĪ‚", "select_face": "Î•Ī€ÎšÎģÎŋÎŗÎŽ ΀΁ÎŋĪƒĪŽĪ€ÎŋĪ…", "select_featured_photo": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯Îą ÎŗÎšÎą ΀΁ÎŋβÎŋÎģÎŽ", "select_from_computer": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎąĪ€ĪŒ Ī…Ī€ÎŋÎģÎŋÎŗÎšĪƒĪ„ÎŽ", "select_keep_all": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Î´ÎšÎąĪ„ÎŽĪÎˇĪƒÎˇ ΌÎģΉÎŊ", "select_library_owner": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎēÎŦĪ„Îŋ·Îŋ βΚβÎģΚÎŋθΎÎēÎˇĪ‚", "select_new_face": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ÎŊέÎŋ Ī€ĪĪŒĪƒĪ‰Ī€Îŋ", + "select_people": "Î•Ī€Î¯ÎģÎĩΞÎĩ ÎŦĪ„ÎŋÎŧÎą", + "select_person": "Î•Ī€Î¯ÎģÎĩΞÎĩ ÎŦĪ„ÎŋÎŧÎŋ", "select_person_to_tag": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ έÎŊÎą ÎŦĪ„ÎŋÎŧÎŋ ÎŗÎšÎą ÎĩĪ€ÎšĪƒÎŽÎŧÎąÎŊĪƒÎˇ", "select_photos": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ΆΉ΄ÎŋÎŗĪÎąĪ†Î¯ÎĩĪ‚", "select_trash_all": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Î´ÎšÎąÎŗĪÎąĪ†ÎŽ ΌÎģΉÎŊ", @@ -1860,11 +2002,11 @@ "server_info_box_app_version": "ΈÎēδÎŋĪƒÎˇ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚", "server_info_box_server_url": "URL δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_offline": "ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ ΕÎēĪ„ĪŒĪ‚ ÎŖĪÎŊδÎĩĪƒÎˇĪ‚", - "server_online": "ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ ÎŖÎĩ ÎŖĪÎŊδÎĩĪƒÎˇ", + "server_online": "ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ ΃Îĩ ĪƒĪÎŊδÎĩĪƒÎˇ", "server_privacy": "Î‘Ī€ĪŒĪĪÎˇĪ„Îŋ ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_restarting_description": "Î‘Ī…Ī„ÎŽ Ρ ΃ÎĩÎģÎ¯Î´Îą θι ÎąÎŊÎąÎŊÎĩĪ‰Î¸Îĩί ΃Îĩ ÎģÎ¯ÎŗÎŋ.", "server_restarting_title": "Ο δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽĪ‚ ÎĩĪ€ÎąÎŊÎĩÎēÎēΚÎŊÎĩί", - "server_stats": "ÎŖĪ„ÎąĪ„ÎšĪƒĪ„ÎšÎēÎŦ ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽ", + "server_stats": "ÎŖĪ„ÎąĪ„ÎšĪƒĪ„ÎšÎēÎŦ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_update_available": "ÎĨĪ€ÎŦ΁·ÎĩΚ Î´ÎšÎąÎ¸Î­ĪƒÎšÎŧΡ ÎĩÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "server_version": "ΈÎēδÎŋĪƒÎˇ ΔιαÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "set": "ÎŸĪÎšĪƒÎŧĪŒĪ‚", @@ -1938,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} Î­Ī„ÎŋĪ‚", "shared_link_edit_password_hint": "Î•ÎšĪƒÎąÎŗÎŦÎŗÎĩĪ„Îĩ Ī„ÎŋÎŊ ÎēĪ‰Î´ÎšÎēΌ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚ ÎēÎŋΚÎŊÎŽĪ‚ Ī‡ĪÎŽĪƒÎˇĪ‚", "shared_link_edit_submit_button": "ΕÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…", - "shared_link_error_server_url_fetch": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ Ī„ÎŋĪ… URL Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", + "shared_link_error_server_url_fetch": "ΔÎĩÎŊ ÎĩίÎŊιΚ Î´Ī…ÎŊÎąĪ„ÎŽ Ρ ÎąÎŊÎŦÎēĪ„ÎˇĪƒÎˇ Ī„ÎŋĪ… url Ī„ÎŋĪ… δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ", "shared_link_expires_day": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ΡÎŧÎ­ĪÎą", "shared_link_expires_days": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ΡÎŧÎ­ĪÎĩĪ‚", "shared_link_expires_hour": "Î›ÎŽÎŗÎĩΚ ΃Îĩ {count} ĪŽĪÎą", @@ -1982,6 +2124,7 @@ "show_password": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎēĪ‰Î´ÎšÎēÎŋĪ", "show_person_options": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎĩĪ€ÎšÎģÎŋÎŗĪŽÎŊ ÎąĪ„ĪŒÎŧÎŋĪ…", "show_progress_bar": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎŗĪÎąÎŧÎŧÎŽĪ‚ ΀΁ÎŋĪŒÎ´ÎŋĪ…", + "show_schema": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ĪƒĪ‡ÎŽÎŧÎąĪ„ÎŋĪ‚", "show_search_options": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎĩĪ€ÎšÎģÎŋÎŗĪŽÎŊ ÎąÎŊÎąÎļÎŽĪ„ÎˇĪƒÎˇĪ‚", "show_shared_links": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎēÎŋΚÎŊĪŽÎŊ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧΉÎŊ", "show_slideshow_transition": "ΕÎŧΆÎŦÎŊÎšĪƒÎˇ ÎŧÎĩĪ„ÎŦÎ˛ÎąĪƒÎˇĪ‚ Ī€ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇĪ‚", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Î ÎąĪÎŦÎēÎąÎŧĪˆÎˇ ĪƒĪ„ÎŋĪ…Ī‚ Ī†ÎąÎēέÎģÎŋĪ…Ī‚", "skip_to_tags": "Î ÎąĪÎŦÎēÎąÎŧĪˆÎˇ ĪƒĪ„ÎšĪ‚ ÎĩĪ„ÎšÎēÎ­Ī„ÎĩĪ‚", "slideshow": "Î ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇ", + "slideshow_repeat": "Î•Ī€ÎąÎŊÎŦÎģÎˇĪˆÎˇ Ī€ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇĪ‚", + "slideshow_repeat_description": "ΕÎēÎēίÎŊÎˇĪƒÎˇ ÎąĪ€ĪŒ Ī„ÎˇÎŊ ÎąĪĪ‡ÎŽ ĪŒĪ„ÎąÎŊ Ī„ÎĩÎģÎĩÎšĪŽĪƒÎĩΚ Ρ Ī€ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇ", "slideshow_settings": "ÎĄĪ…Î¸ÎŧÎ¯ĪƒÎĩÎšĪ‚ Ī€ÎąĪÎŋĪ…ĪƒÎ¯ÎąĪƒÎˇĪ‚", "sort_albums_by": "ΤιΞΚÎŊΌÎŧÎˇĪƒÎˇ ÎŦÎģÎŧĪ€ÎŋĪ…Îŧ ÎēÎąĪ„ÎŦ...", "sort_created": "ΗÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą ΔηÎŧΚÎŋĪ…ĪÎŗÎ¯ÎąĪ‚", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ Ī„Îˇ ĪĪÎ¸ÎŧÎšĪƒÎˇ θέÎŧÎąĪ„ÎŋĪ‚ Ī„ÎˇĪ‚ ÎĩĪ†ÎąĪÎŧÎŋÎŗÎŽĪ‚", "theme_setting_three_stage_loading_subtitle": "Η Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ Ī„ĪÎšĪŽÎŊ ĪƒĪ„ÎąÎ´Î¯Ī‰ÎŊ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ÎąĪ…ÎžÎŽĪƒÎĩΚ Ī„ÎˇÎŊ ÎąĪ€ĪŒÎ´ÎŋĪƒÎˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚, ÎąÎģÎģÎŦ ΀΁ÎŋÎēÎąÎģÎĩί ĪƒÎˇÎŧÎąÎŊĪ„ÎšÎēÎŦ Ī…ĪˆÎˇÎģĪŒĪ„Îĩ΁Îŋ Ī†ĪŒĪĪ„Îŋ δΚÎēĪ„ĪÎŋĪ…", "theme_setting_three_stage_loading_title": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎŽĪƒĪ„Îĩ Ī„Îˇ Ī†ĪŒĪĪ„Ī‰ĪƒÎˇ Ī„ĪÎšĪŽÎŊ ĪƒĪ„ÎąÎ´Î¯Ī‰ÎŊ", + "then": "Î¤ĪŒĪ„Îĩ", "they_will_be_merged_together": "Θι ĪƒĪ…ÎŗĪ‡Ī‰ÎŊÎĩĪ…Î¸ÎŋĪÎŊ ÎŧÎąÎļί", "third_party_resources": "Î ĪŒĪÎŋΚ Ī„ĪÎ¯Ī„Ī‰ÎŊ", "time": "Î§ĪĪŒÎŊÎŋĪ‚", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Î•Ī€ÎšÎģÎ­ÎžĪ„Îĩ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą", "trash_page_title": "ΚÎŦδÎŋĪ‚ Î‘Ī€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ ({count})", "trashed_items_will_be_permanently_deleted_after": "Τι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą Ī€ÎŋĪ… Î˛ĪÎ¯ĪƒÎēÎŋÎŊĪ„ÎąÎš ĪƒĪ„ÎŋÎŊ ÎēÎŦδÎŋ ÎąĪ€ÎŋĪĪÎšÎŧÎŧÎŦ΄ΉÎŊ θι Î´ÎšÎąÎŗĪÎąĪ†ÎŋĪÎŊ ÎŋĪÎšĪƒĪ„ÎšÎēÎŦ ÎŧÎĩĪ„ÎŦ ÎąĪ€ĪŒ {days, plural, one {# ΡÎŧÎ­ĪÎą} other {# ΡÎŧÎ­ĪÎĩĪ‚}}.", + "trigger": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎˇĪ„ÎŽĪ‚", + "trigger_asset_uploaded": "ΤÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ ÎąÎŊέβΡÎēÎĩ", + "trigger_asset_uploaded_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋΚÎĩÎ¯Ī„ÎąÎš ĪŒĪ„ÎąÎŊ ÎąÎŊÎĩÎ˛ÎąÎ¯ÎŊÎĩΚ έÎŊÎą ÎŊέÎŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ", + "trigger_description": "ΈÎŊÎą ĪƒĪ…ÎŧβÎŦÎŊ Ī€ÎŋĪ… ΞÎĩÎēΚÎŊÎŦ Ī„Îˇ ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "trigger_person_recognized": "Î†Ī„ÎŋÎŧÎŋ ΑÎŊÎąÎŗÎŊĪ‰ĪÎ¯ĪƒĪ„ÎˇÎēÎĩ", + "trigger_person_recognized_description": "ΕÎŊÎĩĪÎŗÎŋĪ€ÎŋΚÎĩÎ¯Ī„ÎąÎš ĪŒĪ„ÎąÎŊ ÎąÎŊÎšĪ‡ÎŊÎĩĪÎĩĪ„ÎąÎš ÎŦĪ„ÎŋÎŧÎŋ", + "trigger_type": "Î¤ĪĪ€ÎŋĪ‚ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎˇĪ„ÎŽ", "troubleshoot": "Î•Ī€Î¯ÎģĪ…ĪƒÎˇ ΀΁ÎŋβÎģΡÎŧÎŦ΄ΉÎŊ", "type": "Î¤ĪĪ€ÎŋĪ‚", "unable_to_change_pin_code": "Î‘Î´Ī…ÎŊÎąÎŧÎ¯Îą ÎąÎģÎģÎąÎŗÎŽĪ‚ ÎēĪ‰Î´ÎšÎēÎŋĪ PIN", @@ -2123,6 +2276,7 @@ "unhide_person": "ΑÎŊÎąÎ¯ĪÎĩĪƒÎˇ ÎąĪ€ĪŒÎēĪĪ…ĪˆÎˇĪ‚ ÎąĪ„ĪŒÎŧÎŋĪ…", "unknown": "Î†ÎŗÎŊĪ‰ĪƒĪ„Îŋ", "unknown_country": "Î†ÎŗÎŊĪ‰ĪƒĪ„Îˇ Î§ĪŽĪÎą", + "unknown_date": "Î†ÎŗÎŊĪ‰ĪƒĪ„Îˇ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎ¯Îą", "unknown_year": "Î†ÎŗÎŊĪ‰ĪƒĪ„Îŋ ÎˆĪ„ÎŋĪ‚", "unlimited": "Î‘Ī€ÎĩĪÎšĪŒĪÎšĪƒĪ„Îŋ", "unlink_motion_video": "Î‘Ī€ÎŋĪƒĪ…ÎŊÎ´Î­ĪƒĪ„Îĩ Ī„Îŋ Î˛Î¯ÎŊĪ„ÎĩÎŋ ÎēίÎŊÎˇĪƒÎˇĪ‚", @@ -2139,17 +2293,19 @@ "unstack": "Î‘Ī€ÎŋĪƒĪ„ÎŋÎ¯Î˛ÎąÎžÎˇ", "unstack_action_prompt": "{count} ÎąĪ€ÎŋĪƒĪ…ĪƒĪƒĪ‰ĪÎĩĪĪ„ÎˇÎēÎąÎŊ", "unstacked_assets_count": "Î‘Ī€ÎŋĪƒĪ„ÎŋΚβÎŦÎžÎąĪ„Îĩ {count, plural, one {# ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ} other {# ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą}}", + "unsupported_field_type": "Μη Ī…Ī€ÎŋĪƒĪ„ÎˇĪÎšÎļΌÎŧÎĩÎŊÎŋĪ‚ Ī„ĪĪ€ÎŋĪ‚ Ī€ÎĩδίÎŋĪ…", "untagged": "Î§Ī‰ĪÎ¯Ī‚ ÎĩĪ„ÎšÎēÎ­Ī„Îą", + "untitled_workflow": "Νέα ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", "up_next": "ΑÎēÎŋÎģÎŋĪ…Î¸Îĩί", "update_location_action_prompt": "ΕÎŊΡÎŧÎ­ĪĪ‰ĪƒÎˇ Ī„ÎŋĪ€ÎŋθÎĩĪƒÎ¯ÎąĪ‚ ÎŗÎšÎą {count} ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩ:", "updated_at": "ΕÎŊΡÎŧÎĩ΁ΉÎŧέÎŊÎŋ", "updated_password": "Ο ÎēĪ‰Î´ÎšÎēĪŒĪ‚ Ī€ĪĪŒĪƒÎ˛ÎąĪƒÎˇĪ‚ ÎĩÎŊΡÎŧÎĩĪĪŽÎ¸ÎˇÎēÎĩ", "upload": "ΜÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ", - "upload_action_prompt": "{count} Ī„ÎŋĪ€ÎŋθÎĩĪ„ÎŽÎ¸ÎˇÎēÎąÎŊ ĪƒĪ„ÎˇÎŊ Îŋ΅΁ÎŦ ÎŗÎšÎą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ", "upload_concurrency": "Î¤ÎąĪ…Ī„ĪŒĪ‡ĪÎŋÎŊΡ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ", "upload_details": "ΛÎĩ΀΄ÎŋÎŧÎ­ĪÎĩΚÎĩĪ‚ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚", "upload_dialog_info": "ΘέÎģÎĩĪ„Îĩ ÎŊÎą ÎąÎŊĪ„ÎšÎŗĪÎŦΈÎĩĪ„Îĩ (ÎēÎŦÎŊÎĩĪ„Îĩ backup) Ī„Îą ÎĩĪ€ÎšÎģÎĩÎŗÎŧέÎŊo(Îą) ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ(Îą) ĪƒĪ„Îŋ δΚιÎēÎŋÎŧÎšĪƒĪ„ÎŽ;", "upload_dialog_title": "ΑÎŊÎ­Î˛ÎąĪƒÎŧÎą ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…", + "upload_error_with_count": "ÎŖĪ†ÎŦÎģÎŧÎą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚ ÎŗÎšÎą {count, plural, one {# ÎąĪĪ‡ÎĩίÎŋ} other {# ÎąĪĪ‡ÎĩÎ¯Îą}}", "upload_errors": "Η ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇ ÎŋÎģÎŋÎēÎģÎˇĪĪŽÎ¸ÎˇÎēÎĩ ÎŧÎĩ {count, plural, one {# ĪƒĪ†ÎŦÎģÎŧÎą} other {# ĪƒĪ†ÎŦÎģÎŧÎąĪ„Îą}}, ÎąÎŊÎąÎŊÎĩĪŽĪƒĪ„Îĩ Ī„Îˇ ΃ÎĩÎģÎ¯Î´Îą ÎŗÎšÎą ÎŊÎą δÎĩÎ¯Ī„Îĩ ÎŊέι ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚.", "upload_finished": "ΟÎģÎŋÎēÎģÎŽĪĪ‰ĪƒÎˇ ÎŧÎĩĪ„ÎąĪ†ĪŒĪĪ„Ī‰ĪƒÎˇĪ‚", "upload_progress": "Î‘Ī€ÎŋÎŧέÎŊÎŋĪ…ÎŊ {remaining, number} - ΟÎģÎŋÎēÎģÎˇĪĪŽÎ¸ÎˇÎēÎąÎŊ {processed, number}/{total, number}", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "Î§ĪÎŽĪƒÎˇ", "use_biometric": "Î§ĪÎŽĪƒÎˇ βΚÎŋÎŧÎĩĪ„ĪÎšÎēĪŽÎŊ ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Ī‰ÎŊ", - "use_current_connection": "Ī‡ĪÎŽĪƒÎˇ Ī„ĪÎ­Ī‡ÎŋĪ…ĪƒÎąĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", + "use_current_connection": "Î§ĪÎŽĪƒÎˇ Ī„ĪÎ­Ī‡ÎŋĪ…ĪƒÎąĪ‚ ĪƒĪÎŊδÎĩĪƒÎˇĪ‚", "use_custom_date_range": "Î§ĪÎŽĪƒÎˇ ΀΁ÎŋĪƒÎąĪÎŧÎŋ΃ÎŧέÎŊÎŋĪ… ÎĩĪĪÎŋĪ…Ī‚ ΡÎŧÎĩ΁ÎŋÎŧΡÎŊÎšĪŽÎŊ", "user": "Î§ĪÎŽĪƒĪ„ÎˇĪ‚", "user_has_been_deleted": "Î‘Ī…Ī„ĪŒĪ‚ Îŋ Ī‡ĪÎŽĪƒĪ„ÎˇĪ‚ Î­Ī‡ÎĩΚ δΚÎĩÎŗĪÎąĪ†Îĩί.", @@ -2185,6 +2341,7 @@ "utilities": "ΒÎŋÎˇÎ¸ÎˇĪ„ÎšÎēÎŦ ΀΁ÎŋÎŗĪÎŦÎŧÎŧÎąĪ„Îą", "validate": "Î•Ī€ÎšÎēĪĪĪ‰ĪƒÎˇ", "validate_endpoint_error": "Î ÎąĪÎąÎēÎąÎģĪŽ ÎĩÎšĪƒÎŦÎŗÎĩĪ„Îĩ έÎŊÎą Î­ÎŗÎē΅΁Îŋ URL", + "validation_error": "ÎŖĪ†ÎŦÎģÎŧÎą ÎĩĪ€ÎšÎēĪĪĪ‰ĪƒÎˇĪ‚", "variables": "ΜÎĩĪ„ÎąÎ˛ÎģÎˇĪ„Î­Ī‚", "version": "ΈÎēδÎŋĪƒÎˇ", "version_announcement_closing": "Ο Ī†Î¯ÎģÎŋĪ‚ ΃ÎŋĪ…, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Î ĪÎŋÎĩĪ€ÎšĪƒÎēĪŒĪ€ÎˇĪƒÎˇ Î˛Î¯ÎŊĪ„ÎĩÎŋ ĪŒĪ„ÎąÎŊ Ī„Îŋ Ī€ÎŋÎŊĪ„Î¯ÎēΚ Î˛ĪÎ¯ĪƒÎēÎĩĪ„ÎąÎš Ī€ÎŦÎŊΉ ÎąĪ€ĪŒ Ī„Îŋ ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋ. ΑÎēΌÎŧΡ ÎēιΚ ĪŒĪ„ÎąÎŊ ÎĩίÎŊιΚ ÎąĪ€ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋΚΡÎŧέÎŊΡ, Ρ ÎąÎŊÎąĪ€ÎąĪÎąÎŗĪ‰ÎŗÎŽ ÎŧĪ€Îŋ΁Îĩί ÎŊÎą ΞÎĩÎēΚÎŊÎŽĪƒÎĩΚ Ī„ÎŋĪ€ÎŋθÎĩĪ„ĪŽÎŊĪ„ÎąĪ‚ Ī„Îŋ δÎĩίÎēĪ„Îˇ Ī„ÎŋĪ… Ī€ÎŋÎŊĪ„ÎšÎēΚÎŋĪ Ī€ÎŦÎŊΉ ÎąĪ€ĪŒ Ī„Îŋ ÎĩΚÎēÎŋÎŊίδΚÎŋ ÎąÎŊÎąĪ€ÎąĪÎąÎŗĪ‰ÎŗÎŽĪ‚.", "videos": "ΒίÎŊĪ„ÎĩÎŋ", "videos_count": "{count, plural, one {# ΒίÎŊĪ„ÎĩÎŋ} other {# ΒίÎŊĪ„ÎĩÎŋ}}", + "videos_only": "ÎœĪŒÎŊÎŋ Î˛Î¯ÎŊĪ„ÎĩÎŋ", "view": "Î ĪÎŋβÎŋÎģÎŽ", "view_album": "Î ĪÎŋβÎŋÎģÎŽ ΆÎģÎŧĪ€ÎŋĪ…Îŧ", "view_all": "Î ĪÎŋβÎŋÎģÎŽ ΌÎģΉÎŊ", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Î§ĪÎŽĪƒÎˇ Ή΂ ÎšĪĪÎšÎŋ ÎŖĪ„ÎŋÎšĪ‡ÎĩίÎŋ", "viewer_unstack": "Î‘Ī€ÎŋĪƒĪ„ÎŋÎ¯Î˛ÎąÎžÎĩ", "visibility_changed": "Η ÎŋĪÎąĪ„ĪŒĪ„ÎˇĪ„Îą ÎŦÎģÎģιΞÎĩ ÎŗÎšÎą {count, plural, one {# ÎŦĪ„ÎŋÎŧÎŋ} other {# ÎŦĪ„ÎŋÎŧÎą}}", + "visual": "ÎŸĪ€Ī„ÎšÎēΌ", + "visual_builder": "ÎŸĪ€Ī„ÎšÎēĪŒĪ‚ δΡÎŧΚÎŋĪ…ĪÎŗĪŒĪ‚", "waiting": "ÎŖĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ΃Îĩ ÎąÎŊÎąÎŧÎŋÎŊÎŽ", "waiting_count": "ÎŖÎĩ ÎąÎŊÎąÎŧÎŋÎŊÎŽ: {count}", "warning": "Î ĪÎŋÎĩΚδÎŋĪ€ÎŋÎ¯ÎˇĪƒÎˇ", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "ΚαÎģĪ‰ĪƒÎŋĪÎ¯ĪƒÎąĪ„Îĩ ĪƒĪ„Îŋ Ιmmich", "width": "ΠÎģÎŦĪ„ÎŋĪ‚", "wifi_name": "ΌÎŊÎŋÎŧÎą Wi-Fi", - "workflow": "ÎĄÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_delete_prompt": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ ÎąĪ…Ī„ÎŽ Ī„Îˇ ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚;", + "workflow_deleted": "Η ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ Î´ÎšÎąÎŗĪÎŦĪ†ÎˇÎēÎĩ", + "workflow_description": "ΠÎĩĪÎšÎŗĪÎąĪ†ÎŽ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_info": "ΠÎģÎˇĪÎŋΆÎŋĪÎ¯ÎĩĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_json": "JSON ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_json_help": "Î•Ī€ÎĩΞÎĩĪÎŗÎąĪƒĪ„ÎĩÎ¯Ī„Îĩ Ī„Îˇ ĪĪÎ¸ÎŧÎšĪƒÎˇ Ī„ÎˇĪ‚ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ΃Îĩ ÎŧÎŋĪĪ†ÎŽ JSON. Οι ÎąÎģÎģÎąÎŗÎ­Ī‚ θι ĪƒĪ…ÎŗĪ‡ĪÎŋÎŊÎšĪƒĪ„ÎŋĪÎŊ ÎŧÎĩ Ī„ÎŋÎŊ ÎŋĪ€Ī„ÎšÎēΌ δΡÎŧΚÎŋĪ…ĪÎŗĪŒ.", + "workflow_name": "ΌÎŊÎŋÎŧÎą ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_navigation_prompt": "Î•Î¯ĪƒĪ„Îĩ ĪƒÎ¯ÎŗÎŋ΅΁ÎŋΚ ĪŒĪ„Îš θέÎģÎĩĪ„Îĩ ÎŊÎą Ī†ĪÎŗÎĩĪ„Îĩ Ī‡Ī‰ĪÎ¯Ī‚ ÎŊÎą ÎąĪ€ÎŋθΡÎēÎĩĪĪƒÎĩĪ„Îĩ Ī„ÎšĪ‚ ÎąÎģÎģÎąÎŗÎ­Ī‚ ĪƒÎąĪ‚;", + "workflow_summary": "ÎŖĪÎŊÎŋĪˆÎˇ ΁ÎŋÎŽĪ‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflow_update_success": "Η ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ÎĩÎŊΡÎŧÎĩĪĪŽÎ¸ÎˇÎēÎĩ ÎŧÎĩ ÎĩĪ€ÎšĪ„Ī…Ī‡Î¯Îą", + "workflow_updated": "Η ΁ÎŋÎŽ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ÎĩÎŊΡÎŧÎĩĪĪŽÎ¸ÎˇÎēÎĩ", + "workflows": "ÎĄÎŋÎ­Ī‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚", + "workflows_help_text": "Οι ΁ÎŋÎ­Ī‚ ÎĩĪÎŗÎąĪƒÎ¯ÎąĪ‚ ÎąĪ…Ī„ÎŋÎŧÎąĪ„ÎŋĪ€ÎŋΚÎŋĪÎŊ ÎĩÎŊÎ­ĪÎŗÎĩΚÎĩĪ‚ ĪƒĪ„Îą ĪƒĪ„ÎŋÎšĪ‡ÎĩÎ¯Îą ĪƒÎąĪ‚ ÎŧÎĩ βÎŦĪƒÎˇ ÎĩÎŊÎĩĪÎŗÎŋĪ€ÎŋÎšÎˇĪ„Î­Ī‚ ÎēιΚ Ī†Î¯ÎģĪ„ĪÎą", "wrong_pin_code": "ΛÎŦθÎŋĪ‚ ÎēĪ‰Î´ÎšÎēĪŒĪ‚ PIN", "year": "ÎˆĪ„ÎŋĪ‚", "years_ago": "Ī€ĪÎšÎŊ ÎąĪ€ĪŒ {years, plural, one {# Ī‡ĪĪŒÎŊÎŋ} other {# Ī‡ĪĪŒÎŊΚι}}", "yes": "Ναι", "you_dont_have_any_shared_links": "ΔÎĩÎŊ Î­Ī‡ÎĩĪ„Îĩ ÎēÎŋΚÎŊĪŒĪ‡ĪÎˇĪƒĪ„ÎŋĪ…Ī‚ ĪƒĪ…ÎŊÎ´Î­ĪƒÎŧÎŋĪ…Ī‚", "your_wifi_name": "ΤÎŋ ΌÎŊÎŋÎŧÎą Ī„ÎŋĪ… Wi-Fi ĪƒÎąĪ‚", + "zero_to_clear_rating": "Ī€ÎąĪ„ÎŽĪƒĪ„Îĩ 0 ÎŗÎšÎą ÎŊÎą Î´ÎšÎąÎŗĪÎŦΈÎĩĪ„Îĩ Ī„Îˇ βιθÎŧÎŋÎģÎŋÎŗÎ¯Îą Ī„ÎŋĪ… ĪƒĪ„ÎŋÎšĪ‡ÎĩίÎŋĪ…", "zoom_image": "ΖÎŋĪ…Îŧ ΕιÎēΌÎŊÎąĪ‚", "zoom_to_bounds": "Î•ĪƒĪ„Î¯ÎąĪƒÎˇ ĪƒĪ„Îą ĪŒĪÎšÎą" } diff --git a/i18n/en.json b/i18n/en.json index 5903d7850e..97cff2c69c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5,6 +5,7 @@ "acknowledge": "Acknowledge", "action": "Action", "action_common_update": "Update", + "action_description": "A set of action to perform on the filtered assets", "actions": "Actions", "active": "Active", "active_count": "Active: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Add a location", "add_a_name": "Add a name", "add_a_title": "Add a title", + "add_action": "Add action", + "add_action_description": "Click to add an action to perform", + "add_assets": "Add assets", "add_birthday": "Add a birthday", "add_endpoint": "Add endpoint", "add_exclusion_pattern": "Add exclusion pattern", + "add_filter": "Add filter", + "add_filter_description": "Click to add a filter condition", "add_location": "Add location", "add_more_users": "Add more users", "add_partner": "Add partner", @@ -36,6 +42,7 @@ "add_to_shared_album": "Add to shared album", "add_upload_to_stack": "Add upload to stack", "add_url": "Add URL", + "add_workflow_step": "Add workflow step", "added_to_archive": "Added to archive", "added_to_favorites": "Added to favorites", "added_to_favorites_count": "Added {count, number} to favorites", @@ -97,6 +104,8 @@ "image_preview_description": "Medium-size image with stripped metadata, used when viewing a single asset and for machine learning", "image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.", "image_preview_title": "Preview Settings", + "image_progressive": "Progressive", + "image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on WebP images.", "image_quality": "Quality", "image_resolution": "Resolution", "image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Enable smart search", "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.", "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.", + "maintenance_delete_backup": "Delete Backup", + "maintenance_delete_backup_description": "This file will be irrevocably deleted.", + "maintenance_delete_error": "Failed to delete backup.", + "maintenance_restore_backup": "Restore Backup", + "maintenance_restore_backup_description": "Immich will be wiped and restored from the chosen backup. A backup will be created before continuing.", + "maintenance_restore_backup_different_version": "This backup was created with a different version of Immich!", + "maintenance_restore_backup_unknown_version": "Couldn't determine backup version.", + "maintenance_restore_database_backup": "Restore database backup", + "maintenance_restore_database_backup_description": "Rollback to an earlier database state using a backup file", "maintenance_settings": "Maintenance", "maintenance_settings_description": "Put Immich into maintenance mode.", - "maintenance_start": "Start maintenance mode", + "maintenance_start": "Switch to maintenance mode", "maintenance_start_error": "Failed to start maintenance mode.", + "maintenance_upload_backup": "Upload database backup file", + "maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?", "manage_concurrency": "Manage Concurrency", "manage_concurrency_description": "Navigate to the jobs page to manage job concurrency", "manage_log_settings": "Manage log settings", @@ -252,7 +272,7 @@ "oauth_auto_register": "Auto register", "oauth_auto_register_description": "Automatically register new users after signing in with OAuth", "oauth_button_text": "Button text", - "oauth_client_secret_description": "Required if PKCE (Proof Key for Code Exchange) is not supported by the OAuth provider", + "oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.", "oauth_enable_description": "Login with OAuth", "oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override", @@ -291,7 +311,7 @@ "search_jobs": "Search jobsâ€Ļ", "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", - "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", + "server_external_domain_settings_description": "Domain used for external links", "server_public_users": "Public Users", "server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.", "server_settings": "Server Settings", @@ -431,6 +451,9 @@ "admin_password": "Admin Password", "administration": "Administration", "advanced": "Advanced", + "advanced_settings_clear_image_cache": "Clear Image Cache", + "advanced_settings_clear_image_cache_error": "Failed to clear image cache", + "advanced_settings_clear_image_cache_success": "Successfully cleared {size}", "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}", @@ -467,10 +490,12 @@ "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_selected": "Album selected", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", "album_summary": "Album summary", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", + "album_upload_assets": "Upload assets from your computer and add to album", "album_user_left": "Left {album}", "album_user_removed": "Removed {user}", "album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# album selected} other {# albums selected}}", "all": "All", "all_albums": "All albums", "all_people": "All people", + "all_photos": "All photos", "all_videos": "All videos", "allow_dark_mode": "Allow dark mode", "allow_edits": "Allow edits", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Allow public user to upload", "allowed": "Allowed", "alt_text_qr_code": "QR code image", + "always_keep": "Always keep", + "always_keep_photos_hint": "Free Up Space will keep all photos on this device.", + "always_keep_videos_hint": "Free Up Space will keep all videos on this device.", "anti_clockwise": "Anti-clockwise", "api_key": "API Key", "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Are these the same person?", "are_you_sure_to_do_this": "Are you sure you want to do this?", + "array_field_not_fully_supported": "Array fields require manual JSON editing", "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_added_to_album": "Added to album", "asset_adding_to_album": "Adding to albumâ€Ļ", + "asset_created": "Asset created", "asset_description_updated": "Asset description has been updated", "asset_filename_is_offline": "Asset {filename} is offline", "asset_has_unassigned_faces": "Asset has unassigned faces", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_not_found_on_device_android": "Asset not found on device", + "asset_not_found_on_device_ios": "Asset not found on device. If you are using iCloud, the asset may be inaccessible due to bad file stored on iCloud", + "asset_not_found_on_icloud": "Asset not found on iCloud. the asset may be inaccessible due to bad file stored on iCloud", "asset_offline": "Asset Offline", "asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.", "asset_restored_successfully": "Asset restored successfully", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Select albums", "backup_album_selection_page_selection_info": "Selection Info", "backup_album_selection_page_total_assets": "Total unique assets", - "backup_albums_sync": "Backup albums synchronization", + "backup_albums_sync": "Backup Albums Synchronization", "backup_all": "All", "backup_background_service_backup_failed_message": "Failed to backup assets. Retryingâ€Ļ", "backup_background_service_complete_notification": "Asset backup complete", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", "change_pin_code": "Change PIN code", + "change_trigger": "Change trigger", + "change_trigger_prompt": "Are you sure you want to change the trigger? This will remove all existing actions and filters.", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", "charging": "Charging", @@ -722,6 +759,18 @@ "checksum": "Checksum", "choose_matching_people_to_merge": "Choose matching people to merge", "city": "City", + "cleanup_confirm_description": "Immich found {count} assets (created before {date}) safely backed up to the server. Remove the local copies from this device?", + "cleanup_confirm_prompt_title": "Remove from this device?", + "cleanup_deleted_assets": "Moved {count} assets to device trash", + "cleanup_deleting": "Moving to trash...", + "cleanup_found_assets": "Found {count} backed up assets", + "cleanup_found_assets_with_size": "Found {count} backed up assets ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud Shared Albums are excluded from the scan", + "cleanup_no_assets_found": "No assets found matching the criteria above. Free Up Space can only remove assets that have been backed up to the server", + "cleanup_preview_title": "Assets to remove ({count})", + "cleanup_step3_description": "Scan for backed up assets matching your date and keep settings.", + "cleanup_step4_summary": "{count} assets (created before {date}) to remove from your local device. Photos will remain accessible from the Immich app.", + "cleanup_trash_hint": "To fully reclaim storage space, open the system gallery app and empty the trash", "clear": "Clear", "clear_all": "Clear all", "clear_all_recent_searches": "Clear all recent searches", @@ -733,6 +782,8 @@ "client_cert_import": "Import", "client_cert_import_success_msg": "Client certificate is imported", "client_cert_invalid_msg": "Invalid certificate file or wrong password", + "client_cert_password_message": "Enter the password for this certificate", + "client_cert_password_title": "Certificate Password", "client_cert_remove_msg": "Client certificate is removed", "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate import/removal is available only before login", "client_cert_title": "SSL client certificate [EXPERIMENTAL]", @@ -743,6 +794,11 @@ "color": "Color", "color_theme": "Color theme", "command": "Command", + "command_palette_prompt": "Quickly find pages, actions, or commands", + "command_palette_to_close": "to close", + "command_palette_to_navigate": "to enter", + "command_palette_to_select": "to select", + "command_palette_to_show_all": "to show all", "comment_deleted": "Comment deleted", "comment_options": "Comment options", "comments_and_likes": "Comments & likes", @@ -787,6 +843,7 @@ "create_album": "Create album", "create_album_page_untitled": "Untitled", "create_api_key": "Create API key", + "create_first_workflow": "Create first workflow", "create_library": "Create Library", "create_link": "Create link", "create_link_to_share": "Create link to share", @@ -801,17 +858,25 @@ "create_tag": "Create tag", "create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.", "create_user": "Create user", + "create_workflow": "Create workflow", "created": "Created", "created_at": "Created", "creating_linked_albums": "Creating linked albums...", "crop": "Crop", + "crop_aspect_ratio_fixed": "Fixed", + "crop_aspect_ratio_free": "Free", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Things", "current_device": "Current device", "current_pin_code": "Current PIN code", "current_server_address": "Current server address", + "custom_date": "Custom date", "custom_locale": "Custom Locale", "custom_locale_description": "Format dates and numbers based on the language and the region", "custom_url": "Custom URL", + "cutoff_date_description": "Keep photos from the lastâ€Ļ", + "cutoff_day": "{count, plural, one {day} other {days}}", + "cutoff_year": "{count, plural, one {year} other {years}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Dark", @@ -867,6 +932,7 @@ "deselect_all": "Deselect All", "details": "Details", "direction": "Direction", + "disable": "Disable", "disabled": "Disabled", "disallow_edits": "Disallow edits", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Embedded videos", "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", "download_notfound": "Download not found", + "download_original": "Download original", "download_paused": "Download paused", "download_settings": "Download", "download_settings_description": "Manage settings related to asset download", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "Waiting to retry", "downloading": "Downloading", "downloading_asset_filename": "Downloading asset {filename}", + "downloading_from_icloud": "Downloading from iCloud", "downloading_media": "Downloading media", "drop_files_to_upload": "Drop files anywhere to upload", "duplicates": "Duplicates", @@ -929,11 +997,22 @@ "edit_tag": "Edit tag", "edit_title": "Edit Title", "edit_user": "Edit user", + "edit_workflow": "Edit workflow", "editor": "Editor", "editor_close_without_save_prompt": "The changes will not be saved", "editor_close_without_save_title": "Close editor?", - "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", - "editor_crop_tool_h2_rotation": "Rotation", + "editor_confirm_reset_all_changes": "Are you sure you want to reset all changes?", + "editor_discard_edits_confirm": "Discard edits", + "editor_discard_edits_prompt": "You have unsaved edits. Are you sure you want to discard them?", + "editor_discard_edits_title": "Discard edits?", + "editor_edits_applied_error": "Failed to apply edits", + "editor_edits_applied_success": "Edits applied successfully", + "editor_flip_horizontal": "Flip horizontal", + "editor_flip_vertical": "Flip vertical", + "editor_orientation": "Orientation", + "editor_reset_all_changes": "Reset changes", + "editor_rotate_left": "Rotate 90° counterclockwise", + "editor_rotate_right": "Rotate 90° clockwise", "email": "Email", "email_notifications": "Email notifications", "empty_folder": "This folder is empty", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Failed to change album sort order", "error_delete_face": "Error deleting face from asset", "error_getting_places": "Error getting places", + "error_loading_albums": "Error loading albums", "error_loading_image": "Error loading image", "error_loading_partners": "Error loading partners: {error}", + "error_retrieving_asset_information": "Error retrieving asset information", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error tagging face - cannot get bounding box coordinates", "error_title": "Error - Something went wrong", + "error_while_navigating": "Error while navigating to asset", "errors": { "cannot_navigate_next_asset": "Cannot navigate to the next asset", "cannot_navigate_previous_asset": "Cannot navigate to previous asset", @@ -992,6 +1074,7 @@ "failed_to_update_notification_status": "Failed to update notification status", "incorrect_email_or_password": "Incorrect email or password", "library_folder_already_exists": "This import path already exists.", + "page_not_found": "Page not found :/", "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", @@ -1014,6 +1097,7 @@ "unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_connect": "Unable to connect", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", + "unable_to_create": "Unable to create workflow", "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", "unable_to_create_library": "Unable to create library", @@ -1024,6 +1108,7 @@ "unable_to_delete_exclusion_pattern": "Unable to delete exclusion pattern", "unable_to_delete_shared_link": "Unable to delete shared link", "unable_to_delete_user": "Unable to delete user", + "unable_to_delete_workflow": "Unable to delete workflow", "unable_to_download_files": "Unable to download files", "unable_to_edit_exclusion_pattern": "Unable to edit exclusion pattern", "unable_to_empty_trash": "Unable to empty trash", @@ -1063,6 +1148,7 @@ "unable_to_scan_library": "Unable to scan library", "unable_to_set_feature_photo": "Unable to set feature photo", "unable_to_set_profile_picture": "Unable to set profile picture", + "unable_to_set_rating": "Unable to set rating", "unable_to_submit_job": "Unable to submit job", "unable_to_trash_asset": "Unable to trash asset", "unable_to_unlink_account": "Unable to unlink account", @@ -1074,8 +1160,10 @@ "unable_to_update_settings": "Unable to update settings", "unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_user": "Unable to update user", + "unable_to_update_workflow": "Unable to update workflow", "unable_to_upload_file": "Unable to upload file" }, + "errors_text": "Errors", "exclusion_pattern": "Exclusion pattern", "exif": "Exif", "exif_bottom_sheet_description": "Add Description...", @@ -1086,6 +1174,7 @@ "exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_person_add_person": "Add name", "exit_slideshow": "Exit Slideshow", + "expand": "Expand", "expand_all": "Expand all", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", @@ -1120,14 +1209,18 @@ "features": "Features", "features_in_development": "Features in Development", "features_setting_description": "Manage the app features", - "file_name": "File name", "file_name_or_extension": "File name or extension", + "file_name_text": "File name", + "file_name_with_value": "File name: {file_name}", "file_size": "File size", "filename": "Filename", "filetype": "Filetype", "filter": "Filter", + "filter_description": "Conditions to filter the target assets", "filter_people": "Filter people", "filter_places": "Filter places", + "filter_tags": "Filter tags", + "filters": "Filters", "find_them_fast": "Find them fast by name with search", "first": "First", "fix_incorrect_match": "Fix incorrect match", @@ -1137,12 +1230,16 @@ "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "forgot_pin_code_question": "Forgot your PIN?", "forward": "Forward", + "free_up_space": "Free Up Space", + "free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.", + "free_up_space_settings_subtitle": "Free up device storage", "full_path": "Full path: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", "get_help": "Get Help", + "get_people_error": "Error getting people", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Getting Started", "go_back": "Go back", @@ -1175,6 +1272,7 @@ "hide_named_person": "Hide person {name}", "hide_password": "Hide password", "hide_person": "Hide person", + "hide_schema": "Hide schema", "hide_text_recognition": "Hide text recognition", "hide_unnamed_people": "Hide unnamed people", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", @@ -1247,9 +1345,18 @@ "ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", + "json_editor": "JSON editor", + "json_error": "JSON error", "keep": "Keep", + "keep_albums": "Keep albums", + "keep_albums_count": "Keeping {count} {count, plural, one {album} other {albums}}", "keep_all": "Keep All", + "keep_description": "Choose what stays on your device when freeing up space.", + "keep_favorites": "Keep favorites", + "keep_on_device": "Keep on device", + "keep_on_device_hint": "Select items to keep on this device", "keep_this_delete_others": "Keep this, delete others", + "keeping": "Keeping: {items}", "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Keyboard shortcuts", "language": "Language", @@ -1343,10 +1450,28 @@ "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", "main_branch_warning": "You're using a development version; we strongly recommend using a release version!", "main_menu": "Main menu", + "maintenance_action_restore": "Restoring Database", "maintenance_description": "Immich has been put into maintenance mode.", "maintenance_end": "End maintenance mode", "maintenance_end_error": "Failed to end maintenance mode.", "maintenance_logged_in_as": "Currently logged in as {user}", + "maintenance_restore_from_backup": "Restore From Backup", + "maintenance_restore_library": "Restore Your Library", + "maintenance_restore_library_confirm": "If this looks correct, continue to restoring a backup!", + "maintenance_restore_library_description": "Restoring Database", + "maintenance_restore_library_folder_has_files": "{folder} has {count} folder(s)", + "maintenance_restore_library_folder_no_files": "{folder} is missing files!", + "maintenance_restore_library_folder_pass": "readable and writable", + "maintenance_restore_library_folder_read_fail": "not readable", + "maintenance_restore_library_folder_write_fail": "not writable", + "maintenance_restore_library_hint_missing_files": "You may be missing important files", + "maintenance_restore_library_hint_regenerate_later": "You can regenerate these later in settings", + "maintenance_restore_library_hint_storage_template_missing_files": "Using storage template? You may be missing files", + "maintenance_restore_library_loading": "Loading integrity checks and heuristicsâ€Ļ", + "maintenance_task_backup": "Creating a backup of the existing databaseâ€Ļ", + "maintenance_task_migrations": "Running database migrationsâ€Ļ", + "maintenance_task_restore": "Restoring the chosen backupâ€Ļ", + "maintenance_task_rollback": "Restore failed, rolling back to restore pointâ€Ļ", "maintenance_title": "Temporarily Unavailable", "make": "Make", "manage_geolocation": "Manage location", @@ -1408,6 +1533,8 @@ "minimize": "Minimize", "minute": "Minute", "minutes": "Minutes", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Missing", "mobile_app": "Mobile App", "mobile_app_download_onboarding_note": "Download the companion mobile app using the following options", @@ -1416,11 +1543,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "More", "move": "Move", + "move_down": "Move down", "move_off_locked_folder": "Move out of locked folder", "move_to": "Move to", + "move_to_device_trash": "Move to device trash", "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", + "move_up": "Move up", "moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive", "moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library", "moved_to_trash": "Moved to trash", @@ -1430,6 +1560,7 @@ "my_albums": "My albums", "name": "Name", "name_or_nickname": "Name or nickname", + "name_required": "Name is required", "navigate": "Navigate", "navigate_to_time": "Navigate to Time", "network_requirement_photos_upload": "Use cellular data to backup photos", @@ -1454,20 +1585,24 @@ "next": "Next", "next_memory": "Next memory", "no": "No", + "no_actions_added": "No actions added yet", + "no_albums_found": "No albums found", "no_albums_message": "Create an album to organize your photos and videos", "no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.", "no_albums_yet": "It looks like you do not have any albums yet.", "no_archived_assets_message": "Archive photos and videos to hide them from your Photos view", - "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", + "no_assets_message": "Click to upload your first photo", "no_assets_to_show": "No assets to show", "no_cast_devices_found": "No cast devices found", "no_checksum_local": "No checksum available - cannot fetch local assets", "no_checksum_remote": "No checksum available - cannot fetch remote asset", + "no_configuration_needed": "No configuration needed", "no_devices": "No authorized devices", "no_duplicates_found": "No duplicates were found.", "no_exif_info_available": "No exif info available", "no_explore_results_message": "Upload more photos to explore your collection.", "no_favorites_message": "Add favorites to quickly find your best pictures and videos", + "no_filters_added": "No filters added yet", "no_libraries_message": "Create an external library to view your photos and videos", "no_local_assets_found": "No local assets found with this checksum", "no_location_set": "No location set", @@ -1481,11 +1616,11 @@ "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", + "none": "None", "not_allowed": "Not allowed", "not_available": "N/A", "not_in_any_album": "Not in any album", "not_selected": "Not selected", - "note_apply_storage_label_to_previously_uploaded assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "notes": "Notes", "nothing_here_yet": "Nothing here yet", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", @@ -1515,6 +1650,7 @@ "online": "Online", "only_favorites": "Only favorites", "open": "Open", + "open_calendar": "Open calendar", "open_in_map_view": "Open in map view", "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", @@ -1563,6 +1699,7 @@ "people": "People", "people_edits_count": "Edited {count, plural, one {# person} other {# people}}", "people_feature_description": "Browsing photos and videos grouped by people", + "people_selected": "{count, plural, one {# person selected} other {# people selected}}", "people_sidebar_description": "Display a link to People in the sidebar", "permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", @@ -1587,11 +1724,14 @@ "person_age_years": "{years, plural, other {# years}} old", "person_birthdate": "Born on {date}", "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "person_recognized": "Person recognized", + "person_selected": "Person selected", "photo_shared_all_users": "Looks like you shared your photos with all users or you don't have any user to share with.", "photos": "Photos", "photos_and_videos": "Photos & Videos", "photos_count": "{count, plural, one {{count, number} Photo} other {{count, number} Photos}}", "photos_from_previous_years": "Photos from previous years", + "photos_only": "Photos only", "pick_a_location": "Pick a location", "pick_custom_range": "Custom range", "pick_date_range": "Select a date range", @@ -1667,9 +1807,10 @@ "purchase_settings_server_activated": "The server product key is managed by the admin", "query_asset_id": "Query Asset ID", "queue_status": "Queuing {count}/{total}", + "rate_asset": "Rate Asset", "rating": "Star rating", "rating_clear": "Clear rating", - "rating_count": "{count, plural, one {# star} other {# stars}}", + "rating_count": "{count, plural, =0 {Unrated} one {# star} other {# stars}}", "rating_description": "Display the EXIF rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", @@ -1681,7 +1822,7 @@ "reassigned_assets_to_new_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to a new person", "reassing_hint": "Assign selected assets to an existing person", "recent": "Recent", - "recent-albums": "Recent albums", + "recent_albums": "Recent albums", "recent_searches": "Recent searches", "recently_added": "Recently added", "recently_added_page_title": "Recently Added", @@ -1742,7 +1883,10 @@ "reset_pin_code_success": "Successfully reset PIN code", "reset_pin_code_with_password": "You can always reset your PIN code with your password", "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_clear_app_data": "Clear Data", + "reset_sqlite_confirmation": "Are you sure you want to clear the app data? This will remove all settings and sign you out.", + "reset_sqlite_confirmation_note": "Note: You will need to restart the app after clearing.", + "reset_sqlite_done": "App data has been cleared. Please restart Immich and log in again.", "reset_sqlite_success": "Successfully reset the SQLite database", "reset_to_default": "Reset to default", "resolution": "Resolution", @@ -1770,9 +1914,12 @@ "saved_settings": "Saved settings", "say_something": "Say something", "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_unrecoverable": "An unrecoverable error has occurred. Please share the error and stack trace on Discord or GitHub so we can help. If advised, you can clear the app data below.", + "scan": "Scan", "scan_all_libraries": "Scan All Libraries", "scan_library": "Scan", "scan_settings": "Scan Settings", + "scanning": "Scanning", "scanning_for_album": "Scanning for album...", "search": "Search", "search_albums": "Search albums", @@ -1802,6 +1949,8 @@ "search_filter_media_type_title": "Select media type", "search_filter_ocr": "Search by OCR", "search_filter_people_title": "Select people", + "search_filter_star_rating": "Star Rating", + "search_filter_tags_title": "Select tags", "search_for": "Search for", "search_for_existing_person": "Search for existing person", "search_no_more_result": "No more results", @@ -1836,17 +1985,23 @@ "second": "Second", "see_all_people": "See all people", "select": "Select", + "select_album": "Select album", "select_album_cover": "Select album cover", + "select_albums": "Select albums", "select_all": "Select all", "select_all_duplicates": "Select all duplicates", "select_all_in": "Select all in {group}", "select_avatar_color": "Select avatar color", + "select_count": "{count, plural, one {Select #} other {Select #}}", + "select_cutoff_date": "Select cutoff date", "select_face": "Select face", "select_featured_photo": "Select featured photo", "select_from_computer": "Select from computer", "select_keep_all": "Select keep all", "select_library_owner": "Select library owner", "select_new_face": "Select new face", + "select_people": "Select people", + "select_person": "Select person", "select_person_to_tag": "Select a person to tag", "select_photos": "Select photos", "select_trash_all": "Select trash all", @@ -1875,6 +2030,9 @@ "set_profile_picture": "Set profile picture", "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen", "set_stack_primary_asset": "Set as primary asset", + "setting_image_navigation_enable_subtitle": "If enabled, you can navigate to the previous/next image by tapping the leftmost/rightmost quarter of the screen.", + "setting_image_navigation_enable_title": "Tap to Navigate", + "setting_image_navigation_title": "Image Navigation", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", "setting_image_viewer_original_title": "Load original image", @@ -1982,6 +2140,7 @@ "show_password": "Show password", "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", + "show_schema": "Show schema", "show_search_options": "Show search options", "show_shared_links": "Show shared links", "show_slideshow_transition": "Show slideshow transition", @@ -1999,6 +2158,8 @@ "skip_to_folders": "Skip to folders", "skip_to_tags": "Skip to tags", "slideshow": "Slideshow", + "slideshow_repeat": "Repeat slideshow", + "slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_settings": "Slideshow settings", "sort_albums_by": "Sort albums by...", "sort_created": "Date created", @@ -2038,6 +2199,7 @@ "support": "Support", "support_and_feedback": "Support & Feedback", "support_third_party_description": "Your Immich installation was packaged by a third-party. Issues you experience may be caused by that package, so please raise issues with them in the first instance using the links below.", + "supporter": "Supporter", "swap_merge_direction": "Swap merge direction", "sync": "Sync", "sync_albums": "Sync albums", @@ -2075,6 +2237,7 @@ "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", + "then": "Then", "they_will_be_merged_together": "They will be merged together", "third_party_resources": "Third-Party Resources", "time": "Time", @@ -2109,6 +2272,13 @@ "trash_page_select_assets_btn": "Select assets", "trash_page_title": "Trash ({count})", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", + "trigger": "Trigger", + "trigger_asset_uploaded": "Asset Uploaded", + "trigger_asset_uploaded_description": "Triggered when a new asset is uploaded", + "trigger_description": "An event that kicks off the workflow", + "trigger_person_recognized": "Person Recognized", + "trigger_person_recognized_description": "Triggered when a person is detected", + "trigger_type": "Trigger type", "troubleshoot": "Troubleshoot", "type": "Type", "unable_to_change_pin_code": "Unable to change PIN code", @@ -2123,6 +2293,7 @@ "unhide_person": "Unhide person", "unknown": "Unknown", "unknown_country": "Unknown Country", + "unknown_date": "Unknown date", "unknown_year": "Unknown Year", "unlimited": "Unlimited", "unlink_motion_video": "Unlink motion video", @@ -2139,17 +2310,20 @@ "unstack": "Un-stack", "unstack_action_prompt": "{count} unstacked", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", + "unsupported_field_type": "Unsupported field type", + "unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.", "untagged": "Untagged", + "untitled_workflow": "Untitled workflow", "up_next": "Up next", "update_location_action_prompt": "Update the location of {count} selected assets with:", "updated_at": "Updated", "updated_password": "Updated password", "upload": "Upload", - "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_error_with_count": "Upload error for {count, plural, one {# asset} other {# assets}}", "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}", @@ -2164,7 +2338,7 @@ "url": "URL", "usage": "Usage", "use_biometric": "Use biometric", - "use_current_connection": "use current connection", + "use_current_connection": "Use current connection", "use_custom_date_range": "Use custom date range instead", "user": "User", "user_has_been_deleted": "This user has been deleted.", @@ -2185,6 +2359,7 @@ "utilities": "Utilities", "validate": "Validate", "validate_endpoint_error": "Please enter a valid URL", + "validation_error": "Validation error", "variables": "Variables", "version": "Version", "version_announcement_closing": "Your friend, Alex", @@ -2196,6 +2371,7 @@ "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", "videos": "Videos", "videos_count": "{count, plural, one {# Video} other {# Videos}}", + "videos_only": "Videos only", "view": "View", "view_album": "View Album", "view_all": "View All", @@ -2216,6 +2392,8 @@ "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", + "visual": "Visual", + "visual_builder": "Visual builder", "waiting": "Waiting", "waiting_count": "Waiting: {count}", "warning": "Warning", @@ -2224,13 +2402,26 @@ "welcome_to_immich": "Welcome to Immich", "width": "Width", "wifi_name": "Wi-Fi Name", - "workflow": "Workflow", + "workflow_delete_prompt": "Are you sure you want to delete this workflow?", + "workflow_deleted": "Workflow deleted", + "workflow_description": "Workflow description", + "workflow_info": "Workflow info", + "workflow_json": "Workflow JSON", + "workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.", + "workflow_name": "Workflow name", + "workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?", + "workflow_summary": "Workflow summary", + "workflow_update_success": "Workflow updated successfully", + "workflow_updated": "Workflow updated", + "workflows": "Workflows", + "workflows_help_text": "Workflows automate actions on your assets based on triggers and filters", "wrong_pin_code": "Wrong PIN code", "year": "Year", "years_ago": "{years, plural, one {# year} other {# years}} ago", "yes": "Yes", "you_dont_have_any_shared_links": "You don't have any shared links", "your_wifi_name": "Your Wi-Fi name", + "zero_to_clear_rating": "press 0 to clear asset rating", "zoom_image": "Zoom Image", "zoom_to_bounds": "Zoom to bounds" } diff --git a/i18n/eo.json b/i18n/eo.json index 0967ef424b..6b1ebaacdf 100644 --- a/i18n/eo.json +++ b/i18n/eo.json @@ -1 +1,481 @@ -{} +{ + "about": "Pri", + "account": "Konto", + "account_settings": "AgordaÄĩoj de konto", + "acknowledge": "Komprenite", + "action": "Ago", + "action_common_update": "Ĝisdatigi", + "action_description": "Aro de agoj por fari al filtritaj elementoj", + "actions": "Agoj", + "active": "Aktivaj", + "active_count": "Aktivaj: {count}", + "activity": "OkazaÄĩoj", + "activity_changed": "AktivaÄĩoj estas {enabled,select,true {ŝaltitaj} other {malŝaltitaj}}", + "add": "Aldoni", + "add_a_description": "Aldoni priskribon", + "add_a_location": "Aldoni lokon", + "add_a_name": "Aldoni nomon", + "add_a_title": "Aldoni titolon", + "add_action": "Aldoni agon", + "add_action_description": "Klaku por aldoni agon por fari", + "add_assets": "Aldoni elementojn", + "add_birthday": "Aldoni naskiĝtagon", + "add_endpoint": "Aldoni finpunkton", + "add_exclusion_pattern": "Aldoni skemon de ekskludo", + "add_filter": "Aldoni filtrilon", + "add_filter_description": "Klaku por aldoni kondiĉon por filtri", + "add_location": "Aldoni lokon", + "add_more_users": "Aldoni pli da uzantoj", + "add_partner": "Aldoni partneron", + "add_path": "Aldoni vojon", + "add_photos": "Aldoni fotojn", + "add_tag": "Aldoni etikedon", + "add_to": "Aldoni alâ€Ļ", + "add_to_album": "Aldoni al albumo", + "add_to_album_bottom_sheet_added": "Aldonita(j) al {album}", + "add_to_album_bottom_sheet_already_exists": "Jam en {album}", + "add_to_album_bottom_sheet_some_local_assets": "Ne eblis aldoni kelkajn lokajn elementojn al la albumo", + "add_to_album_toggle": "Baskuli elekton por {album}", + "add_to_albums": "Aldoni al albumoj", + "add_to_albums_count": "Aldoni al albumoj ({count})", + "add_to_bottom_bar": "Aldoni al", + "add_to_shared_album": "Aldoni al dividita albumo", + "add_upload_to_stack": "Aldoni alŝutitajn elementojn al stako", + "add_url": "Aldoni URL-on", + "add_workflow_step": "Aldoni paŝon al laborfluo", + "added_to_archive": "Aldonita(j) al arÄĨivo", + "added_to_favorites": "Aldonita(j) al preferataÄĩoj", + "added_to_favorites_count": "Adonis {count, number} al preferataÄĩoj", + "admin": { + "add_exclusion_pattern_description": "Aldoni skemojn de ekskludo. Ä´okeraj signoj *, ** kaj ? funkcias. Por ignori ĉiujn dosierojn en ujo nomita \"Raw\", uzu \"**/Raw/**\". Por ignori ĉiujn dosierojn kun finaÄĩo \".tif\", uzu \"**/*.tif\". Por ignori iun absolutan vojon, uzu \"/vojo/por/ignori/**\".", + "admin_user": "Administranto", + "asset_offline_description": "Tiu ĉi ekstera biblioteko ne plu ĉeestas sur la disko, kaj estas movita al la rubujo. Se la dosiero estis movita ene de la biblioteko, serĉu la novan korespondan elementon en via kronologio. Por rehavi tiun elementon, kontrolu ke la ĉi-suba dosier-vojo estas atingebla de Immich por analizi la bibliotekon.", + "authentication_settings": "Agordoj pri aÅ­tentigo", + "authentication_settings_description": "Administri agordojn pri pasvortoj, OAuth, kaj aliaj ensalut-metodoj", + "authentication_settings_disable_all": "Ĉu vi certas, ke vi volas malebligi ĉiujn metodojn por ensaluti? Ensalutado estos tute malebligita.", + "authentication_settings_reenable": "Por re-ebligi, uzu servilan komandon.", + "background_task_job": "Fonaj taskoj", + "backup_database": "Krei kopion de la datumbazo", + "backup_database_enable_description": "Ebligi kreon de kopioj de datumbazo", + "backup_keep_last_amount": "Nombro de antaÅ­aj kopioj konservendaj", + "backup_onboarding_1_description": "fora kopio, ĉu en nubo ĉu en alia fizika loko.", + "backup_onboarding_2_description": "lokaj kopioj ĉe diversaj aparatoj, inkluzive ĉefajn dosierojn kaj lokan sekurkopion de tiuj dosieroj.", + "backup_onboarding_3_description": "suma nombro de kopioj de viaj datumoj, inkluzive la originajn dosierojn, t.e. 1 fora kopio kaj 2 lokaj kopioj.", + "backup_onboarding_description": "Ni rekomendas strategion de 3-2-1 por protekti viajn datumojn. Vi devus havi sekurkopiojn kaj de viaj fotoj/videoj kaj de la datumbazo de Immich por esti plene sekura.", + "backup_onboarding_footer": "Por pli da informoj pri sekurkopioj kun Immich, bonvolu legi la dokumentaron.", + "backup_onboarding_parts_title": "Sekur-kopioj laÅ­ strategio 3-2-1 inkluzivas:", + "backup_onboarding_title": "Sekurkopioj", + "backup_settings": "AgordaÄĩoj de kopiado de datumbazo", + "backup_settings_description": "Administri agordojn pri datumbazo-nekropsio.", + "cleared_jobs": "Taskoj forigitaj por: {job}", + "config_set_by_file": "La agordoj estas aktuale regitaj de agordo-dosiero", + "confirm_delete_library": "Ĉu vi certe volas forigi la biblitekon {library}?", + "confirm_delete_library_assets": "Ĉu vi certe volas forigi tiun ĉi bibliotekon? Tio forigos {count, plural, one {# la elementon, kiun} other {all # la elementojn, kiujn}} ĝi enhavas, kaj ne eblas malfari tion. La dosieroj tamen restos sur via disko.", + "confirm_email_below": "Por konfirmi, tajpu \"{email}\" ĉi-sube", + "confirm_reprocess_all_faces": "Ĉu vi certas, ke vi volas retrakti ĉiujn vizaĝojn? Tio forigos ĉies nomon.", + "confirm_user_password_reset": "Ĉu vi certe volas restarigi la pasvorton de {user}?", + "confirm_user_pin_code_reset": "Ĉu vi certe volas restarigi la PIN-kodon de {user}?", + "copy_config_to_clipboard_description": "Kopii la aktualan sistem-agordaÄĩaron, kiel JSON-objekton", + "create_job": "Krei taskon", + "cron_expression": "cron-esprimo", + "cron_expression_description": "Agordu la intervalon de analizado pere de la formato de cron. Por pli da informoj, legu ekzemple Crontab Guru", + "cron_expression_presets": "AntaÅ­agordoj pri la cron-esprimo", + "disable_login": "Malebligi ensalutadon", + "duplicate_detection_job_description": "Komenci permaŝin-lernadon por trovi similajn bildojn. Uzas 'inteligentan serĉadon'", + "exclusion_pattern_description": "Per skemo de ekskludo, vi povas ignori dosierojn kaj dosierujojn dum analizado de la biblioteko. Tio estas utila se vi havas ekz. RAW-dosierojn, kiujn vi ne volas importi.", + "export_config_as_json_description": "Elŝuti la aktualan sistem-agordaÄĩaron kiel JSON-dosieron", + "external_libraries_page_description": "Paĝo por administri eksterajn bibliotekojn", + "face_detection": "Detekto de vizaĝoj", + "face_detection_description": "Detekti vizaĝojn en viaj bildoj pere de maŝin-lernado. Por videoj, nur la titola bildeto estos traktata. \"Denove\" (re-)lanĉos la detektadon. \"Restartigi\" krome forigas ĉiujn aktualajn datumojn pri vizaĝoj. \"Netraktitaj\" vicigas ĉiujn bildojn ankoraÅ­ netraktitajn. Post la detektado, komenciĝos la rekonado, ĉu novaj ĉu jam rekonitaj homoj.", + "facial_recognition_job_description": "Kongruigi detektitajn vizaĝojn al homoj. Tiu ĉi procezo okazas post la fino de Detektado. \"Restartigi\" (re-)kongruigas ĉiujn vizaĝojn. \"Netraktitaj\" lanĉas la kongruigadon nur pri nove rekonitaj vizaĝoj.", + "failed_job_command": "La komando {command} malsukcesis por tasko: {job}", + "force_delete_user_warning": "ATENTU: tio ĉi tuj forigos la uzanton, kune kun ĉiuj ties elementoj. Ne eblas malfari tion, kaj la dosieroj ne povas estas retrovitaj poste.", + "image_format": "Formato", + "image_format_description": "WebP-dosieroj estas ĝenerale malpli grandaj ol JPEG, sed postulas pli da tempo por krei.", + "image_fullsize_description": "Bildoj je plena grandeco, sen meta-datumoj, uzataj dum zomado", + "image_fullsize_enabled": "Ŝalti kreadon de plen-grandaj bildoj", + "image_fullsize_enabled_description": "Krei bildon je plena grandeco por ne TTT-aj formatoj. Kiam la agordo \"Preferi enkorpigitan antaÅ­vidon\" estas ŝaltita, enkorpigitaj antaÅ­vidoj okazas rekte sen konvertado. Tiu ĉi agordo ne influas TTT-kongruajn formatojn kiel ekz. JPEG.", + "image_fullsize_quality_description": "Kvalito de la plen-granda bildo, inter 1 kaj 100. Pli alta numero indikas pli altkvalitan bildon, sed ankaÅ­ pli grandan dosieron por stoki.", + "image_fullsize_title": "Agordoj pri plen-grandaj bildoj", + "image_prefer_embedded_preview": "Preferi enkorpigitan antaÅ­vidon", + "image_prefer_embedded_preview_setting_description": "Uzi enkorpigitan antaÅ­vidon en RAW-fotoj kiel fonton por bildotraktado, kiam ĝi ekzistas. Rezulto estas pli precizaj koloroj por iuj bildoj, sed la kvalito de la antaÅ­vido dependas de la fotilo, kaj estas risko ke la bildo havos pli da artefaktoj de densigo.", + "image_prefer_wide_gamut": "Preferi vastan gamon", + "image_prefer_wide_gamut_setting_description": "Uzi Display P3 por bildetoj. Tio pli bone konservas la brilecon en bildoj kun vasta kolorgamo, sed bildoj povas aspekti strangaj en malnovaj aparatoj kun malnova foliumilo. Bildoj kun sRGB konserviĝas tiel por eviti kolorŝangon.", + "image_preview_description": "Mez-granda bildo, sen metadatumoj, uzata por montri unuopan bildon, kaj por maŝin-lernado", + "image_preview_quality_description": "Kvalito de antaÅ­vido, inter 1 kaj 100. Pli alta numero indikas pli altan kvaliton, sed ankaÅ­ kreas pli grandajn dosierojn, kiuj povas malrapidigi uzadon de la apo. Tro malalta numero povas noci la maŝin-lernadon.", + "image_preview_title": "Agordoj pri antaÅ­vidoj", + "image_progressive": "Poiome", + "image_progressive_description": "Kodigi JPEG-bildojn por poioma vidigo dum ŝargado. Tio ŝanĝas nenion por WebP-bildoj.", + "image_quality": "Kvalito", + "image_resolution": "Distingivo", + "image_resolution_description": "Alta distingivo povas konservi pli da detaloj en bildoj sed postulas pli da tempo por trakti, donas pli grandajn dosierojn por stokie, kaj povas malrapidigi uzadon de la apo.", + "image_settings": "Agordoj pri bildoj", + "image_settings_description": "Administri agordojn pri kvalito kaj distingivo de kreitaj bildoj", + "image_thumbnail_description": "Malgranda bildeto, sen metadatumoj, uzata por vidigi grupojn de fotoj, ekz. en la ĉefa tempolinio", + "image_thumbnail_quality_description": "Kvalito de bildeto, inter 1 kaj 100. Pli alta cifero indikas pli altkvalitan bildon, sed donas pli grandajn dosierojn kaj povas malrapidigi uzadon de la apo.", + "image_thumbnail_title": "Agordoj pri bildetoj", + "import_config_from_json_description": "Importi sistem-agordaÄĩaron de JSON-dosiero", + "job_concurrency": "{job}: nombro de samtempaj taskoj", + "job_created": "Tasko kreita", + "job_not_concurrency_safe": "Estas nesekure fari tiun ĉi taskon samtempe kun aliaj.", + "job_settings": "Agordoj pri tasko", + "job_settings_description": "Administri samtempajn taskojn", + "jobs_delayed": "{jobCount, plural, other {# prokrastitaj}}", + "jobs_failed": "{jobCount, plural, other {# malsukesis}}", + "jobs_over_time": "Taskoj dum tempo", + "library_created": "Kreis bibliotekon: {library}", + "library_deleted": "Biblioteko forigita", + "library_details": "Detaloj de biblioteko", + "library_folder_description": "Indiki dosierujon por importi. La sistemo traserĉos ĝin, inkluzive subdosierujojn, por trovi bildojn kaj videojn.", + "library_remove_exclusion_pattern_prompt": "Ĉu vi certas, ke vi volas forigi tiun ĉi skemon de ekskludo?", + "library_remove_folder_prompt": "Ĉu vi certas, ke vi volas forigi tiun ĉi import-dosieron?", + "library_scanning": "Perioda analizado", + "library_scanning_description": "Administri agordojn pri perioda analizado de la biblioteko", + "library_scanning_enable_description": "Ŝalti periodan analizadon de la biblioteko", + "library_settings": "Ekstera biblioteko", + "library_settings_description": "Administri agordojn pri eksteraj bibliotekoj", + "library_tasks_description": "Analizi eksterajn bibliotekojn por trovi novajn kaj/aÅ­ ŝanĝitajn elementojn", + "library_updated": "Biblioteko ĝisdatigita", + "library_watching_enable_description": "Observi eksterajn bibliotekojn por detekti ŝanĝojn", + "library_watching_settings": "Observado de bibliotekoj [EKSPERIMENTA]", + "library_watching_settings_description": "AÅ­tomate observadi por ŝanĝitaj dosieroj", + "logging_enable_description": "Ŝalti protokoladon", + "logging_level_description": "Nivelo de protokolado, kiam ŝaltita.", + "logging_settings": "Protokolado", + "machine_learning_availability_checks": "Kontroloj de disponebleco", + "machine_learning_availability_checks_description": "AÅ­tomate detekti kaj preferi disponeblajn servilojn por maŝin-lernado", + "machine_learning_availability_checks_enabled": "Ŝalti kontrolojn de disponebleco", + "machine_learning_availability_checks_interval": "Intervalo de kontrolo", + "machine_learning_availability_checks_interval_description": "Intervalo en milisekundoj inter kontroloj de disponebleco", + "machine_learning_availability_checks_timeout": "Tempolimo de peto", + "machine_learning_availability_checks_timeout_description": "Tempolimo (en milisekundoj) por kontrolo de disponebleco", + "machine_learning_clip_model": "Modelo CLIP", + "machine_learning_clip_model_description": "La nomo de la modelo CLIP menciita ĉi tie. Notu, ke vi devas refari la 'inteligentan serĉon' por ĉiuj bildoj post ŝanĝo de modelo.", + "machine_learning_duplicate_detection": "Detektado de duoblaÄĩoj", + "machine_learning_duplicate_detection_enabled": "Ŝalti detektadon de duoblaÄĩoj", + "machine_learning_duplicate_detection_enabled_description": "Eĉ se malŝaltita, precize identaj elementoj tamen estos malduobligitaj.", + "machine_learning_duplicate_detection_setting_description": "Uzi la lingvomodelon CLIP por trovi verŝajnajn duoblaÄĩojn", + "machine_learning_enabled": "Ŝalti maŝin-lernadon", + "machine_learning_enabled_description": "Se malŝaltita, ĉiuj funkcioj rilate al maŝin-lernado malŝaltiĝos, sendepende de la ĉi-subaj agordoj.", + "machine_learning_facial_recognition": "Rekonado de vizaĝoj", + "machine_learning_facial_recognition_description": "Detekti, rekoni kaj grupigi vizaĝojn en bildoj", + "machine_learning_facial_recognition_model": "Modelo de vizaĝ-rekonado", + "machine_learning_facial_recognition_model_description": "Modeloj listiĝas laÅ­ grandeco, kun la plej granda supre. Pli grandaj modeloj funkcias malpli rapide kaj uzas pli da memoro, sed donas pli bonajn rezultojn. Notu, ke vi devos refari detektadon de vizaĝoj en ĉiuj bildoj se vi ŝanĝas la modelon.", + "machine_learning_facial_recognition_setting": "Ŝalti rekonadon de vizaĝoj", + "machine_learning_facial_recognition_setting_description": "Se malŝaltita, bildoj ne estos kodigitaj por rekonado de vizaĝoj, kaj vizaĝoj ne aldoniĝos al la sekcio Homoj en la paĝo Esplori.", + "machine_learning_max_detection_distance": "Maksimuma distanco de detektado", + "machine_learning_max_detection_distance_description": "Maksimuma distanco inter du bildoj por konsideri ilin duoblaÄĩoj, inter 0.001 kaj 0.1. Pli alta valoro detektas pli da duoblaÄĩoj, sed povus ankaÅ­ trovi pli da malprave pozitivaj rezultoj.", + "machine_learning_max_recognition_distance": "Maksimuma distanco de rekonado", + "machine_learning_max_recognition_distance_description": "Maksimuma distanco inter du vizaĝoj por konsideri ilin la sama homo, inter 0 kaj 2. Pli malalta valoro emas malebligi, ke du apartaj homoj estas konsiderataj kiel la sama; pli alta valoro evitas tiun problemon, sed plialtigas la ŝancon, ke la sama homo en apartaj fotoj estos konsiderata kiel malsamaj homoj. Notu, ke estas pli facile kunfandi du identigitajn homojn al unu ol la malo, do prefere uzu pli malaltan ciferon se eblas.", + "machine_learning_min_detection_score": "Sojla numero da poentoj por sukcesa detekto", + "machine_learning_min_detection_score_description": "Minimuma valoro de fido por ke vizaĝo estu detektita, inter 0 kaj 1. Pli malalta valoro detektigas pli da vizaĝoj, sed eble ankaÅ­ malprave pozitivajn rezultojn.", + "machine_learning_min_recognized_faces": "Minimuma nombro da rekontigaj vizaĝoj", + "machine_learning_min_recognized_faces_description": "La minimuma nombro da rekonitaj vizaĝoj de la sama homo por krei novan homon. Pli alta valoro indikas pli precizan rekonadon de vizaĝoj, sed povus esti tiel, ke trovita vizaĝo ne konektiĝas kun konata homo.", + "machine_learning_ocr": "Optika signo-rekono", + "machine_learning_ocr_description": "Uzi maŝin-lernadon por rekoni tekston en bildoj", + "machine_learning_ocr_enabled": "Ŝalti optikan signo-rekonon", + "machine_learning_ocr_enabled_description": "Se malŝaltita, tiam optika signo-rekonado ne aplikiĝas al viaj bildoj.", + "machine_learning_ocr_max_resolution": "Maksimuma distingivo", + "machine_learning_ocr_max_resolution_description": "AntaÅ­vidoj kun pli granda distingivo ol tio ĉi estos ŝanĝitaj, kun konstantaj proporcioj. Pli alta valoro indikas pli da precizeco, sed postulas pli da memoro kaj funkcias malpli rapide.", + "machine_learning_ocr_min_detection_score": "Sojla numero da poentoj por sukcesa detekto", + "machine_learning_ocr_min_detection_score_description": "Minimuma valoro de fido por ke teksto estu detektita, inter 0 kaj 1. Pli malalta valoro detektigas pli da teksto, sed eble ankaÅ­ malprave pozitivajn rezultojn.", + "machine_learning_ocr_min_recognition_score": "Sojla nombro da poentoj por rekono", + "machine_learning_ocr_min_score_recognition_description": "Minimuma valoro de fido por ke detektita teksto estu rekonata, inter 0 kaj 1. Pli malalta valoro rekonigas pli da teksto, sed eble ankaÅ­ donas malprave pozitivajn rezultojn.", + "machine_learning_ocr_model": "Modelo de optika signo-rekono", + "machine_learning_ocr_model_description": "Modeloj en servilo estas pli kapablaj ol tiuj en portebla aparato, sed uzas pli da memoro kaj funkcias pli malrapide.", + "machine_learning_settings": "Agordoj pri maŝin-lernado", + "machine_learning_settings_description": "Administri agordojn pri maŝin-lernado", + "machine_learning_smart_search": "Inteligenta serĉado", + "machine_learning_smart_search_description": "Serĉi bildojn semantike laÅ­ enkorpigitaj CLIP-aÄĩoj", + "machine_learning_smart_search_enabled": "Ŝalti inteligentan serĉadon", + "machine_learning_smart_search_enabled_description": "Se malŝaltita, tiam bildoj ne estos kodigitaj por inteligenta serĉado.", + "machine_learning_url_description": "La URL-o de la maŝin-lerna servilo. Se vi donas pli ol unu URL-o, la sistemo provos ĉiun servilon unu post la alia ĝis kiam unu sukcese respondas, de la unua ĝis la lasta. Serviloj, kiuj ne respondas, estos dumtempe ignoritaj.", + "maintenance_delete_backup": "Forigi savkopion", + "maintenance_delete_backup_description": "La dosiero estos por ĉiam forigita.", + "maintenance_delete_error": "Malsukcesis forigi sekurkopion.", + "maintenance_restore_backup": "RestaÅ­ri savkopion", + "maintenance_restore_backup_description": "Immich estos forigita kaj reinstalita de la elektita sekurkopio. Nova sekurkopio estos kreita antaÅ­e.", + "maintenance_restore_backup_different_version": "Tiu ĉi sekurkopio estis kreita per alia versio de Immich!", + "maintenance_restore_backup_unknown_version": "Ne eblis ektrovi version de la sekurkopio.", + "maintenance_restore_database_backup": "RestaÅ­ri datumbazon el sekurkopio", + "maintenance_restore_database_backup_description": "Reveni al antaÅ­a stato de datumbazo pere de sekurkopio", + "maintenance_settings": "Funkcitenado", + "maintenance_settings_description": "Ŝalti la funkcitenadan reĝimon de Immich.", + "maintenance_start": "Ŝanĝi al funkci-tenada reĝimo", + "maintenance_start_error": "Malsukcesis ŝalti funkci-tenadan reĝimon.", + "maintenance_upload_backup": "Alŝuti dosieron de sekurkopio de datumbazo", + "maintenance_upload_backup_error": "Malsukcesis alŝuti sekurkopion, ĉu ĝi havas formaton .sql aÅ­ .sql.gz?", + "manage_concurrency": "Administri samtempajn taskojn", + "manage_concurrency_description": "Vizitu la paĝon Taskoj por agordi la nombron de samtempaj taskoj", + "manage_log_settings": "Administri agordojn pri protokolado", + "map_dark_style": "Malhela stilo", + "map_enable_description": "Ŝalti map-funkciojn", + "map_gps_settings": "AgordaÄĩoj pri mapoj kaj GPS", + "map_gps_settings_description": "Administri agordojn pri mapoj kaj GPS", + "map_implications": "Montri mapojn de dependas de ekstera servo (tiles.immich.cloud)", + "map_light_style": "Hela stilo", + "map_manage_reverse_geocoding_settings": "Administri agordojn pri inversa geo-kodigo", + "map_reverse_geocoding": "Inversa geo-kodigo", + "map_reverse_geocoding_enable_description": "Ŝalti inversan geo-kodigon", + "map_reverse_geocoding_settings": "AgordaÄĩoj de inversa geo-kodigo", + "map_settings": "Mapo", + "map_settings_description": "Administri agordojn pri mapoj", + "map_style_description": "URL-o de dosiero style.json por difini map-stilon", + "memory_cleanup_job": "Purigado de memoraÄĩoj", + "memory_generate_job": "Kreado de memoraÄĩoj", + "metadata_extraction_job": "Eltiri metadatumojn", + "metadata_extraction_job_description": "Eltiri metadatumojn el ĉiuj elementoj, ekz. GPS-on, vizaĝojn, kaj distingivon", + "metadata_faces_import_setting": "Ŝalti importadon de vizaĝoj", + "metadata_faces_import_setting_description": "Importi vizaĝojn el EXIF-datumoj kaj dosieroj sidecar", + "metadata_settings": "Agordoj pri metadatumoj", + "metadata_settings_description": "Administri agordojn pri metadatumoj", + "migration_job": "Migrado", + "migration_job_description": "Migrigi bildetojn pri elementoj kaj vizaĝoj al la nova strukturo de dosierujoj", + "nightly_tasks_cluster_faces_setting_description": "Ekfari nun rekonadon de nove detektitaj vizaĝoj", + "nightly_tasks_cluster_new_faces_setting": "Grupigi novajn vizaĝojn", + "nightly_tasks_database_cleanup_setting": "Taskoj pri purigado de datumbazo", + "nightly_tasks_database_cleanup_setting_description": "Forigi malnovajn, eksvalidajn datumojn de la datumbazo", + "nightly_tasks_generate_memories_setting": "Generi memoraÄĩojn", + "nightly_tasks_generate_memories_setting_description": "Krei novajn memoraÄĩojn el elementoj", + "nightly_tasks_missing_thumbnails_setting": "Generi mankantajn bildetojn", + "nightly_tasks_missing_thumbnails_setting_description": "Vicigi elementojn sen bildetoj por generado de bildetoj", + "nightly_tasks_settings": "Agordoj pri ĉiunoktaj taskoj", + "nightly_tasks_settings_description": "Administri ĉiunoktajn taskojn", + "nightly_tasks_start_time_setting": "Komencohoro", + "nightly_tasks_start_time_setting_description": "La horo kiam la servilo komencos la ĉiunoktajn taskojn", + "nightly_tasks_sync_quota_usage_setting": "Sinkronigi uzadon de kvotoj", + "nightly_tasks_sync_quota_usage_setting_description": "Ĝisdatigi kvoton de uzo de stokado, laÅ­ aktuala uzo", + "no_paths_added": "Neniuj vojoj aldonitaj", + "no_pattern_added": "Neniu skemo aldonita", + "note_apply_storage_label_previous_assets": "Notu: por aldoni la etikedon de stokado al antaÅ­e alŝutitaj elementoj, ekfaru nun la taskon de migrado de stokado.", + "note_cannot_be_changed_later": "NOTU: ne eblas poste ŝanĝi tion ĉi!", + "notification_email_from_address": "Adreso de sendanto", + "notification_email_from_address_description": "Retadreso, kiu aperos kiel \"sendinto\" de retmesaĝoj, ekz. \"Immich foto-servilo \". Uzu nur adreson, kiun vi rajtas uzi tiel.", + "notification_email_host_description": "Gastiganto de la retmesaĝa servilo (ekz. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "Ignori erarojn pri atestiloj", + "notification_email_ignore_certificate_errors_description": "Ignori erarojn pri valideco de TLS-atestiloj (malrekomendite)", + "notification_email_password_description": "Pasvorto por uzi kun la retmesaĝa servilo", + "notification_email_port_description": "Pordo de la retmesaĝa servilo (ekz. 25, 465 aÅ­ 587)", + "notification_email_secure": "SMTPS", + "notification_email_secure_description": "Uzi SMTPS (SMTP pere de TLS)", + "notification_email_sent_test_email_button": "Sendi testmesaĝon kaj konservi", + "notification_email_setting_description": "Agordoj pri atentigoj per retmesaĝoj", + "notification_email_test_email": "Sendi testmesaĝon", + "notification_email_test_email_failed": "Malsukcesis sendi testmesaĝon, kontrolu la agordaÄĩojn", + "notification_email_test_email_sent": "Testmesaĝo estas sendita al {email}. Bonvolu kontroli ĉu ĝi bone alvenis.", + "notification_email_username_description": "Uzantonomo por uzi kun la retmesaĝa servilo", + "notification_enable_email_notifications": "Ŝalti retmesaĝajn atentigilojn", + "notification_settings": "Agordoj pri atentigiloj", + "notification_settings_description": "Administri agordojn pri atentigiloj, inkluzive tiujn per retmesaĝoj", + "oauth_auto_launch": "Startigi aÅ­tomate", + "oauth_auto_launch_description": "AÅ­tomate startigi la OAuth-procezon tuj ĉe la ensaluta paĝo", + "oauth_auto_register": "Registri aÅ­tomate", + "oauth_auto_register_description": "AÅ­tomate registri novajn uzantojn tuj post ensaluto per OAuth", + "oauth_button_text": "Teksto de butono", + "oauth_client_secret_description": "Bezonata por privata kliento, aÅ­ se PKCE (Proof Key for Code Exchange) ne estas subtenata de publika kliento.", + "oauth_enable_description": "Ensaluti per OAuth", + "oauth_mobile_redirect_uri": "Resenda URI por poŝ-aparatoj", + "oauth_mobile_redirect_uri_override": "Insisti pri resenda URI por poŝ-aparatoj", + "oauth_mobile_redirect_uri_override_description": "Ŝaltu tion ĉi kiam la provizanto de OAuth ne permesas URI-on por poŝ-aparatoj, kiel \"{callback}\"", + "oauth_role_claim": "Petita rolo", + "oauth_role_claim_description": "AÅ­tomate doni rolon de administranto laÅ­ tiu ĉi peto. La peto povas esti aÅ­ 'user' (uzanto) aÅ­ 'admin' (administranto).", + "oauth_settings": "OAuth", + "oauth_settings_description": "Administri agordojn pri OAuth-ensalutado", + "oauth_settings_more_details": "Por pli da detaloj pri tio ĉi, bonvolu legi la dokumentaron.", + "oauth_storage_label_claim": "Petita etikedo de stokado", + "oauth_storage_label_claim_description": "AÅ­tomate uzi la petitan etikedon por la stokado de la uzanto.", + "oauth_storage_quota_claim": "Petita kvoto de stokado", + "oauth_storage_quota_claim_description": "AÅ­tomate doni kvoton de stokado laÅ­ tiu ĉi peto.", + "oauth_storage_quota_default": "DefaÅ­lta kvoto de stokado (GiB)", + "oauth_storage_quota_default_description": "Kvoto en GiB, uzata kiam mankas specifa peto pri tio.", + "oauth_timeout": "Tempolimo de petoj", + "oauth_timeout_description": "Tempolimo por petoj, en milisekundoj", + "ocr_job_description": "Uzi maŝin-lernadon por rekoni tekston en bildoj", + "password_enable_description": "Ensaluti per retadreso kaj pasvorto", + "password_settings": "Ensaluti per pasvorto", + "password_settings_description": "Administri agordojn pri ensalutado per pasvorto", + "paths_validated_successfully": "Ĉiuj vojoj sukcese validigitaj", + "person_cleanup_job": "Purigado de homoj", + "queue_details": "Detaloj pri la atendovico", + "queues": "Atendovicoj de taskoj", + "queues_page_description": "Administri la atendovicojn de taskoj", + "quota_size_gib": "Kvoto (GiB)", + "refreshing_all_libraries": "Aktualigado de ĉiuj bibliotekoj", + "registration": "Registrado de administranto", + "registration_description": "Vi estas la unua uzanto de tiu ĉi sistemo, do vi aÅ­tomate havos la rolon de administranto. Vi respondecos pri administraj taskoj, kaj vi povos krei pliajn uzantojn.", + "remove_failed_jobs": "Forigi malsukcesajn taskojn", + "require_password_change_on_login": "Devigi al uzantoj ŝanĝi pasvorton post unua ensaluto", + "reset_settings_to_default": "Restarigi agordaÄĩojn al defaÅ­ltoj", + "reset_settings_to_recent_saved": "Restarigi agordaÄĩojn al la lastatempe konservitaj valoroj", + "scanning_library": "Analizado de biblioteko", + "search_jobs": "Serĉi taskojnâ€Ļ", + "send_welcome_email": "Sendi bonvenan retmesaĝon", + "server_external_domain_settings": "Ekstera domajno", + "server_external_domain_settings_description": "Domajno por publike dividitaj ligiloj, inkl. http(s)://", + "server_public_users": "Publikaj uzantoj", + "server_public_users_description": "Nomo kaj retadreso de ĉiuj uzantoj estas listigitaj kiam oni aldonas uzanton al dividita albumo. Kiam malŝaltita, la listo de uzantoj estos videbla nur por administrantoj.", + "server_settings": "Agordoj de servilo", + "server_settings_description": "Administri agordojn pri servilo", + "server_stats_page_description": "Paĝo de statistikoj pri la servilo", + "server_welcome_message": "Bonvena mesaĝo", + "server_welcome_message_description": "Mesaĝo afiŝita ĉe la ensaluta paĝo.", + "settings_page_description": "Paĝo de administraj agordaÄĩoj", + "sidecar_job": "Metadatumoj de sidecar-dosieroj", + "sidecar_job_description": "Trovi aÅ­ sinkronigi metadatumojn de sidecar-dosieroj", + "slideshow_duration_description": "Montri ĉiun bildon dum tiu nombro da sekundoj", + "smart_search_job_description": "Ekigi maŝin-lernadon pri elemetoj por ebligi uzon de inteligenta serĉo", + "storage_template_date_time_description": "La tempindiko de la elemento uziĝas por doni daton kaj horon", + "storage_template_date_time_sample": "Ekzempla horo {date}", + "storage_template_enable_description": "Ŝalti motoron de skemoj de stokado", + "storage_template_hash_verification_enabled": "Kontrolo de haketoj estas ŝaltita", + "storage_template_hash_verification_enabled_description": "Ŝaltas kontroladon de haketoj. Ne malŝaltu krom se vi certas, ke vi komprenas la konsekvencojn", + "storage_template_migration": "Migrado de skemoj de stokado", + "storage_template_migration_description": "Apliki la aktualan {template} al antaÅ­e alŝutitaj elementoj", + "storage_template_migration_info": "La skemo de stokado ŝanĝas ĉiun sufikson de dosiernomo al minuskloj. Tio aplikiĝos nur al novaj elementoj. Por fari tion ankaÅ­ al jam alŝutitaj elementoj, ekfunkciigu tiun ĉi taskon: {job}.", + "storage_template_migration_job": "Tasko de migrado de skemoj de stokado", + "storage_template_more_details": "Por pli da informoj pri tiu funkcio, rigardu la skemon de stokado kaj ĝiajn konsekvencojn", + "storage_template_onboarding_description_v2": "Tiu ĉi funkcio aÅ­tomate organizas dosierojn laÅ­ ŝablono difinita de la uzanto. Por pli da informoj, legu la dokumentaron.", + "storage_template_path_length": "Proksimuma limo de longeco de vojo: {length, number}/{limit, number}", + "storage_template_settings": "Skemo de stokado", + "storage_template_settings_description": "Administri la strukturon de dosierujoj kaj la dosiernomon de la alŝutita elemento", + "storage_template_user_label": "{label} estas la etikedo de stokado de la uzanto", + "system_settings": "Agordoj de la sistemo", + "tag_cleanup_job": "Purigado de etikedoj", + "template_email_available_tags": "Vi rajtas uzi tiujn ĉi variablojn en via ŝablono: {tags}", + "template_email_if_empty": "Se la ŝablono estas malplena, la defaÅ­lta retadreso estas uzita.", + "template_email_invite_album": "Ŝablono de invitilo al albumo", + "template_email_preview": "AntaÅ­vido", + "template_email_settings": "Ŝablonoj de retmesaĝoj", + "template_email_update_album": "Ŝablono por retmesaĝo por ĝisdatigi albumon", + "template_email_welcome": "Ŝablono de bonvena retmesaĝo", + "template_settings": "Ŝablonoj de atentigiloj", + "template_settings_description": "Administri tajloritajn skemojn por atentigiloj", + "theme_custom_css_settings": "Tajlorita CSS", + "theme_custom_css_settings_description": "Vi povas ŝanĝi la vidan aspekton de Immich per CSS.", + "theme_settings": "Agordoj de la etoso", + "theme_settings_description": "Administri tajloradon de la reta interfaco de Immich", + "thumbnail_generation_job": "Generi bildetojn", + "thumbnail_generation_job_description": "Kreas grandan, malgrandan, kaj malklaran bildetojn por ĉiu elemento, kune kun bildeto por ĉiu homo", + "transcoding_acceleration_api": "API de pliradidigo", + "transcoding_acceleration_api_description": "La API, kiu interagos kun via aparato por plirapidigi la transkodadon. Tiu ĉi agordaÄĩo indikas preferon – kaze de malsukceso, ĝi retropaŝas al softvara transkodado. VP9 povas funkcii aÅ­ ne, depende de viaj aparatoj.", + "transcoding_acceleration_nvenc": "NVENC (postulas GPU de NVIDIA)", + "transcoding_acceleration_qsv": "Quick Sync (postulas ĉefprocesoron Intel de minimume 7-a generacio)", + "transcoding_acceleration_rkmpp": "RKMPP (nur por SOC-oj de Rockchip)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Akceptitaj sonkodekoj", + "transcoding_accepted_audio_codecs_description": "Elektu senkodekojn, kiuj ne bezonas transkodadon. Uziĝas nur por specifaj politikoj de transkodado.", + "transcoding_accepted_containers": "Akceptitaj ujoj", + "transcoding_accepted_containers_description": "Elektu la uj-formatojn, kiuj ne bezonas esti remiksitaj al MP4. Uziĝas nur por specifaj politikoj de transkodado.", + "transcoding_accepted_video_codecs": "Akceptitaj video-kodekoj", + "transcoding_accepted_video_codecs_description": "Elektu video-kodekojn, kiuj ne bezonas transkodadon. Uziĝas nur por specifaj politikoj de transkodado.", + "transcoding_advanced_options_description": "Agordoj, kiujn plej multaj uzantoj ne bezonas ŝanĝi", + "transcoding_audio_codec": "Sonkodeko", + "transcoding_audio_codec_description": "Opus estas la plej altkvalita elekto, sed ĝi ne kongruas kun malnovaj aparatoj kaj softvaroj.", + "transcoding_bitrate_description": "Videoj kun bitrapido pli alta ol maksimumo, aÅ­ ne en akceptita formato", + "transcoding_codecs_learn_more": "Per lerni pli pri la terminaro uzata ĉi tie, legu la dokumentaron de FFmpeg pri kodeko H.264, kodeko HEVC kaj kodeko VP9.", + "transcoding_constant_quality_mode": "Reĝimo de konstanta kvalito", + "transcoding_constant_quality_mode_description": "ICQ estas pli bona ol CQP, sed kelkaj aparatoj de plirapidigo ne subtenas ĝin. Ŝalti tion ĉi privilegiigas la elektitan reĝimon dum uzo de kodado bazita sur kvalito. Ignorita de NVENC ĉar ĝi ne subtenas ICQ.", + "transcoding_constant_rate_factor": "Konstanta rapida faktoro (-crf)", + "transcoding_constant_rate_factor_description": "Nivelo de video-kvalito. Tipaj valoroj estas 23 por H.264, 28 por HEVC, 31 por VP9, kaj 35 por AV1. Pli malalta cifero indikas pli altan kvaliton, sed kreas pli pezajn dosierojn.", + "transcoding_disabled_description": "Ne transkodigi videojn. Tio povas perturbi vidigon en kelkaj klientoj", + "transcoding_encoding_options": "Agordoj de kodigo", + "transcoding_encoding_options_description": "Administri agordojn pri kodekoj, distingivo, kvalito, ktp. por la kodigitaj videoj", + "transcoding_hardware_acceleration": "Aparata plirapidigo", + "transcoding_hardware_acceleration_description": "Eksperimenta: pli rapida kodado, sed eble kun malpli bona kvalito je sama bitrapido", + "transcoding_hardware_decoding": "Aparata malkodado", + "transcoding_hardware_decoding_setting_description": "Ŝaltas tutvojan plirapidigon anstataÅ­ nur pliradidan kodadon. Povus ne funkcii por kelkaj videoj.", + "transcoding_max_b_frames": "Makimuma nombro de B-kadroj", + "transcoding_max_b_frames_description": "Pli alta valoro indikas pli efikan densigon, sed malpli rapidan kodadon. Eble ne funkcios kun pli malnova aparata plirapidigo. Valoro de 0 malŝaltas B-kadrojn. Valoro de -1 indikas aÅ­tomate elektitan valoron.", + "transcoding_max_bitrate": "Maksimuma bitrapido", + "transcoding_max_bitrate_description": "Agordi maksimuman bitrapidon rezultas je dosieroj kun pli antaÅ­videbla grandeco, kun nur malgranda perdo de kvalito. Por 720p, tipaj valoroj estas 2600 kbit/s por VP9 aÅ­ HEVC, aÅ­ 4500 kbit/s por H.264. Valoro de 0 indikas 'malŝaltita'. DefaÅ­lta unuo estas k (t.e. kbit/s), do '5000', '5000k' kaj '5M' estas ekvivalentaj.", + "transcoding_max_keyframe_interval": "Maksimuma intervalo inter ĉefaj kadroj", + "transcoding_max_keyframe_interval_description": "Agordas la maksimuman distancon inter ĉefaj kadroj. Malaltaj valoroj malhelpas densigon, sed povas plibonigi kvaliton en scenoj kun rapidaj movoj. Valoro de 0 indikas aÅ­tomatan agordigon.", + "transcoding_optimal_description": "La videoj havas distingivon pli altan ol tiu celita, aÅ­ ne havas akcepteblan formaton", + "transcoding_policy": "Politiko de transkodado", + "transcoding_policy_description": "Kriterioj por indiki ĉu video estas transkodita aÅ­ ne", + "transcoding_preferred_hardware_device": "Preferita aparato", + "transcoding_preferred_hardware_device_description": "Aplikiĝas nur al VAAPI kaj QSV. Indikas la DRI-nodoj uzataj por transkodado per aparato.", + "transcoding_preset_preset": "AntaÅ­elekto (-preset)", + "transcoding_preset_preset_description": "Rapideco de densigo. Malplia rapideco rezultas je pli malgrandaj dosieroj, kaj plibonigas kvaliton por donita bitrapido. VP9 ignoras rapidecojn pli grandajn ol 'faster'.", + "transcoding_reference_frames": "Referencaj kadroj", + "transcoding_reference_frames_description": "La nombro da apudaj kadroj uzataj dum densigo de iu kadro. Pli granda valoro rezultas je pli bona densigo, sed malpli rapida laboro. Valoro de 0 indikas aÅ­tomatan agordon.", + "transcoding_required_description": "Nur videoj kun neakceptataj formatoj", + "transcoding_settings": "Agordoj de transkodado de videoj", + "transcoding_settings_description": "Administri transkodadon de videoj", + "transcoding_target_resolution": "Celita distingivo", + "transcoding_target_resolution_description": "Pli alta distingivo konservas pli da detaloj, sed bezonas pli da tempo por kodigi, donas pli grandajn dosierojn, kaj povas kaÅ­zi malrapidecon ĉe la apo.", + "transcoding_temporal_aq": "Adaptema kvantigo de tempo (AQ)", + "transcoding_temporal_aq_description": "Aplikiĝas nur al NVENC. Adaptema kvantigo de tempo (AQ) plibonigas kvaliton de scenoj kun multe da detaloj kaj malmulte da movado. Eble ne funkcios kun malnovaj aparatoj.", + "transcoding_threads": "Fadenoj", + "transcoding_threads_description": "Pli alta valoro ebligas pli rapidan kodadon, sed dume lasas malpli da servila kapacito por aliaj taskoj. La numero ne estu pli ol la nombro da disponeblaj CPU-kernoj. Valoro de 0 indikas maksimuma uzo de disponeblaj rimedoj.", + "transcoding_tone_mapping": "Mapado de tonoj", + "transcoding_tone_mapping_description": "Klopodas konservi aspekton de HDR-videoj dum transkodigo al SDR. Ĉiu algoritmo faras proprajn kompromisojn pri koloroj, detaloj kaj heleco. Hable konservas detalojn, Mobius konservas kolorojn, kaj Reinhard konservas helecon.", + "transcoding_transcode_policy": "Politiko de transkodado", + "transcoding_transcode_policy_description": "Politiko pri kiam video estos transkodita. HDR-videoj ĉiam estas transkoditaj (krom se transkodado estas malŝaltita).", + "transcoding_two_pass_encoding": "Dupasa kodigo", + "transcoding_two_pass_encoding_setting_description": "Transkodigo per du pasoj por krei pli bone kodigitajn videojn. Kiam eblas uzi maksimuman bitrapidon (bezonate por funkcii kun H.264 kaj kun HEVC), tiu ĉi modo uzas gamon de bitrapidoj surbaze de tiu maksimumo, kaj ignoras CRF. Por VP9, eblas uzi CRF se maksimuma bitrapido estas malŝaltita.", + "transcoding_video_codec": "Videa kodeko", + "transcoding_video_codec_description": "VP9 havas altan rendimenton kaj taÅ­gas por retumiloj, sed bezonas pli da tempo por kodigi. HEVC donas similajn rezultojn, sed malpli da retumiloj rekonas ĝin. H.264 estas vaste rekonata kaj rapide transkodebla, sed la dosieroj estas multe pli grandaj. AV1 estas la plej efika kodeko sed ne bone funkcias kun pli malnovaj aparatoj.", + "trash_enabled_description": "Ŝalti la rubujon", + "trash_number_of_days": "Nombro da tagoj", + "trash_number_of_days_description": "Kiom da tagoj oni konservu elementojn en la rubujo antaÅ­ ol forigi ilin por ĉiam", + "trash_settings": "Agordoj pri rubujo", + "trash_settings_description": "Administri agordojn pri rubaÄĩoj", + "unlink_all_oauth_accounts": "Malligi ĉiujn OAuth-kontojn", + "unlink_all_oauth_accounts_description": "Ne forgesu malligi ĉiujn OAuth-kontojn antaÅ­ ol migri al nova provizanto.", + "unlink_all_oauth_accounts_prompt": "Ĉu vi certas, ke vi volas malligi ĉiujn OAuth-kontojn? Tio kreos novan OAuth-identigilon por ĉiu uzanto, kaj ne eblos malfari tion.", + "user_cleanup_job": "Purigado de uzantoj", + "user_delete_delay": "La konto de {user} kaj ĝiaj elementoj estos por ĉiam forigitaj post {delay, plural, one {# tago} other {# tagoj}}.", + "user_delete_delay_settings": "Prokrasto de forigo", + "user_delete_delay_settings_description": "Agordas la nombron da tagoj konserviĝos forigita konto de uzanto, antaÅ­ ol porĉiama forigo. La porĉiama forigo okazas aÅ­tomate je noktomezo. Ŝanĝoj al tiu ĉi numero ekhavos efikon je venonta noktomezo.", + "user_delete_immediately": "La konto de {user} estos tuj forigita sed eblos dum kelkaj tagoj retrovi ĝin laÅ­bezone.", + "user_delete_immediately_checkbox": "Envicigi uzanton kaj ties elementojn por tuja forigo", + "user_details": "Detaloj pri uzanto", + "user_management": "Administrado de uzantoj", + "user_password_has_been_reset": "Pasvorto de tiu ĉi uzanto estas restarigita:", + "user_settings_description": "Administri agordojn pri uzantoj" + }, + "asset_viewer_settings_subtitle": "Administri agordojn pri vidilo de galerioj", + "backup_setting_subtitle": "Administri agordojn pri fona kaj malfona alŝutado", + "backup_settings_subtitle": "Administri agordojn pri alŝutado", + "cleanup_icloud_shared_albums_excluded": "Dividitaj albumoj ĉe iCloud estas ekskluditaj de la analizado", + "cleanup_step3_description": "Serĉi fotojn kaj videojn kun sekurkopio ĉe la servilo, laÅ­ la elektita limdato kaj filtriloj.", + "download_settings_description": "Administri agordojn pri elŝutado de elementoj", + "edit_exclusion_pattern": "Redakti skemon de ekskludo", + "errors": { + "exclusion_pattern_already_exists": "Tiu ĉi skemo de ekskludo jam ekzistas.", + "unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo", + "unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo", + "unable_to_edit_exclusion_pattern": "Ne eblas redakti skemon de ekskludo", + "unable_to_scan_libraries": "Ne eblas analizi biblitekojn", + "unable_to_scan_library": "Ne eblas analizi biblitekon" + }, + "exclusion_pattern": "Skemo de ekskludo", + "explore": "Esplori", + "explorer": "Foliumilo", + "manage_media_access_settings": "Malfermi agordaÄĩaron", + "manage_the_app_settings": "Agordi la apon", + "missing": "Netraktitaj", + "networking_subtitle": "Administri agordojn pri finpunktoj de la servilo", + "no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.", + "preferences_settings_subtitle": "Administri agordojn pri la apo", + "purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aÅ­tentikeco por la servilo", + "refresh": "Denove", + "rescan": "Reanalizi", + "reset": "Restartigi", + "scan": "Analizi", + "scan_all_libraries": "Analizi ĉiujn bibliotekojn", + "scan_library": "Analizi", + "scan_settings": "Agordoj pri analizado", + "scanning": "Analizado", + "scanning_for_album": "Serĉado de albumo...", + "search_suggestion_list_smart_search_hint_1": "Inteligenta serĉado defaÅ­lte estas ŝaltita. Por serĉi metadatumojn, uzu sintakson tiel ", + "upload_concurrency": "Nombro da samtempaj alŝutoj", + "user_pin_code_settings_description": "Administri vian PIN-kodon", + "user_purchase_settings_description": "Administri vian aĉeton", + "view_links": "Vidi ligilojn", + "week": "Semajno", + "wifi_name": "Nomo de Vifireto", + "year": "Jaro", + "yes": "Jes" +} diff --git a/i18n/es.json b/i18n/es.json index 5a16946039..49e58e3beb 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -5,6 +5,7 @@ "acknowledge": "Aceptar", "action": "AcciÃŗn", "action_common_update": "Actualizar", + "action_description": "Un conjunto de acciones a realizar en los recursos filtrados", "actions": "Acciones", "active": "Activo", "active_count": "Activo: {count}", @@ -15,9 +16,14 @@ "add_a_location": "AÃąadir una ubicaciÃŗn", "add_a_name": "AÃąadir un nombre", "add_a_title": "AÃąadir título", + "add_action": "AÃąadir acciÃŗn", + "add_action_description": "Haga clic para aÃąadir una acciÃŗn a realizar", + "add_assets": "AÃąadir recursos", "add_birthday": "AÃąadir un cumpleaÃąos", "add_endpoint": "AÃąadir punto final", "add_exclusion_pattern": "AÃąadir patrÃŗn de exclusiÃŗn", + "add_filter": "AÃąadir filtro", + "add_filter_description": "Haga clic para aÃąadir una condiciÃŗn de filtro", "add_location": "AÃąadir ubicaciÃŗn", "add_more_users": "AÃąadir mÃĄs usuarios", "add_partner": "AÃąadir miembro", @@ -36,6 +42,7 @@ "add_to_shared_album": "AÃąadir al ÃĄlbum compartido", "add_upload_to_stack": "AÃąadir subida a la cola", "add_url": "AÃąadir URL", + "add_workflow_step": "AÃąadir paso al flujo de trabajo", "added_to_archive": "AÃąadido al archivo", "added_to_favorites": "AÃąadido a favoritos", "added_to_favorites_count": "AÃąadido {count, number} a favoritos", @@ -63,26 +70,26 @@ "cleared_jobs": "Trabajos borrados para: {job}", "config_set_by_file": "La configuraciÃŗn estÃĄ definida por un archivo de configuraciÃŗn", "confirm_delete_library": "ÂŋEstÃĄs seguro de que quieres eliminar la biblioteca {library}?", - "confirm_delete_library_assets": "ÂŋEstÃĄs seguro de que quieras eliminar esta biblioteca? Esto eliminarÃĄ los {count, plural, one {# contained asset} other {all # contained assets}} elementos en Immich y no puede deshacerse. Los archivos permanecerÃĄn en disco.", + "confirm_delete_library_assets": "ÂŋEstÃĄs seguro de que quieras eliminar esta biblioteca? Esto eliminarÃĄ {count, plural, one {# recurso contenido} other {todos # recursos contenidos}} de Immich y no puede deshacerse. Los archivos permanecerÃĄn en disco.", "confirm_email_below": "Para confirmar, escribe \"{email}\" a continuaciÃŗn", "confirm_reprocess_all_faces": "ÂŋEstÃĄs seguro de que deseas reprocesar todas las caras? Esto borrarÃĄ a todas las personas que nombraste.", "confirm_user_password_reset": "ÂŋEstÃĄs seguro de que quieres restablecer la contraseÃąa de {user}?", "confirm_user_pin_code_reset": "ÂŋSeguro que quieres restablecer el PIN de {user}?", "copy_config_to_clipboard_description": "Copiar la configuraciÃŗn actual del sistema como un objeto JSON al", "create_job": "Crear trabajo", - "cron_expression": "ExpresiÃŗn CRON", - "cron_expression_description": "Establece el intervalo de escaneo utilizando el formato CRON. Para mÃĄs informaciÃŗn puedes consultar, por ejemplo, Crontab Guru", - "cron_expression_presets": "Valores predefinidos de expresiÃŗn CRON", + "cron_expression": "ExpresiÃŗn cron", + "cron_expression_description": "Establece el intervalo de escaneo utilizando el formato cron. Para mÃĄs informaciÃŗn puedes consultar, por ejemplo, Crontab Guru", + "cron_expression_presets": "Valores predefinidos de expresiones cron", "disable_login": "Deshabilitar inicio de sesiÃŗn", - "duplicate_detection_job_description": "Lanza el aprendizaje automÃĄtico para detectar imÃĄgenes similares. Necesita tener activado \"BÃēsqueda Inteligente\"", + "duplicate_detection_job_description": "Ejecuta el aprendizaje automÃĄtico en los recursos para detectar imÃĄgenes similares. Se basa en la bÃēsqueda inteligente", "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.", "export_config_as_json_description": "Descargar la configuraciÃŗn actual del sistema como un archivo JSON", "external_libraries_page_description": "PÃĄgina de biblioteca externa del administrador", "face_detection": "DetecciÃŗn de caras", - "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.", + "face_detection_description": "Detecta las caras en los recursos mediante aprendizaje automÃĄtico. En el caso de los vídeos, solo se tiene en cuenta la miniatura. \"Actualizar\" (re)procesarÃĄ todos los recursos. \"Restablecer\" borra ademÃĄs todos los datos de caras actuales. \"Faltante\" pone en cola los recursos 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.", + "force_delete_user_warning": "CUIDADO: Esta acciÃŗn eliminarÃĄ inmediatamente el usuario y todos los recursos. Esta acciÃŗn no se puede deshacer y los archivos no pueden ser recuperados.", "image_format": "Formato", "image_format_description": "WebP genera archivos mÃĄs pequeÃąos que JPEG, pero es mÃĄs lento al codificarlos.", "image_fullsize_description": "Imagen de tamaÃąo completo con metadatos removidos, usado cuando se hace zoom", @@ -94,9 +101,11 @@ "image_prefer_embedded_preview_setting_description": "Usar vistas previas embebidas en fotos RAW como entrada para el procesamiento de imÃĄgenes y cuando estÊn disponibles. Esto puede producir colores mÃĄs precisos en algunas imÃĄgenes, pero la calidad de la vista previa depende de la cÃĄmara y la imagen puede tener mÃĄs artefactos de compresiÃŗn.", "image_prefer_wide_gamut": "Preferir 'gamut' amplio", "image_prefer_wide_gamut_setting_description": "Usar \"Display P3\" para las miniaturas. Preserva mejor la vivacidad de las imÃĄgenes con espacios de color amplios pero las imÃĄgenes pueden aparecer de manera diferente en dispositivos antiguos con una versiÃŗn antigua del navegador. Las imÃĄgenes sRGB se mantienen como sRGB para evitar cambios de color.", - "image_preview_description": "Imagen de tamaÃąo mediano con metadatos eliminados. Es utilizado al visualizar un solo activo y para el aprendizaje automÃĄtico", + "image_preview_description": "Imagen de tamaÃąo mediano con metadatos eliminados. Es utilizado al visualizar un solo recurso y para el aprendizaje automÃĄtico", "image_preview_quality_description": "Calidad de vista previa de 1 a 100. Es mejor cuanto mÃĄs alta sea la calidad pero genera archivos mÃĄs grandes y puede reducir la capacidad de respuesta de la aplicaciÃŗn. Establecer un valor bajo puede afectar la calidad del aprendizaje automÃĄtico.", "image_preview_title": "Ajustes de las vistas previas", + "image_progressive": "Progressivo", + "image_progressive_description": "Codifica imÃĄgenes JPEG progresivamente para una visualizaciÃŗn con carga gradual. Esto no afecta a las imÃĄgenes WebP.", "image_quality": "Calidad", "image_resolution": "ResoluciÃŗn", "image_resolution_description": "Las resoluciones mÃĄs altas pueden conservar mÃĄs detalles pero requieren mÃĄs tiempo para codificar, tienen tamaÃąos de archivo mÃĄs grandes y pueden afectar la capacidad de respuesta de la aplicaciÃŗn.", @@ -125,7 +134,7 @@ "library_scanning_enable_description": "Activar el escaneo periÃŗdico de la biblioteca", "library_settings": "Biblioteca externa", "library_settings_description": "Administrar configuraciÃŗn biblioteca externa", - "library_tasks_description": "Buscar elementos nuevos o modificados en bibliotecas externas", + "library_tasks_description": "Buscar recursos nuevos o modificados en bibliotecas externas", "library_updated": "Biblioteca actualizada", "library_watching_enable_description": "Vigilar las bibliotecas externas para detectar cambios en los archivos", "library_watching_settings": "Vigilancia de la biblioteca [EXPERIMENTAL]", @@ -144,7 +153,7 @@ "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado aquí. TendrÃĄs que relanzar el trabajo 'BÃēsqueda Inteligente' para todos los elementos al cambiar de modelo.", "machine_learning_duplicate_detection": "DetecciÃŗn de duplicados", "machine_learning_duplicate_detection_enabled": "Habilitar detecciÃŗn de duplicados", - "machine_learning_duplicate_detection_enabled_description": "Si estÃĄ deshabilitado, los activos exactamente idÊnticos seguirÃĄn siendo eliminados.", + "machine_learning_duplicate_detection_enabled_description": "Si estÃĄ deshabilitado, los recursos exactamente idÊnticos seguirÃĄn siendo eliminados.", "machine_learning_duplicate_detection_setting_description": "Usa incrustaciones de CLIP (Contrastive Language-Image Pre-Training) para encontrar posibles duplicados", "machine_learning_enabled": "Habilitar aprendizaje automÃĄtico", "machine_learning_enabled_description": "Al desactivarla todas las funciones de ML se deshabilitarÃĄn independientemente de la configuraciÃŗn a continuaciÃŗn.", @@ -173,18 +182,29 @@ "machine_learning_ocr_min_recognition_score": "PuntuaciÃŗn mínima de reconocimiento", "machine_learning_ocr_min_score_recognition_description": "PuntuaciÃŗn mínima de confianza para que el texto detectado sea reconocido de 0 a 1. Los valores mÃĄs bajos reconocerÃĄn mÃĄs texto, pero pueden producir falsos positivos.", "machine_learning_ocr_model": "Modelo de OCR", - "machine_learning_ocr_model_description": "Los modelos del servidor son mÃĄs precisos que los modelos para mÃŗviles mÃŗviles, pero tardan mÃĄs en procesar y consumen mÃĄs memoria.", + "machine_learning_ocr_model_description": "Los modelos del servidor son mÃĄs precisos que los modelos mÃŗviles, pero tardan mÃĄs en procesar y consumen mÃĄs memoria.", "machine_learning_settings": "ConfiguraciÃŗn de aprendizaje automÃĄtico", "machine_learning_settings_description": "Administrar funciones y configuraciones de aprendizaje automÃĄtico", - "machine_learning_smart_search": "Busqueda inteligente", + "machine_learning_smart_search": "BÃēsqueda inteligente", "machine_learning_smart_search_description": "Busque imÃĄgenes semÃĄnticamente utilizando incrustaciones CLIP (Contrastive Language-Image Pre-Training)", "machine_learning_smart_search_enabled": "Habilitar bÃēsqueda inteligente", "machine_learning_smart_search_enabled_description": "Al desactivarlo las imÃĄgenes no se procesarÃĄn para usar la bÃēsqueda inteligente.", "machine_learning_url_description": "La URL del servidor de aprendizaje automÃĄtico. Si se proporciona mÃĄs de una URL se intentarÃĄ acceder a cada servidor sucesivamente hasta que uno responda correctamente en el orden especificado. Los servidores que no respondan serÃĄn ignorados temporalmente hasta que vuelvan a estar en línea.", + "maintenance_delete_backup": "Eliminar copia de seguridad", + "maintenance_delete_backup_description": "Este archivo serÃĄ eliminado de forma permanente.", + "maintenance_delete_error": "Fallo al eliminar la copia de seguridad.", + "maintenance_restore_backup": "Restaurar copia de seguridad", + "maintenance_restore_backup_description": "Se borrarÃĄ el historial de Immich y se restaurarÃĄ desde la copia de seguridad seleccionada. Se crearÃĄ una copia de seguridad antes de continuar.", + "maintenance_restore_backup_different_version": "ÂĄEsta copia de seguridad se creÃŗ con una versiÃŗn diferente de Immich!", + "maintenance_restore_backup_unknown_version": "No se pudo determinar la versiÃŗn del respaldo.", + "maintenance_restore_database_backup": "Restaurar copia de seguridad de la base de datos", + "maintenance_restore_database_backup_description": "Revertir a un estado anterior de la base de datos mediante un archivo de respaldo", "maintenance_settings": "Mantenimiento", "maintenance_settings_description": "Poner Immich en modo de mantenimiento.", - "maintenance_start": "Iniciar el modo de mantenimiento", + "maintenance_start": "Cambiar al modo de mantenimiento", "maintenance_start_error": "Error al iniciar el modo de mantenimiento.", + "maintenance_upload_backup": "Subir archivo de copia de seguridad de la base de datos", + "maintenance_upload_backup_error": "No se pudo cargar la copia de seguridad, Âŋes un archivo .sql/.sql.gz?", "manage_concurrency": "Ajustes de concurrencia", "manage_concurrency_description": "Navegar a la pÃĄgina de trabajos para administrar la concurrencia de trabajos", "manage_log_settings": "Administrar la configuraciÃŗn de los registros", @@ -204,30 +224,30 @@ "memory_cleanup_job": "Limpieza de recuerdos", "memory_generate_job": "GeneraciÃŗn de recuerdos", "metadata_extraction_job": "ExtracciÃŗn de metadatos", - "metadata_extraction_job_description": "Extraer informaciÃŗn de metadatos de cada activo, como GPS, caras y resoluciÃŗn", + "metadata_extraction_job_description": "Extraer informaciÃŗn de metadatos de cada recurso, como GPS, caras y resoluciÃŗn", "metadata_faces_import_setting": "Activar importaciÃŗn de caras", "metadata_faces_import_setting_description": "Importar caras desde los metadatos EXIF y auxiliares de una imagen", "metadata_settings": "ConfiguraciÃŗn de metadatos", "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", + "migration_job_description": "Migrar miniaturas de recursos 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_generate_memories_setting_description": "Crear nuevos recuerdos a partir de recursos", "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_missing_thumbnails_setting_description": "Poner en cola recursos 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_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 rutas", "no_pattern_added": "No se agregÃŗ ningÃēn patrÃŗn", - "note_apply_storage_label_previous_assets": "Nota: Para aplicar la Etiqueta de Almacenamiento a los elementos previamente subidos, ejecuta la", + "note_apply_storage_label_previous_assets": "Nota: Para aplicar la Etiqueta de almacenamiento a los recursos previamente subidos, ejecuta la", "note_cannot_be_changed_later": "NOTA: ÂĄNo se puede cambiar posteriormente!", "notification_email_from_address": "Desde", "notification_email_from_address_description": "DirecciÃŗn de correo electrÃŗnico del remitente, por ejemplo: \"Immich Photo Server \". AsegÃērate de utilizar una direcciÃŗn desde la que puedas enviar correos electrÃŗnicos.", @@ -252,7 +272,7 @@ "oauth_auto_register": "Registro automÃĄtico", "oauth_auto_register_description": "Registre automÃĄticamente nuevos usuarios despuÊs de iniciar sesiÃŗn con OAuth", "oauth_button_text": "Texto del botÃŗn", - "oauth_client_secret_description": "Requerido si PKCE (Prueba de clave para el intercambio de cÃŗdigos) no es compatible con el proveedor OAuth", + "oauth_client_secret_description": "Requerido para clientes confidenciales, o si PKCE (Prueba de clave para el intercambio de cÃŗdigos) no es compatible con clientes pÃēblicos.", "oauth_enable_description": "Iniciar sesiÃŗn con OAuth", "oauth_mobile_redirect_uri": "URI de redireccionamiento mÃŗvil", "oauth_mobile_redirect_uri_override": "Sobreescribir URI de redirecciÃŗn mÃŗvil", @@ -272,7 +292,7 @@ "oauth_timeout_description": "Tiempo de espera de solicitudes en milisegundos", "ocr_job_description": "Usar aprendizaje automÃĄtico para reconocer texto en imÃĄgenes", "password_enable_description": "Iniciar sesiÃŗn con correo electrÃŗnico y contraseÃąa", - "password_settings": "ContraseÃąa de Acceso", + "password_settings": "ContraseÃąa de inicio de sesiÃŗn", "password_settings_description": "Administrar la configuraciÃŗn de inicio de sesiÃŗn con contraseÃąa", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente", "person_cleanup_job": "Limpieza de personas", @@ -291,7 +311,7 @@ "search_jobs": "Buscar trabajosâ€Ļ", "send_welcome_email": "Enviar correo de bienvenida", "server_external_domain_settings": "Dominio externo", - "server_external_domain_settings_description": "Dominio para enlaces pÃēblicos compartidos, incluidos http(s)://", + "server_external_domain_settings_description": "Dominio usado para enlaces externos", "server_public_users": "Usuarios pÃēblicos", "server_public_users_description": "Cuando se aÃąade un usuario a los ÃĄlbumes compartidos, todos los usuarios aparecen en una lista con su nombre y su correo electrÃŗnico. Si deshabilita esta opciÃŗn, solo los administradores podrÃĄn ver la lista de usuarios.", "server_settings": "ConfiguraciÃŗn del servidor", @@ -303,15 +323,15 @@ "sidecar_job": "Metadatos de archivos sidecar", "sidecar_job_description": "Descubrir o sincronizar metadatos sidecar desde el sistema de archivos", "slideshow_duration_description": "NÃēmero de segundos para mostrar cada imagen", - "smart_search_job_description": "Ejecute aprendizaje automÃĄtico en archivos para respaldar la bÃēsqueda inteligente", - "storage_template_date_time_description": "La fecha y hora de creaciÃŗn del elemento serÃĄ usada para la informaciÃŗn sobre la fecha", + "smart_search_job_description": "Ejecute aprendizaje automÃĄtico en recursos para respaldar la bÃēsqueda inteligente", + "storage_template_date_time_description": "La fecha y hora de creaciÃŗn del recurso serÃĄ usada para la informaciÃŗn sobre la fecha", "storage_template_date_time_sample": "Hora de la muestra {date}", "storage_template_enable_description": "Habilitar el motor de plantillas de almacenamiento", "storage_template_hash_verification_enabled": "VerificaciÃŗn de hash habilitada", "storage_template_hash_verification_enabled_description": "Habilita la verificaciÃŗn de hash, no la desactive a menos que estÊ seguro de las implicaciones", "storage_template_migration": "MigraciÃŗn de plantillas de almacenamiento", - "storage_template_migration_description": "Aplicar la {template} actual a los elementos subidos previamente", - "storage_template_migration_info": "La plantilla de almacenamiento convertirÃĄ todas las extensiones a minÃēscula. Los cambios en las plantillas solo se aplican a los elementos nuevos. Para aplicarlos retroactivamente a los elementos subidos previamente ejecute la {job}.", + "storage_template_migration_description": "Aplicar la {template} actual a los recursos subidos previamente", + "storage_template_migration_info": "La plantilla de almacenamiento convertirÃĄ todas las extensiones a minÃēscula. Los cambios en las plantillas solo se aplican a los recursos nuevos. Para aplicarlos retroactivamente a los recursos subidos previamente ejecute la {job}.", "storage_template_migration_job": "Tarea de migraciÃŗn de la plantilla de almacenamiento", "storage_template_more_details": "Para obtener mÃĄs detalles sobre esta funciÃŗn, consulte la Plantilla de almacenamiento y sus implicaciones", "storage_template_onboarding_description_v2": "Al habilitar esta funciÃŗn, los archivos se organizarÃĄn automÃĄticamente segÃēn la plantilla definida por el usuario. Para mÃĄs informaciÃŗn, consulte la documentaciÃŗn.", @@ -330,13 +350,13 @@ "template_email_welcome": "Plantilla de correo electrÃŗnico de bienvenida", "template_settings": "Plantillas de notificaciÃŗn", "template_settings_description": "Gestione plantillas personalizadas para las notificaciones", - "theme_custom_css_settings": "CSS Personalizado", - "theme_custom_css_settings_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseÃąo de Immich.", + "theme_custom_css_settings": "CSS personalizado", + "theme_custom_css_settings_description": "El CSS permite personalizar el diseÃąo de Immich.", "theme_settings": "Ajustes del tema", "theme_settings_description": "Gestionar la personalizaciÃŗn de la interfaz web de Immich", - "thumbnail_generation_job": "Generar Miniaturas", - "thumbnail_generation_job_description": "Genere miniaturas grandes, pequeÃąas y borrosas para cada archivo, así como miniaturas para cada persona", - "transcoding_acceleration_api": "API AceleraciÃŗn", + "thumbnail_generation_job": "Generar miniaturas", + "thumbnail_generation_job_description": "Genere miniaturas grandes, pequeÃąas y borrosas para cada recurso, así como miniaturas para cada persona", + "transcoding_acceleration_api": "API de aceleraciÃŗn", "transcoding_acceleration_api_description": "La API que interactuarÃĄ con su dispositivo para acelerar la transcodificaciÃŗn. Esta configuraciÃŗn es el \"mejor esfuerzo\": recurrirÃĄ a la transcodificaciÃŗn del software en caso de error. VP9 puede funcionar o no dependiendo de su hardware.", "transcoding_acceleration_nvenc": "NVENC (requiere GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (requiere procesador Intel de 7ÂĒ generaciÃŗn o superior)", @@ -360,7 +380,7 @@ "transcoding_disabled_description": "No transcodifique ningÃēn vídeo; puede interrumpir la reproducciÃŗn en algunos clientes", "transcoding_encoding_options": "Opciones de codificaciÃŗn", "transcoding_encoding_options_description": "Establecer cÃŗdecs, resoluciÃŗn, calidad y otras opciones para los vídeos codificados", - "transcoding_hardware_acceleration": "AceleraciÃŗn por Hardware", + "transcoding_hardware_acceleration": "AceleraciÃŗn por hardware", "transcoding_hardware_acceleration_description": "Experimental: transcodificaciÃŗn mÃĄs rÃĄpida, pero puede reducir la calidad con la misma tasa de bits", "transcoding_hardware_decoding": "DecodificaciÃŗn por hardware", "transcoding_hardware_decoding_setting_description": "Permite la aceleraciÃŗn de extremo a extremo en lugar de acelerar Ãēnicamente la codificaciÃŗn. Puede que no funcione en todos los vídeos.", @@ -380,7 +400,7 @@ "transcoding_reference_frames": "Frames de referencia", "transcoding_reference_frames_description": "El nÃēmero de fotogramas a los que hacer referencia al comprimir un fotograma determinado. Los valores mÃĄs altos mejoran la eficiencia de la compresiÃŗn, pero ralentizan la codificaciÃŗn. 0 establece este valor automÃĄticamente.", "transcoding_required_description": "SÃŗlo vídeos que no estÊn en un formato soportado", - "transcoding_settings": "ConfiguraciÃŗn de TranscodificaciÃŗn de Vídeo", + "transcoding_settings": "ConfiguraciÃŗn de transcodificaciÃŗn de vídeo", "transcoding_settings_description": "Administrar quÊ vídeos transcodificar y cÃŗmo procesarlos", "transcoding_target_resolution": "ResoluciÃŗn deseada", "transcoding_target_resolution_description": "Las resoluciones mÃĄs altas pueden conservar mÃĄs detalles, pero la codificaciÃŗn tarda mÃĄs, tienen tamaÃąos de archivo mÃĄs grandes y pueden reducir la capacidad de respuesta de la aplicaciÃŗn.", @@ -394,22 +414,22 @@ "transcoding_transcode_policy_description": "Política sobre cuÃĄndo se debe transcodificar un vídeo. Los vídeos HDR siempre se transcodificarÃĄn (excepto si la transcodificaciÃŗn estÃĄ desactivada).", "transcoding_two_pass_encoding": "CodificaciÃŗn en dos pasadas", "transcoding_two_pass_encoding_setting_description": "Transcodifica en dos pasadas para producir vídeos mejor codificados. Cuando la velocidad de bits mÃĄxima estÃĄ habilitada (es necesaria para que funcione con H.264 y HEVC), este modo utiliza un rango de velocidad de bits basado en la velocidad de bits mÃĄxima e ignora CRF. Para VP9, se puede utilizar CRF si la tasa de bits mÃĄxima estÃĄ deshabilitada.", - "transcoding_video_codec": "CÃŗdecs de Video", + "transcoding_video_codec": "CÃŗdecs de video", "transcoding_video_codec_description": "VP9 tiene alta eficiencia y compatibilidad web, pero lleva mucho tiempo transcodificarlo. HEVC ofrece un rendimiento similar, pero tiene menor compatibilidad web. H.264 es ampliamente compatible y se transcodifica muy rÃĄpido, pero los archivos producidos son mucho mÃĄs grandes. AV1 es el cÃŗdec mÃĄs eficiente, pero no es compatible con los dispositivos mÃĄs antiguos.", "trash_enabled_description": "Habilitar papelera", "trash_number_of_days": "NÃēmero de días", - "trash_number_of_days_description": "NÃēmero de días para mantener los archivos en la papelera antes de eliminarlos permanentemente", + "trash_number_of_days_description": "NÃēmero de días para mantener los recursos en la papelera antes de eliminarlos permanentemente", "trash_settings": "ConfiguraciÃŗn papelera", "trash_settings_description": "Administrar la configuraciÃŗn de la papelera", "unlink_all_oauth_accounts": "Desvincular todas las cuentas de OAuth", "unlink_all_oauth_accounts_description": "Recuerda desvincular todas las cuentas de OAuth antes de migrar a un proveedor nuevo.", "unlink_all_oauth_accounts_prompt": "ÂŋSeguro que deseas desvincular todas las cuentas de OAuth? Se restablecerÃĄ el id. de OAuth de cada usuario. La acciÃŗn no se podrÃĄ deshacer.", "user_cleanup_job": "Limpieza de usuarios", - "user_delete_delay": "La cuenta {user} y los archivos se programarÃĄn para su eliminaciÃŗn permanente en {delay, plural, one {# día} other {# días}}.", + "user_delete_delay": "La cuenta {user} y los recursos se programarÃĄn para su eliminaciÃŗn permanente en {delay, plural, one {# día} other {# días}}.", "user_delete_delay_settings": "Eliminar retardo", - "user_delete_delay_settings_description": "NÃēmero de días despuÊs de la eliminaciÃŗn para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminaciÃŗn de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estÊn listos para su eliminaciÃŗn. Los cambios a esta configuraciÃŗn se evaluarÃĄn en la prÃŗxima ejecuciÃŗn.", - "user_delete_immediately": "La cuenta {user} y los archivos se pondrÃĄn en cola para su eliminaciÃŗn permanente inmediatamente.", - "user_delete_immediately_checkbox": "Poner en cola la eliminaciÃŗn inmediata de usuarios y elementos", + "user_delete_delay_settings_description": "NÃēmero de días despuÊs de la eliminaciÃŗn para eliminar permanentemente la cuenta y los recursos de un usuario. El trabajo de eliminaciÃŗn de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estÊn listos para su eliminaciÃŗn. Los cambios a esta configuraciÃŗn se evaluarÃĄn en la prÃŗxima ejecuciÃŗn.", + "user_delete_immediately": "La cuenta {user} y los recursos se pondrÃĄn en cola para su eliminaciÃŗn permanente inmediatamente.", + "user_delete_immediately_checkbox": "Poner en cola la eliminaciÃŗn inmediata de usuarios y recursos", "user_details": "Detalles del usuario", "user_management": "GestiÃŗn de usuarios", "user_password_has_been_reset": "La contraseÃąa del usuario ha sido restablecida:", @@ -422,7 +442,7 @@ "users_page_description": "PÃĄgina de usuarios administradores", "version_check_enabled_description": "Activar la comprobaciÃŗn de la versiÃŗn", "version_check_implications": "La funciÃŗn de comprobaciÃŗn de versiones depende de la comunicaciÃŗn periÃŗdica con github.com", - "version_check_settings": "Verificar VersiÃŗn", + "version_check_settings": "Verificar versiÃŗn", "version_check_settings_description": "Activar/desactivar la notificaciÃŗn de nueva versiÃŗn", "video_conversion_job": "Transcodificar vídeos", "video_conversion_job_description": "Transcodifique vídeos para una mayor compatibilidad con navegadores y dispositivos" @@ -431,12 +451,15 @@ "admin_password": "ContraseÃąa del administrador", "administration": "AdministraciÃŗn", "advanced": "Avanzada", + "advanced_settings_clear_image_cache": "Borrar cachÊ de imÃĄgenes", + "advanced_settings_clear_image_cache_error": "No se pudo borrar la cachÊ de imÃĄgenes", + "advanced_settings_clear_image_cache_success": "Limpiado con Êxito {size}", "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 desde los archivos locales. 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 recursos 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_subtitle": "Configura encabezados HTTP que Immich incluirÃĄ en cada peticiÃŗn de red", "advanced_settings_proxy_headers_title": "Cabeceras proxy personalizadas [EXPERIMENTAL]", "advanced_settings_readonly_mode_subtitle": "Habilita el modo de solo lectura donde las fotografías sÃŗlo pueden ser vistas, funciones como seleccionar mÃēltiples imÃĄgenes, compartir, transmitir, eliminar son deshabilitadas. Habilita/Deshabilita solo lectura vía el avatar del usuario en la pantalla principal", "advanced_settings_readonly_mode_title": "Modo solo lectura", @@ -462,24 +485,26 @@ "album_info_updated": "InformaciÃŗn del ÃĄlbum actualizada", "album_leave": "ÂŋAbandonar el ÃĄlbum?", "album_leave_confirmation": "ÂŋEstÃĄs seguro de que quieres dejar {album}?", - "album_name": "Nombre del Álbum", - "album_options": "Opciones del Album", + "album_name": "Nombre del ÃĄlbum", + "album_options": "Opciones del ÃĄlbum", "album_remove_user": "ÂŋEliminar usuario?", "album_remove_user_confirmation": "ÂŋEstÃĄs seguro de que quieres eliminar a {user}?", "album_search_not_found": "No se encontraron ÃĄlbumes que coincidan con tu bÃēsqueda", + "album_selected": "Álbum seleccionado", "album_share_no_users": "Parece que has compartido este ÃĄlbum con todos los usuarios o no tienes ningÃēn usuario con quien compartirlo.", "album_summary": "Resumen del ÃĄlbum", - "album_updated": "Album actualizado", - "album_updated_setting_description": "Reciba una notificaciÃŗn por correo electrÃŗnico cuando un ÃĄlbum compartido tenga nuevos archivos", - "album_user_left": "Salida {album}", + "album_updated": "Álbum actualizado", + "album_updated_setting_description": "Reciba una notificaciÃŗn por correo electrÃŗnico cuando un ÃĄlbum compartido tenga nuevos recursos", + "album_upload_assets": "AÃąadir recursos desde tu computadora y aÃąadir a un ÃĄlbum", + "album_user_left": "AbandonÃŗ {album}", "album_user_removed": "Eliminado a {user}", "album_viewer_appbar_delete_confirm": "ÂŋEstÃĄs seguro/a que quieres borrar este ÃĄlbum de tu cuenta?", "album_viewer_appbar_share_err_delete": "No ha podido eliminar el ÃĄlbum", "album_viewer_appbar_share_err_leave": "No se ha podido abandonar el ÃĄlbum", - "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los elementos del ÃĄlbum", + "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los recursos del ÃĄlbum", "album_viewer_appbar_share_err_title": "Error al cambiar el título del ÃĄlbum", "album_viewer_appbar_share_leave": "Abandonar ÃĄlbum", - "album_viewer_appbar_share_to": "Compartir Con", + "album_viewer_appbar_share_to": "Compartir con", "album_viewer_page_share_add_users": "AÃąadir usuarios", "album_with_link_access": "Permitir que cualquiera que tenga el enlace vea las fotos y las personas en este ÃĄlbum.", "albums": "Álbumes", @@ -488,9 +513,11 @@ "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.", "albums_on_device_count": "Álbumes en el dispositivo ({count})", + "albums_selected": "{count, plural, one {# ÃĄlbum seleccionado} other {# ÃĄlbumes seleccionados}}", "all": "Todos", "all_albums": "Todos los ÃĄlbumes", "all_people": "Todas las personas", + "all_photos": "Todas las fotos", "all_videos": "Todos los videos", "allow_dark_mode": "Permitir modo oscuro", "allow_edits": "Permitir ediciÃŗn", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permitir a los usuarios pÃēblicos subir fotos", "allowed": "Permitido", "alt_text_qr_code": "CÃŗdigo QR", + "always_keep": "Mantener siempre", + "always_keep_photos_hint": "El liberador de espacio en disco mantendrÃĄ todas las fotos en este dispositivo.", + "always_keep_videos_hint": "El liberador de espacio en disco mantendrÃĄ todos las vídeos en este dispositivo.", "anti_clockwise": "En sentido antihorario", "api_key": "Clave API", "api_key_description": "Este valor sÃŗlo se mostrarÃĄ una vez. AsegÃērese de copiarlo antes de cerrar la ventana.", @@ -507,74 +537,79 @@ "app_bar_signout_dialog_content": "ÂŋEstÃĄs seguro que quieres cerrar sesiÃŗn?", "app_bar_signout_dialog_ok": "Sí", "app_bar_signout_dialog_title": "Cerrar sesiÃŗn", - "app_download_links": "Enlaces de Descarga de la AplicaciÃŗn", - "app_settings": "Ajustes de la aplicacion", - "app_stores": "Tiendas de Aplicaciones", + "app_download_links": "Enlaces de descarga de la aplicaciÃŗn", + "app_settings": "Ajustes de la aplicaciÃŗn", + "app_stores": "Tiendas de aplicaciones", "app_update_available": "ActualizaciÃŗn de aplicaciÃŗn estÃĄ disponible", "appears_in": "Aparece en", "apply_count": "Aplicar ({count, number})", "archive": "Archivo", "archive_action_prompt": "{count} aÃąadido(s) al archivo", "archive_or_unarchive_photo": "Archivar o restaurar foto", - "archive_page_no_archived_assets": "No se encontraron elementos archivados", + "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({count})", "archive_size": "TamaÃąo de archivo comprimido", - "archive_size_description": "Configure el tamaÃąo del archivo para descargas (en GB)", + "archive_size_description": "Configure el tamaÃąo del archivo para descargas (en GiB)", "archived": "Archivado", "archived_count": "{count, plural, one {# archivado} other {# archivados}}", "are_these_the_same_person": "ÂŋSon la misma persona?", "are_you_sure_to_do_this": "ÂŋEstÃĄs seguro de que quieres hacer esto?", - "asset_action_delete_err_read_only": "No se puede borrar archivo(s) de solo lectura, omitiendo", - "asset_action_share_err_offline": "No se pudo obtener archivo(s) sin conexiÃŗn, omitiendo", + "array_field_not_fully_supported": "Los campos de la matriz requieren ediciÃŗn manual de JSON", + "asset_action_delete_err_read_only": "No se puede borrar recurso(s) de solo lectura, omitiendo", + "asset_action_share_err_offline": "No se pudo obtener recurso(s) sin conexiÃŗn, omitiendo", "asset_added_to_album": "AÃąadido al ÃĄlbum", "asset_adding_to_album": "AÃąadiendo al ÃĄlbumâ€Ļ", - "asset_description_updated": "La descripciÃŗn del elemento ha sido actualizada", - "asset_filename_is_offline": "El archivo {filename} estÃĄ offline", - "asset_has_unassigned_faces": "El archivo no tiene rostros asignados", + "asset_created": "Recurso creado", + "asset_description_updated": "La descripciÃŗn del recurso ha sido actualizada", + "asset_filename_is_offline": "El recurso {filename} estÃĄ desconectado", + "asset_has_unassigned_faces": "El recurso no tiene rostros asignados", "asset_hashing": "Calculando hashâ€Ļ", "asset_list_group_by_sub_title": "Agrupar por", "asset_list_layout_settings_dynamic_layout_title": "DiseÃąo dinÃĄmico", - "asset_list_layout_settings_group_automatically": "Automatico", - "asset_list_layout_settings_group_by": "Agrupar elementos por", + "asset_list_layout_settings_group_automatically": "AutomÃĄtico", + "asset_list_layout_settings_group_by": "Agrupar recursos por", "asset_list_layout_settings_group_by_month_day": "Mes + día", "asset_list_layout_sub_title": "DisposiciÃŗn", "asset_list_settings_subtitle": "Configuraciones del diseÃąo de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", - "asset_offline": "Archivos sin conexiÃŗn", - "asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, pÃŗngase en contacto con su administrador de Immich para obtener ayuda.", - "asset_restored_successfully": "Elementos restaurados exitosamente", + "asset_not_found_on_device_android": "Recurso no encontrado en el dispositivo", + "asset_not_found_on_device_ios": "No se encuentra el recurso en el dispositivo. Si usa iCloud, es posible que no pueda acceder al recurso debido a un archivo defectuoso almacenado en iCloud", + "asset_not_found_on_icloud": "No se ha encontrado el recurso en iCloud. Es posible que no se pueda acceder al recurso debido a un archivo defectuoso almacenado en iCloud", + "asset_offline": "Recurso sin conexiÃŗn", + "asset_offline_description": "Este recurso externo ya no se encuentra en el disco. Por favor, pÃŗngase en contacto con su administrador de Immich para obtener ayuda.", + "asset_restored_successfully": "Recursos restaurados exitosamente", "asset_skipped": "Omitido", "asset_skipped_in_trash": "En la papelera", - "asset_trashed": "Elemento eliminado", - "asset_troubleshoot": "DiagnÃŗstico del elemento", + "asset_trashed": "Recurso eliminado", + "asset_troubleshoot": "DiagnÃŗstico del recurso", "asset_uploaded": "Subido", "asset_uploading": "Subiendoâ€Ļ", - "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos", - "asset_viewer_settings_title": "Visor de Archivos", - "assets": "elementos", - "assets_added_count": "{count, plural, one {# elemento aÃąadido} other {# elementos aÃąadidos}}", - "assets_added_to_album_count": "{count, plural, one {# elemento aÃąadido} other {# elementos aÃąadidos}} al ÃĄlbum", + "asset_viewer_settings_subtitle": "Administra las configuraciones de tu visor de fotos", + "asset_viewer_settings_title": "Visor de recursos", + "assets": "Recursos", + "assets_added_count": "{count, plural, one {# recurso aÃąadido} other {# recursos aÃąadidos}}", + "assets_added_to_album_count": "{count, plural, one {# recurso aÃąadido} other {# recurso aÃąadidos}} al ÃĄlbum", "assets_added_to_albums_count": "{assetTotal, plural, one {# aÃąadido} other {# aÃąadidos}} {albumTotal, plural, one {# al ÃĄlbum} other {# a los ÃĄlbumes}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {El elemento no se puede aÃąadir al ÃĄlbum} other {Los elementos no se pueden aÃąadir al ÃĄlbum}}", - "assets_cannot_be_added_to_albums": "{count, plural, one {El elemento} other {Los elementos}} no se {count, plural, one {puede} other {pueden}} aÃąadir a ninguno de los ÃĄlbumes", - "assets_count": "{count, plural, one {# activo} other {# activos}}", - "assets_deleted_permanently": "{count} elemento(s) eliminado(s) permanentemente", + "assets_cannot_be_added_to_album_count": "{count, plural, one {El recurso no se puede aÃąadir al ÃĄlbum} other {Los recursos no se pueden aÃąadir al ÃĄlbum}}", + "assets_cannot_be_added_to_albums": "{count, plural, one {El recurso} other {Los recursos}} no se {count, plural, one {puede} other {pueden}} aÃąadir a ninguno de los ÃĄlbumes", + "assets_count": "{count, plural, one {# recurso} other {# recursos}}", + "assets_deleted_permanently": "{count} recurso(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 {# 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}}", - "assets_removed_permanently_from_device": "{count} elemento(s) eliminado(s) permanentemente de su dispositivo", - "assets_restore_confirmation": "ÂŋEstÃĄs seguro de que quieres restaurar todos tus activos eliminados? ÂĄNo puede deshacer esta acciÃŗn! Tenga en cuenta que los archivos sin conexiÃŗn no se pueden restaurar de esta manera.", - "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}", - "assets_restored_successfully": "{count} elemento(s) restaurado(s) exitosamente", - "assets_trashed": "{count} elemento(s) eliminado(s)", - "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", + "assets_moved_to_trash_count": "{count, plural, one {# recurso movido} other {# recursos movidos}} a la papelera", + "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# recurso} other {# recursos}}", + "assets_removed_count": "Eliminado {count, plural, one {# recurso} other {# recursos}}", + "assets_removed_permanently_from_device": "{count} recurso(s) eliminado(s) permanentemente de su dispositivo", + "assets_restore_confirmation": "ÂŋEstÃĄs seguro de que quieres restaurar todos tus recursos eliminados? ÂĄNo puede deshacer esta acciÃŗn! Tenga en cuenta que los recursos sin conexiÃŗn no se pueden restaurar de esta manera.", + "assets_restored_count": "Restaurado {count, plural, one {# recurso} other {# recursos}}", + "assets_restored_successfully": "{count} recurso(s) restaurado(s) exitosamente", + "assets_trashed": "{count} recurso(s) eliminado(s)", + "assets_trashed_count": "Borrado {count, plural, one {# recurso} other {# recursos}}", "assets_trashed_from_server": "{count} recurso(s) enviado(s) a la papelera desde el servidor de Immich", - "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del ÃĄlbum", - "assets_were_part_of_albums_count": "{count, plural, one {El elemento ya es} other {Los elementos ya son}} parte de los ÃĄlbumes", - "authorized_devices": "Dispositivos Autorizados", + "assets_were_part_of_album_count": "{count, plural, one {El recurso ya forma} other {Los recursos ya forman}} parte del ÃĄlbum", + "assets_were_part_of_albums_count": "{count, plural, one {El recurso ya es} other {Los recursos ya son}} parte de los ÃĄlbumes", + "authorized_devices": "Dispositivos autorizados", "automatic_endpoint_switching_subtitle": "Conectarse localmente a travÊs de la Wi-Fi designada cuando estÊ disponible y usar conexiones alternativas en otros lugares", "automatic_endpoint_switching_title": "Cambio automÃĄtico de URL", "autoplay_slideshow": "PresentaciÃŗn con reproducciÃŗn automÃĄtica", @@ -584,50 +619,50 @@ "background_location_permission": "Permiso de ubicaciÃŗn en segundo plano", "background_location_permission_content": "Para poder cambiar de red mientras se ejecuta en segundo plano, Immich debe tener *siempre* acceso a la ubicaciÃŗn precisa para que la aplicaciÃŗn pueda leer el nombre de la red Wi-Fi", "background_options": "Opciones de segundo plano", - "backup": "Copia de Seguridad", + "backup": "Copia de seguridad", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", - "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios ÃĄlbumes. De este modo, los ÃĄlbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", + "backup_album_selection_page_assets_scatter": "Los recursos pueden dispersarse en varios ÃĄlbumes. De este modo, los ÃĄlbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", "backup_album_selection_page_select_albums": "Seleccionar ÃĄlbumes", - "backup_album_selection_page_selection_info": "InformaciÃŗn sobre la SelecciÃŗn", - "backup_album_selection_page_total_assets": "Total de elementos Ãēnicos", + "backup_album_selection_page_selection_info": "InformaciÃŗn sobre la selecciÃŗn", + "backup_album_selection_page_total_assets": "Total de recursos Ãēnicos", "backup_albums_sync": "SincronizaciÃŗn de ÃĄlbumes de respaldo", "backup_all": "Todos", - "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentandoâ€Ļ", - "backup_background_service_complete_notification": "Copia de seguridad de activos completada", + "backup_background_service_backup_failed_message": "Error al copiar recursos. Reintentandoâ€Ļ", + "backup_background_service_complete_notification": "Copia de seguridad de recursos completada", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentandoâ€Ļ", "backup_background_service_current_upload_notification": "Subiendo {filename}", - "backup_background_service_default_notification": "Comprobando nuevos elementosâ€Ļ", + "backup_background_service_default_notification": "Comprobando nuevos recursosâ€Ļ", "backup_background_service_error_title": "Error de copia de seguridad", - "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementosâ€Ļ", + "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus recursosâ€Ļ", "backup_background_service_upload_failure_notification": "Error al subir {filename}", "backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualizaciÃŗn en segundo plano de la aplicaciÃŗn en ConfiguraciÃŗn > General > ActualizaciÃŗn en segundo plano para usar la copia de seguridad en segundo plano.", "backup_controller_page_background_app_refresh_disabled_title": "ActualizaciÃŗn en segundo plano desactivada", "backup_controller_page_background_app_refresh_enable_button_text": "Ir a configuraciÃŗn", - "backup_controller_page_background_battery_info_link": "Muestrame cÃŗmo", + "backup_controller_page_background_battery_info_link": "MuÊstrame cÃŗmo", "backup_controller_page_background_battery_info_message": "Para obtener la mejor experiencia de copia de seguridad en segundo plano, desactiva cualquier optimizaciÃŗn de batería que restrinja la actividad en segundo plano para Immich.\n\nDado que esto es específico en cada dispositivo, busca la informaciÃŗn necesaria de el fabricante de tu dispositivo.", - "backup_controller_page_background_battery_info_ok": "Ok", + "backup_controller_page_background_battery_info_ok": "Aceptar", "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos elementos: {duration}", - "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automÃĄticamente cualquier nuevos elementos sin necesidad de abrir la aplicaciÃŗn", + "backup_controller_page_background_delay": "Retrasar la copia de seguridad de los nuevos recursos: {duration}", + "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automÃĄticamente cualquier recurso nuevo sin necesidad de abrir la aplicaciÃŗn", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automÃĄtica estÃĄ desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automÃĄtica estÃĄ activada", "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano", "backup_controller_page_background_turn_on": "Activar el servicio en segundo plano", "backup_controller_page_background_wifi": "Solo en Wi-Fi", - "backup_controller_page_backup": "Copia de Seguridad", + "backup_controller_page_backup": "Copia de seguridad", "backup_controller_page_backup_selected": "Seleccionado: ", "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_created": "Creado el: {date}", - "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automÃĄticamente los nuevos elementos al servidor cuando se abre la aplicaciÃŗn.", + "backup_controller_page_desc_backup": "Active la copia de seguridad para subir automÃĄticamente los nuevos recursos al servidor cuando se abre la aplicaciÃŗn.", "backup_controller_page_excluded": "Excluido: ", "backup_controller_page_failed": "Fallidos ({count})", "backup_controller_page_filename": "Nombre del archivo: {filename} [{size}]", "backup_controller_page_id": "Id.: {id}", - "backup_controller_page_info": "InformaciÃŗn de la Copia de Seguridad", + "backup_controller_page_info": "InformaciÃŗn de la copia de seguridad", "backup_controller_page_none_selected": "Ninguno seleccionado", "backup_controller_page_remainder": "Restante", "backup_controller_page_remainder_sub": "Fotos y videos restantes para hacer una copia de seguridad de la selecciÃŗn", @@ -643,13 +678,13 @@ "backup_controller_page_uploading_file_info": "Subiendo informaciÃŗn del archivo", "backup_err_only_album": "No se puede eliminar el Ãēnico ÃĄlbum", "backup_error_sync_failed": "La sincronizaciÃŗn fallÃŗ. No es posible procesar la copia de seguridad.", - "backup_info_card_assets": "elementos", + "backup_info_card_assets": "recursos", "backup_manual_cancelled": "Cancelado", "backup_manual_in_progress": "Subida ya en progreso. Vuelve a intentarlo mÃĄs tarde", "backup_manual_success": "Éxito", "backup_manual_title": "Estado de la subida", "backup_options": "Opciones de copia de seguridad", - "backup_options_page_title": "Opciones de Copia de Seguridad", + "backup_options_page_title": "Opciones de copia de seguridad", "backup_setting_subtitle": "Administra las configuraciones de respaldo en segundo y primer plano", "backup_settings_subtitle": "Configura las opciones de subida", "backup_upload_details_page_more_details": "Toca para mÃĄs detalles", @@ -664,15 +699,15 @@ "bugs_and_feature_requests": "Errores y solicitudes de funciones", "build": "CompilaciÃŗn", "build_image": "Imagen de compilaciÃŗn", - "bulk_delete_duplicates_confirmation": "ÂŋEstÃĄs seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrÃĄ el activo mÃĄs grande de cada grupo y eliminarÃĄ permanentemente todos los demÃĄs duplicados. ÂĄEsta acciÃŗn no se puede deshacer!", - "bulk_keep_duplicates_confirmation": "ÂŋEstas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverÃĄ todos los grupos duplicados sin borrar nada.", - "bulk_trash_duplicates_confirmation": "ÂŋEstas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrÃĄ el archivo mÃĄs grande de cada grupo y eliminarÃĄ todos los demÃĄs duplicados.", + "bulk_delete_duplicates_confirmation": "ÂŋEstÃĄs seguro de que deseas eliminar de forma masiva {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto mantendrÃĄ el recurso mÃĄs grande de cada grupo y eliminarÃĄ permanentemente todos los demÃĄs duplicados. ÂĄEsta acciÃŗn no se puede deshacer!", + "bulk_keep_duplicates_confirmation": "ÂŋEstas seguro de que desea mantener {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto resolverÃĄ todos los grupos duplicados sin borrar nada.", + "bulk_trash_duplicates_confirmation": "ÂŋEstas seguro de que desea eliminar masivamente {count, plural, one {# recurso duplicado} other {# recursos duplicados}}? Esto mantendrÃĄ el recurso mÃĄs grande de cada grupo y eliminarÃĄ todos los demÃĄs duplicados.", "buy": "Comprar Immich", "cache_settings_clear_cache_button": "Borrar cachÊ", "cache_settings_clear_cache_button_title": "Borra la cachÊ de la aplicaciÃŗn. Esto afectarÃĄ significativamente el rendimiento de la aplicaciÃŗn hasta que se reconstruya la cachÊ.", "cache_settings_duplicated_assets_clear_button": "LIMPIAR", "cache_settings_duplicated_assets_subtitle": "Fotos y vídeos ignorados por la aplicaciÃŗn", - "cache_settings_duplicated_assets_title": "Elementos duplicados ({count})", + "cache_settings_duplicated_assets_title": "Recursos duplicados ({count})", "cache_settings_statistics_album": "Miniaturas de la biblioteca", "cache_settings_statistics_full": "ImÃĄgenes completas", "cache_settings_statistics_shared": "Miniaturas de ÃĄlbumes compartidos", @@ -711,17 +746,31 @@ "change_password_form_password_mismatch": "Las contraseÃąas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseÃąa", "change_pin_code": "Cambiar PIN", + "change_trigger": "Cambiar disparador", + "change_trigger_prompt": "ÂŋSeguro que quieres cambiar el disparador? Esto eliminarÃĄ todas las acciones y filtros existentes.", "change_your_password": "Cambia tu contraseÃąa", "changed_visibility_successfully": "Visibilidad cambiada correctamente", "charging": "Cargando", "charging_requirement_mobile_backup": "La copia de seguridad en segundo plano requiere que el dispositivo se estÊ cargando", - "check_corrupt_asset_backup": "Comprobar copias de seguridad de archivos corruptos", + "check_corrupt_asset_backup": "Comprobar copias de seguridad de recursos corruptos", "check_corrupt_asset_backup_button": "Realizar comprobaciÃŗn", - "check_corrupt_asset_backup_description": "Ejecutar esta comprobaciÃŗn solo por Wi-Fi y una vez que todos los archivos hayan sido respaldados. El procedimiento puede tardar unos minutos.", + "check_corrupt_asset_backup_description": "Ejecutar esta comprobaciÃŗn solo por Wi-Fi y una vez que todos los recursos hayan sido respaldados. El procedimiento puede tardar unos minutos.", "check_logs": "Comprobar Registros", "checksum": "Suma de comprobaciÃŗn", "choose_matching_people_to_merge": "Elija ocurrencias duplicadas de la misma persona para fusionar", "city": "Ciudad", + "cleanup_confirm_description": "Immich encontrÃŗ {count} recursos (creados antes de {date}) respaldados de manera segura en el servidor. ÂŋDesea eliminar las copias locales de este dispositivo?", + "cleanup_confirm_prompt_title": "ÂŋEliminar de este dispositivo?", + "cleanup_deleted_assets": "Moviendo {count} recursos del dispositivo a la papelera", + "cleanup_deleting": "Moviendo a la papelera...", + "cleanup_found_assets": "Se han encontrado {count} recursos respaldados", + "cleanup_found_assets_with_size": "Se encontraron {count} recursos respaldados ({size})", + "cleanup_icloud_shared_albums_excluded": "Los ÃĄlbumes compartidos de iCloud estÃĄn excluidos del escaneo", + "cleanup_no_assets_found": "No se encontraron recursos que coincidan con los criterios anteriores. Liberar espacio solo puede eliminar recursos respaldados en el servidor", + "cleanup_preview_title": "{count} recursos a remover", + "cleanup_step3_description": "Busque recursos respaldados que coincidan con su fecha y conserve la configuraciÃŗn.", + "cleanup_step4_summary": "{count} recursos (creados antes del {date}) para eliminar de tu dispositivo local. Las fotos seguirÃĄn accesibles desde la app de Immich.", + "cleanup_trash_hint": "Para completar la liberaciÃŗn de espacio, abra la aplicaciÃŗn de fotos y vacíe la papelera", "clear": "Limpiar", "clear_all": "Limpiar todo", "clear_all_recent_searches": "Borrar bÃēsquedas recientes", @@ -729,10 +778,12 @@ "clear_message": "Limpiar mensaje", "clear_value": "Limpiar valor", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Introduzca contraseÃąa", + "client_cert_enter_password": "Introduzca la contraseÃąa", "client_cert_import": "Importar", "client_cert_import_success_msg": "El certificado de cliente estÃĄ importado", "client_cert_invalid_msg": "Archivo de certificado no vÃĄlido o contraseÃąa incorrecta", + "client_cert_password_message": "Introduzca la contraseÃąa para este certificado", + "client_cert_password_title": "ContraseÃąa del certificado", "client_cert_remove_msg": "El certificado de cliente se ha eliminado", "client_cert_subtitle": "Solo se admite el formato PKCS12 (.p12, .pfx). La importaciÃŗn/eliminaciÃŗn de certificados solo estÃĄ disponible antes de iniciar sesiÃŗn", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -743,6 +794,11 @@ "color": "Color", "color_theme": "Color del tema", "command": "Comando", + "command_palette_prompt": "Encuentra rÃĄpidamente pÃĄginas, acciones o comandos", + "command_palette_to_close": "para cerrar", + "command_palette_to_navigate": "para entrar", + "command_palette_to_select": "para seleccionar", + "command_palette_to_show_all": "para mostrar todo", "comment_deleted": "Comentario borrado", "comment_options": "Opciones de comentarios", "comments_and_likes": "Comentarios y me gusta", @@ -751,9 +807,9 @@ "completed": "Completado", "confirm": "Confirmar", "confirm_admin_password": "Confirmar contraseÃąa del administrador", - "confirm_delete_face": "ÂŋEstÃĄs seguro que deseas eliminar la cara de {name} del archivo?", + "confirm_delete_face": "ÂŋEstÃĄs seguro que deseas eliminar la cara de {name} del recurso?", "confirm_delete_shared_link": "ÂŋEstÃĄs seguro de que deseas eliminar este enlace compartido?", - "confirm_keep_this_delete_others": "Todos los demÃĄs activos de la pila se eliminarÃĄn excepto este activo. ÂŋEstÃĄ seguro de que quiere continuar?", + "confirm_keep_this_delete_others": "Todos los demÃĄs recursos de la pila se eliminarÃĄn excepto este recurso. ÂŋEstÃĄ seguro de que quiere continuar?", "confirm_new_pin_code": "Confirmar nuevo PIN", "confirm_password": "Confirmar contraseÃąa", "confirm_tag_face": "ÂŋQuieres etiquetar esta cara como {name}?", @@ -779,7 +835,7 @@ "copy_link": "Copiar enlace", "copy_link_to_clipboard": "Copiar enlace al portapapeles", "copy_password": "Copiar contraseÃąa", - "copy_to_clipboard": "Copiar al Portapapeles", + "copy_to_clipboard": "Copiar al portapapeles", "country": "País", "cover": "Portada", "covers": "Portadas", @@ -787,38 +843,47 @@ "create_album": "Crear ÃĄlbum", "create_album_page_untitled": "Sin título", "create_api_key": "Crear clave API", + "create_first_workflow": "Crear el primer flujo de trabajo", "create_library": "Crear biblioteca", "create_link": "Crear enlace", "create_link_to_share": "Crear enlace compartido", "create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)", - "create_new": "Crear nuevo", + "create_new": "CREAR NUEVO", "create_new_person": "Crear nueva persona", - "create_new_person_hint": "Asignar los archivos seleccionados a una nueva persona", + "create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona", "create_new_user": "Crear nuevo usuario", - "create_shared_album_page_share_add_assets": "AÑADIR ELEMENTOS", + "create_shared_album_page_share_add_assets": "AÑADIR RECURSOS", "create_shared_album_page_share_select_photos": "Seleccionar fotos", "create_shared_link": "Crear un enlace compartido", "create_tag": "Crear etiqueta", "create_tag_description": "Crear una nueva etiqueta. Para las etiquetas anidadas, ingresa la ruta completa de la etiqueta, incluidas las barras diagonales.", "create_user": "Crear usuario", + "create_workflow": "Crear flujo de trabajo", "created": "Creado", "created_at": "Creado", "creating_linked_albums": "Creando ÃĄlbumes vinculados...", "crop": "Recortar", + "crop_aspect_ratio_fixed": "Fijado", + "crop_aspect_ratio_free": "Libre", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Objetos", "current_device": "Dispositivo actual", "current_pin_code": "PIN actual", "current_server_address": "DirecciÃŗn actual del servidor", + "custom_date": "Fecha personalizada", "custom_locale": "ConfiguraciÃŗn regional personalizada", "custom_locale_description": "Formatear fechas y nÃēmeros segÃēn el idioma y la regiÃŗn", "custom_url": "URL personalizada", + "cutoff_date_description": "Conserva fotos del Ãēltimoâ€Ļ", + "cutoff_day": "{count, plural, one {día} other {días}}", + "cutoff_year": "{count, plural, one {aÃąo} other {aÃąos}}", "daily_title_text_date": "E dd, MMM", "daily_title_text_date_year": "E dd de MMM, yyyy", "dark": "Oscuro", "dark_theme": "Alternar tema oscuro", "date": "Fecha", "date_after": "Fecha posterior", - "date_and_time": "Fecha y Hora", + "date_and_time": "Fecha y hora", "date_before": "Fecha anterior", "date_format": "E d, LLL y â€ĸ h:mm a", "date_of_birth_saved": "Guardada con Êxito la fecha de nacimiento", @@ -833,7 +898,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_confirmation_message": "ÂŋEstÃĄ seguro que desea eliminar este archivo? Esta acciÃŗn lo moverÃĄ a la papelera del servidor y le preguntarÃĄ si desea eliminarlo localmente", + "delete_action_confirmation_message": "ÂŋEstÃĄ seguro que desea eliminar este recurso? Esta acciÃŗn lo moverÃĄ a la papelera del servidor y le preguntarÃĄ si desea eliminarlo localmente", "delete_action_prompt": "{count} eliminados", "delete_album": "Eliminar ÃĄlbum", "delete_api_key_prompt": "ÂŋEstÃĄ seguro de que desea eliminar esta clave API?", @@ -842,7 +907,7 @@ "delete_dialog_alert_local_non_backed_up": "Algunos de los elementos no tienen copia de seguridad en Immich y serÃĄn borrados permanentemente de tu dispositivo", "delete_dialog_alert_remote": "Estas imÃĄgenes van a ser borradas permanentemente del servidor de Immich", "delete_dialog_ok_force": "Borrar de todos modos", - "delete_dialog_title": "Eliminar Permanentemente", + "delete_dialog_title": "Eliminar permanentemente", "delete_duplicates_confirmation": "ÂŋEstÃĄ seguro de que desea eliminar permanentemente estos duplicados?", "delete_face": "Eliminar cara", "delete_key": "Eliminar clave", @@ -860,13 +925,14 @@ "delete_tag_confirmation_prompt": "ÂŋEstÃĄs seguro de que deseas eliminar la etiqueta {tagName} ?", "delete_user": "Eliminar usuario", "deleted_shared_link": "Enlace compartido eliminado", - "deletes_missing_assets": "Elimina archivos que faltan en el disco duro", + "deletes_missing_assets": "Elimina recursos que faltan en el disco duro", "description": "DescripciÃŗn", "description_input_hint_text": "AÃąadir descripciÃŗn...", "description_input_submit_error": "Error al actualizar la descripciÃŗn, comprueba el registro para obtener mÃĄs detalles", - "deselect_all": "Deseleccionar Todo", + "deselect_all": "Deseleccionar todo", "details": "Detalles", "direction": "DirecciÃŗn", + "disable": "Desactivar", "disabled": "Deshabilitado", "disallow_edits": "Bloquear ediciÃŗn", "discord": "Discord", @@ -877,12 +943,12 @@ "display_options": "Opciones de pantalla", "display_order": "Orden de visualizaciÃŗn", "display_original_photos": "Mostrar fotos originales", - "display_original_photos_setting_description": "Preferir mostrar la foto original al ver un archivo en lugar de miniaturas cuando el archivo original es compatible con la web. Esto puede resultar en velocidades de visualizaciÃŗn de fotografías mÃĄs lentas.", + "display_original_photos_setting_description": "Preferir mostrar la foto original al ver un recurso en lugar de miniaturas cuando el recurso original es compatible con la web. Esto puede resultar en velocidades de visualizaciÃŗn de fotografías mÃĄs lentas.", "do_not_show_again": "No volver a mostrar este mensaje otra vez", "documentation": "DocumentaciÃŗn", "done": "Hecho", "download": "Descargar", - "download_action_prompt": "Descargando {count} archivos", + "download_action_prompt": "Descargando {count} recursos", "download_canceled": "Descarga cancelada", "download_complete": "Descarga completada", "download_enqueue": "Descarga en cola", @@ -892,22 +958,24 @@ "download_include_embedded_motion_videos": "Vídeos incrustados", "download_include_embedded_motion_videos_description": "Incluir vídeos incrustados en fotografías en movimiento como un archivo separado", "download_notfound": "Descarga no encontrada", + "download_original": "Descargar original", "download_paused": "Descarga en pausa", "download_settings": "Descargar", - "download_settings_description": "Administrar configuraciones relacionadas con la descarga de archivos", + "download_settings_description": "Administrar configuraciones relacionadas con la descarga de recursos", "download_started": "Descarga iniciada", - "download_sucess": "Descarga Exitosa", + "download_sucess": "Descarga exitosa", "download_sucess_android": "Los archivos se han descargado en DCIM/Immich", "download_waiting_to_retry": "Esperando para reintentar", "downloading": "Descargando", - "downloading_asset_filename": "Descargando archivo {filename}", + "downloading_asset_filename": "Descargando recurso {filename}", + "downloading_from_icloud": "Descargando desde iCloud", "downloading_media": "Descargando medios", "drop_files_to_upload": "Suelta los archivos en cualquier lugar para subirlos", "duplicates": "Duplicados", "duplicates_description": "Resuelva cada grupo indicando, en cada caso, cuales estÃĄn duplicados", "duration": "DuraciÃŗn", "edit": "Editar", - "edit_album": "Editar album", + "edit_album": "Editar ÃĄlbum", "edit_avatar": "Editar avatar", "edit_birthday": "Editar cumpleaÃąos", "edit_date": "Editar fecha", @@ -929,18 +997,29 @@ "edit_tag": "Editar etiqueta", "edit_title": "Editar Titulo", "edit_user": "Editar usuario", + "edit_workflow": "Editar flujo de trabajo", "editor": "Editor", "editor_close_without_save_prompt": "No se guardarÃĄn los cambios", "editor_close_without_save_title": "ÂŋCerrar el editor?", - "editor_crop_tool_h2_aspect_ratios": "Proporciones del aspecto", - "editor_crop_tool_h2_rotation": "RotaciÃŗn", - "email": "Correo", - "email_notifications": "Notificaciones por correo electrÃŗnico", + "editor_confirm_reset_all_changes": "ÂŋSeguro que quieres restablecer los cambios?", + "editor_discard_edits_confirm": "Descartar ediciones", + "editor_discard_edits_prompt": "Tiene ediciones sin guardar. ÂŋEstÃĄ seguro de que quieres descartarlas?", + "editor_discard_edits_title": "ÂŋDescartar ediciones?", + "editor_edits_applied_error": "Fallo al aplicar las ediciones", + "editor_edits_applied_success": "EdiciÃŗn aplicada con Êxito", + "editor_flip_horizontal": "Girar horizontalmente", + "editor_flip_vertical": "Girar verticalmente", + "editor_orientation": "OrientaciÃŗn", + "editor_reset_all_changes": "Restablecer cambios", + "editor_rotate_left": "Rotar 90Âē sentido antihorario", + "editor_rotate_right": "Rotar 90Âē sentido horario", + "email": "Correo electrÃŗnico", + "email_notifications": "Notificaciones por correo", "empty_folder": "Esta carpeta estÃĄ vacía", "empty_trash": "Vaciar papelera", - "empty_trash_confirmation": "ÂŋEstÃĄs seguro de que quieres vaciar la papelera? Esto eliminarÃĄ permanentemente todos los archivos de la basura de Immich.\nÂĄNo puedes deshacer esta acciÃŗn!", + "empty_trash_confirmation": "ÂŋEstÃĄs seguro de que quieres vaciar la papelera? Esto eliminarÃĄ permanentemente todos los recursos de la papelera de Immich.\nÂĄNo podrÃĄs deshacer esta acciÃŗn!", "enable": "Habilitar", - "enable_backup": "Habilitar Copia de Seguridad", + "enable_backup": "Habilitar copia de seguridad", "enable_biometric_auth_description": "Introduce tu cÃŗdigo PIN para habilitar la autentificaciÃŗn biomÊtrica", "enabled": "Habilitado", "end_date": "Fecha final", @@ -950,45 +1029,48 @@ "enter_your_pin_code_subtitle": "Introduce tu cÃŗdigo PIN para acceder a la carpeta protegida", "error": "Error", "error_change_sort_album": "No se pudo cambiar el orden de visualizaciÃŗn del ÃĄlbum", - "error_delete_face": "Error al eliminar la cara del archivo", + "error_delete_face": "Error al eliminar la cara del recurso", "error_getting_places": "Error obteniendo lugares", + "error_loading_albums": "Error al cargar ÃĄlbumes", "error_loading_image": "Error al cargar la imagen", "error_loading_partners": "Error al cargar miembros: {error}", + "error_retrieving_asset_information": "Error al recuperar la informaciÃŗn del recurso", "error_saving_image": "Error: {error}", "error_tag_face_bounding_box": "Error al etiquetar la cara: no se pueden obtener las coordenadas del marco", "error_title": "Error: algo saliÃŗ mal", + "error_while_navigating": "Error al navegar al recurso", "errors": { - "cannot_navigate_next_asset": "No puedes navegar al siguiente archivo", - "cannot_navigate_previous_asset": "No puedes navegar al archivo anterior", + "cannot_navigate_next_asset": "No puedes navegar al siguiente recurso", + "cannot_navigate_previous_asset": "No puedes navegar al recurso anterior", "cant_apply_changes": "No se pueden aplicar los cambios", "cant_change_activity": "No se puede realizar la actividad {enabled, select, true {disable} other {enable}}", - "cant_change_asset_favorite": "No se puede cambiar favorito para este archivo", - "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# elemento} other {# elementos}}", + "cant_change_asset_favorite": "No se puede cambiar favorito para este recurso", + "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos {count, plural, one {# del recurso} other {# de los recursos}}", "cant_get_faces": "No se encuentran caras", "cant_get_number_of_comments": "No se puede obtener la cantidad de comentarios", "cant_search_people": "No se puede buscar a personas", "cant_search_places": "No se pueden buscar lugares", - "error_adding_assets_to_album": "Error al aÃąadir los elementos al ÃĄlbum", + "error_adding_assets_to_album": "Error al aÃąadir los recursos al ÃĄlbum", "error_adding_users_to_album": "Error al aÃąadir los usuarios al ÃĄlbum", "error_deleting_shared_user": "Error al eliminar usuario compartido", "error_downloading": "Error al descargar {filename}", "error_hiding_buy_button": "Error al ocultar el botÃŗn de compra", - "error_removing_assets_from_album": "Error al eliminar archivos del ÃĄlbum; consulte la consola para obtener mÃĄs detalles", - "error_selecting_all_assets": "Error al seleccionar todos los archivos", + "error_removing_assets_from_album": "Error al eliminar recursos del ÃĄlbum; consulte la consola para obtener mÃĄs detalles", + "error_selecting_all_assets": "Error al seleccionar todos los recursos", "exclusion_pattern_already_exists": "Este patrÃŗn de exclusiÃŗn ya existe.", "failed_to_create_album": "Error al crear el ÃĄlbum", "failed_to_create_shared_link": "Error al crear el enlace compartido", "failed_to_edit_shared_link": "Error al editar el enlace compartido", - "failed_to_get_people": "Error al obtener personas", - "failed_to_keep_this_delete_others": "No se pudo conservar este activo y eliminar los demÃĄs", - "failed_to_load_asset": "Error al cargar el elemento", - "failed_to_load_assets": "Error al cargar los elementos", + "failed_to_get_people": "No se logrÃŗ conseguir gente", + "failed_to_keep_this_delete_others": "No se pudo conservar este recurso y eliminar los demÃĄs", + "failed_to_load_asset": "Error al cargar el recurso", + "failed_to_load_assets": "Error al cargar los recursos", "failed_to_load_notifications": "Error al cargar las notificaciones", "failed_to_load_people": "Error al cargar a los usuarios", "failed_to_remove_product_key": "No se pudo eliminar la clave del producto", "failed_to_reset_pin_code": "No se pudo restablecer el cÃŗdigo PIN", - "failed_to_stack_assets": "No se pudieron agrupar los archivos", - "failed_to_unstack_assets": "Error al desagrupar los archivos", + "failed_to_stack_assets": "No se pudieron agrupar los recursos", + "failed_to_unstack_assets": "Error al desagrupar los recursos", "failed_to_update_notification_status": "Error al actualizar el estado de la notificaciÃŗn", "incorrect_email_or_password": "ContraseÃąa o email incorrecto", "library_folder_already_exists": "Esta ruta de importaciÃŗn ya existe.", @@ -997,33 +1079,35 @@ "quota_higher_than_disk_size": "Se ha establecido una cuota superior al tamaÃąo del disco", "something_went_wrong": "Algo saliÃŗ mal", "unable_to_add_album_users": "No se pueden aÃąadir usuarios al ÃĄlbum", - "unable_to_add_assets_to_shared_link": "No se pueden aÃąadir archivos al enlace compartido", + "unable_to_add_assets_to_shared_link": "No se pueden aÃąadir recursos al enlace compartido", "unable_to_add_comment": "No se puede aÃąadir comentario", "unable_to_add_exclusion_pattern": "No se puede aÃąadir el patrÃŗn de exclusiÃŗn", "unable_to_add_partners": "No se pueden aÃąadir miembros", - "unable_to_add_remove_archive": "No se puede archivar {archived, select, true {remove asset from} other {add asset to}}", - "unable_to_add_remove_favorites": "No se pudo {favorite, select, true {aÃąadir el elemento a} other {eliminar el elemento de}} los favoritos", + "unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {aÃąadir el recurso al}} archivo", + "unable_to_add_remove_favorites": "No se pudo {favorite, select, true {aÃąadir el recuso a} other {eliminar el recurso de}} los favoritos", "unable_to_archive_unarchive": "No se pudo {archived, select, true {agregar el elemento al} other {quitar el elemento del}} archivo", "unable_to_change_album_user_role": "No se puede cambiar la funciÃŗn del usuario del ÃĄlbum", "unable_to_change_date": "No se puede cambiar la fecha", "unable_to_change_description": "Imposible cambiar la descripciÃŗn", - "unable_to_change_favorite": "Imposible cambiar el archivo favorito", + "unable_to_change_favorite": "Imposible cambiar el recurso favorito", "unable_to_change_location": "No se puede cambiar de ubicaciÃŗn", "unable_to_change_password": "No se puede cambiar la contraseÃąa", "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# persona} other {# personas}}", "unable_to_complete_oauth_login": "No se puede completar el inicio de sesiÃŗn de OAuth", "unable_to_connect": "No puede conectarse", "unable_to_copy_to_clipboard": "No se puede copiar al portapapeles, asegÃērese de acceder a la pÃĄgina a travÊs de https", + "unable_to_create": "No se puede crear el flujo de trabajo", "unable_to_create_admin_account": "No se puede crear una cuenta de administrador", "unable_to_create_api_key": "No se puede crear una nueva clave API", "unable_to_create_library": "No se puede crear la biblioteca", "unable_to_create_user": "No se puede crear usuario", "unable_to_delete_album": "No se puede eliminar el ÃĄlbum", - "unable_to_delete_asset": "No se puede eliminar el archivo", - "unable_to_delete_assets": "Error al eliminar archivos", + "unable_to_delete_asset": "No se puede eliminar el recurso", + "unable_to_delete_assets": "Error al eliminar recursos", "unable_to_delete_exclusion_pattern": "No se puede eliminar el patrÃŗn de exclusiÃŗn", "unable_to_delete_shared_link": "No se puede eliminar el enlace compartido", "unable_to_delete_user": "No se puede eliminar el usuario", + "unable_to_delete_workflow": "No se puede eliminar el flujo de trabajo", "unable_to_download_files": "No se pueden descargar archivos", "unable_to_edit_exclusion_pattern": "No se puede editar el patrÃŗn de exclusiÃŗn", "unable_to_empty_trash": "No se puede vaciar la papelera", @@ -1038,19 +1122,19 @@ "unable_to_log_out_device": "No se puede cerrar la sesiÃŗn en el dispositivo", "unable_to_login_with_oauth": "No se puede iniciar sesiÃŗn con OAuth", "unable_to_play_video": "No se puede reproducir el vídeo", - "unable_to_reassign_assets_existing_person": "No se pueden reasignar a {name, select, null {an existing person} other {{name}}}", - "unable_to_reassign_assets_new_person": "No se pueden reasignar archivos a una nueva persona", + "unable_to_reassign_assets_existing_person": "No se pueden reasignar los recursos a {name, select, null {una persona existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "No se pueden reasignar recursos a una nueva persona", "unable_to_refresh_user": "No se puede actualizar el usuario", "unable_to_remove_album_users": "No se pueden eliminar usuarios del ÃĄlbum", "unable_to_remove_api_key": "No se puede eliminar la clave API", - "unable_to_remove_assets_from_shared_link": "No se pueden eliminar archivos desde el enlace compartido", + "unable_to_remove_assets_from_shared_link": "No se pueden eliminar recursos desde el enlace compartido", "unable_to_remove_library": "No se puede eliminar la biblioteca", "unable_to_remove_partner": "No se puede eliminar el invitado", "unable_to_remove_reaction": "No se puede eliminar la reacciÃŗn", "unable_to_reset_password": "No se puede restablecer la contraseÃąa", "unable_to_reset_pin_code": "No se ha podido restablecer el PIN", "unable_to_resolve_duplicate": "No se resolver duplicado", - "unable_to_restore_assets": "No se pueden restaurar los archivos", + "unable_to_restore_assets": "No se pueden restaurar los recursos", "unable_to_restore_trash": "No se puede restaurar la papelera", "unable_to_restore_user": "No se puede restaurar el usuario", "unable_to_save_album": "No se puede guardar el ÃĄlbum", @@ -1063,8 +1147,9 @@ "unable_to_scan_library": "No se puede escanear la biblioteca", "unable_to_set_feature_photo": "No se puede configurar la foto seleccionada", "unable_to_set_profile_picture": "No se puede configurar la imagen de perfil", + "unable_to_set_rating": "No se ha podido establecer la calificaciÃŗn", "unable_to_submit_job": "No se puede enviar el trabajo", - "unable_to_trash_asset": "No se puede eliminar el archivo", + "unable_to_trash_asset": "No se puede mover a la papelera el recurso", "unable_to_unlink_account": "No se puede desvincular la cuenta", "unable_to_unlink_motion_video": "No se puede desvincular el vídeo en movimiento", "unable_to_update_album_cover": "No se puede actualizar la portada del ÃĄlbum", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "No se puede actualizar la configuraciÃŗn", "unable_to_update_timeline_display_status": "No se puede actualizar el estado de visualizaciÃŗn de la línea de tiempo", "unable_to_update_user": "No se puede actualizar el usuario", + "unable_to_update_workflow": "No se puede actualizar el flujo de trabajo", "unable_to_upload_file": "Error al subir el archivo" }, + "errors_text": "Errores", "exclusion_pattern": "PatrÃŗn de exclusiÃŗn", "exif": "EXIF", "exif_bottom_sheet_description": "AÃąadir descripciÃŗnâ€Ļ", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONAS", "exif_bottom_sheet_person_add_person": "AÃąadir nombre", "exit_slideshow": "Salir de la presentaciÃŗn", + "expand": "Expandir", "expand_all": "Expandir todo", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotogrÃĄfica experimental", @@ -1098,8 +1186,8 @@ "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar a JSON", - "export_database": "Exportar Base de Datos", - "export_database_description": "Exportar la Base de Datos SQLite", + "export_database": "Exportar base de datos", + "export_database_description": "Exportar la base de datos SQLite", "extension": "ExtensiÃŗn", "external": "Externo", "external_libraries": "Bibliotecas externas", @@ -1109,25 +1197,28 @@ "failed": "Fallido", "failed_count": "Fallido: {count}", "failed_to_authenticate": "Fallo al autentificar", - "failed_to_load_assets": "Error al cargar los activos", + "failed_to_load_assets": "Error al cargar los recursos", "failed_to_load_folder": "No se pudo cargar la carpeta", "favorite": "Favorito", "favorite_action_prompt": "{count} aÃąadido(s) a Favoritos", "favorite_or_unfavorite_photo": "Foto favorita o no favorita", "favorites": "Favoritos", - "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos", + "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "feature_photo_updated": "Foto destacada actualizada", "features": "Características", - "features_in_development": "Funciones en Desarrollo", - "features_setting_description": "Administrar las funciones de la aplicaciÃŗn", - "file_name": "Nombre de archivo", + "features_in_development": "Características en desarrollo", + "features_setting_description": "Administrar las características de la aplicaciÃŗn", "file_name_or_extension": "Nombre del archivo o extensiÃŗn", + "file_name_text": "Nombre del archivo", + "file_name_with_value": "Nombre del archivo: {file_name}", "file_size": "TamaÃąo del archivo", "filename": "Nombre del archivo", "filetype": "Tipo de archivo", - "filter": "Filtros", + "filter": "Filtro", + "filter_description": "Condiciones para filtrar los recursos objetivo", "filter_people": "Filtrar personas", "filter_places": "Filtrar lugares", + "filters": "Filtros", "find_them_fast": "EncuÊntrelos rÃĄpidamente por nombre con la bÃēsqueda", "first": "Primero", "fix_incorrect_match": "Corregir coincidencia incorrecta", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Explorar la vista de carpetas para las fotos y los videos en el sistema de archivos", "forgot_pin_code_question": "ÂŋOlvidaste tu cÃŗdigo PIN?", "forward": "Avanzar", + "free_up_space": "Liberar espacio", + "free_up_space_description": "Elimina tus fotos y videos de tu dispositivo para liberar espacio. Los respaldos en el servidor se mantendrÃĄn seguros.", + "free_up_space_settings_subtitle": "Liberar espacio del dispositivo", "full_path": "Ruta completa: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidad carga recursos externos desde Google para poder funcionar.", "general": "General", - "geolocation_instruction_location": "Da click en un asset con coordenadas GPS para usar su ubicacion, o selecciona una ubicacion directamente en el mapa", + "geolocation_instruction_location": "Clica en un recurso con coordenadas GPS para usar su ubicaciÃŗn, o selecciona una ubicaciÃŗn directamente en el mapa", "get_help": "Solicitar ayuda", + "get_people_error": "Error al obtener gente", "get_wifiname_error": "No se pudo obtener el nombre de la red Wi-Fi. AsegÃērate de haber concedido los permisos necesarios y de estar conectado a una red Wi-Fi", "getting_started": "Comenzamos", "go_back": "Volver atrÃĄs", @@ -1158,10 +1253,10 @@ "group_places_by": "Agrupar lugares por...", "group_year": "Agrupar por aÃąo", "haptic_feedback_switch": "Activar respuesta hÃĄptica", - "haptic_feedback_title": "Respuesta HÃĄptica", + "haptic_feedback_title": "Respuesta hÃĄptica", "has_quota": "Cuota asignada", - "hash_asset": "Generar hash del archivo", - "hashed_assets": "Archivos con hash generado", + "hash_asset": "Generar hash del recurso", + "hashed_assets": "Recursos con hash generado", "hashing": "Generando hash", "header_settings_add_header_tip": "AÃąadir cabecera", "header_settings_field_validator_msg": "El valor no puede estar vacío", @@ -1175,24 +1270,25 @@ "hide_named_person": "Ocultar persona {name}", "hide_password": "Ocultar contraseÃąa", "hide_person": "Ocultar persona", + "hide_schema": "Ocultar esquema", "hide_text_recognition": "Ocultar reconocimiento de texto", "hide_unnamed_people": "Ocultar personas anÃŗnimas", - "home_page_add_to_album_conflicts": "{added} elementos aÃąadidos al ÃĄlbum {album}.{failed} elementos ya existen en el ÃĄlbum.", - "home_page_add_to_album_err_local": "AÃēn no se pueden aÃąadir elementos locales a ÃĄlbumes, omitiendo", - "home_page_add_to_album_success": "Se aÃąadieron {added} elementos al ÃĄlbum {album}.", - "home_page_album_err_partner": "AÃēn no se pueden aÃąadir elementos de un compaÃąero a un ÃĄlbum , omitiendo", - "home_page_archive_err_local": "Los elementos locales no pueden ser archivados, omitiendo", - "home_page_archive_err_partner": "No se pueden archivar los elementos de un compaÃąero, omitiendo", + "home_page_add_to_album_conflicts": "{added} recursos aÃąadidos al ÃĄlbum {album}.{failed} recursos ya existen en el ÃĄlbum.", + "home_page_add_to_album_err_local": "AÃēn no se pueden aÃąadir recursos locales a ÃĄlbumes, omitiendo", + "home_page_add_to_album_success": "Se aÃąadieron {added} recursos al ÃĄlbum {album}.", + "home_page_album_err_partner": "AÃēn no se pueden aÃąadir recursos de un compaÃąero a un ÃĄlbum, omitiendo", + "home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", + "home_page_archive_err_partner": "No se pueden archivar los recursos de un compaÃąero, omitiendo", "home_page_building_timeline": "Construyendo la línea de tiempo", - "home_page_delete_err_partner": "No se pueden eliminar los elementos de un compaÃąero, omitiendo", - "home_page_delete_remote_err_local": "Elementos locales en la selecciÃŗn de eliminaciÃŗn remota, omitiendo", - "home_page_favorite_err_local": "AÃēn no se pueden marcar como favoritos los elementos locales, omitiendo", - "home_page_favorite_err_partner": "AÃēn no se pueden marcar los como favoritos los elementos de un compaÃąero, omitiendo", + "home_page_delete_err_partner": "No se pueden eliminar los recursos de un compaÃąero, omitiendo", + "home_page_delete_remote_err_local": "Recursos locales en la selecciÃŗn de eliminaciÃŗn remota, omitiendo", + "home_page_favorite_err_local": "AÃēn no se pueden marcar como favoritos los recursos locales, omitiendo", + "home_page_favorite_err_partner": "AÃēn no se pueden marcar como favoritos los recursos de un compaÃąero, omitiendo", "home_page_first_time_notice": "Si es la primera vez que usas la aplicaciÃŗn, asegÃērate de elegir un ÃĄlbum como copia de seguridad para que la línea de tiempo pueda mostrar fotos y vídeos en Êl", - "home_page_locked_error_local": "No se pueden mover elementos locales a una carpeta protegida, omitiendo", - "home_page_locked_error_partner": "No se pueden mover los elementos de un compaÃąero a una carpeta protegida; omitiendo", - "home_page_share_err_local": "No se pueden compartir elementos locales a travÊs de un enlace, omitiendo", - "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultÃĄneamente, omitiendo", + "home_page_locked_error_local": "No se pueden mover recursos locales a una carpeta protegida, omitiendo", + "home_page_locked_error_partner": "No se pueden mover los recursos de un compaÃąero a una carpeta protegida, omitiendo", + "home_page_share_err_local": "No se pueden compartir recursos locales a travÊs de un enlace, omitiendo", + "home_page_upload_err_limit": "Solo se pueden subir 30 recursos simultÃĄneamente, omitiendo", "host": "Host", "hour": "Hora", "hours": "Horas", @@ -1212,11 +1308,11 @@ "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} mÃĄs el {date}", "image_saved_successfully": "ImÃĄgenes guardas", - "image_viewer_page_state_provider_download_started": "Descarga Iniciada", + "image_viewer_page_state_provider_download_started": "Descarga iniciada", "image_viewer_page_state_provider_download_success": "Descarga exitosa", "image_viewer_page_state_provider_share_error": "Error al compartir", "immich_logo": "Logo de Immich", - "immich_web_interface": "Interfaz Web de Immich", + "immich_web_interface": "Interfaz web de Immich", "import_from_json": "Importar desde JSON", "import_path": "Importar ruta", "in_albums": "En {count, plural, one {# ÃĄlbum} other {# ÃĄlbumes}}", @@ -1225,7 +1321,7 @@ "in_year_selector": "En", "include_archived": "Incluir archivados", "include_shared_albums": "Incluir ÃĄlbumes compartidos", - "include_shared_partner_assets": "Incluir elementos compartidos por compaÃąeros", + "include_shared_partner_assets": "Incluir recursos compartidos por compaÃąeros", "individual_share": "Compartir individualmente", "individual_shares": "Acciones individuales", "info": "InformaciÃŗn", @@ -1237,7 +1333,7 @@ }, "invalid_date": "Fecha incorrecta", "invalid_date_format": "Formato de fecha incorrecto", - "invite_people": "Invitar a Personas", + "invite_people": "Invitar a personas", "invite_to_album": "Invitar al ÃĄlbum", "ios_debug_info_fetch_ran_at": "Busca ejecuciÃŗn en {dateTime}", "ios_debug_info_last_sync_at": "Última sincronizaciÃŗn en {dateTime}", @@ -1247,17 +1343,26 @@ "ios_debug_info_processing_ran_at": "El procesamiento se ejecutÃŗ el {dateTime}", "items_count": "{count, plural, one {# elemento} other {# elementos}}", "jobs": "Tareas", + "json_editor": "Editor JSON", + "json_error": "Error JSON", "keep": "Conservar", - "keep_all": "Conservar Todo", + "keep_albums": "Conservar ÃĄlbumes", + "keep_albums_count": "Mantener {count} {count, plural, one {ÃĄlbum} other {ÃĄlbumes}}", + "keep_all": "Conservar todo", + "keep_description": "Elige quÊ permanece en tu dispositivo al liberar espacio.", + "keep_favorites": "Mantener favoritos", + "keep_on_device": "Mantener en el dispositivo", + "keep_on_device_hint": "Seleccionar elementos para conservar en este dispositivo", "keep_this_delete_others": "Mantener este, eliminar los otros", - "kept_this_deleted_others": "Mantuvo este activo y eliminÃŗ {count, plural, one {# activo} other {# activos}}", + "keeping": "Manteniendo: {items}", + "kept_this_deleted_others": "Mantuvo este recurso y eliminÃŗ {count, plural, one {# recurso} other {# recursos}}", "keyboard_shortcuts": "Atajos de teclado", "language": "Idioma", "language_no_results_subtitle": "Intente ajustar el tÊrmino de bÃēsqueda", "language_no_results_title": "No se han encontrado idiomas", "language_search_hint": "Buscar idiomas...", "language_setting_description": "Selecciona tu idioma preferido", - "large_files": "Archivos Grandes", + "large_files": "Archivos grandes", "last": "Último", "last_months": "{count, plural, one {Último mes} other {Últimos # meses}}", "last_seen": "Ultima vez visto", @@ -1274,7 +1379,7 @@ "library_options": "Opciones de biblioteca", "library_page_device_albums": "Álbumes en el dispositivo", "library_page_new_album": "Nuevo ÃĄlbum", - "library_page_sort_asset_count": "NÃēmero de elementos", + "library_page_sort_asset_count": "NÃēmero de recursos", "library_page_sort_created": "Creado mÃĄs recientemente", "library_page_sort_last_modified": "Última modificaciÃŗn", "library_page_sort_title": "Título del ÃĄlbum", @@ -1290,9 +1395,9 @@ "loading_search_results_failed": "Error al cargar los resultados de la bÃēsqueda", "local": "Local", "local_asset_cast_failed": "No es posible transmitir un recurso que no estÃĄ subido al servidor", - "local_assets": "Archivos Locales", + "local_assets": "Recursos locales", "local_id": "ID local", - "local_media_summary": "Resumen de Medios Locales", + "local_media_summary": "Resumen de medios locales", "local_network": "Red local", "local_network_sheet_info": "La aplicaciÃŗn se conectarÃĄ al servidor a travÊs de esta URL cuando utilice la red Wi-Fi especificada", "location": "UbicaciÃŗn", @@ -1329,7 +1434,7 @@ "login_form_handshake_exception": "Hubo una excepciÃŗn de handshake con el servidor. Activa la compatibilidad con certificados autofirmados en la configuraciÃŗn si estÃĄs utilizando un certificado autofirmado.", "login_form_password_hint": "contraseÃąa", "login_form_save_login": "Mantener la sesiÃŗn iniciada", - "login_form_server_empty": "Agrega la URL del servidor.", + "login_form_server_empty": "Introduce la URL del servidor.", "login_form_server_error": "No se pudo conectar al servidor.", "login_has_been_disabled": "El inicio de sesiÃŗn ha sido deshabilitado.", "login_password_changed_error": "Hubo un error actualizando la contraseÃąa", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Habilite la reproducciÃŗn automÃĄtica de un video en el visor de detalles.", "main_branch_warning": "EstÃĄ utilizando una versiÃŗn de desarrollo; ÂĄle recomendamos encarecidamente que utilice una versiÃŗn de lanzamiento!", "main_menu": "MenÃē principal", + "maintenance_action_restore": "Restaurando base de datos", "maintenance_description": "Immich se ha puesto en modo de mantenimiento.", "maintenance_end": "Finalizar el modo de mantenimiento", "maintenance_end_error": "Error al finalizar el modo de mantenimiento.", "maintenance_logged_in_as": "SesiÃŗn iniciada actualmente como {user}", + "maintenance_restore_from_backup": "Restaurar desde una copia de seguridad", + "maintenance_restore_library": "Restaura tu biblioteca", + "maintenance_restore_library_confirm": "ÂĄSi esto parece correcto, continÃēe restaurando una copia de seguridad!", + "maintenance_restore_library_description": "Restaurando base de datos", + "maintenance_restore_library_folder_has_files": "{folder} tiene {count} carpeta(s)", + "maintenance_restore_library_folder_no_files": "ÂĄA {folder} le faltan archivos!", + "maintenance_restore_library_folder_pass": "legible y escribible", + "maintenance_restore_library_folder_read_fail": "no legible", + "maintenance_restore_library_folder_write_fail": "no escribible", + "maintenance_restore_library_hint_missing_files": "Es posible que le falten archivos importantes", + "maintenance_restore_library_hint_regenerate_later": "Puedes regenerarlos mÃĄs tarde en la configuraciÃŗn", + "maintenance_restore_library_hint_storage_template_missing_files": "ÂŋEstÃĄs usando una plantilla de almacenamiento? Es posible que te falten archivos", + "maintenance_restore_library_loading": "Cargando comprobaciones de integridad y heurísticasâ€Ļ", + "maintenance_task_backup": "Creando una copia de seguridad de la base de datos existenteâ€Ļ", + "maintenance_task_migrations": "Ejecutando migraciones de bases de datosâ€Ļ", + "maintenance_task_restore": "Restaurando la copia de seguridad elegidaâ€Ļ", + "maintenance_task_rollback": "La restauraciÃŗn fallÃŗ, volviendo al punto de restauraciÃŗnâ€Ļ", "maintenance_title": "No disponible temporalmente", "make": "Marca", "manage_geolocation": "Administrar ubicaciÃŗn", @@ -1366,11 +1489,11 @@ "map_cannot_get_user_location": "No se pudo obtener la posiciÃŗn del usuario", "map_location_dialog_yes": "Sí", "map_location_picker_page_use_location": "Usar esta ubicaciÃŗn", - "map_location_service_disabled_content": "Los servicios de ubicaciÃŗn deben estar activados para mostrar elementos de tu ubicaciÃŗn actual. ÂŋDeseas activarlos ahora?", + "map_location_service_disabled_content": "Los servicios de ubicaciÃŗn deben estar activados para mostrar recursos de tu ubicaciÃŗn actual. ÂŋDeseas activarlos ahora?", "map_location_service_disabled_title": "Servicios de ubicaciÃŗn desactivados", "map_marker_for_images": "Marcador de mapa para imÃĄgenes tomadas en {city}, {country}", "map_marker_with_image": "Marcador de mapa con imagen", - "map_no_location_permission_content": "Se necesitan permisos de ubicaciÃŗn para mostrar elementos de tu ubicaciÃŗn actual. ÂŋDeseas activarlos ahora?", + "map_no_location_permission_content": "Se necesitan permisos de ubicaciÃŗn para mostrar recursos de tu ubicaciÃŗn actual. ÂŋDeseas activarlos ahora?", "map_no_location_permission_title": "Permisos de ubicaciÃŗn denegados", "map_settings": "Ajustes del mapa", "map_settings_dark_mode": "Modo oscuro", @@ -1382,13 +1505,13 @@ "map_settings_include_show_archived": "Incluir archivados", "map_settings_include_show_partners": "Incluir miembros", "map_settings_only_show_favorites": "Mostrar solo favoritas", - "map_settings_theme_settings": "Apariencia del Mapa", + "map_settings_theme_settings": "Tema del mapa", "map_zoom_to_see_photos": "Alejar para ver fotos", "mark_all_as_read": "Marcar todo como leído", "mark_as_read": "Marcar como leído", "marked_all_as_read": "Todos marcados como leídos", "matches": "Coincidencias", - "matching_assets": "Elementos Coincidentes", + "matching_assets": "Recursos coincidentes", "media_type": "Tipo de medio", "memories": "Recuerdos", "memories_all_caught_up": "Puesto al día", @@ -1408,36 +1531,42 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Faltante", - "mobile_app": "AplicaciÃŗn MÃŗvil", + "mobile_app": "AplicaciÃŗn mÃŗvil", "mobile_app_download_onboarding_note": "Descarga la aplicaciÃŗn mÃŗvil utilizando las siguientes opciones", "model": "Modelo", "month": "Mes", "monthly_title_text_date_format": "MMMM a", "more": "Mas", "move": "Mover", + "move_down": "Bajar", "move_off_locked_folder": "Sacar de la carpeta protegida", "move_to": "Mover a", + "move_to_device_trash": "Mover a la papelera del dispositivo", "move_to_lock_folder_action_prompt": "{count} aÃąadido(s) a la carpeta protegida", "move_to_locked_folder": "Mover a la carpeta protegida", "move_to_locked_folder_confirmation": "Estas fotos y vídeos se eliminarÃĄn de todos los ÃĄlbumes; solo se podrÃĄn ver en la carpeta protegida", + "move_up": "Subir", "moved_to_archive": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a archivo", "moved_to_library": "Movido(s) {count, plural, one {# recurso} other {# recursos}} a biblioteca", "moved_to_trash": "Movido a la papelera", - "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", - "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicaciÃŗn de activos de solo lectura, omitiendo", - "mute_memories": "Silenciar Recuerdos", + "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del recurso(s) de solo lectura, omitiendo", + "multiselect_grid_edit_gps_err_read_only": "No se puede editar la ubicaciÃŗn de recursos de solo lectura, omitiendo", + "mute_memories": "Silenciar recuerdos", "my_albums": "Mis ÃĄlbumes", "name": "Nombre", "name_or_nickname": "Nombre o apodo", + "name_required": "El nombre es obligatorio", "navigate": "Navegar", - "navigate_to_time": "Navegar a Hora", + "navigate_to_time": "Navegar a la hora", "network_requirement_photos_upload": "Usar datos mÃŗviles para crear una copia de seguridad de las fotos", "network_requirement_videos_upload": "Usar datos mÃŗviles para crear una copia de seguridad de los videos", "network_requirements": "Requisitos de red", "network_requirements_updated": "Los requisitos de red han cambiado, reiniciando la cola de copias de seguridad", "networking_settings": "Red", - "networking_subtitle": "Configuraciones de acceso por URL al servidor", + "networking_subtitle": "Administrar la configuraciÃŗn de la url del servidor", "never": "Nunca", "new_album": "Nuevo ÃĄlbum", "new_api_key": "Nueva clave API", @@ -1446,7 +1575,7 @@ "new_person": "Nueva persona", "new_pin_code": "Nuevo PIN", "new_pin_code_subtitle": "Esta es la primera vez que accedes a la carpeta protegida. Crea un cÃŗdigo PIN seguro para acceder a esta pÃĄgina", - "new_timeline": "Nueva Línea de tiempo", + "new_timeline": "Nueva línea de tiempo", "new_update": "Nueva actualizaciÃŗn", "new_user_created": "Nuevo usuario creado", "new_version_available": "NUEVA VERSIÓN DISPONIBLE", @@ -1454,44 +1583,48 @@ "next": "Siguiente", "next_memory": "Siguiente recuerdo", "no": "No", + "no_actions_added": "No hay acciones aÃąadidas aÃēn", + "no_albums_found": "No se encontraron ÃĄlbumes", "no_albums_message": "Crea un ÃĄlbum para organizar tus fotos y vídeos", "no_albums_with_name_yet": "Parece que todavía no tienes ningÃēn ÃĄlbum con este nombre.", "no_albums_yet": "Parece que aÃēn no tienes ningÃēn ÃĄlbum.", "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_assets_message": "Haz clic para subir tu primera foto", + "no_assets_to_show": "No hay recursos a mostrar", "no_cast_devices_found": "No se encontraron dispositivos de transmisiÃŗn", - "no_checksum_local": "Suma de verificaciÃŗn no disponible. No se pueden obtener los elementos locales", - "no_checksum_remote": "Suma de verificaciÃŗn no disponible. No se puede obtener el elemento remoto", + "no_checksum_local": "Suma de verificaciÃŗn no disponible. No se pueden obtener los recursos locales", + "no_checksum_remote": "Suma de verificaciÃŗn no disponible. No se puede obtener el recurso remoto", + "no_configuration_needed": "No se necesita configuraciÃŗn", "no_devices": "Dispositivos no autorizados", "no_duplicates_found": "No se encontraron duplicados.", "no_exif_info_available": "No hay informaciÃŗn exif disponible", "no_explore_results_message": "Sube mÃĄs fotos para explorar tu colecciÃŗn.", "no_favorites_message": "AÃąade favoritos para encontrar rÃĄpidamente sus mejores fotos y videos", + "no_filters_added": "AÃēn no se han aÃąadido filtros", "no_libraries_message": "Crea una biblioteca externa para ver tus fotos y vídeos", - "no_local_assets_found": "No se encontraron elementos locales con esta suma de comprobaciÃŗn", + "no_local_assets_found": "No se encontraron recursos locales con esta suma de comprobaciÃŗn", "no_location_set": "No se ha establecido ninguna ubicaciÃŗn", "no_locked_photos_message": "Las fotos y los vídeos de la carpeta protegida se mantienen ocultos; no aparecerÃĄn cuando veas o busques elementos en tu biblioteca.", "no_name": "Sin nombre", "no_notifications": "Ninguna notificaciÃŗn", "no_people_found": "No se encontraron personas coincidentes", "no_places": "Sin lugares", - "no_remote_assets_found": "No se encontraron elementos remotos con esta suma de comprobaciÃŗn", + "no_remote_assets_found": "No se encontraron recursos remotos con esta suma de comprobaciÃŗn", "no_results": "Sin resultados", "no_results_description": "Pruebe con un sinÃŗnimo o una palabra clave mÃĄs general", "no_shared_albums_message": "Crea un ÃĄlbum para compartir fotos y vídeos con personas de tu red", "no_uploads_in_progress": "No hay cargas en progreso", + "none": "Ninguno", "not_allowed": "No permitido", "not_available": "N/D", "not_in_any_album": "Sin ÃĄlbum", "not_selected": "No seleccionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar la etiqueta de almacenamiento a los archivos que ya se subieron, ejecute la", "notes": "Notas", "nothing_here_yet": "Sin nada aÃēn", "notification_permission_dialog_content": "Para activar las notificaciones, ve a ConfiguraciÃŗn y selecciona permitir.", "notification_permission_list_tile_content": "Concede permiso para habilitar las notificaciones.", "notification_permission_list_tile_enable_button": "Permitir notificaciones", - "notification_permission_list_tile_title": "Permisos de Notificacion", + "notification_permission_list_tile_title": "Permiso de notificaciÃŗn", "notification_toggle_setting_description": "Habilitar notificaciones de correo electrÃŗnico", "notifications": "Notificaciones", "notifications_setting_description": "Administrar notificaciones", @@ -1515,6 +1648,7 @@ "online": "En línea", "only_favorites": "Solo favoritos", "open": "Abierto", + "open_calendar": "Abrir calendario", "open_in_map_view": "Abrir en la vista del mapa", "open_in_openstreetmap": "Abrir en OpenStreetMap", "open_the_search_filters": "Abre los filtros de bÃēsqueda", @@ -1533,7 +1667,7 @@ "page": "PÃĄgina", "partner": "CompaÃąero", "partner_can_access": "{partner} tiene acceso", - "partner_can_access_assets": "Todas tus fotos y vídeos excepto los Archivados y Eliminados", + "partner_can_access_assets": "Todas tus fotos y vídeos excepto los archivados y eliminados", "partner_can_access_location": "UbicaciÃŗn donde fueron realizadas tus fotos", "partner_list_user_photos": "Fotos de {user}", "partner_list_view_all": "Ver todas", @@ -1563,14 +1697,15 @@ "people": "Personas", "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}", "people_feature_description": "Explorar fotos y vídeos agrupados por personas", - "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral", + "people_selected": "{count, plural, one {# persona seleccionada} other {# personas seleccionadas}}", + "people_sidebar_description": "Mostrar un enlace a personas en la barra lateral", "permanent_deletion_warning": "Advertencia de eliminaciÃŗn permanente", - "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar archivos permanentemente", + "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar recursos permanentemente", "permanently_delete": "Borrar permanentemente", - "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {elemento} other {elementos}}", - "permanently_delete_assets_prompt": "ÂŋEstÃĄ seguro de que desea eliminar permanentemente {count, plural, one {este activo?} other {estos # activos?}} Esto tambiÊn eliminarÃĄ {count, plural, one {de tu} other {de tus}} ÃĄlbum(es).", - "permanently_deleted_asset": "Archivo eliminado permanentemente", - "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {recurso} other {recursos}}", + "permanently_delete_assets_prompt": "ÂŋEstÃĄ seguro de que desea eliminar permanentemente {count, plural, one {este recurso?} other {estos # recursos?}} Esto tambiÊn eliminarÃĄ {count, plural, one {de tu} other {de tus}} ÃĄlbum(es).", + "permanently_deleted_asset": "Recurso eliminado permanentemente", + "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# recurso} other {# recursos}}", "permission": "Permiso", "permission_empty": "Tus permisos no deben estar vacíos", "permission_onboarding_back": "Volver", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# aÃąos}}", "person_birthdate": "Nacido el {date}", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "person_recognized": "Persona reconocida", + "person_selected": "Persona seleccionada", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningÃēn usuario con quien compartirlas.", "photos": "Fotos", - "photos_and_videos": "Fotos y Vídeos", + "photos_and_videos": "Fotos y vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de aÃąos anteriores", + "photos_only": "Solo fotos", "pick_a_location": "Elige una ubicaciÃŗn", "pick_custom_range": "Rango personalizado", "pick_date_range": "Seleccione un rango de fechas", @@ -1626,14 +1764,14 @@ "privacy": "Privacidad", "profile": "Perfil", "profile_drawer_app_logs": "Registros", - "profile_drawer_client_server_up_to_date": "Cliente y Servidor estÃĄn actualizados", + "profile_drawer_client_server_up_to_date": "Cliente y servidor estÃĄn actualizados", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "Modo Solo lectura habilitado. MantÊn pulsado el icono del avatar del usuario para salir.", + "profile_drawer_readonly_mode": "Modo solo lectura habilitado. MantÊn pulsado el icono del avatar del usuario para salir.", "profile_image_of_user": "Foto de perfil de {user}", "profile_picture_set": "Conjunto de imÃĄgenes de perfil.", "public_album": "Álbum pÃēblico", "public_share": "Compartir pÃēblicamente", - "purchase_account_info": "Seguidor", + "purchase_account_info": "Colaborador", "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de cÃŗdigo abierto", "purchase_activated_time": "Activado el {date}", "purchase_activated_title": "Su clave ha sido activada correctamente", @@ -1646,7 +1784,7 @@ "purchase_button_select": "Seleccionar", "purchase_failed_activation": "ÂĄError al activar! ÂĄPor favor, revisa tu correo electrÃŗnico para obtener la clave del producto correcta!", "purchase_individual_description_1": "Para un usuario", - "purchase_individual_description_2": "Estado de soporte", + "purchase_individual_description_2": "Estatus de colaborador", "purchase_individual_title": "Individual", "purchase_input_suggestion": "ÂŋTiene una clave de producto? IntrodÃēzcala a continuaciÃŗn", "purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio", @@ -1662,31 +1800,33 @@ "purchase_remove_server_product_key": "Eliminar la clave de producto del servidor", "purchase_remove_server_product_key_prompt": "ÂŋEstÃĄ seguro de que desea eliminar la clave de producto del servidor?", "purchase_server_description_1": "Para todo el servidor", - "purchase_server_description_2": "Estado del soporte", + "purchase_server_description_2": "Estatus de colaborador", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", - "query_asset_id": "Consultar ID de elemento", + "query_asset_id": "Consultar ID de recurso", "queue_status": "Poniendo en cola {count}/{total}", + "rate_asset": "Valorar recurso", "rating": "ValoraciÃŗn", "rating_clear": "Borrar calificaciÃŗn", "rating_count": "{count, plural, one {# estrella} other {# estrellas}}", "rating_description": "Mostrar la clasificaciÃŗn exif en el panel de informaciÃŗn", + "rating_set": "CalificaciÃŗn establecida en {rating, plural, one {# estrella} other {# estrellas}}", "reaction_options": "Opciones de reacciÃŗn", "read_changelog": "Leer registro de cambios", - "readonly_mode_disabled": "Modo Solo lectura deshabilitado", - "readonly_mode_enabled": "Modo Solo lectura habilitado", + "readonly_mode_disabled": "Modo solo lectura deshabilitado", + "readonly_mode_enabled": "Modo solo lectura habilitado", "ready_for_upload": "Listo para subir", "reassign": "Reasignar", - "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}", - "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", - "reassing_hint": "Asignar archivos seleccionados a una persona existente", + "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# recurso} other {# recursos}} a {name, select, null {una persona existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# recurso} other {# recursos}} a un nuevo usuario", + "reassing_hint": "Asignar recursos seleccionados a una persona existente", "recent": "Reciente", - "recent-albums": "Últimos ÃĄlbumes", + "recent_albums": "Últimos ÃĄlbumes", "recent_searches": "BÃēsquedas recientes", "recently_added": "AÃąadidos recientemente", "recently_added_page_title": "ReciÊn aÃąadidos", "recently_taken": "Tomadas recientemente", - "recently_taken_page_title": "Tomadas Recientemente", + "recently_taken_page_title": "Tomadas recientemente", "refresh": "Actualizar", "refresh_encoded_videos": "Recargar los vídeos codificados", "refresh_faces": "Actualizar caras", @@ -1699,14 +1839,14 @@ "refreshing_metadata": "Recargando metadatos", "regenerating_thumbnails": "Recargando miniaturas", "remote": "Remoto", - "remote_assets": "Elementos remotos", - "remote_media_summary": "Resumen de Medios Remotos", + "remote_assets": "Recursos remotos", + "remote_media_summary": "Resumen de medios remotos", "remove": "Eliminar", - "remove_assets_album_confirmation": "ÂŋEstÃĄs seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del ÃĄlbum?", - "remove_assets_shared_link_confirmation": "ÂŋEstÃĄs seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", - "remove_assets_title": "ÂŋEliminar activos?", + "remove_assets_album_confirmation": "ÂŋEstÃĄs seguro que quieres eliminar {count, plural, one {# recurso} other {# recursos}} del ÃĄlbum?", + "remove_assets_shared_link_confirmation": "ÂŋEstÃĄs seguro que quieres eliminar {count, plural, one {# recurso} other {# recursos}} del enlace compartido?", + "remove_assets_title": "ÂŋEliminar recursos?", "remove_custom_date_range": "Eliminar intervalo de fechas personalizado", - "remove_deleted_assets": "Eliminar archivos sin conexiÃŗn", + "remove_deleted_assets": "Eliminar recursos sin conexiÃŗn", "remove_from_album": "Eliminar del ÃĄlbum", "remove_from_album_action_prompt": "{count} eliminado del ÃĄlbum", "remove_from_favorites": "Quitar de favoritos", @@ -1725,7 +1865,7 @@ "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", "removed_memory": "Recuerdo eliminado", "removed_photo_from_memory": "Foto eliminada del recuerdo", - "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# recurso} other {# recursos}}", "rename": "Renombrar", "repair": "Reparar", "repair_no_results_message": "Los archivos perdidos y sin seguimiento aparecerÃĄn aquí", @@ -1741,7 +1881,7 @@ "reset_pin_code_description": "Si olvidaste tu cÃŗdigo PIN, puedes comunicarte con el administrador del servidor para restablecerlo", "reset_pin_code_success": "CÃŗdigo PIN restablecido correctamente", "reset_pin_code_with_password": "Siempre puedes restablecer tu cÃŗdigo PIN usando tu contraseÃąa", - "reset_sqlite": "Restablecer la Base de Datos SQLite", + "reset_sqlite": "Restablecer la base de datos SQLite", "reset_sqlite_confirmation": "ÂŋEstÃĄs seguro que deseas restablecer la base de datos SQLite? DeberÃĄs cerrar sesiÃŗn y volver a iniciarla para resincronizar los datos", "reset_sqlite_success": "Restablecer exitosamente la base de datos SQLite", "reset_to_default": "Restablecer los valores predeterminados", @@ -1752,7 +1892,7 @@ "restore_all": "Restaurar todo", "restore_trash_action_prompt": "{count} restaurado de la papelera", "restore_user": "Restaurar usuario", - "restored_asset": "Archivo restaurado", + "restored_asset": "Recurso restaurado", "resume": "Continuar", "resume_paused_jobs": "Reanudar {count, plural, one {# tarea en pausa} other {# tareas en pausa}}", "retry_upload": "Reintentar subida", @@ -1763,16 +1903,18 @@ "role_viewer": "Visor", "running": "En ejecuciÃŗn", "save": "Guardar", - "save_to_gallery": "Guardado en la galería", + "save_to_gallery": "Guardar en la galería", "saved": "Guardado", "saved_api_key": "Clave API guardada", "saved_profile": "Perfil guardado", "saved_settings": "Configuraciones guardadas", "say_something": "Comenta algo", "scaffold_body_error_occurred": "Ha ocurrido un error", + "scan": "Escanear", "scan_all_libraries": "Escanear todas las bibliotecas", "scan_library": "Escanear", "scan_settings": "ConfiguraciÃŗn de escaneo", + "scanning": "Escaneando", "scanning_for_album": "Buscando ÃĄlbum...", "search": "Buscar", "search_albums": "Buscar ÃĄlbumes", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Seleccionar el tipo de archivo", "search_filter_ocr": "Buscar por OCR", "search_filter_people_title": "Seleccionar personas", + "search_filter_star_rating": "ClasificaciÃŗn de estrellas", "search_for": "Buscar", "search_for_existing_person": "Buscar persona existente", "search_no_more_result": "No hay mÃĄs resultados", @@ -1810,7 +1953,7 @@ "search_no_result": "No se encontraron resultados, prueba con un tÊrmino o combinaciÃŗn de bÃēsqueda diferente", "search_options": "Opciones de bÃēsqueda", "search_page_categories": "Categorías", - "search_page_motion_photos": "Foto en Movimiento", + "search_page_motion_photos": "Fotos en movimiento", "search_page_no_objects": "No hay informaciÃŗn de objetos disponibles", "search_page_no_places": "No hay informaciÃŗn de lugares disponibles", "search_page_screenshots": "Capturas de pantalla", @@ -1819,7 +1962,7 @@ "search_page_things": "Cosas", "search_page_view_all_button": "Ver todo", "search_page_your_activity": "Tu actividad", - "search_page_your_map": "Tu Mapa", + "search_page_your_map": "Tu mapa", "search_people": "Buscar personas", "search_places": "Buscar lugar", "search_rating": "Buscar por calificaciÃŗn...", @@ -1836,19 +1979,25 @@ "second": "Segundo", "see_all_people": "Ver todas las personas", "select": "Seleccionar", + "select_album": "Seleccionar ÃĄlbum", "select_album_cover": "Seleccionar portada del ÃĄlbum", + "select_albums": "Seleccionar ÃĄlbumes", "select_all": "Seleccionar todo", "select_all_duplicates": "Seleccionar todos los duplicados", "select_all_in": "Seleccionar todos en {group}", "select_avatar_color": "Seleccionar color del avatar", + "select_count": "{count, plural, one {Seleccionar #} other {Seleccionar #}}", + "select_cutoff_date": "Seleccione fecha límite", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto principal", - "select_from_computer": "Seleccionar desde el PC", + "select_from_computer": "Seleccionar desde el equipo", "select_keep_all": "Conservar todo", "select_library_owner": "Seleccionar propietario de la biblioteca", "select_new_face": "Seleccionar nueva cara", + "select_people": "Seleccionar gente", + "select_person": "Seleccionar persona", "select_person_to_tag": "Elija una persona a etiquetar", - "select_photos": "Seleccionar Fotos", + "select_photos": "Seleccionar fotos", "select_trash_all": "Seleccionar eliminar todo", "select_user_for_sharing_page_err_album": "Fallo al crear el ÃĄlbum", "selected": "Seleccionado", @@ -1861,7 +2010,7 @@ "server_info_box_server_url": "Enlace del servidor", "server_offline": "Servidor desconectado", "server_online": "Servidor en línea", - "server_privacy": "Privacidad del Servidor", + "server_privacy": "Privacidad del servidor", "server_restarting_description": "Esta pÃĄgina se actualizarÃĄ en breve.", "server_restarting_title": "El servidor se estÃĄ reiniciando", "server_stats": "Estadísticas del servidor", @@ -1889,10 +2038,10 @@ "setting_notifications_notify_minutes": "{count} minutos", "setting_notifications_notify_never": "nunca", "setting_notifications_notify_seconds": "{count} segundos", - "setting_notifications_single_progress_subtitle": "InformaciÃŗn detallada del progreso de subida de cada archivo", + "setting_notifications_single_progress_subtitle": "InformaciÃŗn detallada del progreso de subida de cada recurso", "setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano", "setting_notifications_subtitle": "Ajusta tus preferencias de notificaciÃŗn", - "setting_notifications_total_progress_subtitle": "Progreso general de subida (elementos completados/total)", + "setting_notifications_total_progress_subtitle": "Progreso general de subida (recursos completados/total)", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_video_viewer_auto_play_subtitle": "Reproducir vídeos automÃĄticamente al abrirlos", "setting_video_viewer_auto_play_title": "Reproducir vídeos automÃĄticamente", @@ -1908,12 +2057,12 @@ "share_add_photos": "AÃąadir fotos", "share_assets_selected": "{count} seleccionado(s)", "share_dialog_preparing": "Preparando...", - "share_link": "Compartir Enlace", + "share_link": "Compartir enlace", "shared": "Compartidos", "shared_album_activities_input_disable": "Los comentarios estÃĄn deshabilitados", "shared_album_activity_remove_content": "ÂŋDeseas eliminar esta actividad?", - "shared_album_activity_remove_title": "Eliminar Actividad", - "shared_album_section_people_action_error": "Error retirando/eliminando del album", + "shared_album_activity_remove_title": "Eliminar actividad", + "shared_album_section_people_action_error": "Error retirando/eliminando del ÃĄlbum", "shared_album_section_people_action_leave": "Eliminar usuario del ÃĄlbum", "shared_album_section_people_action_remove_user": "Eliminar usuario del ÃĄlbum", "shared_album_section_people_title": "PERSONAS", @@ -1921,7 +2070,7 @@ "shared_by_user": "Compartido por {user}", "shared_by_you": "Compartido por ti", "shared_from_partner": "Fotos de {partner}", - "shared_intent_upload_button_progress_text": "{current} / {total} Cargado(s)", + "shared_intent_upload_button_progress_text": "{current} / {total} cargado(s)", "shared_link_app_bar_title": "Enlaces compartidos", "shared_link_clipboard_copied_massage": "Copiado al portapapeles", "shared_link_clipboard_text": "Enlace: {link}\nContraseÃąa: {password}", @@ -1938,7 +2087,7 @@ "shared_link_edit_expire_after_option_year": "{count} aÃąo", "shared_link_edit_password_hint": "Introduce la contraseÃąa del enlace", "shared_link_edit_submit_button": "Actualizar enlace", - "shared_link_error_server_url_fetch": "No se puede adquirir la URL del servidor", + "shared_link_error_server_url_fetch": "No se puede obtener la url del servidor", "shared_link_expires_day": "Caduca en {count} día", "shared_link_expires_days": "Caduca en {count} días", "shared_link_expires_hour": "Caduca en {count} hora", @@ -1955,7 +2104,7 @@ "shared_link_password_description": "Requerir una contraseÃąa para acceder a este enlace compartido", "shared_links": "Enlaces compartidos", "shared_links_description": "Comparte fotos y vídeos con un enlace", - "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos y vídeos compartidos.}}", + "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos y vídeos compartidos.}}", "shared_with_me": "Compartidos conmigo", "shared_with_partner": "Compartido con {partner}", "sharing": "Compartidos", @@ -1966,7 +2115,7 @@ "sharing_sidebar_description": "Muestra un enlace a \"Compartido\" en el menÃē lateral", "sharing_silver_appbar_create_shared_album": "Crear un ÃĄlbum compartido", "sharing_silver_appbar_share_partner": "Compartir con compaÃąero", - "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", + "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el recurso", "show_album_options": "Mostrar opciones del ÃĄlbum", "show_albums": "Mostrar ÃĄlbumes", "show_all_people": "Mostrar todas las personas", @@ -1982,6 +2131,7 @@ "show_password": "Mostrar contraseÃąa", "show_person_options": "Mostrar opciones de la persona", "show_progress_bar": "Mostrar barra de progreso", + "show_schema": "Mostrar esquema", "show_search_options": "Mostrar opciones de bÃēsqueda", "show_shared_links": "Mostrar enlaces compartidos", "show_slideshow_transition": "Mostrar la transiciÃŗn de las diapositivas", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Ir a las carpetas", "skip_to_tags": "Ir a las etiquetas", "slideshow": "Pase de diapositivas", + "slideshow_repeat": "Repetir presentaciÃŗn de diapositivas", + "slideshow_repeat_description": "Volver al inicio cuando finaliza la presentaciÃŗn de diapositivas", "slideshow_settings": "Ajustes de diapositivas", "sort_albums_by": "Ordenar ÃĄlbumes porâ€Ļ", "sort_created": "Fecha de creaciÃŗn", @@ -2015,7 +2167,7 @@ "stack_duplicates": "Apilar duplicados", "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", - "stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}", + "stacked_assets_count": "Apilado(s) {count, plural, one {# recurso} other {# recursos}}", "stacktrace": "Seguimiento de pila", "start": "Inicio", "start_date": "Fecha de inicio", @@ -2038,23 +2190,24 @@ "support": "Soporte", "support_and_feedback": "Soporte y comentarios", "support_third_party_description": "Esta instalaciÃŗn de Immich fue empaquetada por un tercero. Los problemas actuales pueden ser ocasionados por ese paquete; por favor, discuta sus inconvenientes con el empaquetador antes de usar los enlaces de abajo.", + "supporter": "Colaborador", "swap_merge_direction": "Alternar direcciÃŗn de mezcla", "sync": "Sincronizar", "sync_albums": "Sincronizar ÃĄlbumes", "sync_albums_manual_subtitle": "Sincroniza todos los videos y fotos subidos con los ÃĄlbumes seleccionados a respaldar", - "sync_local": "SincronizaciÃŗn Local", - "sync_remote": "SincronizaciÃŗn Remota", + "sync_local": "SincronizaciÃŗn local", + "sync_remote": "SincronizaciÃŗn remota", "sync_status": "Estado de la sincronizaciÃŗn", "sync_status_subtitle": "Ver y gestionar el estado de la sincronizaciÃŗn", "sync_upload_album_setting_subtitle": "Crea y sube tus fotos y videos a los ÃĄlbumes seleccionados en Immich", "tag": "Etiqueta", - "tag_assets": "Etiquetar activos", + "tag_assets": "Etiquetar recursos", "tag_created": "Etiqueta creada: {tag}", "tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lÃŗgicas", "tag_not_found_question": "ÂŋNo encuentra una etiqueta? Crea una nueva etiqueta.", "tag_people": "Etiquetar personas", "tag_updated": "Etiqueta actualizada: {tag}", - "tagged_assets": "Etiquetado(s) {count, plural, one {# activo} other {# activos}}", + "tagged_assets": "Etiquetado(s) {count, plural, one {# recurso} other {# recursos}}", "tags": "Etiquetas", "tap_to_run_job": "Toca para ejecutar la tarea", "template": "Plantilla", @@ -2062,8 +2215,8 @@ "theme": "Tema", "theme_selection": "SelecciÃŗn de tema", "theme_selection_description": "Establece el tema automÃĄticamente como \"claro\" u \"oscuro\" segÃēn las preferencias del sistema/navegador", - "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "NÃēmero de elementos por fila ({count})", + "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los recursos", + "theme_setting_asset_list_tiles_per_row_title": "NÃēmero de recursos por fila ({count})", "theme_setting_colorful_interface_subtitle": "Aplicar el color primario a las superficies de fondo.", "theme_setting_colorful_interface_title": "Color de Interfaz", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imÃĄgenes", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Elige la configuraciÃŗn del tema de la aplicaciÃŗn", "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", + "then": "Entonces", "they_will_be_merged_together": "Se fusionarÃĄn entre sí", "third_party_resources": "Recursos de terceros", "time": "Tiempo", @@ -2085,7 +2239,7 @@ "to_archive": "Archivar", "to_change_password": "Cambiar contraseÃąa", "to_favorite": "A los favoritos", - "to_login": "Iniciar SesiÃŗn", + "to_login": "Iniciar sesiÃŗn", "to_multi_select": "para multi selecciÃŗn", "to_parent": "Ir a los padres", "to_select": "para seleccionar", @@ -2098,17 +2252,24 @@ "trash_action_prompt": "{count} movidos a la papelera", "trash_all": "Descartar todo", "trash_count": "Descartar {count, number}", - "trash_delete_asset": "Borrar/Eliminar archivo", + "trash_delete_asset": "Borrar/Eliminar recurso", "trash_emptied": "Papelera vaciada", "trash_no_results_message": "Las fotos y videos que se envíen a la papelera aparecerÃĄn aquí.", "trash_page_delete_all": "Eliminar todos", - "trash_page_empty_trash_dialog_content": "ÂŋEstÃĄ seguro que quiere eliminar los elementos? Estos elementos serÃĄn eliminados de Immich permanentemente", + "trash_page_empty_trash_dialog_content": "ÂŋEstÃĄ seguro que quiere eliminar los recursos? Estos recursos serÃĄn eliminados de Immich permanentemente", "trash_page_info": "Los archivos en la papelera serÃĄn eliminados automÃĄticamente de forma permanente despuÊs de {days} días", - "trash_page_no_assets": "No hay elementos en la papelera", + "trash_page_no_assets": "No hay recursos en la papelera", "trash_page_restore_all": "Restaurar todos", - "trash_page_select_assets_btn": "Seleccionar elementos", + "trash_page_select_assets_btn": "Seleccionar recursos", "trash_page_title": "Papelera ({count})", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serÃĄn eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", + "trigger": "Disparador", + "trigger_asset_uploaded": "Recurso subido", + "trigger_asset_uploaded_description": "Se activa cuando se carga un nuevo recurso", + "trigger_description": "Un evento que inicia el flujo de trabajo", + "trigger_person_recognized": "Persona reconocida", + "trigger_person_recognized_description": "Se activa cuando se detecta una persona", + "trigger_type": "Tipo de disparador", "troubleshoot": "Solucionar problemas", "type": "Tipo", "unable_to_change_pin_code": "No se ha podido cambiar el PIN", @@ -2123,13 +2284,14 @@ "unhide_person": "Mostrar persona", "unknown": "Desconocido", "unknown_country": "País desconocido", + "unknown_date": "Fecha desconocida", "unknown_year": "AÃąo desconocido", "unlimited": "Sin límites", "unlink_motion_video": "Desvincular vídeo en movimiento", "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Cuenta OAuth desconectada", "unmute_memories": "Habilitar sonido recuerdos", - "unnamed_album": "Album sin nombre", + "unnamed_album": "Álbum sin nombre", "unnamed_album_delete_confirmation": "ÂŋSeguro que quieres borrar este ÃĄlbum?", "unnamed_share": "Compartido sin nombre", "unsaved_change": "Cambio no guardado", @@ -2138,22 +2300,24 @@ "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}}", + "unstacked_assets_count": "Desapilado(s) {count, plural, one {# recurso} other {# recursos}}", + "unsupported_field_type": "Tipo de campo no soportado", "untagged": "Sin etiqueta", + "untitled_workflow": "Flujo de trabajo sin título", "up_next": "A continuaciÃŗn", - "update_location_action_prompt": "Actualiza la ubicaciÃŗn de {count} assets seleccionados con:", + "update_location_action_prompt": "Actualiza la ubicaciÃŗn de {count} recursos seleccionados con:", "updated_at": "Actualizado", "updated_password": "ContraseÃąa actualizada", "upload": "Subir", - "upload_action_prompt": "{count} en cola para carga", "upload_concurrency": "Subidas simultÃĄneas", - "upload_details": "Cargar Detalles", - "upload_dialog_info": "ÂŋQuieres hacer una copia de seguridad al servidor de los elementos seleccionados?", - "upload_dialog_title": "Subir elementos", + "upload_details": "Cargar detalles", + "upload_dialog_info": "ÂŋQuieres hacer una copia de seguridad al servidor de los recursos seleccionados?", + "upload_dialog_title": "Subir recursos", + "upload_error_with_count": "Error al cargar {count, plural, one {# el recurso} other {# los recursos}}", "upload_errors": "Subida completada con {count, plural, one {# error} other {# errores}}, actualice la pÃĄgina para ver los nuevos recursos de la subida.", "upload_finished": "Carga finalizada", "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", - "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", + "upload_skipped_duplicates": "Saltado {count, plural, one {# recurso duplicado} other {# recursos duplicados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Errores", "upload_status_uploaded": "Subido", @@ -2164,15 +2328,15 @@ "url": "URL", "usage": "Uso", "use_biometric": "Uso biomÊtrico", - "use_current_connection": "Usar conexiÃŗn actual", + "use_current_connection": "Utilice la conexiÃŗn actual", "use_custom_date_range": "Usa un intervalo de fechas personalizado", "user": "Usuario", "user_has_been_deleted": "Este usuario ha sido eliminado.", "user_id": "Id. de usuario", - "user_liked": "{user} le gustÃŗ {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_liked": "{user} le gustÃŗ {type, select, photo {esta foto} video {este vídeo} asset {este recurso} other {esto}}", "user_pin_code_settings": "PIN", "user_pin_code_settings_description": "Gestione su PIN", - "user_privacy": "Privacidad del Usuario", + "user_privacy": "Privacidad del usuario", "user_purchase_settings": "Compra", "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Establecer {user} como {role}", @@ -2185,6 +2349,7 @@ "utilities": "Utilidades", "validate": "Validar", "validate_endpoint_error": "Por favor, introduce una URL vÃĄlida", + "validation_error": "Error de validaciÃŗn", "variables": "Variables", "version": "VersiÃŗn", "version_announcement_closing": "Tu amigo, Alex", @@ -2196,41 +2361,57 @@ "video_hover_setting_description": "Reproducir el vídeo cuando el ratÃŗn estÃĄ encima de un vídeo. Aunque estÊ desactivado, se iniciarÃĄ cuando el cursor del ratÃŗn estÊ sobre el icono de \"reproducir\".", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "videos_only": "Solo vídeos", "view": "Ver", - "view_album": "Ver Álbum", + "view_album": "Ver ÃĄlbum", "view_all": "Ver todas", "view_all_users": "Mostrar todos los usuarios", - "view_asset_owners": "Ver propietarios", - "view_details": "Ver Detalles", + "view_asset_owners": "Ver propietarios del recurso", + "view_details": "Ver detalles", "view_in_timeline": "Ver en la línea de tiempo", "view_link": "Ver enlace", "view_links": "Mostrar enlaces", "view_name": "Ver", - "view_next_asset": "Mostrar siguiente elemento", - "view_previous_asset": "Mostrar elemento anterior", + "view_next_asset": "Mostrar siguiente recurso", + "view_previous_asset": "Mostrar recurso anterior", "view_qr_code": "Ver cÃŗdigo QR", "view_similar_photos": "Ver fotografías similares", - "view_stack": "Ver Pila", - "view_user": "Ver Usuario", + "view_stack": "Ver pila", + "view_user": "Ver usuario", "viewer_remove_from_stack": "Quitar de la pila", - "viewer_stack_use_as_main_asset": "Usar como elemento principal", + "viewer_stack_use_as_main_asset": "Usar como recurso principal", "viewer_unstack": "Desapilar", "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}", + "visual": "Visual", + "visual_builder": "Constructor visual", "waiting": "Esperando", "waiting_count": "Esperando: {count}", "warning": "Advertencia", "week": "Semana", - "welcome": "Bienvenido", - "welcome_to_immich": "Bienvenido a Immich", + "welcome": "Bienvenido/a", + "welcome_to_immich": "Bienvenido/a a Immich", "width": "Ancho", - "wifi_name": "Nombre Wi-Fi", - "workflow": "Flujo de trabajo", + "wifi_name": "Nombre del Wi-Fi", + "workflow_delete_prompt": "ÂŋEstÃĄs seguro de que quieres eliminar este flujo de trabajo?", + "workflow_deleted": "Flujo de trabajo eliminado", + "workflow_description": "DescripciÃŗn del flujo de trabajo", + "workflow_info": "InformaciÃŗn del flujo de trabajo", + "workflow_json": "JSON del flujo de trabajo", + "workflow_json_help": "Edite la configuraciÃŗn del flujo de trabajo en formato JSON. Los cambios se sincronizarÃĄn con el generador visual.", + "workflow_name": "Nombre del flujo de trabajo", + "workflow_navigation_prompt": "ÂŋEstÃĄs seguro que deseas salir sin guardar los cambios?", + "workflow_summary": "Resumen del flujo de trabajo", + "workflow_update_success": "Flujo de trabajo actualizado con Êxito", + "workflow_updated": "Flujo de trabajo actualizado", + "workflows": "Flujos de trabajo", + "workflows_help_text": "Los flujos de trabajo automatizan acciones en sus recursos segÃēn activadores y filtros", "wrong_pin_code": "CÃŗdigo PIN incorrecto", "year": "AÃąo", "years_ago": "Hace {years, plural, one {# aÃąo} other {# aÃąos}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningÃēn enlace compartido", "your_wifi_name": "El nombre de tu Wi-Fi", - "zoom_image": "Acercar Imagen", + "zero_to_clear_rating": "presione 0 para borrar la calificaciÃŗn del recurso", + "zoom_image": "Acercar imagen", "zoom_to_bounds": "Ajustar a los límites" } diff --git a/i18n/et.json b/i18n/et.json index b1db7e0466..7b85dcfda6 100644 --- a/i18n/et.json +++ b/i18n/et.json @@ -5,6 +5,7 @@ "acknowledge": "Sain aru", "action": "Tegevus", "action_common_update": "Uuenda", + "action_description": "Komplekt tegevusi, mida teostada filtreeritud Ãŧksustega", "actions": "Tegevused", "active": "Aktiivne", "active_count": "Aktiivsed: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Lisa asukoht", "add_a_name": "Lisa nimi", "add_a_title": "Lisa pealkiri", + "add_action": "Lisa tegevus", + "add_action_description": "KlÃĩpsa, et lisada teostatav tegevus", + "add_assets": "Lisa Ãŧksuseid", "add_birthday": "Lisa sÃŧnnipäev", "add_endpoint": "Lisa lÃĩpp-punkt", "add_exclusion_pattern": "Lisa välistamismuster", + "add_filter": "Lisa filter", + "add_filter_description": "KlÃĩpsa, et lisada filtreerimistingimus", "add_location": "Lisa asukoht", "add_more_users": "Lisa rohkem kasutajaid", "add_partner": "Lisa partner", @@ -36,6 +42,7 @@ "add_to_shared_album": "Lisa jagatud albumisse", "add_upload_to_stack": "Virnasta Ãŧleslaaditud Ãŧksus", "add_url": "Lisa URL", + "add_workflow_step": "Lisa tÃļÃļvoo samm", "added_to_archive": "Lisatud arhiivi", "added_to_favorites": "Lisatud lemmikutesse", "added_to_favorites_count": "{count, number} pilti lisatud lemmikutesse", @@ -63,7 +70,7 @@ "cleared_jobs": "TÃļÃļted eemaldatud: {job}", "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 toimingut 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 tegevust 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?", @@ -77,12 +84,12 @@ "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 selle kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", "export_config_as_json_description": "Laadi praegune sÃŧsteemi seadistus JSON-failina alla", - "external_libraries_page_description": "Administraatori väliste kogude leht", + "external_libraries_page_description": "Väliste kogude haldamise leht", "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ä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 tema Ãŧksused. Toimingut ei saa tagasi vÃĩtta ja faile ei saa taastada.", + "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kÃĩik tema Ãŧksused. Tegevust 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", @@ -97,6 +104,8 @@ "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. Madal väärtus vÃĩib mÃĩjutada masinÃĩppe kvaliteeti.", "image_preview_title": "Eelvaate seaded", + "image_progressive": "Progressiivne", + "image_progressive_description": "Kodeeri JPEG-pildid järk-järguliseks laadimiseks. See ei mÃĩjuta WebP-pilte.", "image_quality": "Kvaliteet", "image_resolution": "Resolutsioon", "image_resolution_description": "KÃĩrgemad resolutsioonid säilitavad rohkem detaile, aga kodeerimine vÃĩtab kauem aega, tekitab suuremaid faile ning vÃĩib mÃĩjutada rakenduse tÃļÃļkiirust.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Luba nutiotsing", "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.", "machine_learning_url_description": "MasinÃĩppe serveri URL. Kui ette on antud rohkem kui Ãŧks URL, proovitakse neid järjest Ãŧkshaaval, kuni Ãŧks edukalt vastab. Servereid, mis ei vasta, ignoreeritakse ajutiselt, kuni Ãŧhendus taastub.", + "maintenance_delete_backup": "Kustuta varukoopia", + "maintenance_delete_backup_description": "See fail kustutatakse jäädavalt.", + "maintenance_delete_error": "Varukoopia kustutamine ebaÃĩnnestus.", + "maintenance_restore_backup": "Taasta varukoopia", + "maintenance_restore_backup_description": "Immich lähtestatakse ning taastatakse valitud varukoopiast. Enne jätkamist tehakse uus varukoopia.", + "maintenance_restore_backup_different_version": "See varukoopia loodi erineva Immich'i versiooniga!", + "maintenance_restore_backup_unknown_version": "Varukoopia versiooni tuvastamine ebaÃĩnnestus.", + "maintenance_restore_database_backup": "Taasta andmebaasi varukoopia", + "maintenance_restore_database_backup_description": "PÃļÃļra andmebaas tagasi varasemasse seisu varukoopia faili abil", "maintenance_settings": "Hooldus", "maintenance_settings_description": "Pane Immich hooldusreÅžiimi.", - "maintenance_start": "Käivita hooldusreÅžiim", + "maintenance_start": "LÃŧlitu hooldusreÅžiimi", "maintenance_start_error": "HooldusreÅžiimi käivitamine ebaÃĩnnestus.", + "maintenance_upload_backup": "Laadi andmebaasi varukoopia fail Ãŧles", + "maintenance_upload_backup_error": "Varukoopia Ãŧleslaadimine ebaÃĩnnestus. Kas see on .sql vÃĩi .sql.gz fail?", "manage_concurrency": "Halda samaaegsust", "manage_concurrency_description": "TÃļÃļdete samaaegsuse haldamiseks mine tÃļÃļdete lehele", "manage_log_settings": "Halda logi seadeid", @@ -252,7 +272,7 @@ "oauth_auto_register": "Automaatne registreerimine", "oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel", "oauth_button_text": "Nupu tekst", - "oauth_client_secret_description": "NÃĩutud, kui PKCE (Proof Key for Code Exchange) ei ole OAuth pakkuja poolt toetatud", + "oauth_client_secret_description": "NÃĩutud konfidentsiaalse kliendi jaoks, vÃĩi avaliku kliendi jaoks, kui PKCE (Proof Key for Code Exchange) ei ole toetatud.", "oauth_enable_description": "Sisene OAuth abil", "oauth_mobile_redirect_uri": "Mobiilne Ãŧmbersuunamise URI", "oauth_mobile_redirect_uri_override": "Mobiilse Ãŧmbersuunamise URI Ãŧlekirjutamine", @@ -278,7 +298,7 @@ "person_cleanup_job": "Isikute korrastamine", "queue_details": "Järjekorra Ãŧksikasjad", "queues": "TÃļÃļdete järjekorrad", - "queues_page_description": "Administraatori tÃļÃļdete järjekordade leht", + "queues_page_description": "TÃļÃļdete järjekordade haldamise leht", "quota_size_gib": "Kvoot (GiB)", "refreshing_all_libraries": "KÃĩikide kogude värskendamine", "registration": "Administraatori registreerimine", @@ -291,15 +311,15 @@ "search_jobs": "Otsi tÃļÃļdetâ€Ļ", "send_welcome_email": "Saada tervituskiri", "server_external_domain_settings": "Väline domeen", - "server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://", + "server_external_domain_settings_description": "Domeen väliste linkide jaoks", "server_public_users": "Avalikud kasutajad", "server_public_users_description": "Kasutaja jagatud albumisse lisamisel kuvatakse kÃĩiki kasutajaid (nime ja e-posti aadressiga). Kui keelatud, kuvatakse kasutajate nimekirja ainult administraatoritele.", "server_settings": "Serveri seaded", "server_settings_description": "Halda serveri seadeid", - "server_stats_page_description": "Administraatori serveri statistika leht", + "server_stats_page_description": "Serveri statistika leht", "server_welcome_message": "Tervitusteade", "server_welcome_message_description": "Teade, mida kuvatakse sisselogimise lehel.", - "settings_page_description": "Administraatori seadete leht", + "settings_page_description": "SÃŧsteemi seadete leht", "sidecar_job": "Väliste failide metaandmed", "sidecar_job_description": "Avasta vÃĩi sÃŧnkroniseeri väliste failide metaandmed failisÃŧsteemist", "slideshow_duration_description": "Mitu sekundit igat pilti kuvada", @@ -419,7 +439,7 @@ "user_settings": "Kasutajate seaded", "user_settings_description": "Halda kasutajate seadeid", "user_successfully_removed": "Kasutaja {email} edukalt eemaldatud.", - "users_page_description": "Administraatori kasutajate leht", + "users_page_description": "Kasutajate haldamise leht", "version_check_enabled_description": "Luba versioonikontroll", "version_check_implications": "Versioonikontroll vajab perioodilist Ãŧhendumist github.com-iga", "version_check_settings": "Versioonikontroll", @@ -431,6 +451,9 @@ "admin_password": "Administraatori parool", "administration": "Administratsioon", "advanced": "Täpsemad valikud", + "advanced_settings_clear_image_cache": "TÃŧhjenda pildipuhver", + "advanced_settings_clear_image_cache_error": "Pildipuhvri tÃŧhjendamine ebaÃĩnnestus", + "advanced_settings_clear_image_cache_success": "{size} edukalt tÃŧhjendatud", "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}", @@ -467,10 +490,12 @@ "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_selected": "Album valitud", "album_share_no_users": "Paistab, et oled seda albumit kÃĩikide kasutajatega jaganud, vÃĩi pole Ãŧhtegi kasutajat, kellega jagada.", "album_summary": "Albumi kokkuvÃĩte", "album_updated": "Album muudetud", "album_updated_setting_description": "Saa teavitus e-posti teel, kui jagatud albumis on uusi Ãŧksuseid", + "album_upload_assets": "Laadi Ãŧksused oma arvutist Ãŧles ja lisa albumisse", "album_user_left": "Lahkutud albumist {album}", "album_user_removed": "Kasutaja {user} eemaldatud", "album_viewer_appbar_delete_confirm": "Kas oled kindel, et soovid selle albumi oma kontolt kustutada?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# album valitud} other {# albumit valitud}}", "all": "KÃĩik", "all_albums": "KÃĩik albumid", "all_people": "KÃĩik isikud", + "all_photos": "KÃĩik fotod", "all_videos": "KÃĩik videod", "allow_dark_mode": "Luba tume teema", "allow_edits": "Luba muutmine", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Luba avalikul kasutajal Ãŧles laadida", "allowed": "Lubatud", "alt_text_qr_code": "QR kood", + "always_keep": "Jäta alati alles", + "always_keep_photos_hint": "Talletusruumi vabastamine jätab kÃĩik fotod selles seadmes alles.", + "always_keep_videos_hint": "Talletusruumi vabastamine jätab kÃĩik videod selles seadmes alles.", "anti_clockwise": "Vastupäeva", "api_key": "API vÃĩti", "api_key_description": "Seda väärtust kuvatakse ainult Ãŧks kord. Kopeeri see enne akna sulgemist.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {# arhiveeritud}}", "are_these_the_same_person": "Kas need on sama isik?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", + "array_field_not_fully_supported": "Massiivi väljad vajavad JSON-i käsitsi muutmist", "asset_action_delete_err_read_only": "Kirjutuskaitstud Ãŧksuseid ei saa kustutada, jäetakse vahele", "asset_action_share_err_offline": "Ühenduseta Ãŧksuseid ei saa pärida, jäetakse vahele", "asset_added_to_album": "Lisatud albumisse", "asset_adding_to_album": "Albumisse lisamineâ€Ļ", + "asset_created": "Üksus loodud", "asset_description_updated": "Üksuse kirjeldus on muudetud", "asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav", "asset_has_unassigned_faces": "Üksusel on seostamata nägusid", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Asetus", "asset_list_settings_subtitle": "Fotoruudustiku asetuse sätted", "asset_list_settings_title": "Fotoruudustik", + "asset_not_found_on_device_android": "Üksust ei leitud seadmest", + "asset_not_found_on_device_ios": "Üksust ei leitud seadmest. Kui kasutad iCloud'i, vÃĩib Ãŧksus olla iCloud'is oleva vigase faili tÃĩttu kättesaamatu", + "asset_not_found_on_icloud": "Üksust ei leitud iCloud'ist. Üksus vÃĩib olla iCloud'is oleva vigase faili tÃĩttu kättesaamatu", "asset_offline": "Üksus pole kättesaadav", "asset_offline_description": "Seda välise kogu Ãŧksust ei leitud kettalt. Abi saamiseks palun vÃĩta Ãŧhendust oma Immich'i administraatoriga.", "asset_restored_successfully": "Üksus edukalt taastatud", @@ -690,7 +725,7 @@ "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_undo_this_action": "Seda tegevust ei saa tagasi vÃĩtta!", "cannot_update_the_description": "Kirjelduse muutmine ebaÃĩnnestus", "cast": "Edasta", "cast_description": "Seadista saadavalolevaid voogedastuse sihtpunkte", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Paroolid ei klapi", "change_password_form_reenter_new_password": "Korda uut parooli", "change_pin_code": "Muuda PIN-koodi", + "change_trigger": "Muuda päästikut", + "change_trigger_prompt": "Kas oled kindel, et soovid päästikut muuta? See eemaldab kÃĩik olemasolevad tegevused ja filtrid.", "change_your_password": "Muuda oma parooli", "changed_visibility_successfully": "Nähtavus muudetud", "charging": "Laadimine", @@ -722,6 +759,18 @@ "checksum": "Kontrollsumma", "choose_matching_people_to_merge": "Vali kattuvad isikud, mida Ãŧhendada", "city": "Linn", + "cleanup_confirm_description": "Immich leidis {count} Ãŧksus(t) (lisatud enne {date}), mis on turvaliselt serverisse varundatud. Kas eemaldada sellest seadmest lokaalsed koopiad?", + "cleanup_confirm_prompt_title": "Eemalda sellest seadmest?", + "cleanup_deleted_assets": "{count} Ãŧksust liigutatud seadme prÃŧgikasti", + "cleanup_deleting": "Liigutatakse prÃŧgikasti...", + "cleanup_found_assets": "Leitud {count} varundatud Ãŧksus(t)", + "cleanup_found_assets_with_size": "Leitud {count} varundatud Ãŧksust ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud jagatud albumid jäävad otsingust välja", + "cleanup_no_assets_found": "Ülalolevatele tingimustele vastavaid Ãŧksuseid ei leitud. Talletusruumi vabastamine saab eemaldada ainult Ãŧksuseid, mis on serverisse varundatud", + "cleanup_preview_title": "Üksused, mida eemaldada ({count})", + "cleanup_step3_description": "Otsi varundatud Ãŧksuseid, mis vastavad sinu kuupäeva ja alleshoidmise seadetele.", + "cleanup_step4_summary": "{count} Ãŧksust (loodud enne {date}) eemaldatakse lokaalsest seadmest. Fotod jäävad Immich'i rakenduse kaudu kättesaadavaks.", + "cleanup_trash_hint": "Talletusruumi vabastamiseks ava galeriirakendus ja tÃŧhjenda prÃŧgikast", "clear": "TÃŧhjenda", "clear_all": "TÃŧhjenda kÃĩik", "clear_all_recent_searches": "TÃŧhjenda hiljutised otsingud", @@ -733,6 +782,8 @@ "client_cert_import": "Impordi", "client_cert_import_success_msg": "Klientsertifikaat on imporditud", "client_cert_invalid_msg": "Vigane sertifikaadi fail vÃĩi vale parool", + "client_cert_password_message": "Sisesta sertifikaadi salasÃĩna", + "client_cert_password_title": "Sertifikaadi salasÃĩna", "client_cert_remove_msg": "Klientsertifikaat on eemaldatud", "client_cert_subtitle": "Toetab ainult PKCS12 (.p12, .pfx) formaati. Sertifikaadi importimine/eemaldamine on saadaval ainult enne sisselogimist", "client_cert_title": "SSL klientsertifikaat [EKSPERIMENTAALNE]", @@ -743,6 +794,11 @@ "color": "Värv", "color_theme": "Värviteema", "command": "Käsk", + "command_palette_prompt": "Leia kiirelt lehti, tegevusi vÃĩi käske", + "command_palette_to_close": "sulge", + "command_palette_to_navigate": "sisene", + "command_palette_to_select": "vali", + "command_palette_to_show_all": "näita kÃĩiki", "comment_deleted": "Kommentaar kustutatud", "comment_options": "Kommentaari valikud", "comments_and_likes": "Kommentaarid ja meeldimised", @@ -787,6 +843,7 @@ "create_album": "Lisa album", "create_album_page_untitled": "Pealkirjata", "create_api_key": "Lisa API vÃĩti", + "create_first_workflow": "Lisa esimene tÃļÃļvoog", "create_library": "Lisa kogu", "create_link": "Lisa link", "create_link_to_share": "Lisa jagamiseks link", @@ -801,17 +858,25 @@ "create_tag": "Lisa silt", "create_tag_description": "Lisa uus silt. Pesastatud siltide jaoks sisesta täielik tee koos kaldkriipsudega.", "create_user": "Lisa kasutaja", + "create_workflow": "Lisa tÃļÃļvoog", "created": "Lisatud", "created_at": "Lisatud", "creating_linked_albums": "Lingitud albumite loomine...", "crop": "Kärpimine", + "crop_aspect_ratio_fixed": "Fikseeritud", + "crop_aspect_ratio_free": "Vaba", + "crop_aspect_ratio_original": "Originaalne", "curated_object_page_title": "Asjad", "current_device": "Praegune seade", "current_pin_code": "Praegune PIN-kood", "current_server_address": "Praegune serveri aadress", + "custom_date": "Muu kuupäev", "custom_locale": "Kohandatud lokaat", "custom_locale_description": "Vorminda kuupäevad ja arvud vastavalt keelele ja regioonile", "custom_url": "Kohandatud URL", + "cutoff_date_description": "Jäta alles fotod ja videod viimasestâ€Ļ", + "cutoff_day": "{count, plural, one {päev} other {päeva}}", + "cutoff_year": "{count, plural, one {aasta} other {aastat}}", "daily_title_text_date": "d. MMMM", "daily_title_text_date_year": "d. MMMM yyyy", "dark": "Tume", @@ -867,6 +932,7 @@ "deselect_all": "Eemalda kÃĩik valikust", "details": "Üksikasjad", "direction": "Suund", + "disable": "Keela", "disabled": "Välja lÃŧlitatud", "disallow_edits": "Keela muutmine", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Manustatud videod", "download_include_embedded_motion_videos_description": "Lisa liikuvatesse fotodesse manustatud videod eraldi failidena", "download_notfound": "Allalaadimist ei leitud", + "download_original": "Laadi originaal alla", "download_paused": "Allalaadimine peatatud", "download_settings": "Allalaadimine", "download_settings_description": "Halda Ãŧksuste allalaadimise seadeid", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "Uuesti proovimise ootel", "downloading": "Allalaadimine", "downloading_asset_filename": "Üksuse {filename} allalaadimine", + "downloading_from_icloud": "iCloud'ist allalaadimine", "downloading_media": "Üksuste allalaadimine", "drop_files_to_upload": "Failide Ãŧleslaadimiseks sikuta need ÃŧkskÃĩik kuhu", "duplicates": "Duplikaadid", @@ -929,11 +997,22 @@ "edit_tag": "Muuda silti", "edit_title": "Muuda pealkirja", "edit_user": "Muuda kasutajat", - "editor": "Muutja", + "edit_workflow": "Muuda tÃļÃļvoogu", + "editor": "Redaktor", "editor_close_without_save_prompt": "Muudatusi ei salvestata", - "editor_close_without_save_title": "Sulge muutja?", - "editor_crop_tool_h2_aspect_ratios": "Kuvasuhted", - "editor_crop_tool_h2_rotation": "PÃļÃļre", + "editor_close_without_save_title": "Sulge redaktor?", + "editor_confirm_reset_all_changes": "Kas oled kindel, et soovid kÃĩik muudatused tÃŧhistada?", + "editor_discard_edits_confirm": "TÃŧhista muudatused", + "editor_discard_edits_prompt": "Sul on salvestamata muudatusi. Kas oled kindel, et soovid need tÃŧhistada?", + "editor_discard_edits_title": "TÃŧhista muudatused?", + "editor_edits_applied_error": "Muudatuste rakendamine ebaÃĩnnestus", + "editor_edits_applied_success": "Muudatused edukalt rakendatud", + "editor_flip_horizontal": "Peegelda horisontaalselt", + "editor_flip_vertical": "Peegelda vertikaalselt", + "editor_orientation": "Orientatsioon", + "editor_reset_all_changes": "TÃŧhista muudatused", + "editor_rotate_left": "PÃļÃļra 90° vastupäeva", + "editor_rotate_right": "PÃļÃļra 90° päripäeva", "email": "E-post", "email_notifications": "E-posti teavitused", "empty_folder": "See kaust on tÃŧhi", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Albumi sorteerimisjärjestuse muutmine ebaÃĩnnestus", "error_delete_face": "Viga näo kustutamisel", "error_getting_places": "Viga kohtade pärimisel", + "error_loading_albums": "Viga albumite laadimisel", "error_loading_image": "Viga pildi laadimisel", "error_loading_partners": "Viga partnerite laadimisel: {error}", + "error_retrieving_asset_information": "Viga Ãŧksuse info pärimisel", "error_saving_image": "Viga: {error}", "error_tag_face_bounding_box": "Viga näo sildistamisel - Ãŧmbritseva kasti koordinaate ei Ãĩnnestunud leida", "error_title": "Viga - midagi läks valesti", + "error_while_navigating": "Viga Ãŧksuse juurde navigeerimisel", "errors": { "cannot_navigate_next_asset": "Järgmise Ãŧksuse juurde liikumine ebaÃĩnnestus", "cannot_navigate_previous_asset": "Eelmise Ãŧksuse juurde liikumine ebaÃĩnnestus", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "OAuth sisselogimine ebaÃĩnnestus", "unable_to_connect": "Ühendumine ebaÃĩnnestus", "unable_to_copy_to_clipboard": "Ei saanud kopeerida lÃĩikelauale, kontrolli, kas kasutad lehte Ãŧle https-i", + "unable_to_create": "TÃļÃļvoo lisamine ebaÃĩnnestus", "unable_to_create_admin_account": "Administraatori konto loomine ebaÃĩnnestus", "unable_to_create_api_key": "Uue API vÃĩtme lisamine ebaÃĩnnestus", "unable_to_create_library": "Kogu lisamine ebaÃĩnnestus", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Välistamismustri kustutamine ebaÃĩnnestus", "unable_to_delete_shared_link": "Jagatud lingi kustutamine ebaÃĩnnestus", "unable_to_delete_user": "Kasutaja kustutamine ebaÃĩnnestus", + "unable_to_delete_workflow": "TÃļÃļvoo kustutamine ebaÃĩnnestus", "unable_to_download_files": "Failide allalaadimine ebaÃĩnnestus", "unable_to_edit_exclusion_pattern": "Välistamismustri muutmine ebaÃĩnnestus", "unable_to_empty_trash": "PrÃŧgikasti tÃŧhjendamine ebaÃĩnnestus", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "Kogu skaneerimine ebaÃĩnnestus", "unable_to_set_feature_photo": "EsiletÃĩstetud foto seadmine ebaÃĩnnestus", "unable_to_set_profile_picture": "Profiilipildi seadmine ebaÃĩnnestus", + "unable_to_set_rating": "Hinnangu seadmine ebaÃĩnnestus", "unable_to_submit_job": "TÃļÃļte edastamine ebaÃĩnnestus", "unable_to_trash_asset": "Üksuse prÃŧgikasti liigutamine ebaÃĩnnestus", "unable_to_unlink_account": "Konto lahtiÃŧhendamine ebaÃĩnnestus", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Seadete muutmine ebaÃĩnnestus", "unable_to_update_timeline_display_status": "Ajajoonel kuvamise uuendamine ebaÃĩnnestus", "unable_to_update_user": "Kasutaja muutmine ebaÃĩnnestus", + "unable_to_update_workflow": "TÃļÃļvoo uuendamine ebaÃĩnnestus", "unable_to_upload_file": "Faili Ãŧleslaadimine ebaÃĩnnestus" }, + "errors_text": "Vead", "exclusion_pattern": "Välistamismuster", "exif": "Exif", "exif_bottom_sheet_description": "Lisa kirjeldus...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "ISIKUD", "exif_bottom_sheet_person_add_person": "Lisa nimi", "exit_slideshow": "Sulge slaidiesitlus", + "expand": "Laienda", "expand_all": "Näita kÃĩik", "experimental_settings_new_asset_list_subtitle": "TÃļÃļs", "experimental_settings_new_asset_list_title": "Luba eksperimentaalne fotoruudistik", @@ -1120,14 +1208,17 @@ "features": "Funktsioonid", "features_in_development": "Arendusjärgus olevad funktsioonid", "features_setting_description": "Halda rakenduse funktsioone", - "file_name": "Failinimi", "file_name_or_extension": "Failinimi vÃĩi -laiend", + "file_name_text": "Faili nimi", + "file_name_with_value": "Faili nimi: {file_name}", "file_size": "Failisuurus", "filename": "Failinimi", "filetype": "FailitÃŧÃŧp", "filter": "Filter", + "filter_description": "Tingimused, mille alusel Ãŧksuseid filtreerida", "filter_people": "Filtreeri isikuid", "filter_places": "Filtreeri kohti", + "filters": "Filtrid", "find_them_fast": "Leia teda kiiresti nime järgi otsides", "first": "Esimene", "fix_incorrect_match": "Paranda ebaÃĩige vaste", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Kaustavaate abil failisÃŧsteemis olevate fotode ja videote sirvimine", "forgot_pin_code_question": "Unustasid oma PIN-koodi?", "forward": "Edasi", + "free_up_space": "Vabasta talletusruumi", + "free_up_space_description": "Liiguta varundatud fotod ja videod prÃŧgikasti, et talletusruumi vabastada. Serveris olevad koopiad jäävad alles.", + "free_up_space_settings_subtitle": "Vabasta seadme talletusruumi", "full_path": "Täielik tee: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "See funktsionaalsus laadib tÃļÃļtamiseks Google'st väliseid ressursse.", "general": "Üldine", "geolocation_instruction_location": "KlÃĩpsa GPS-koordinaatidega Ãŧksusel, et kasutada selle asukohta, vÃĩi vali asukoht otse kaardilt", "get_help": "KÃŧsi abi", + "get_people_error": "Viga isikute pärimisel", "get_wifiname_error": "WiFi-vÃĩrgu nime ei Ãĩnnestunud lugeda. Veendu, et oled andnud vajalikud load ja oled WiFi-vÃĩrguga Ãŧhendatud", "getting_started": "Alustamine", "go_back": "Tagasi", @@ -1175,6 +1270,7 @@ "hide_named_person": "Peida isik {name}", "hide_password": "Peida parool", "hide_person": "Peida isik", + "hide_schema": "Peida skeem", "hide_text_recognition": "Peida tekstituvastus", "hide_unnamed_people": "Peida nimetud isikud", "home_page_add_to_album_conflicts": "{added} Ãŧksust lisati albumisse {album}. {failed} Ãŧksust oli juba albumis.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "TÃļÃļtlemine toimus {dateTime}", "items_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}}", "jobs": "TÃļÃļted", + "json_editor": "JSON-redaktor", + "json_error": "JSON-i viga", "keep": "Jäta alles", + "keep_albums": "Jäta albumid alles", + "keep_albums_count": "{count} {count, plural, one {album} other {albumit}} jäetakse alles", "keep_all": "Jäta kÃĩik alles", + "keep_description": "Vali, mis talletusruumi vabastamise käigus su seadmesse alles jääb.", + "keep_favorites": "Jäta lemmikud alles", + "keep_on_device": "Hoia seadmes", + "keep_on_device_hint": "Vali Ãŧksused, mida selles seadmes hoida", "keep_this_delete_others": "Säilita see, kustuta Ãŧlejäänud", + "keeping": "Jäetakse alles: {items}", "kept_this_deleted_others": "See Ãŧksus säilitatud ning {count, plural, one {# Ãŧksus} other {# Ãŧksust}} kustutatud", "keyboard_shortcuts": "Kiirklahvid", "language": "Keel", @@ -1343,10 +1448,28 @@ "loop_videos_description": "LÃŧlita sisse, et detailvaates videot automaatselt taasesitada.", "main_branch_warning": "Sa kasutad arendusversiooni; soovitame tungivalt kasutada väljalaskeversiooni!", "main_menu": "PeamenÃŧÃŧ", + "maintenance_action_restore": "Andmebaasi taastamine", "maintenance_description": "Immich on hooldusreÅžiimis.", "maintenance_end": "LÃĩpeta hooldusreÅžiim", "maintenance_end_error": "HooldusreÅžiimi lÃĩpetamine ebaÃĩnnestus.", "maintenance_logged_in_as": "Logitud sisse kasutajana {user}", + "maintenance_restore_from_backup": "Taasta varukoopiast", + "maintenance_restore_library": "Taasta oma kogu", + "maintenance_restore_library_confirm": "Kui kÃĩik tundub Ãĩige, jätka varukoopiast taastamisega!", + "maintenance_restore_library_description": "Andmebaasi taastamine", + "maintenance_restore_library_folder_has_files": "Kaustas {folder} on {count} kaust(a)", + "maintenance_restore_library_folder_no_files": "Kaustas {folder} ei ole faile!", + "maintenance_restore_library_folder_pass": "loetav ja kirjutatav", + "maintenance_restore_library_folder_read_fail": "mitteloetav", + "maintenance_restore_library_folder_write_fail": "mittekirjutatav", + "maintenance_restore_library_hint_missing_files": "Olulised failid vÃĩivad puudu olla", + "maintenance_restore_library_hint_regenerate_later": "Saad need hiljem seadetes taastekitada", + "maintenance_restore_library_hint_storage_template_missing_files": "Kasutad talletusmalli? Faile vÃĩib puudu olla", + "maintenance_restore_library_loading": "Tervikluskontrollide ja heuristika laadimineâ€Ļ", + "maintenance_task_backup": "Olemasoleva andmebaasi varukoopia loomineâ€Ļ", + "maintenance_task_migrations": "Andmebaasi migratsioonide käivitamineâ€Ļ", + "maintenance_task_restore": "Valitud varukoopiast taastamineâ€Ļ", + "maintenance_task_rollback": "Taaste ebaÃĩnnestus, pÃļÃļrdutakse tagasi taastepunktiâ€Ļ", "maintenance_title": "Ajutiselt mittesaadaval", "make": "Mark", "manage_geolocation": "Halda asukohta", @@ -1408,6 +1531,8 @@ "minimize": "Minimeeri", "minute": "Minut", "minutes": "Minutit", + "mirror_horizontal": "Horisontaalne", + "mirror_vertical": "Vertikaalne", "missing": "Puuduvad", "mobile_app": "Mobiilirakendus", "mobile_app_download_onboarding_note": "Mobiilirakenduse allalaadimiseks kasuta järgnevaid valikuid", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Rohkem", "move": "Liiguta", + "move_down": "Liiguta alla", "move_off_locked_folder": "Liiguta lukustatud kaustast välja", "move_to": "Liiguta", + "move_to_device_trash": "Liiguta seadme prÃŧgikasti", "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", + "move_up": "Liiguta Ãŧles", "moved_to_archive": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} liigutatud arhiivi", "moved_to_library": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} liigutatud kogusse", "moved_to_trash": "Liigutatud prÃŧgikasti", @@ -1430,6 +1558,7 @@ "my_albums": "Minu albumid", "name": "Nimi", "name_or_nickname": "Nimi vÃĩi hÃŧÃŧdnimi", + "name_required": "Nimi on nÃĩutud", "navigate": "Navigeeri", "navigate_to_time": "Navigeeri aega", "network_requirement_photos_upload": "Kasuta fotode varundamiseks mobiilset andmesidet", @@ -1447,27 +1576,31 @@ "new_pin_code": "Uus PIN-kood", "new_pin_code_subtitle": "See on sul esimene kord lukustatud kausta kasutada. Turvaliseks ligipääsuks loo PIN-kood", "new_timeline": "Uus ajajoon", - "new_update": "Uus uuendus", + "new_update": "Uus versioon", "new_user_created": "Uus kasutaja lisatud", "new_version_available": "UUS VERSIOON SAADAVAL", "newest_first": "Uuemad eespool", "next": "Järgmine", "next_memory": "Järgmine mälestus", "no": "Ei", + "no_actions_added": "Ühtegi tegevust pole veel lisatud", + "no_albums_found": "Albumeid ei leitud", "no_albums_message": "Lisa album fotode ja videote organiseerimiseks", "no_albums_with_name_yet": "Paistab, et sul pole veel Ãŧhtegi selle nimega albumit.", "no_albums_yet": "Paistab, et sul pole veel Ãŧhtegi albumit.", "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita", - "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", + "no_assets_message": "Kliki esimese foto Ãŧleslaadimiseks", "no_assets_to_show": "Pole Ãŧksuseid, mida kuvada", "no_cast_devices_found": "Edastamise seadmeid ei leitud", "no_checksum_local": "Kontrollsumma pole saadaval - lokaalse Ãŧksuse pärimine ebaÃĩnnestus", "no_checksum_remote": "Kontrollsumma pole saadaval - kaugÃŧksuse pärimine ebaÃĩnnestus", + "no_configuration_needed": "Seadistus pole vajalik", "no_devices": "Autoriseeritud seadmeid pole", "no_duplicates_found": "Ühtegi duplikaati ei leitud.", "no_exif_info_available": "Exif info pole saadaval", "no_explore_results_message": "Oma kogu avastamiseks laadi Ãŧles rohkem fotosid.", "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida", + "no_filters_added": "Ühtegi filtrit pole veel lisatud", "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", "no_local_assets_found": "Selle kontrollsummaga lokaalseid Ãŧksuseid ei leitud", "no_location_set": "Asukoht pole määratud", @@ -1481,11 +1614,11 @@ "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", + "none": "Puudub", "not_allowed": "Keelatud", "not_available": "Pole saadaval", "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", "notes": "Märkused", "nothing_here_yet": "Siin pole veel midagi", "notification_permission_dialog_content": "Teavituste lubamiseks mine Seadetesse ja vali lubamine.", @@ -1515,6 +1648,7 @@ "online": "Ühendatud", "only_favorites": "Ainult lemmikud", "open": "Ava", + "open_calendar": "Ava kalender", "open_in_map_view": "Ava kaardi vaates", "open_in_openstreetmap": "Ava OpenStreetMap", "open_the_search_filters": "Ava otsingufiltrid", @@ -1563,6 +1697,7 @@ "people": "Isikud", "people_edits_count": "{count, plural, one {# isik} other {# isikut}} muudetud", "people_feature_description": "Fotode ja videote sirvimine inimeste kaupa grupeeritult", + "people_selected": "{count, plural, one {# isik valitud} other {# isikut valitud}}", "people_sidebar_description": "Kuva kÃŧlgmenÃŧÃŧs Isikute link", "permanent_deletion_warning": "Jäädavalt kustutamise hoiatus", "permanent_deletion_warning_setting_description": "Kuva hoiatust Ãŧksuste jäädaval kustutamisel", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# aastat}} vana", "person_birthdate": "SÃŧndinud {date}", "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}", + "person_recognized": "Isik tuvastatud", + "person_selected": "Isik valitud", "photo_shared_all_users": "Paistab, et oled oma fotosid kÃĩigi kasutajatega jaganud, vÃĩi pole Ãŧhtegi kasutajat, kellega jagada.", "photos": "Fotod", "photos_and_videos": "Fotod ja videod", "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}", "photos_from_previous_years": "Fotod varasematest aastatest", + "photos_only": "Ainult fotod", "pick_a_location": "Vali asukoht", "pick_custom_range": "Kohandatud vahemik", "pick_date_range": "Vali kuupäevavahemik", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "Serveri tootevÃĩtit haldab administraator", "query_asset_id": "Päringu Ãŧksuse ID", "queue_status": "Järjekorras {count}/{total}", + "rate_asset": "Hinda Ãŧksust", "rating": "Hinnang", "rating_clear": "TÃŧhjenda hinnang", "rating_count": "{count, plural, one {# tärn} other {# tärni}}", "rating_description": "Kuva infopaneelis EXIF hinnangut", + "rating_set": "Hinnanguks seatud {rating, plural, one {# tärn} other {# tärni}}", "reaction_options": "Reaktsiooni valikud", "read_changelog": "Vaata muudatuste Ãŧlevaadet", "readonly_mode_disabled": "KirjutuskaitsereÅžiim välja lÃŧlitatud", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} seostatud uue isikuga", "reassing_hint": "Seosta valitud Ãŧksused olemasoleva isikuga", "recent": "Hiljutine", - "recent-albums": "Hiljutised albumid", + "recent_albums": "Hiljutised albumid", "recent_searches": "Hiljutised otsingud", "recently_added": "Hiljuti lisatud", "recently_added_page_title": "Hiljuti lisatud", @@ -1770,9 +1910,11 @@ "saved_settings": "Seaded salvestatud", "say_something": "Ütle midagi", "scaffold_body_error_occurred": "Tekkis viga", + "scan": "Otsi", "scan_all_libraries": "Skaneeri kÃĩik kogud", "scan_library": "Skaneeri", "scan_settings": "Skaneerimise seaded", + "scanning": "Otsimine", "scanning_for_album": "Albumi skaneerimine...", "search": "Otsi", "search_albums": "Otsi albumeid", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Vali Ãŧksuse tÃŧÃŧp", "search_filter_ocr": "Otsi OCR-i abil", "search_filter_people_title": "Vali isikud", + "search_filter_star_rating": "Hinnang", "search_for": "Otsi", "search_for_existing_person": "Otsi olemasolevat isikut", "search_no_more_result": "Rohkem vasteid pole", @@ -1836,17 +1979,23 @@ "second": "Sekund", "see_all_people": "Vaata kÃĩiki isikuid", "select": "Vali", + "select_album": "Vali album", "select_album_cover": "Vali albumi kaanepilt", + "select_albums": "Vali albumid", "select_all": "Vali kÃĩik", "select_all_duplicates": "Vali kÃĩik duplikaadid", "select_all_in": "Vali kÃĩik grupis {group}", "select_avatar_color": "Vali avatari värv", + "select_count": "{count, plural, one {Vali #} other {Vali #}}", + "select_cutoff_date": "Vali kuupäev", "select_face": "Vali nägu", "select_featured_photo": "Vali esiletÃĩstetud foto", "select_from_computer": "Vali arvutist", "select_keep_all": "Vali jäta kÃĩik alles", "select_library_owner": "Vali kogu omanik", "select_new_face": "Vali uus nägu", + "select_people": "Vali isikud", + "select_person": "Vali isik", "select_person_to_tag": "Vali sildistamiseks isik", "select_photos": "Vali fotod", "select_trash_all": "Vali kÃĩik prÃŧgikasti", @@ -1982,6 +2131,7 @@ "show_password": "Kuva parooli", "show_person_options": "Näita isiku valikuid", "show_progress_bar": "Kuva edenemisriba", + "show_schema": "Kuva skeem", "show_search_options": "Kuva otsingu valikud", "show_shared_links": "Näita jagatud linke", "show_slideshow_transition": "Kuva slaidiesitluse Ãŧleminekud", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Kaustade juurde", "skip_to_tags": "Siltide juurde", "slideshow": "Slaidiesitlus", + "slideshow_repeat": "Korda slaidiesitlust", + "slideshow_repeat_description": "Mine slaidiesitluse lÃĩppedes tagasi algusesse", "slideshow_settings": "Slaidiesitluse seaded", "sort_albums_by": "Järjesta albumid...", "sort_created": "Loomise aeg", @@ -2038,6 +2190,7 @@ "support": "Tugi", "support_and_feedback": "Tugi ja tagasiside", "support_third_party_description": "Sinu Immich'i install on kolmanda osapoole pakendatud. Probleemid, mida täheldad, vÃĩivad olla pÃĩhjustatud selle pakendamise poolt, seega vÃĩta esmajärjekorras nendega Ãŧhendust, kasutades allolevaid linke.", + "supporter": "Toetaja", "swap_merge_direction": "Muuda Ãŧhendamise suunda", "sync": "SÃŧnkrooni", "sync_albums": "SÃŧnkrooni albumid", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Vali rakenduse teema seade", "theme_setting_three_stage_loading_subtitle": "Kolmeastmeline laadimine vÃĩib parandada laadimise jÃĩudlust, aga pÃĩhjustab oluliselt suuremat vÃĩrgukoormust", "theme_setting_three_stage_loading_title": "Luba kolmeastmeline laadimine", + "then": "Siis", "they_will_be_merged_together": "Nad Ãŧhendatakse kokku", "third_party_resources": "Kolmanda osapoole ressursid", "time": "Aeg", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Vali Ãŧksused", "trash_page_title": "PrÃŧgikast ({count})", "trashed_items_will_be_permanently_deleted_after": "PrÃŧgikasti tÃĩstetud Ãŧksused kustutatakse jäädavalt {days, plural, one {# päeva} other {# päeva}} pärast.", + "trigger": "Päästik", + "trigger_asset_uploaded": "Üksus Ãŧles laaditud", + "trigger_asset_uploaded_description": "Käivitub uue Ãŧksuse Ãŧleslaadimisel", + "trigger_description": "SÃŧndmus, mis käivitab tÃļÃļvoo", + "trigger_person_recognized": "Isik tuvastatud", + "trigger_person_recognized_description": "Käivitub isiku tuvastamisel", + "trigger_type": "Päästiku tÃŧÃŧp", "troubleshoot": "TÃĩrkeotsing", "type": "TÃŧÃŧp", "unable_to_change_pin_code": "PIN-koodi muutmine ebaÃĩnnestus", @@ -2123,6 +2284,7 @@ "unhide_person": "Ära peida isikut", "unknown": "Teadmata", "unknown_country": "Tundmatu riik", + "unknown_date": "Tundmatu kuupäev", "unknown_year": "Teadmata aasta", "unlimited": "Piiramatu", "unlink_motion_video": "TÃŧhista liikuva video linkimine", @@ -2139,17 +2301,19 @@ "unstack": "Eralda", "unstack_action_prompt": "{count} eraldatud", "unstacked_assets_count": "{count, plural, one {# Ãŧksus} other {# Ãŧksust}} eraldatud", + "unsupported_field_type": "Mittetoetatud välja tÃŧÃŧp", "untagged": "Sildistamata", + "untitled_workflow": "Pealkirjata tÃļÃļvoog", "up_next": "Järgmine", "update_location_action_prompt": "Uuenda {count} valitud Ãŧksuse asukoht:", "updated_at": "Uuendatud", "updated_password": "Parool muudetud", "upload": "Laadi Ãŧles", - "upload_action_prompt": "{count} Ãŧleslaadimise ootel", "upload_concurrency": "Üleslaadimise samaaegsus", "upload_details": "Üleslaadimise Ãŧksikasjad", "upload_dialog_info": "Kas soovid valitud Ãŧksuse(d) serverisse varundada?", "upload_dialog_title": "Üksuse Ãŧleslaadimine", + "upload_error_with_count": "Viga {count, plural, one {# Ãŧksuse} other {# Ãŧksuse}} Ãŧleslaadimisel", "upload_errors": "Üleslaadimine lÃĩpetatud {count, plural, one {# veaga} other {# veaga}}, uute Ãŧksuste nägemiseks värskenda lehte.", "upload_finished": "Üleslaadimine lÃĩpetatud", "upload_progress": "Ootel {remaining, number} - TÃļÃļdeldud {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "Kasutus", "use_biometric": "Kasuta biomeetriat", - "use_current_connection": "kasuta praegust Ãŧhendust", + "use_current_connection": "Kasuta praegust Ãŧhendust", "use_custom_date_range": "Kasuta kohandatud kuupäevavahemikku", "user": "Kasutaja", "user_has_been_deleted": "See kasutaja on kustutatud.", @@ -2185,6 +2349,7 @@ "utilities": "TÃļÃļriistad", "validate": "Valideeri", "validate_endpoint_error": "Sisesta korrektne URL", + "validation_error": "Valideerimise viga", "variables": "Muutujad", "version": "Versioon", "version_announcement_closing": "Sinu sÃĩber Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hÃĩljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hÃĩljutades.", "videos": "Videod", "videos_count": "{count, plural, one {# video} other {# videot}}", + "videos_only": "Ainult videod", "view": "Vaata", "view_album": "Vaata albumit", "view_all": "Vaata kÃĩiki", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "Kasuta peamise Ãŧksusena", "viewer_unstack": "Eralda", "visibility_changed": "{count, plural, one {# isiku} other {# isiku}} nähtavus muudetud", + "visual": "Visuaalne", + "visual_builder": "Visuaalne koostaja", "waiting": "Ootel", "waiting_count": "Ootel: {count}", "warning": "Hoiatus", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Tere tulemast Immich'isse", "width": "Laius", "wifi_name": "WiFi-vÃĩrgu nimi", - "workflow": "TÃļÃļvoog", + "workflow_delete_prompt": "Kas oled kindel, et soovid selle tÃļÃļvoo kustutada?", + "workflow_deleted": "TÃļÃļvoog kustutatud", + "workflow_description": "TÃļÃļvoo kirjeldus", + "workflow_info": "TÃļÃļvoo info", + "workflow_json": "TÃļÃļvoo JSON", + "workflow_json_help": "Muuda tÃļÃļvoo seadistust JSON-formaadis. Muudatused sÃŧnkroonitakse visuaalsesse koostajasse.", + "workflow_name": "TÃļÃļvoo nimi", + "workflow_navigation_prompt": "Kas oled kindel, et soovid lahkuda ilma muudatusi salvestamata?", + "workflow_summary": "TÃļÃļvoo kokkuvÃĩte", + "workflow_update_success": "TÃļÃļvoog edukalt uuendatud", + "workflow_updated": "TÃļÃļvoog uuendatud", + "workflows": "TÃļÃļvood", + "workflows_help_text": "TÃļÃļvood automatiseerivad tegevusi Ãŧksustega päästikute ja filtrite alusel", "wrong_pin_code": "Vale PIN-kood", "year": "Aasta", "years_ago": "{years, plural, one {# aasta} other {# aastat}} tagasi", "yes": "Jah", "you_dont_have_any_shared_links": "Sul pole Ãŧhtegi jagatud linki", "your_wifi_name": "Sinu WiFi-vÃĩrgu nimi", + "zero_to_clear_rating": "Ãŧksuse hinnangu tÃŧhistamiseks vajuta 0", "zoom_image": "Suumi pilti", "zoom_to_bounds": "Suumi piiridesse" } diff --git a/i18n/fa.json b/i18n/fa.json index 16937fd3ef..e7d681d92f 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -7,6 +7,7 @@ "action_common_update": "Ø¨Ų‡â€Œ ØąŲˆØ˛â€ŒØąØŗØ§Ų†ÛŒ", "actions": "ØšŲ…Ų„ÚŠØąØ¯", "active": "ŲØšØ§Ų„", + "active_count": "ŲØšØ§Ų„: {count}", "activity": "ŲØšØ§Ų„ÛŒØĒ", "add": "Ø§ŲØ˛ŲˆØ¯Ų†", "add_a_description": "ØĒ؈ØļیحاØĒ", @@ -28,6 +29,7 @@ "add_to_album_bottom_sheet_some_local_assets": "Ø¨ØąØŽÛŒ Ø§Ø˛ Ų…Ø­ØĒŲˆØ§Ų‡Ø§ÛŒ Ų…Ø­Ų„ÛŒ ØąØ§ Ų†Ø´Ø¯ Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… اØļØ§ŲŲ‡ ÚŠØąØ¯", "add_to_albums": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ…", "add_to_albums_count": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… Ų‡Ø§ {count}", + "add_to_bottom_bar": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡", "add_to_shared_album": "Ø§ŲØ˛ŲˆØ¯Ų† Ø¨Ų‡ ØĸŲ„Ø¨ŲˆŲ… اشØĒØąØ§ÚŠÛŒ", "add_upload_to_stack": "Ø§ŲØ˛ŲˆØ¯Ų† ŲØ§ÛŒŲ„ Ø§ØąØŗØ§Ų„ÛŒ Ø¨Ų‡ Ų…ØŦŲ…ŲˆØšŲ‡", "add_url": "Ø§ŲØ˛ŲˆØ¯Ų† ØĸØ¯ØąØŗ URL", @@ -42,6 +44,8 @@ "authentication_settings_disable_all": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ØĒŲ…Ø§Ų… ØąŲˆØ´â€ŒŲ‡Ø§ÛŒ ŲˆØąŲˆØ¯ ØąØ§ ØēÛŒØąŲØšØ§Ų„ ÚŠŲ†ÛŒØ¯ØŸ ŲˆØąŲˆØ¯ Ø¨Ų‡ ØˇŲˆØą ÚŠØ§Ų…Ų„ ØēÛŒØąŲØšØ§Ų„ ØŽŲˆØ§Ų‡Ø¯ شد.", "authentication_settings_reenable": "Ø¨ØąØ§ÛŒ ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ Ų…ØŦدد Ø§Ø˛ Ø¯ØŗØĒŲˆØą ØŗØąŲˆØą Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯.", "background_task_job": "ŲˆØ¸Ø§ÛŒŲ ŲžØŗâ€ŒØ˛Ų…ÛŒŲ†Ų‡", + "backup_onboarding_footer": "Ø¨ØąØ§ÛŒ Ø§ØˇŲ„Ø§ØšØ§ØĒ بیشØĒØą Ø¯ØąØ¨Ø§ØąŲ‡ بڊ ØĸŲž Ú¯ÛŒØąÛŒ Ø§Ø˛ Immich، Ų„ØˇŲØ§ Ø¨Ų‡ Ų…ØŗØĒŲ†Ø¯Ø§ØĒ Ų…ØąØ§ØŦØšŲ‡ ÚŠŲ†ÛŒØ¯.", + "backup_onboarding_title": "بڊ ØĸŲž Ų‡Ø§", "cleared_jobs": "ŲˆØ¸Ø§ÛŒŲ ŲžØ§ÚŠ Ø´Ø¯Ų‡ Ø¨ØąØ§ÛŒ:{job}", "config_set_by_file": "ØĒŲ†Ø¸ÛŒŲ… ŲØšŲ„ÛŒ ØĒŲˆØŗØˇ یڊ ŲØ§ÛŒŲ„ ŲžÛŒÚŠØąØ¨Ų†Ø¯ÛŒ Ø§Ų†ØŦØ§Ų… Ø´Ø¯Ų‡ Ø§ØŗØĒ", "confirm_delete_library": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ {library} ØąØ§ Ø­Ø°Ų ÚŠŲ†ÛŒØ¯ØŸ", @@ -50,9 +54,12 @@ "confirm_reprocess_all_faces": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ØĒŲ…Ø§Ų… Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ ØąØ§ Ų…ØŦددا ŲžØąØ¯Ø§Ø˛Ø´ ÚŠŲ†ÛŒØ¯ØŸ Ø§ÛŒŲ† ØšŲ…Ų„ باؚØĢ ŲžØ§ÚŠ Ø´Ø¯Ų† Ø§ŲØąØ§Ø¯ Ų…Ø´ØŽØĩ Ø´Ø¯Ų‡ Ų†ÛŒØ˛ ØŽŲˆØ§Ų‡Ø¯ شد.", "confirm_user_password_reset": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ØąŲ…Ø˛ ØšØ¨ŲˆØą {user} ØąØ§ Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ÚŠŲ†ÛŒØ¯ØŸ", "confirm_user_pin_code_reset": "Øĸیا Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید ÚŠŲ‡ Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ڊد PIN ‏{user} ØąØ§ Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ÚŠŲ†ÛŒØ¯ØŸ", + "copy_config_to_clipboard_description": "ÚŠŲžÛŒ ÚŠØ§Ų†ŲÛŒÚ¯ ŲØšŲ„ÛŒ ØŗÛŒØŗØĒŲ… Ø¯Øą Ų‚Ø§Ų„Ø¨ یڊ ØĸبØŦÚŠØĒ JSON Ø¯Øą ÚŠŲ„ÛŒŲž Ø¨ŲˆØąØ¯", + "create_job": "ایØŦاد ØŦاب", "disable_login": "ØēÛŒØąŲØšØ§Ų„ ÚŠØąØ¯Ų† ŲˆØąŲˆØ¯", "duplicate_detection_job_description": "اØŦØąØ§ÛŒ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ† Ø¨Øą ØąŲˆÛŒ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ Ø¨ØąØ§ÛŒ Ø´Ų†Ø§ØŗØ§ÛŒÛŒ ØĒØĩØ§ŲˆÛŒØą Ų…Ø´Ø§Ø¨Ų‡. Ø§ÛŒŲ† ŲˆØ§Ø¨ØŗØĒŲ‡ Ø¨Ų‡ ØŦØŗØĒØŦŲˆÛŒ Ų‡ŲˆØ´Ų…Ų†Ø¯ Ø§ØŗØĒ", "exclusion_pattern_description": "Ø§Ų„Ú¯ŲˆŲ‡Ø§ÛŒ Ø§ØŗØĒØĢŲ†Ø§ Ø¨Ų‡ Ø´Ų…Ø§ Ø§Ų…ÚŠØ§Ų† Ų…ÛŒâ€ŒØ¯Ų‡Ø¯ Ų‡Ų†Ú¯Ø§Ų… Ø§ØŗÚŠŲ† ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØŽŲˆØ¯ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ؈ ŲžŲˆØ´Ų‡â€ŒŲ‡Ø§ ØąØ§ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ø¨Ú¯ÛŒØąÛŒØ¯ . Ø§ÛŒŲ† Ų…ŲÛŒØ¯ Ø§ØŗØĒ Ø§Ú¯Øą ŲžŲˆØ´Ų‡â€ŒŲ‡Ø§ÛŒÛŒ Ø¯Ø§ØąÛŒØ¯ ÚŠŲ‡ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒÛŒ ØąØ§ Ø´Ø§Ų…Ų„ Ų…ÛŒâ€ŒØ´ŲˆŲ†Ø¯ ÚŠŲ‡ Ų†Ų…ÛŒâ€ŒØŽŲˆØ§Ų‡ÛŒØ¯ ŲˆØ§ØąØ¯ ÚŠŲ†ÛŒØ¯ØŒ Ų…Ø§Ų†Ų†Ø¯ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ RAW.", + "export_config_as_json_description": "Ø¯Ø§Ų†Ų„ŲˆØ¯ ÚŠØ§Ų†ŲÛŒÚ¯ ŲØšŲ„ÛŒ ØŗÛŒØŗØĒŲ… Ø¯Øą Ų‚Ø§Ų„Ø¨ یڊ ŲØ§ÛŒŲ„ JSON", "face_detection": "ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡", "face_detection_description": "ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ Ø¯Øą ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ با Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ†. Ø¨ØąØ§ÛŒ ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ØŒ ØĒŲ†Ų‡Ø§ ØĒØĩŲˆÛŒØą Ø¨Ų†Ø¯Ø§Ų†Ú¯Ø´ØĒی Ø¯Øą Ų†Ø¸Øą Ú¯ØąŲØĒŲ‡ Ų…ÛŒâ€ŒØ´ŲˆØ¯. Ú¯Ø˛ÛŒŲ†Ų‡ \"Ų‡Ų…Ų‡\" ØĒŲ…Ø§Ų… ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ØąØ§ (Ų…ØŦددا) ŲžØąØ¯Ø§Ø˛Ø´ Ų…ÛŒâ€ŒÚŠŲ†Ø¯. Ú¯Ø˛ÛŒŲ†Ų‡ \"Ú¯Ų…Ø´Ø¯Ų‡\" ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ØąØ§ Ø¯Øą Øĩ؁ Ų‚ØąØ§Øą Ų…ÛŒâ€ŒØ¯Ų‡Ø¯ ÚŠŲ‡ Ų‡Ų†ŲˆØ˛ ŲžØąØ¯Ø§Ø˛Ø´ Ų†Ø´Ø¯Ų‡â€ŒØ§Ų†Ø¯. Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ÛŒ ØĒØ´ØŽÛŒØĩ Ø¯Ø§Ø¯Ų‡ Ø´Ø¯Ų‡ ŲžØŗ Ø§Ø˛ اØĒŲ…Ø§Ų… ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡ØŒ Ø¨ØąØ§ÛŒ ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡ Ø¨Ų‡ ØĩŲˆØąØĒ Øĩ؁ Ø§Ų†ØĒØ¸Ø§Øą Ų‚ØąØ§Øą Ų…ÛŒâ€ŒÚ¯ÛŒØąŲ†Ø¯ØŒ ØĸŲ†â€ŒŲ‡Ø§ ØąØ§ Ø¨Ų‡ Ø§ŲØąØ§Ø¯ Ų…ŲˆØŦŲˆØ¯ یا ØŦدید Ú¯ØąŲˆŲ‡â€ŒØ¨Ų†Ø¯ÛŒ Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", "facial_recognition_job_description": "Ú¯ØąŲˆŲ‡â€ŒØ¨Ų†Ø¯ÛŒ Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ÛŒ ØĒØ´ØŽÛŒØĩ Ø¯Ø§Ø¯Ų‡ Ø´Ø¯Ų‡ Ø¨Ų‡ Ø§ŲØąØ§Ø¯. Ø§ÛŒŲ† Ų…ØąØ­Ų„Ų‡ ŲžØŗ Ø§Ø˛ ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡ Ø§Ų†ØŦØ§Ų… Ų…ÛŒâ€ŒØ´ŲˆØ¯. Ú¯Ø˛ÛŒŲ†Ų‡ \"Ų‡Ų…Ų‡\" ØĒŲ…Ø§Ų… Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ ØąØ§ (Ų…ØŦددا) Ø¯ØŗØĒŲ‡ Ø¨Ų†Ø¯ÛŒ Ų…ÛŒâ€ŒÚŠŲ†Ø¯. Ú¯Ø˛ÛŒŲ†Ų‡ \"Ú¯Ų…Ø´Ø¯Ų‡\" Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ ØąØ§ Ø¯Øą Øĩ؁ Ų‚ØąØ§Øą Ų…ÛŒâ€ŒØ¯Ų‡Ø¯ ÚŠŲ‡ Ø¨Ų‡ Ų‡ÛŒÚ† ŲØąØ¯ÛŒ ا؎ØĒØĩاØĩ Ø¯Ø§Ø¯Ų‡ Ų†Ø´Ø¯Ų‡â€ŒØ§Ų†Ø¯.", @@ -76,24 +83,36 @@ "image_resolution_description": "؈ØļŲˆØ­ Ø¨Ø§Ų„Ø§ØĒØą Ų…ÛŒâ€ŒØĒŲˆØ§Ų†Ø¯ ØŦØ˛ØĻیاØĒ بیشØĒØąÛŒ ØąØ§ Ø­ŲØ¸ ÚŠŲ†Ø¯ØŒ Ø§Ų…Ø§ ØĒØ¨Ø¯ÛŒŲ„ ØĸŲ† Ø˛Ų…Ø§Ų† بیشØĒØąÛŒ Ų…ÛŒâ€ŒØ¨ØąØ¯ØŒ Ø­ØŦŲ… ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ØąØ§ Ø§ŲØ˛Ø§ÛŒØ´ Ų…ÛŒâ€ŒØ¯Ų‡Ø¯ ؈ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ ŲžØ§ØŗØŽâ€ŒÚ¯ŲˆÛŒÛŒ Ø¨ØąŲ†Ø§Ų…Ų‡ ØąØ§ ÚŠØ§Ų‡Ø´ Ø¯Ų‡Ø¯.", "image_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØšÚŠØŗ", "image_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ÚŠÛŒŲÛŒØĒ ؈ ؈ØļŲˆØ­ ØĒØĩØ§ŲˆÛŒØą ØĒŲˆŲ„ÛŒØ¯ Ø´Ø¯Ų‡", + "import_config_from_json_description": "ŲˆØ§ØąØ¯ ÚŠØąØ¯Ų† ÚŠØ§Ų†ŲÛŒÚ¯ ØŗÛŒØŗØĒŲ… با ØĸŲžŲ„ŲˆØ¯ یڊ ŲØ§ÛŒŲ„ JSON", "job_concurrency": "Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ {job}", + "job_created": "ØŦاب ØŗØ§ØŽØĒŲ‡ شد", "job_not_concurrency_safe": "Ø§ÛŒŲ† ÚŠØ§Øą Ø§ÛŒŲ…Ų†ÛŒ Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ ØąØ§ ØĒØļŲ…ÛŒŲ† Ų†Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", "job_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØ§Øą", "job_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ ÚŠØ§Øą", "library_created": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ایØŦاد Ø´Ø¯Ų‡: {library}", "library_deleted": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ Ø­Ø°Ų شد", + "library_folder_description": "یڊ ŲžŲˆØ´Ų‡ Ø¨ØąØ§ÛŒ Ø§ÛŒŲ…ŲžŲˆØąØĒ Ų…Ø´ØŽØĩ ÚŠŲ†ÛŒØ¯. Ø§ÛŒŲ† ŲžŲˆØ´Ų‡ ؈ ŲžŲˆØ´Ų‡ Ų‡Ø§ÛŒ Ø¯Ø§ØŽŲ„ ØĸŲ†ØŒ Ø¨ØąØ§ÛŒ ØšÚŠØŗ Ų‡Ø§ ؈ ŲˆÛŒØ¯ÛŒŲˆ Ų‡Ø§ Ø§ØŗÚŠŲ† Ų…ÛŒ Ø´ŲˆŲ†Ø¯.", + "library_remove_folder_prompt": "Øĸیا Ø§Ø˛ Ø­Ø°Ų Ø§ÛŒŲ† ŲžŲˆØ´Ų‡ Ø§ÛŒŲ…ŲžŲˆØąØĒ Ų…ØˇŲ…ØĻŲ† Ų‡ØŗØĒید؟", "library_scanning": "Ø§ØŗÚŠŲ† Ø¯ŲˆØąŲ‡ ای", "library_scanning_description": "ØĒŲ†Ø¸ÛŒŲ… Ø§ØŗÚŠŲ† Ø¯ŲˆØąŲ‡â€ŒØ§ÛŒ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡", "library_scanning_enable_description": "ŲØšØ§Ų„ ÚŠØąØ¯Ų† Ø§ØŗÚŠŲ† Ø¯ŲˆØąŲ‡â€ŒØ§ÛŒ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡", "library_settings": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØŽØ§ØąØŦی", "library_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØŽØ§ØąØŦی", - "library_tasks_description": "Ø§Ų†ØŦØ§Ų… ŲˆØ¸Ø§ÛŒŲ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡", + "library_tasks_description": "Ø§ØŗÚŠŲ† ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ Ų‡Ø§ÛŒ ØŽØ§ØąØŦی Ø¨ØąØ§ÛŒ ŲØ§ÛŒŲ„ Ų‡Ø§ÛŒ ØŦدید ؈/یا ŲØ§ÛŒŲ„ Ų‡Ø§ÛŒ ØĒØēÛŒÛŒØą ÚŠØąØ¯Ų‡", + "library_updated": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ ØĸŲžØ¯ÛŒØĒ Ø´Ø¯Ų‡", "library_watching_enable_description": "Ų†Ø¸Ø§ØąØĒ Ø¨Øą ØĒØēÛŒÛŒØąØ§ØĒ ŲØ§ÛŒŲ„ Ø¯Øą ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡â€ŒŲ‡Ø§ÛŒ ØŽØ§ØąØŦی", - "library_watching_settings": "Ų†Ø¸Ø§ØąØĒ Ø¨Øą ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ (ØĸØ˛Ų…Ø§ÛŒØ´ÛŒ)", + "library_watching_settings": "Ų†Ø¸Ø§ØąØĒ Ø¨Øą ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ [ØĸØ˛Ų…Ø§ÛŒØ´ÛŒ]", "library_watching_settings_description": "Ų†Ø¸Ø§ØąØĒ ØŽŲˆØ¯ÚŠØ§Øą Ø¨Øą ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ ØĒØēÛŒÛŒØą ÛŒØ§ŲØĒŲ‡", "logging_enable_description": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ ŲˆØąŲˆØ¯", "logging_level_description": "ŲˆŲ‚ØĒی ŲØšØ§Ų„ باشد، Ø§Ø˛ Ú†Ų‡ ØŗØˇØ­ Ú¯Ø˛Ø§ØąØ´ Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø´ŲˆØ¯.", "logging_settings": "Ú¯Ø˛Ø§ØąØ´Ø§ØĒ", + "machine_learning_availability_checks": "Ø¨ØąØąØŗÛŒ Ų‡Ø§ÛŒ Ø¯ØŗØĒØąØŗ ŲžØ°ÛŒØąÛŒ", + "machine_learning_availability_checks_description": "ØĒØ´ØŽÛŒØĩ ØŽŲˆØ¯ÚŠØ§Øą ؈ ØĒØąØŦیح ØŗØąŲˆØą Ų‡Ø§ÛŒ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ† Ų…ŲˆØŦŲˆØ¯", + "machine_learning_availability_checks_enabled": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ Ø¨ØąØąØŗÛŒ Ų‡Ø§ÛŒ Ø¯ØŗØĒØąØŗ ŲžØ°ÛŒØ˛ÛŒ", + "machine_learning_availability_checks_interval": "ŲˆŲ‚ŲŲ‡ Ø¨ØąØąØŗÛŒ", + "machine_learning_availability_checks_interval_description": "Ų…Ø¯ØĒ ŲˆŲ‚ŲŲ‡ بØĩŲˆØąØĒ Ų…ÛŒŲ„ÛŒ ØĢØ§Ų†ÛŒŲ‡ Ø¨ÛŒŲ† Ø¨ØąØąØŗÛŒ Ų‡Ø§ÛŒ Ø¯ØŗØĒØąØŗ ŲžØ°ÛŒØąÛŒ", + "machine_learning_availability_checks_timeout": "ØĒØ§ÛŒŲ… Ø§ŲˆØĒ Ø¯ØąØŽŲˆØ§ØŗØĒ", + "machine_learning_availability_checks_timeout_description": "Ø˛Ų…Ø§Ų† ØĒØ§ÛŒŲ… Ø§ŲˆØĒ بØĩŲˆØąØĒ Ų…ÛŒŲ„ÛŒ ØĢØ§Ų†ÛŒŲ‡ Ø¨ØąØ§ÛŒ Ø¨ØąØąØŗÛŒ Ø¯ØŗØĒØąØŗ ŲžØ°ÛŒØąÛŒ", "machine_learning_clip_model": "Ų…Ø¯Ų„ CLIP", "machine_learning_clip_model_description": "Ų†Ø§Ų… یڊ Ų…Ø¯Ų„ CLIP ÚŠŲ‡ Ø¯Øą Ø§ÛŒŲ†ØŦا ŲŲ‡ØąØŗØĒ Ø´Ø¯Ų‡ Ø§ØŗØĒ. ØĒ؈ØŦŲ‡ داشØĒŲ‡ باشید ÚŠŲ‡ ŲžØŗ Ø§Ø˛ ØĒØēÛŒÛŒØą Ų…Ø¯Ų„ØŒ باید ÚŠØ§Øą 'ØŦØŗØĒØŦŲˆÛŒ Ų‡ŲˆØ´Ų…Ų†Ø¯' ØąØ§ Ø¨ØąØ§ÛŒ Ų‡Ų…Ų‡ ØĒØĩØ§ŲˆÛŒØą Ø¯ŲˆØ¨Ø§ØąŲ‡ اØŦØąØ§ ÚŠŲ†ÛŒØ¯.", "machine_learning_duplicate_detection": "ØĒØ´ØŽÛŒØĩ ØĒÚŠØąØ§ØąÛŒ Ų‡Ø§", @@ -116,19 +135,32 @@ "machine_learning_min_detection_score_description": "Ø­Ø¯Ø§Ų‚Ų„ Ø§Ų…ØĒÛŒØ§Ø˛ اؚØĒŲ…Ø§Ø¯ Ø¨ØąØ§ÛŒ ØĒØ´ØŽÛŒØĩ یڊ Ú†Ų‡ØąŲ‡ØŒ Ø¯Øą Ø¨Ø§Ø˛Ų‡ 0 ØĒا 1 Ų‚ØąØ§Øą Ø¯Ø§ØąØ¯. Ų…Ų‚Ø§Ø¯ÛŒØą ÚŠŲ…ØĒØą باؚØĢ ØĒØ´ØŽÛŒØĩ بیشØĒØą Ú†Ų‡ØąŲ‡ Ų…ÛŒâ€ŒØ´ŲˆØ¯ØŒ Ø§Ų…Ø§ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ Ų…Ų†ØŦØą Ø¨Ų‡ ØĒØ´ØŽÛŒØĩâ€ŒŲ‡Ø§ÛŒ اشØĒØ¨Ø§Ų‡ Ø´ŲˆØ¯.", "machine_learning_min_recognized_faces": "Ø­Ø¯Ø§Ų‚Ų„ Ú†Ų‡ØąŲ‡ Ų‡Ø§ÛŒ Ø´Ų†Ø§ØŽØĒŲ‡ Ø´Ø¯Ų‡", "machine_learning_min_recognized_faces_description": "Ø­Ø¯Ø§Ų‚Ų„ ØĒؚداد Ú†Ų‡ØąŲ‡â€ŒŲ‡Ø§ÛŒ ØĒØ´ØŽÛŒØĩ Ø¯Ø§Ø¯Ų‡ Ø´Ø¯Ų‡ Ø¨ØąØ§ÛŒ ایØŦاد یڊ Ø´ØŽØĩ. Ø§ŲØ˛Ø§ÛŒØ´ Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą باؚØĢ Ø¯Ų‚ÛŒŲ‚â€ŒØĒØą Ø´Ø¯Ų† ØĒØ´ØŽÛŒØĩ Ú†Ų‡ØąŲ‡ Ų…ÛŒâ€ŒØ´ŲˆØ¯ØŒ Ø§Ų…Ø§ Ų‡Ų…Ø˛Ų…Ø§Ų† باؚØĢ Ø§ŲØ˛Ø§ÛŒØ´ احØĒŲ…Ø§Ų„ Ø§ÛŒŲ† Ų…ÛŒâ€ŒØ´ŲˆØ¯ ÚŠŲ‡ یڊ Ú†Ų‡ØąŲ‡ Ø¨Ų‡ یڊ Ø´ØŽØĩ Ų†ØŗØ¨ØĒ Ø¯Ø§Ø¯Ų‡ Ų†Ø´ŲˆØ¯.", + "machine_learning_ocr_description": "Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ† Ø¨ØąØ§ÛŒ ØĒØ´ØŽÛŒØĩ Ų…ØĒŲ† Ø¯Ø§ØŽŲ„ ØšÚŠØŗ Ų‡Ø§", + "machine_learning_ocr_enabled": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ OCR", + "machine_learning_ocr_enabled_description": "Ø§Ú¯Øą ØēÛŒØą ŲØšØ§Ų„ باشد، ØĒØ´ØŽÛŒØĩ Ų…ØĒŲ† ØąŲˆÛŒ ØšÚŠØŗ Ų‡Ø§ Ø§Ų†ØŦØ§Ų… Ų†Ų…ÛŒ Ø´ŲˆØ¯.", + "machine_learning_ocr_max_resolution": "ØąØ˛ŲˆŲ„ŲˆØ´Ų† Ų…Ø§ÚŠØŗÛŒŲ…Ų…", + "machine_learning_ocr_max_resolution_description": "ŲžÛŒØ´ Ų†Ų…Ø§ÛŒØ´ Ų‡Ø§ÛŒ Ø¨Ø§Ų„Ø§ÛŒ Ø§ÛŒŲ† ØąØ˛ŲˆŲ„ŲˆØ´Ų† با Ø­ŲØ¸ Ų†ØŗØ¨ØĒ ØĒØĩŲˆÛŒØą ØĒØēÛŒÛŒØą Ø§Ų†Ø¯Ø§Ø˛Ų‡ Ø¯Ø§Ø¯Ų‡ Ų…ÛŒ Ø´ŲˆŲ†Ø¯. Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø˛ØąÚ¯ØĒØą Ø¯Ų‚ØĒ Ø¨Ø§Ų„Ø§ØĒØąÛŒ Ø¯Ø§ØąŲ†Ø¯ØŒ ŲˆŲ„ÛŒ Ø˛Ų…Ø§Ų† ؈ Ø­Ø§ŲØ¸Ų‡ بیشØĒØąÛŒ Ø¨ØąØ§ÛŒ ŲžØąØ¯Ø§Ø˛Ø´ Ų†ÛŒØ§Ø˛ Ø¯Ø§ØąŲ†Ø¯.", + "machine_learning_ocr_min_detection_score": "Ø­Ø¯Ø§Ų‚Ų„ Ø§Ų…ØĒÛŒØ§Ø˛ ØĒØ´ØŽÛŒØĩ", + "machine_learning_ocr_min_detection_score_description": "Ø­Ø¯Ø§Ų‚Ų„ Ø§Ų…ØĒÛŒØ§Ø˛ Ø§ØˇŲ…ÛŒŲ†Ø§Ų† Ø¨ÛŒŲ† 0 ØĒا 1 Ø¨ØąØ§ÛŒ Ų…ØĒŲ† Ų‡Ø§ ØĒا ØĒØ´ØŽÛŒØĩ Ø¯Ø§Ø¯Ų‡ Ø¨Ø´ŲˆŲ†Ø¯. Ų…Ų‚Ø§Ø¯ÛŒØą ÚŠŲ…ØĒØą Ų…ØĒŲ† بیشØĒØąÛŒ ØĒØ´ØŽÛŒØĩ Ų…ÛŒ Ø¯Ų‡Ų†Ø¯ ŲˆŲ„ÛŒ ØĒØ´ØŽÛŒØĩ Ų‡Ø§ÛŒ اشØĒØ¨Ø§Ų‡ بیشØĒØą Ų…ÛŒ Ø´ŲˆŲ†Ø¯.", + "machine_learning_ocr_min_recognition_score": "Ø­Ø¯Ø§Ų‚Ų„ Ø§Ų…ØĒÛŒØ§Ø˛ Ø´Ų†Ø§ØŗØ§ÛŒÛŒ", + "machine_learning_ocr_min_score_recognition_description": "Ø­Ø¯Ø§Ų‚Ų„ Ø§Ų…ØĒÛŒØ§Ø˛ Ø§ØˇŲ…ÛŒŲ†Ø§Ų† Ø¨ÛŒŲ† 0 ØĒا 1 Ø¨ØąØ§ÛŒ Ų…ØĒŲ† Ų‡Ø§ ØĒا Ø´Ų†Ø§ØŗØ§ÛŒÛŒ Ø´ŲˆŲ†Ø¯. Ų…Ų‚Ø§Ø¯ÛŒØą ÚŠŲ…ØĒØą Ų…ØĒŲˆŲ† بیشØĒØąÛŒ ØąØ§ Ø´Ų†Ø§ØŗØ§ÛŒÛŒ Ų…ÛŒ ÚŠŲ†Ų†Ø¯ ŲˆŲ„ÛŒ Ø´Ų†Ø§ØŗØ§ÛŒÛŒ Ų‡Ø§ÛŒ اشØĒØ¨Ø§Ų‡ ØĸŲ† Ų‡Ø§ بیشØĒØą Ų…ÛŒ Ø´ŲˆØ¯.", + "machine_learning_ocr_model": "Ų…Ø¯Ų„ OCR", + "machine_learning_ocr_model_description": "Ų…Ø¯Ų„ Ų‡Ø§ÛŒ ØŗØąŲˆØą Ø¯Ų‚ØĒ Ø¨Ø§Ų„Ø§ØĒØąÛŒ Ø§Ø˛ Ų…Ø¯Ų„ Ų‡Ø§ÛŒ Ų…ŲˆØ¨Ø§ÛŒŲ„ Ų‡ØŗØĒŲ†Ø¯ØŒ Ø§Ų…Ø§ ŲžØąØ¯Ø§Ø˛Ø´ ØĸŲ†Ų‡Ø§ ØˇŲˆŲ„Ø§Ų†ÛŒ ØĒØą Ø§ØŗØĒ ؈ Ø­Ø§ŲØ¸Ų‡ بیشØĒØąÛŒ Ų…ØĩØąŲ Ų…ÛŒ ÚŠŲ†Ų†Ø¯.", "machine_learning_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ†", "machine_learning_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ŲˆÛŒÚ˜Ú¯ÛŒâ€ŒŲ‡Ø§ ؈ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ†", "machine_learning_smart_search": "ØŦØŗØĒØŦŲˆÛŒ Ų‡ŲˆØ´Ų…Ų†Ø¯", "machine_learning_smart_search_description": "ØŦØŗØĒØŦŲˆÛŒ ØĒØĩØ§ŲˆÛŒØą با Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ ØĒØšØ¨ÛŒŲ‡â€ŒŲ‡Ø§ÛŒ CLIP Ø¨Ų‡ ØĩŲˆØąØĒ Ų…ØšŲ†Ø§ÛŒÛŒ", "machine_learning_smart_search_enabled": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ ØŦØŗØĒØŦŲˆÛŒ Ų‡ŲˆØ´Ų…Ų†Ø¯", "machine_learning_smart_search_enabled_description": "Ø§Ú¯Øą ØēÛŒØąŲØšØ§Ų„ باشد، ØĒØĩØ§ŲˆÛŒØą Ø¨ØąØ§ÛŒ ØŦØŗØĒØŦŲˆÛŒ Ų‡ŲˆØ´Ų…Ų†Ø¯ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ Ų†ØŽŲˆØ§Ų‡Ų†Ø¯ شد.", - "machine_learning_url_description": "ØĸØ¯ØąØŗÛŒ Ø§ÛŒŲ†ØĒØąŲ†ØĒی ØŗØąŲˆØą ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ†", + "machine_learning_url_description": "ØĸØ¯ØąØŗ ØŗØąŲˆØą ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ†. Ø§Ú¯Øą بیش Ø§Ø˛ یڊ ØĸØ¯ØąØŗ Ø¯Ø§Ø¯Ų‡ Ø´ŲˆØ¯ØŒ Ų‡Øą ØŗØąŲˆØą بØĩŲˆØąØĒ یڊی Ø¯Øą Ų„Ø­Ø¸Ų‡ Ø§Ų…ØĒØ­Ø§Ų† Ų…ÛŒ Ø´ŲˆŲ†Ø¯ ØĒا Ø˛Ų…Ø§Ų†ÛŒ ÚŠŲ‡ یڊی Ø§Ø˛ ØĸŲ†Ų‡Ø§ با Ų…ŲˆŲŲ‚ÛŒØĒ ŲžØ§ØŗØŽ Ø¯Ų‡Ø¯ØŒ Ø§Ø˛ Ø§ŲˆŲ„ Ø¨Ų‡ ØĸØŽØą. ØŗØąŲˆØą Ų‡Ø§ÛŒÛŒ ÚŠŲ‡ ŲžØ§ØŗØŽ Ų†Ø¯Ų‡Ų†Ø¯ بØĩŲˆØąØĒ Ų…ŲˆŲ‚ØĒ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ú¯ØąŲØĒŲ‡ Ų…ÛŒ Ø´ŲˆŲ†Ø¯ ØĒا Ø˛Ų…Ø§Ų†ÛŒ ÚŠŲ‡ Ø¨Ų‡ ؈ØļØšÛŒØĒ ØĸŲ†Ų„Ø§ÛŒŲ† Ø¨ØąÚ¯ØąØ¯Ų†Ø¯.", "manage_concurrency": "Ų…Ø¯ÛŒØąÛŒØĒ Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ", + "manage_concurrency_description": "ØąŲØĒŲ† Ø¨Ų‡ ØĩŲØ­Ų‡ ØŦاب Ų‡Ø§ Ø¨ØąØ§ÛŒ Ų…Ø¯ÛŒØąÛŒØĒ Ų‡Ų…Ø˛Ų…Ø§Ų†ÛŒ ØŦاب Ų‡Ø§", "manage_log_settings": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ú¯Ø˛Ø§ØąØ´", "map_dark_style": "Ø­Ø§Ų„ØĒ ØĒÛŒØąŲ‡", "map_enable_description": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ ŲˆÛŒÚ˜Ú¯ÛŒ Ų‡Ø§ÛŒ Ų†Ų‚Ø´Ų‡", "map_gps_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų‚Ø´Ų‡ ؈ ØŦی ŲžÛŒ Ø§Øŗ", "map_gps_settings_description": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų‚Ø´Ų‡ ؈ ØŦÛŒâ€ŒŲžÛŒâ€ŒØ§Øŗ (ژØĻŲˆÚŠØ¯ÛŒŲ†Ú¯ Ų…ØšÚŠŲˆØŗ) ØąØ§ Ų…Ø¯ÛŒØąÛŒØĒ ÚŠŲ†ÛŒØ¯", + "map_implications": "Ų‚Ø§Ø¨Ų„ÛŒØĒ Ų†Ų‚Ø´Ų‡ Ø¨Ų‡ یڊ ØŗØąŲˆÛŒØŗ tile ØŽØ§ØąØŦی Ų†ÛŒØ§Ø˛ Ø¯Ø§ØąØ¯ (tiles.immich.cloud)", "map_light_style": "Ø­Ø§Ų„ØĒ ØąŲˆØ´Ų†", "map_manage_reverse_geocoding_settings": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ Ų…ÚŠØ§Ų†ÛŒ Ų…ØšÚŠŲˆØŗ ", "map_reverse_geocoding": "ژØĻŲˆÚŠØ¯ÛŒŲ†Ú¯ Ų…ØšÚŠŲˆØŗ", @@ -137,15 +169,29 @@ "map_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų‚Ø´Ų‡ ؈ Ų…ÚŠØ§Ų†Ų‡Ø§ÛŒ ØąŲˆÛŒ Ų†Ų‚Ø´Ų‡", "map_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ų†Ų‚Ø´Ų‡", "map_style_description": "ØĸØ¯ØąØŗ Ø§ÛŒŲ†ØĒØąŲ†ØĒی (style.json) Ų†ŲˆØš Ų†Ų…Ø§ÛŒØ´ Ų†Ų‚Ø´Ų‡", + "memory_cleanup_job": "ŲžØ§ÚŠ ØŗØ§Ø˛ÛŒ Ø­Ø§ŲØ¸Ų‡", "metadata_extraction_job": "Ø§ØŗØĒØŽØąØ§ØŦ ŲØąØ§ Ø¯Ø§Ø¯Ų‡", "metadata_extraction_job_description": "Ø§ØŗØĒØŽØąØ§ØŦ Ø§ØˇŲ„Ø§ØšØ§ØĒ Ø§Ø¨ØąØ¯Ø§Ø¯Ų‡ØŒ Ų…Ø§Ų†Ų†Ø¯ Ų…ŲˆŲ‚ØšÛŒØĒ ØŦØēØąØ§ŲÛŒØ§ÛŒÛŒ ؈ ÚŠÛŒŲÛŒØĒ Ø§Ø˛ Ų‡Øą ŲØ§ÛŒŲ„", + "metadata_faces_import_setting": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ Ø§ÛŒŲ…ŲžŲˆØąØĒ ØĩŲˆØąØĒ", + "metadata_faces_import_setting_description": "Ø§ÛŒŲ…ŲžŲˆØąØĒ ØĩŲˆØąØĒ Ų‡Ø§ Ø§Ø˛ Ø§ØˇŲ„Ø§ØšØ§ØĒ EXIF ØšÚŠØŗ ؈ ŲØ§ÛŒŲ„ Ų‡Ø§ÛŒ sidecar", + "metadata_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Metadata", + "metadata_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ metadata", "migration_job": "Ų…Ų‡Ø§ØŦØąØĒ", + "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_start_time_setting": "Ø˛Ų…Ø§Ų† Ø´ØąŲˆØš", + "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 ØŗØąŲˆØą ØšÚŠØŗ \"", + "notification_email_from_address_description": "ØĸØ¯ØąØŗ Ø§ÛŒŲ…ÛŒŲ„ ŲØąØŗØĒŲ†Ø¯Ų‡ØŒ Ø¨Ų‡ ØšŲ†ŲˆØ§Ų† Ų…ØĢØ§Ų„:\"ØŗØąŲˆØą ØšÚŠØŗ Immich \". Ų…ØˇŲ…ØĻŲ† باشید Ø§Ø˛ ØĸØ¯ØąØŗÛŒ Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯ ÚŠŲ‡ اØŦØ§Ø˛Ų‡ Ø§ØąØŗØ§Ų„ Ø§ÛŒŲ…ÛŒŲ„ Ø§Ø˛ ØĸŲ† ØąØ§ Ø¯Ø§ØąÛŒØ¯.", "notification_email_host_description": "Ų…ÛŒØ˛Ø¨Ø§Ų† ØŗØąŲˆØą Ø§ÛŒŲ…ÛŒŲ„ (Ų…ØĢŲ„Ø§Ų‹ smtp.immich.app)", "notification_email_ignore_certificate_errors": "ØŽØˇØ§Ų‡Ø§ÛŒ Ú¯ŲˆØ§Ų‡ÛŒ ØąØ§ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ø¨Ú¯ÛŒØą", "notification_email_ignore_certificate_errors_description": "ØŽØˇØ§Ų‡Ø§ÛŒ اؚØĒØ¨Ø§ØąØŗŲ†ØŦی Ú¯ŲˆØ§Ų‡ÛŒ TLS ØąØ§ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ø¨Ú¯ÛŒØą (ØĒ؈ØĩÛŒŲ‡ Ų†Ų…ÛŒâ€ŒØ´ŲˆØ¯)", @@ -168,7 +214,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 اØŦØ§Ø˛Ų‡ Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ ØĸØ¯ØąØŗ Ų…ŲˆØ¨Ø§ÛŒŲ„ØŒ Ų…Ø§Ų†Ų†Ø¯ ''{callback}'' ØąØ§ Ų†Ų…ÛŒ Ø¯Ų‡Ø¯", "oauth_settings": "OAuth", "oauth_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ŲˆØąŲˆØ¯ Ø¨Ų‡ ØŗÛŒØŗØĒŲ… OAuth", "oauth_settings_more_details": "Ø¨ØąØ§ÛŒ ØŦØ˛ØĻیاØĒ بیشØĒØą Ø¯Øą Ų…ŲˆØąØ¯ Ø§ÛŒŲ† ŲˆÛŒÚ˜Ú¯ÛŒØŒ Ø¨Ų‡ Ų…ØŗØĒŲ†Ø¯Ø§ØĒ Ų…ØąØ§ØŦØšŲ‡ ÚŠŲ†ÛŒØ¯.", @@ -177,25 +223,36 @@ "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": "Ø˛Ų…Ø§Ų† ØĒØ§ÛŒŲ… Ø§ŲˆØĒ Ø¨ØąØ§ÛŒ Ø¯ØąØŽŲˆØ§ØŗØĒ Ų‡Ø§ بØĩŲˆØąØĒ Ų…ÛŒŲ„ÛŒ ØĢØ§Ų†ÛŒŲ‡", + "ocr_job_description": "Ø§ØŗØĒŲØ§Ø¯Ų‡ Ø§Ø˛ ÛŒØ§Ø¯Ú¯ÛŒØąÛŒ Ų…Ø§Ø´ÛŒŲ† Ø¨ØąØ§ÛŒ Ø´Ų†Ø§ØŗØ§ÛŒÛŒ Ų…ØĒŲ† Ø¯Øą ØšÚŠØŗ Ų‡Ø§", "password_enable_description": "ŲˆØąŲˆØ¯ با Ø§ÛŒŲ…ÛŒŲ„ ؈ Ú¯Ø°ØąŲˆØ§Ú˜Ų‡", "password_settings": "Ú¯Ø°ØąŲˆØ§Ú˜Ų‡ ŲˆØąŲˆØ¯", "password_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ú¯Ø°ØąŲˆØ§Ú˜Ų‡ ŲˆØąŲˆØ¯", "paths_validated_successfully": "ØĒŲ…Ø§Ų…ÛŒ Ų…ØŗÛŒØąŲ‡Ø§ با Ų…ŲˆŲŲ‚ÛŒØĒ ØĒØŖÛŒÛŒØ¯ Ø´Ø¯Ų†Ø¯", + "queue_details": "ØŦØ˛ØĻیاØĒ Øĩ؁", + "queues": "Øĩ؁ Ų‡Ø§ÛŒ ØŦاب", + "queues_page_description": "ØĩŲØ­Ų‡ Ø§Ø¯Ų…ÛŒŲ† Øĩ؁ Ų‡Ø§ÛŒ ØŦاب", "quota_size_gib": "Ų…Ų‚Ø¯Ø§Øą ØŗŲ‡Ų…ÛŒŲ‡ (گیگابایØĒ)", "refreshing_all_libraries": "Ø¨ØąŲˆØ˛ ØąØŗØ§Ų†ÛŒ Ų‡Ų…Ų‡ ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡ Ų‡Ø§", "registration": "ØĢبØĒ Ų†Ø§Ų… Ų…Ø¯ÛŒØą", "registration_description": "Ø§Ø˛ ØĸŲ†ØŦایی ÚŠŲ‡ Ø´Ų…Ø§ Ø§ŲˆŲ„ÛŒŲ† ÚŠØ§ØąØ¨Øą Ø¯Øą ØŗÛŒØŗØĒŲ… Ų‡ØŗØĒید، Ø¨Ų‡ ØšŲ†ŲˆØ§Ų† Ų…Ø¯ÛŒØą ØĒØšÛŒÛŒŲ† Ø´Ø¯Ų‡â€ŒØ§ÛŒØ¯ ؈ Ų…ØŗØĻŲˆŲ„ÛŒØĒ Ø§Ų†ØŦØ§Ų… ŲˆØ¸Ø§ÛŒŲ Ų…Ø¯ÛŒØąÛŒØĒی Ø¨Øą ØšŲ‡Ø¯Ų‡ Ø´Ų…Ø§ ØŽŲˆØ§Ų‡Ø¯ Ø¨ŲˆØ¯ ؈ ÚŠØ§ØąØ¨ØąØ§Ų† اØļØ§ŲÛŒ ØĒŲˆØŗØˇ Ø´Ų…Ø§ ایØŦاد ØŽŲˆØ§Ų‡Ų†Ø¯ شد.", + "remove_failed_jobs": "Ø­Ø°Ų ØŦاب Ų‡Ø§ÛŒ Ų†Ø§Ų…ŲˆŲŲ‚", "require_password_change_on_login": "Ø§Ų„Ø˛Ø§Ų… ÚŠØ§ØąØ¨Øą Ø¨Ų‡ ØĒØēÛŒÛŒØą Ú¯Ø°ØąŲˆØ§Ú˜Ų‡ Ø¯Øą Ø§ŲˆŲ„ÛŒŲ† ŲˆØąŲˆØ¯", "reset_settings_to_default": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨Ų‡ Ø­Ø§Ų„ØĒ ŲžÛŒØ´â€ŒŲØąØļ", "reset_settings_to_recent_saved": "Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø¨Ų‡ ØĸØŽØąÛŒŲ† ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø°ØŽÛŒØąŲ‡ Ø´Ø¯Ų‡", + "search_jobs": "ØŦاب Ų‡Ø§ÛŒ ØŦØŗØĒØŦ؈â€Ļ", "send_welcome_email": "Ø§ØąØŗØ§Ų„ Ø§ÛŒŲ…ÛŒŲ„ ØŽŲˆØ´ ØĸŲ…Ø¯ Ú¯ŲˆÛŒÛŒ", "server_external_domain_settings": "Ø¯Ø§Ų…Ų†Ų‡ ØŽØ§ØąØŦی", "server_external_domain_settings_description": "Ø¯Ø§Ų…Ų†Ų‡ Ø¨ØąØ§ÛŒ Ų„ÛŒŲ†ÚŠ Ų‡Ø§ÛŒ ØšŲ…ŲˆŲ…ÛŒ Ø¨Ų‡ اشØĒØąØ§ÚŠ گذاشØĒŲ‡ Ø´Ø¯Ų‡ØŒ Ø´Ø§Ų…Ų„ //:(s)http", + "server_public_users": "ÚŠØ§ØąØ¨ØąØ§Ų† ØšŲ…ŲˆŲ…ÛŒ", + "server_public_users_description": "ØĒŲ…Ø§Ų…ÛŒ ÚŠØ§ØąØ¨ØąØ§Ų† (Ø§ØŗŲ… ؈ Ø§ÛŒŲ…ÛŒŲ„) Ų‡Ų†Ú¯Ø§Ų… اØļØ§ŲŲ‡ ÚŠØąØ¯Ų† یڊ ÚŠØ§ØąØ¨Øą Ø¨Ų‡ یڊ ØĸŲ„Ø¨ŲˆŲ… Ų…Ø´ØĒØąÚŠ Ų„ÛŒØŗØĒ Ų…ÛŒ Ø´ŲˆŲ†Ø¯. ŲˆŲ‚ØĒی ØēÛŒØą ŲØšØ§Ų„ باشد، Ų„ÛŒØŗØĒ ÚŠØ§ØąØ¨ØąØ§Ų† ŲŲ‚Øˇ Ø¨ØąØ§ÛŒ Ø§Ø¯Ų…ÛŒŲ† Ų‚Ø§Ø¨Ų„ Ų…Ø´Ø§Ų‡Ø¯Ų‡ Ø§ØŗØĒ.", "server_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØŗØąŲˆØą", "server_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØŗØąŲˆØą", "server_welcome_message": "ŲžÛŒØ§Ų… ØŽŲˆØ´ ØĸŲ…Ø¯ Ú¯ŲˆÛŒÛŒ", "server_welcome_message_description": "ŲžÛŒØ§Ų…ÛŒ ÚŠŲ‡ Ø¯Øą ØĩŲØ­Ų‡ ŲˆØąŲˆØ¯ Ø¨Ų‡ ØŗÛŒØŗØĒŲ… Ų†Ų…Ø§ÛŒØ´ Ø¯Ø§Ø¯Ų‡ Ų…ÛŒ Ø´ŲˆØ¯.", + "settings_page_description": "ØĩŲØ­Ų‡ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ Ø§Ø¯Ų…ÛŒŲ†", "sidecar_job": "Ø§ØˇŲ„Ø§ØšØ§ØĒ ØŦØ§Ų†Ø¨ÛŒ", "sidecar_job_description": "ÛŒØ§ŲØĒŲ† یا Ų‡Ų…Ú¯Ø§Ų…â€ŒØŗØ§Ø˛ÛŒ Ø§ØˇŲ„Ø§ØšØ§ØĒ ØŦØ§Ų†Ø¨ÛŒ Ø§Ø˛ ŲØ§ÛŒŲ„ ØŗÛŒØŗØĒŲ…", "slideshow_duration_description": "Ø˛Ų…Ø§Ų† ( Ø¨Ų‡ ØĢØ§Ų†ÛŒŲ‡ ) Ų†Ø´Ø§Ų† Ø¯Ø§Ø¯Ų† Ų‡Øą ØšÚŠØŗ", @@ -214,6 +271,15 @@ "storage_template_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØŗØ§ØŽØĒØ§Øą ŲžŲˆØ´Ų‡ ؈ Ų†Ø§Ų… ŲØ§ÛŒŲ„ Ø¯Ø§ØąØ§ÛŒÛŒ Ø¨Ø§ØąÚ¯Ø°Ø§ØąÛŒ Ø´Ø¯Ų‡", "storage_template_user_label": "{label} Ø¨ØąÚ†ØŗØ¨ Ø°ØŽÛŒØąŲ‡â€ŒØŗØ§Ø˛ÛŒ ÚŠØ§ØąØ¨Øą Ø§ØŗØĒ", "system_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØŗÛŒØŗØĒŲ…", + "tag_cleanup_job": "ŲžØ§ÚŠ ØŗØ§Ø˛ÛŒ ØĒÚ¯", + "template_email_available_tags": "Ø´Ų…Ø§ Ų…ÛŒØĒŲˆØ§Ų†ÛŒØ¯ Ø§Ø˛ Ų…ØĒØēÛŒØą Ų‡Ø§ÛŒ ØąŲˆØ¨ØąŲˆ Ø¯Øą Ų‚Ø§Ų„Ø¨ ØŽŲˆØ¯ Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠŲ†ÛŒØ¯: {tags}", + "template_email_if_empty": "Ø§Ú¯Øą Ų‚Ø§Ų„Ø¨ ØŽØ§Ų„ÛŒ باشد، Ø§ÛŒŲ…ÛŒŲ„ ŲžÛŒØ´ŲØąØļ Ø§ØŗØĒŲØ§Ø¯Ų‡ ØŽŲˆØ§Ų‡Ø¯ شد.", + "template_email_preview": "ŲžÛŒØ´ Ų†Ų…Ø§ÛŒØ´", + "template_email_settings": "Ų‚Ø§Ų„Ø¨ Ų‡Ø§ÛŒ Ø§ÛŒŲ…ÛŒŲ„", + "template_email_update_album": "Ų‚Ø§Ų„Ø¨ Ø¨ØąŲˆØ˛ØąØŗØ§Ų†ÛŒ ØĸŲ„Ø¨ŲˆŲ…", + "template_email_welcome": "Ų‚Ø§Ų„Ø¨ Ø§ÛŒŲ…ÛŒŲ„ ØŽŲˆØ´ ØĸŲ…Ø¯ Ú¯ŲˆÛŒÛŒ", + "template_settings": "Ų‚Ø§Ų„Ø¨ Ų‡Ø§ÛŒ Ø§ØšŲ„Ø§Ų† Ų‡Ø§", + "template_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ Ų‚Ø§Ų„Ø¨ Ų‡Ø§ÛŒ ØŗŲØ§ØąØ´ÛŒ Ø¨ØąØ§ÛŒ Ø§ØšŲ„Ø§Ų† Ų‡Ø§", "theme_custom_css_settings": "CSS ØŗŲØ§ØąØ´ÛŒ", "theme_custom_css_settings_description": "Ø¨ØąÚ¯Ų‡â€ŒŲ‡Ø§ÛŒ ØŗØ¨ÚŠ ØĸØ¨Ø´Ø§ØąÛŒ (CSS) Ø§Ų…ÚŠØ§Ų† ØŗŲØ§ØąØ´ÛŒâ€ŒØŗØ§Ø˛ÛŒ ØˇØąØ§Ø­ÛŒ Immich ØąØ§ ŲØąØ§Ų‡Ų… Ų…ÛŒâ€ŒÚŠŲ†Ų†Ø¯.", "theme_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ŲžŲˆØŗØĒŲ‡", @@ -243,12 +309,12 @@ "transcoding_constant_rate_factor_description": "ØŗØˇØ­ ÚŠÛŒŲÛŒØĒ ŲˆÛŒØ¯ÛŒŲˆ. Ų‡ØąÚ†Ų‡ ؚدد ÚŠŲ…ØĒØą باشد، ÚŠÛŒŲÛŒØĒ Ø¨Ų‡ØĒØą Ø§ØŗØĒ، Ø§Ų…Ø§ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ Ø¨Ø˛ØąÚ¯â€ŒØĒØąÛŒ ØĒŲˆŲ„ÛŒØ¯ Ų…ÛŒâ€ŒÚŠŲ†Ø¯. Ų…Ų‚Ø§Ø¯ÛŒØą Ų…ØšŲ…ŲˆŲ„ ØšØ¨Ø§ØąØĒŲ†Ø¯ Ø§Ø˛: (23 <-- H.264) - (28 --> HEVC) - (31 --> VP9) - (35 --> AV1).", "transcoding_disabled_description": "Ų‡ÛŒÚ† ŲˆÛŒØ¯ÛŒŲˆÛŒÛŒ ØąØ§ ØĒØ¨Ø¯ÛŒŲ„ ŲØąŲ…ØĒ Ų†ÚŠŲ†ÛŒØ¯ØŒ Ø˛ÛŒØąØ§ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ ŲžØŽØ´ Ø¯Øą Ø¨ØąØŽÛŒ Ø§Ø˛ ÚŠŲ„Ø§ÛŒŲ†ØĒâ€ŒŲ‡Ø§ ØąØ§ Ų…ØŽØĒŲ„ ÚŠŲ†Ø¯", "transcoding_hardware_acceleration": "Ø´ØĒاب Ø¯Ų‡Ų†Ø¯Ų‡ ØŗØŽØĒ Ø§ŲØ˛Ø§ØąÛŒ", - "transcoding_hardware_acceleration_description": "ØĸØ˛Ų…Ø§ÛŒØ´ÛŒØ› Ø¨ØŗÛŒØ§Øą ØŗØąÛŒØšâ€ŒØĒØą Ø§ØŗØĒ، Ø§Ų…Ø§ Ø¯Øą Ų‡Ų…Ø§Ų† بیØĒâ€ŒØąÛŒØĒ ÚŠÛŒŲÛŒØĒ ÚŠŲ…ØĒØąÛŒ ØŽŲˆØ§Ų‡Ø¯ داشØĒ", + "transcoding_hardware_acceleration_description": "ØĸØ˛Ų…Ø§ÛŒØ´ÛŒ: Transcoding ØŗØąÛŒØš ØĒØą Ø§Ų…Ø§ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ Ø¯Øą bitrate ÛŒÚŠØŗØ§Ų† ÚŠÛŒŲÛŒØĒ ØąØ§ ÚŠØ§Ų‡Ø´ Ø¯Ų‡Ø¯", "transcoding_hardware_decoding": "ØąŲ…Ø˛Ú¯Ø´Ø§ÛŒÛŒ ØŗØŽØĒ Ø§ŲØ˛Ø§ØąÛŒ", "transcoding_max_b_frames": "بیشØĒØąÛŒŲ† B-frames", "transcoding_max_b_frames_description": "Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø§Ų„Ø§ØĒØą ÚŠØ§ØąØ§ÛŒÛŒ ŲØ´ØąØ¯Ų‡ ØŗØ§Ø˛ÛŒ ØąØ§ Ø¨Ų‡Ø¨ŲˆØ¯ Ų…ÛŒâ€ŒØ¨ØŽØ´Ų†Ø¯ØŒ Ø§Ų…Ø§ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ ØąØ§ ÚŠŲ†Ø¯ Ų…ÛŒâ€ŒÚŠŲ†Ų†Ø¯. Ų…Ų…ÚŠŲ† Ø§ØŗØĒ با Ø´ØĒاب Ø¯Ų‡ÛŒ ØŗØŽØĒâ€ŒØ§ŲØ˛Ø§ØąÛŒ Ø¯Øą Ø¯ØŗØĒÚ¯Ø§Ų‡â€ŒŲ‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØŗØ§Ø˛Ú¯Ø§Øą Ų†Ø¨Ø§Ø´Ø¯. Ų…Ų‚Ø¯Ø§Øą( 0 ) B-frames ØąØ§ ØēÛŒØąŲØšØ§Ų„ Ų…ÛŒâ€ŒÚŠŲ†Ø¯ØŒ Ø¯Øą Ø­Ø§Ų„ÛŒ ÚŠŲ‡ Ų…Ų‚Ø¯Ø§Øą ( 1 ) Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą ØąØ§ Ø¨Ų‡ ØĩŲˆØąØĒ ØŽŲˆØ¯ÚŠØ§Øą ØĒŲ†Ø¸ÛŒŲ… Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", "transcoding_max_bitrate": "بیشØĒØąÛŒŲ† بیØĒ ØąÛŒØĒ", - "transcoding_max_bitrate_description": "ØĒŲ†Ø¸ÛŒŲ… حداڊØĢØą بیØĒâ€ŒØąÛŒØĒ Ų…ÛŒâ€ŒØĒŲˆØ§Ų†Ø¯ Ø§Ų†Ø¯Ø§Ø˛Ų‡ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ØąØ§ Ø¯Øą حدی Ų‚Ø§Ø¨Ų„ ŲžÛŒØ´â€ŒØ¨ÛŒŲ†ÛŒâ€ŒØĒØą ÚŠŲ†Ø¯ØŒ Ų‡ØąÚ†Ų†Ø¯ ÚŠŲ‡ Ų‡Ø˛ÛŒŲ†Ų‡ ÚŠŲ…ÛŒ Ø¨ØąØ§ÛŒ ÚŠÛŒŲÛŒØĒ Ø¯Ø§ØąØ¯. Ø¯Øą ؈ØļŲˆØ­ 720p، Ų…Ų‚Ø§Ø¯ÛŒØą Ų…ØšŲ…ŲˆŲ„ 2600 kbit/s Ø¨ØąØ§ÛŒ VP9 یا HEVC ؈ 4500 kbit/s Ø¨ØąØ§ÛŒ H.264 Ø§ØŗØĒ. Ø§Ú¯Øą Ø¨Ų‡ 0 ØĒŲ†Ø¸ÛŒŲ… Ø´ŲˆØ¯ØŒ ØēÛŒØąŲØšØ§Ų„ Ų…ÛŒâ€ŒØ´ŲˆØ¯.", + "transcoding_max_bitrate_description": "ØĒŲ†Ø¸ÛŒŲ… حداڊØĢØą بیØĒâ€ŒØąÛŒØĒ Ų…ÛŒâ€ŒØĒŲˆØ§Ų†Ø¯ Ø§Ų†Ø¯Ø§Ø˛Ų‡ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ ØąØ§ Ø¯Øą حدی Ų‚Ø§Ø¨Ų„ ŲžÛŒØ´â€ŒØ¨ÛŒŲ†ÛŒâ€ŒØĒØą ÚŠŲ†Ø¯ØŒ Ų‡ØąÚ†Ų†Ø¯ ÚŠŲ‡ Ų‡Ø˛ÛŒŲ†Ų‡ ÚŠŲ…ÛŒ Ø¨ØąØ§ÛŒ ÚŠÛŒŲÛŒØĒ Ø¯Ø§ØąØ¯. Ø¯Øą ؈ØļŲˆØ­ 720p، Ų…Ų‚Ø§Ø¯ÛŒØą Ų…ØšŲ…ŲˆŲ„ 2600 kbit/s Ø¨ØąØ§ÛŒ VP9 یا HEVC ؈ 4500 kbit/s Ø¨ØąØ§ÛŒ H.264 Ø§ØŗØĒ. Ø§Ú¯Øą Ø¨Ų‡ 0 ØĒŲ†Ø¸ÛŒŲ… Ø´ŲˆØ¯ØŒ ØēÛŒØąŲØšØ§Ų„ Ų…ÛŒâ€ŒØ´ŲˆØ¯. Ø˛Ų…Ø§Ų†ÛŒ ÚŠŲ‡ ŲˆØ§Ø­Ø¯ Ø§Ų†Ø¯Ø§Ø˛Ų‡ ŲØ§ÛŒŲ„ Ų…Ø´ØŽØĩ Ų†Ø´ŲˆØ¯ØŒ ÚŠÛŒŲ„ŲˆØ¨Ø§ÛŒØĒ Ø¯Øą Ų†Ø¸Øą Ú¯ØąŲØĒŲ‡ Ų…ÛŒØ´ŲˆØ¯; Ø¯Øą Ų†ØĒیØŦŲ‡ 5000، 5000k , 5M (Ø¨ØąØ§ÛŒ Mbit/s) ÛŒÚŠØŗØ§Ų†Ų†Ø¯.", "transcoding_max_keyframe_interval": "حداڊØĢØą ŲØ§ØĩŲ„Ų‡ ÚŠŲ„ÛŒØ¯ ŲØąÛŒŲ…", "transcoding_max_keyframe_interval_description": "حداڊØĢØą ŲØ§ØĩŲ„Ų‡ ŲØąÛŒŲ… Ø¨ÛŒŲ† ÚŠŲ„ÛŒØ¯ŲØąÛŒŲ…â€ŒŲ‡Ø§ ØąØ§ ØĒŲ†Ø¸ÛŒŲ… Ų…ÛŒâ€ŒÚŠŲ†Ø¯. Ų…Ų‚Ø§Ø¯ÛŒØą ŲžØ§ÛŒÛŒŲ†â€ŒØĒØą ÚŠØ§ØąØ§ÛŒÛŒ ŲØ´ØąØ¯Ų‡â€ŒØŗØ§Ø˛ÛŒ ØąØ§ ÚŠØ§Ų‡Ø´ Ų…ÛŒâ€ŒØ¯Ų‡Ų†Ø¯ØŒ Ø§Ų…Ø§ Ø˛Ų…Ø§Ų† ØŦØŗØĒØŦ؈ ØąØ§ Ø¨Ų‡Ø¨ŲˆØ¯ Ų…ÛŒâ€ŒØ¨ØŽØ´Ų†Ø¯ ؈ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ ÚŠÛŒŲÛŒØĒ ØąØ§ Ø¯Øą ØĩØ­Ų†Ų‡â€ŒŲ‡Ø§ÛŒ با Ø­ØąÚŠØĒ ØŗØąÛŒØš Ø¨Ų‡Ø¨ŲˆØ¯ Ø¯Ų‡Ų†Ø¯. Ų…Ų‚Ø¯Ø§Øą 0 Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą ØąØ§ Ø¨Ų‡â€ŒØˇŲˆØą ØŽŲˆØ¯ÚŠØ§Øą ØĒŲ†Ø¸ÛŒŲ… Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", "transcoding_optimal_description": "ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒÛŒ ÚŠŲ‡ Ø§Ø˛ ØąØ˛ŲˆŲ„ŲˆØ´Ų† Ų‡Ø¯Ų Ø¨Ø§Ų„Ø§ØĒØą Ų‡ØŗØĒŲ†Ø¯ یا Ø¯Øą Ų‚Ø§Ų„Ø¨ ŲžØ°ÛŒØąŲØĒŲ‡ Ø´Ø¯Ų‡ Ų†ÛŒØŗØĒŲ†Ø¯", @@ -260,11 +326,11 @@ "transcoding_reference_frames_description": "ØĒؚداد ŲØąÛŒŲ…â€ŒŲ‡Ø§ÛŒÛŒ ÚŠŲ‡ Ų‡Ų†Ú¯Ø§Ų… ŲØ´ØąØ¯Ų‡â€ŒØŗØ§Ø˛ÛŒ یڊ ŲØąÛŒŲ… Ų…Ø´ØŽØĩ Ø¨Ų‡ ØĸŲ†â€ŒŲ‡Ø§ Ø§ØąØŦاؚ Ø¯Ø§Ø¯Ų‡ Ų…ÛŒâ€ŒØ´ŲˆØ¯. Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø§Ų„Ø§ØĒØą ÚŠØ§ØąØ§ÛŒÛŒ ŲØ´ØąØ¯Ų‡â€ŒØŗØ§Ø˛ÛŒ ØąØ§ Ø¨Ų‡Ø¨ŲˆØ¯ Ų…ÛŒâ€ŒØ¨ØŽØ´Ų†Ø¯ØŒ Ø§Ų…Ø§ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ ØąØ§ ÚŠŲ†Ø¯ØĒØą Ų…ÛŒâ€ŒÚŠŲ†Ų†Ø¯. Ų…Ų‚Ø¯Ø§Øą 0 Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą ØąØ§ Ø¨Ų‡â€ŒØˇŲˆØą ØŽŲˆØ¯ÚŠØ§Øą ØĒŲ†Ø¸ÛŒŲ… Ų…ÛŒâ€ŒÚŠŲ†Ø¯.", "transcoding_required_description": "ŲŲ‚Øˇ ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒÛŒ ÚŠŲ‡ Ø¯Øą ŲØąŲ…ØĒ ŲžØ°ÛŒØąŲØĒŲ‡â€ŒØ´Ø¯Ų‡ Ų†ÛŒØŗØĒŲ†Ø¯", "transcoding_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØĒØ¨Ø¯ÛŒŲ„ ŲˆÛŒØ¯ÛŒŲˆ", - "transcoding_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ؈ØļŲˆØ­ ؈ Ø§ØˇŲ„Ø§ØšØ§ØĒ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ ŲˆÛŒØ¯ØĻŲˆÛŒÛŒ", + "transcoding_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ Ø§ÛŒŲ†ÚŠŲ‡ ÚŠØ¯Ø§Ų… ŲˆÛŒØ¯ÛŒŲˆ Ų‡Ø§ transcode Ø´ŲˆŲ†Ø¯ ؈ Ú†Ú¯ŲˆŲ†Ú¯ÛŒ ŲžØąØ¯Ø§Ø˛Ø´ ØĸŲ†Ų‡Ø§", "transcoding_target_resolution": "؈ØļŲˆØ­ Ų‡Ø¯Ų", "transcoding_target_resolution_description": "؈ØļŲˆØ­â€ŒŲ‡Ø§ÛŒ Ø¨Ø§Ų„Ø§ØĒØą Ų…ÛŒâ€ŒØĒŲˆØ§Ų†Ų†Ø¯ ØŦØ˛ØĻیاØĒ بیشØĒØąÛŒ ØąØ§ Ø­ŲØ¸ ÚŠŲ†Ų†Ø¯ØŒ Ø§Ų…Ø§ Ø˛Ų…Ø§Ų† بیشØĒØąÛŒ Ø¨ØąØ§ÛŒ ÚŠØ¯Ú¯Ø°Ø§ØąÛŒ Ų†ÛŒØ§Ø˛ Ø¯Ø§ØąŲ†Ø¯ØŒ Ø§Ų†Ø¯Ø§Ø˛Ų‡ ŲØ§ÛŒŲ„â€ŒŲ‡Ø§ÛŒ Ø¨Ø˛ØąÚ¯â€ŒØĒØąÛŒ Ø¯Ø§ØąŲ†Ø¯ ؈ Ų…Ų…ÚŠŲ† Ø§ØŗØĒ باؚØĢ ÚŠØ§Ų‡Ø´ ŲžØ§ØŗØŽÚ¯ŲˆÛŒÛŒ Ø¨ØąŲ†Ø§Ų…Ų‡ Ø´ŲˆŲ†Ø¯.", "transcoding_temporal_aq": "AQ Ų…ŲˆŲ‚ØĒی", - "transcoding_temporal_aq_description": "Ø§ÛŒŲ† Ų…ŲˆØąØ¯ ŲŲ‚Øˇ Ø¨ØąØ§ÛŒ NVENC Ø§ØšŲ…Ø§Ų„ Ų…ÛŒ Ø´ŲˆØ¯. Ø§ŲØ˛Ø§ÛŒØ´ ÚŠÛŒŲÛŒØĒ Ø¯Øą ØĩØ­Ų†Ų‡ Ų‡Ø§ÛŒ با ØŦØ˛ØĻیاØĒ Ø¨Ø§Ų„Ø§ ؈ Ø­ØąÚŠØĒ ÚŠŲ…. Ų…Ų…ÚŠŲ† Ø§ØŗØĒ با Ø¯ØŗØĒÚ¯Ø§Ų‡ Ų‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØĒØą ØŗØ§Ø˛Ú¯Ø§Øą Ų†Ø¨Ø§Ø´Ø¯.", + "transcoding_temporal_aq_description": "Ø§ÛŒŲ† Ų…ŲˆØąØ¯ ŲŲ‚Øˇ Ø¨ØąØ§ÛŒ NVENC Ø§ØšŲ…Ø§Ų„ Ų…ÛŒ Ø´ŲˆØ¯. Temporal Adaptive Quantization ÚŠÛŒŲÛŒØĒ ØĩØ­Ų†Ų‡ Ų‡Ø§ÛŒ با ØŦØ˛ØĻیاØĒ Ø¨Ø§Ų„Ø§ØŒ ØĒØ­ØąÚŠ ÚŠŲ… ØąØ§ Ø§ŲØ˛Ø§ÛŒØ´ Ų…ÛŒ Ø¯Ų‡Ø¯. Ų…Ų…ÚŠŲ† Ø§ØŗØĒ با Ø¯ØŗØĒÚ¯Ø§Ų‡ Ų‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØĒØą ØŗØ§Ø˛Ú¯Ø§Øą Ų†Ø¨Ø§Ø´Ø¯.", "transcoding_threads": "ØąØ´ØĒŲ‡ Ų‡Ø§ ( Ų…ŲˆØļŲˆØšØ§ØĒ )", "transcoding_threads_description": "Ų…Ų‚Ø§Ø¯ÛŒØą Ø¨Ø§Ų„Ø§ØĒØą Ų…Ų†ØŦØą Ø¨Ų‡ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ ØŗØąÛŒØš ØĒØą Ų…ÛŒ Ø´ŲˆØ¯ØŒ Ø§Ų…Ø§ ؁Øļای ÚŠŲ…ØĒØąÛŒ Ø¨ØąØ§ÛŒ ŲžØąØ¯Ø§Ø˛Ø´ ØŗØ§ÛŒØą ŲˆØ¸Ø§ÛŒŲ ØŗØąŲˆØą Ø¯Øą Ø­ÛŒŲ† ŲØšØ§Ų„ÛŒØĒ Ø¨Ø§Ų‚ÛŒ Ų…ÛŒ Ú¯Ø°Ø§ØąØ¯. Ø§ÛŒŲ† Ų…Ų‚Ø¯Ø§Øą Ų†Ø¨Ø§ÛŒØ¯ بیشØĒØą Ø§Ø˛ ØĒؚداد Ų‡ØŗØĒŲ‡ Ų‡Ø§ÛŒ CPU باشد. Ø§Ú¯Øą ØąŲˆÛŒ 0 ØĒŲ†Ø¸ÛŒŲ… Ø´ŲˆØ¯ØŒ بیشØĒØąÛŒŲ† Ø§ØŗØĒŲØ§Ø¯Ų‡ ØąØ§ ØŽŲˆØ§Ų‡Ø¯ داشØĒ.", "transcoding_tone_mapping_description": "ØĒŲ„Ø§Ø´ Ø¨ØąØ§ÛŒ Ø­ŲØ¸ Ø¸Ø§Ų‡Øą ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ HDR Ų‡Ų†Ú¯Ø§Ų… ØĒØ¨Ø¯ÛŒŲ„ Ø¨Ų‡ SDR. Ų‡Øą Ø§Ų„Ú¯ŲˆØąÛŒØĒŲ… ØĒØšØ§Ø¯Ų„ Ų‡Ø§ÛŒ Ų…ØĒŲØ§ŲˆØĒی ØąØ§ Ø¨ØąØ§ÛŒ ØąŲ†Ú¯ØŒ ØŦØ˛ØĻیاØĒ ؈ ØąŲˆØ´Ų†Ø§ÛŒÛŒ ایØŦاد Ų…ÛŒ ÚŠŲ†Ø¯. Hable ØŦØ˛ØĻیاØĒ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯ØŒ Mobius ØąŲ†Ú¯ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯ ؈ Reinhard ØąŲˆØ´Ų†Ø§ÛŒÛŒ ØąØ§ Ø­ŲØ¸ Ų…ÛŒ ÚŠŲ†Ø¯.", @@ -272,18 +338,23 @@ "transcoding_transcode_policy_description": "ØŗÛŒØ§ØŗØĒ Ø¨ØąØ§ÛŒ Ø˛Ų…Ø§Ų†ÛŒ ÚŠŲ‡ ŲˆÛŒØ¯ÛŒŲˆÛŒÛŒ باید Ų…ØŦددا ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ø´ŲˆØ¯. ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ HDR Ų‡Ų…ÛŒØ´Ų‡ ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ų…ØŦدد ØŽŲˆØ§Ų‡Ų†Ø¯ شد (Ų…Ú¯Øą ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ Ų…ØŦدد ØēÛŒØąŲØšØ§Ų„ باشد).", "transcoding_two_pass_encoding": "ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ø¯Ųˆ Ų…ØąØ­Ų„Ų‡ ای", "transcoding_two_pass_encoding_setting_description": "ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) ŲˆÛŒØ¯ÛŒŲˆ Ø¯Øą Ø¯Ųˆ Ų…ØąØ­Ų„Ų‡ Ø¨ØąØ§ÛŒ ØĒŲˆŲ„ÛŒØ¯ ŲˆÛŒØ¯ÛŒŲˆŲ‡Ø§ÛŒ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ Ø´Ø¯Ų‡ Ø¨Ų‡ØĒØą. ŲˆŲ‚ØĒی حداڊØĢØą Ų†ØąØŽ بیØĒ ŲØšØ§Ų„ باشد (Ø¨ØąØ§ÛŒ ÚŠØ§Øą با H.264 ؈ HEVC Ų„Ø§Ø˛Ų… Ø§ØŗØĒ)، Ø§ÛŒŲ† Ø­Ø§Ų„ØĒ Ø§Ø˛ یڊ Ų…Ø­Ø¯ŲˆØ¯Ų‡ Ų†ØąØŽ بیØĒ Ø¨Øą Ø§ØŗØ§Øŗ حداڊØĢØą Ų†ØąØŽ بیØĒ Ø§ØŗØĒŲØ§Ø¯Ų‡ Ų…ÛŒ ÚŠŲ†Ø¯ ؈ CRF ØąØ§ Ų†Ø§Ø¯ÛŒØ¯Ų‡ Ų…ÛŒ Ú¯ÛŒØąØ¯. Ø¨ØąØ§ÛŒ VP9، Ø§Ú¯Øą حداڊØĢØą Ų†ØąØŽ بیØĒ ØēÛŒØąŲØšØ§Ų„ باشد، Ų…ÛŒ ØĒŲˆØ§Ų† Ø§Ø˛ CRF Ø§ØŗØĒŲØ§Ø¯Ų‡ ÚŠØąØ¯.", - "transcoding_video_codec": "ڊدڊ ŲˆÛŒØ¯ÛŒŲˆÛŒÛŒ", + "transcoding_video_codec": "Codec ŲˆÛŒØ¯ÛŒŲˆÛŒÛŒ", "transcoding_video_codec_description": "VP9 ÚŠØ§ØąØ§ÛŒÛŒ Ø¨Ø§Ų„Ø§ ؈ ØŗØ§Ø˛Ú¯Ø§ØąÛŒ ŲˆØ¨ ØąØ§ Ø¯Ø§ØąØ¯ØŒ Ø§Ų…Ø§ ØĒØ¨Ø¯ÛŒŲ„ (ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ) Ų…ØŦدد ØĸŲ† Ø˛Ų…Ø§Ų† بیشØĒØąÛŒ Ų…ÛŒ Ú¯ÛŒØąØ¯. HEVC ØšŲ…Ų„ÚŠØąØ¯ Ų…Ø´Ø§Ø¨Ų‡ÛŒ Ø¯Ø§ØąØ¯ØŒ Ø§Ų…Ø§ ØŗØ§Ø˛Ú¯Ø§ØąÛŒ ŲˆØ¨ ÚŠŲ…ØĒØąÛŒ Ø¯Ø§ØąØ¯. H.264 ØŗØ§Ø˛Ú¯Ø§ØąÛŒ Ú¯ØŗØĒØąØ¯Ų‡ ؈ ØąŲ…Ø˛Ú¯Ø°Ø§ØąÛŒ ØŗØąÛŒØš Ø¯Ø§ØąØ¯ØŒ Ø§Ų…Ø§ ŲØ§ÛŒŲ„ Ų‡Ø§ÛŒ Ø¨Ø˛ØąÚ¯ØĒØąÛŒ ØĒŲˆŲ„ÛŒØ¯ Ų…ÛŒ ÚŠŲ†Ø¯. AV1 ڊدڊ ÚŠØ§ØąØĸŲ…Ø¯ØĒØąÛŒŲ† Ø§ØŗØĒ، Ø§Ų…Ø§ Ø§Ø˛ ŲžØ´ØĒÛŒØ¨Ø§Ų†ÛŒ Ø¯Øą Ø¯ØŗØĒÚ¯Ø§Ų‡ Ų‡Ø§ÛŒ Ų‚Ø¯ÛŒŲ…ÛŒ ØĒØą Ø¨ØąØŽŲˆØąØ¯Ø§Øą Ų†ÛŒØŗØĒ.", "trash_enabled_description": "ŲØšØ§Ų„ ØŗØ§Ø˛ÛŒ ŲˆÛŒÚ˜Ú¯ÛŒ Ų‡Ø§ÛŒ ØŗØˇŲ„ Ø¨Ø§Ø˛ÛŒØ§ŲØĒ (ØŗØˇŲ„ Ø˛Ø¨Ø§Ų„Ų‡)", "trash_number_of_days": "ØĒؚداد ØąŲˆØ˛Ų‡Ø§", "trash_number_of_days_description": "ØĒؚداد ØąŲˆØ˛Ų‡Ø§ÛŒÛŒ ÚŠŲ‡ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§(ØšÚŠØŗŲ‡Ø§ ؈ ŲÛŒŲ…Ų„Ų‡Ø§) Ø¯Øą Ø˛Ø¨Ø§Ų„Ų‡ Ø¯Ø§Ų†(ØŗØˇŲ„ Ø¨Ø§Ø˛ÛŒØ§ŲØĒ) Ų‚Ø¨Ų„ Ø§Ø˛ Ø­Ø°Ų داØĻŲ…ÛŒ Ų†Ú¯Ų‡Ø¯Ø§ØąÛŒ Ų…ÛŒØ´ŲˆŲ†Ø¯", "trash_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØŗØˇŲ„ Ø¨Ø§Ø˛ÛŒØ§ŲØĒ (ØŗØˇŲ„ Ø˛Ø¨Ø§Ų„Ų‡)", "trash_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ØŗØˇŲ„ Ø¨Ø§Ø˛ÛŒØ§ŲØĒ (ØŗØˇŲ„ Ø˛Ø¨Ø§Ų„Ų‡)", + "unlink_all_oauth_accounts": "ØŦØ¯Ø§ØŗØ§Ø˛ÛŒ ØĒŲ…Ø§Ų…ÛŒ Ø§ÚŠØ§Ų†ØĒ Ų‡Ø§ÛŒ OAuth", + "unlink_all_oauth_accounts_description": "Ø¨Ų‡ یاد داشØĒŲ‡ باشید Ø­ØĒŲ…Ø§ Ų‚Ø¨Ų„ Ø§Ø˛ Ø§Ų†ØĒŲ‚Ø§Ų„ Ø¨Ų‡ Ø§ØąØ§ØĻŲ‡ Ø¯Ų‡Ų†Ø¯Ų‡ ØŦدید ØĒŲ…Ø§Ų…ÛŒ Ø§ÚŠØ§Ų†ØĒ Ų‡Ø§ÛŒ OAuth ØąØ§ ØŦØ¯Ø§ØŗØ§Ø˛ÛŒ ÚŠŲ†ÛŒØ¯.", + "unlink_all_oauth_accounts_prompt": "Øĸیا Ø§ØˇŲ…ÛŒŲ†Ø§Ų† Ø¯Ø§ØąÛŒØ¯ Ų…ÛŒØŽŲˆØ§Ų‡ÛŒØ¯ ØĒŲ…Ø§Ų…ÛŒ Ø§ÚŠØ§Ų†ØĒ Ų‡Ø§ÛŒ OAuth ØąØ§ ØŦدا ØŗØ§Ø˛ÛŒ ÚŠŲ†ÛŒØ¯ØŸ Ø§ÛŒŲ†ÚŠØ§Øą OAuth ID ØĒŲ…Ø§Ų…ÛŒ ÚŠØ§ØąØ¨ØąØ§Ų† ØąØ§ ØąÛŒØŗØĒ Ų…ÛŒ ÚŠŲ†Ø¯ ؈ Ų‚Ø§Ø¨Ų„ Ø¨ØąÚ¯Ø´ØĒ Ų†ÛŒØŗØĒ.", + "user_cleanup_job": "ŲžØ§ÚŠ ØŗØ§Ø˛ÛŒ ÚŠØ§ØąØ¨Øą", "user_delete_delay": "{user}'s Ø­ØŗØ§Ø¨ ÚŠØ§ØąØ¨ØąÛŒ ؈ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§(ØšÚŠØŗ ؈ ŲÛŒŲ„Ų…) Ø¨ØąØ§ÛŒ Ø­Ø°Ų داØĻŲ…ÛŒ Ø¯Øą {delay, plural, one {# ØąŲˆØ˛} other {# ØąŲˆØ˛}} Ø¨ØąŲ†Ø§Ų…Ų‡ ØąÛŒØ˛ÛŒ ØŽŲˆØ§Ų‡Ų†Ø¯ شد.", "user_delete_delay_settings": "ØĒØŖØŽÛŒØą Ø¯Øą Ø­Ø°Ų", "user_delete_delay_settings_description": "ØĒؚداد ØąŲˆØ˛Ų‡Ø§ÛŒÛŒ ÚŠŲ‡ ŲžØŗ Ø§Ø˛ Ø­Ø°ŲØŒ Ø­ØŗØ§Ø¨ ÚŠØ§ØąØ¨ØąÛŒ ؈ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§ÛŒ(ØšÚŠØŗ ؈ ŲÛŒŲ„Ų…) ÚŠØ§ØąØ¨Øą Ø¨Ų‡ ØˇŲˆØą داØĻŲ…ÛŒ Ø­Ø°Ų Ų…ÛŒ Ø´ŲˆŲ†Ø¯. ÚŠØ§Øą Ø­Ø°Ų ÚŠØ§ØąØ¨Øą Ø¯Øą Ų†ÛŒŲ…Ų‡ شب اØŦØąØ§ Ų…ÛŒ Ø´ŲˆØ¯ ØĒا ÚŠØ§ØąØ¨ØąØ§Ų†ÛŒ ÚŠŲ‡ ØĸŲ…Ø§Ø¯Ų‡ Ø­Ø°Ų Ų‡ØŗØĒŲ†Ø¯ ØąØ§ Ø¨ØąØąØŗÛŒ ÚŠŲ†Ø¯. ØĒØēÛŒÛŒØąØ§ØĒ Ø¯Øą Ø§ÛŒŲ† ØĒŲ†Ø¸ÛŒŲ… Ø¯Øą اØŦØąØ§ÛŒ بؚدی Ø§ØąØ˛ÛŒØ§Ø¨ÛŒ ØŽŲˆØ§Ų‡Ų†Ø¯ شد.", "user_delete_immediately": "{user}'s Ø­ØŗØ§Ø¨ ÚŠØ§ØąØ¨ØąÛŒ ؈ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§ (ØšÚŠØŗ ؈ ŲÛŒŲ„Ų…) ŲŲˆØąØ§Ų‹ Ø¨ØąØ§ÛŒ Ø­Ø°Ų داØĻŲ…ÛŒ Ø¯Øą Øĩ؁ Ų‚ØąØ§Øą ØŽŲˆØ§Ų‡Ų†Ø¯ Ú¯ØąŲØĒ.", "user_delete_immediately_checkbox": "ÚŠØ§ØąØ¨Øą ؈ Ø¯Ø§ØąØ§ÛŒÛŒ Ų‡Ø§ (ØšÚŠØŗ ؈ ŲÛŒŲ„Ų…) ØąØ§ Ø¨ØąØ§ÛŒ Ø­Ø°Ų ŲŲˆØąÛŒ Ø¯Øą Øĩ؁ Ų‚ØąØ§Øą Ø¨Ø¯Ų‡", + "user_details": "ØŦØ˛ØĻیاØĒ ÚŠØ§ØąØ¨Øą", "user_management": "Ų…Ø¯ÛŒØąÛŒØĒ ÚŠØ§ØąØ¨Øą", "user_password_has_been_reset": "ØąŲ…Ø˛ ØšØ¨ŲˆØą ÚŠØ§ØąØ¨Øą Ø¨Ø§Ø˛Ų†Ø´Ø§Ų†ÛŒ شد:", "user_password_reset_description": "Ų„ØˇŲØ§Ų‹ ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…ŲˆŲ‚ØĒ ØąØ§ Ø¨Ų‡ ÚŠØ§ØąØ¨Øą Ø§ØąØ§ØĻŲ‡ Ø¯Ų‡ÛŒØ¯ ؈ Ø¨Ų‡ Ø§Ųˆ Ø§ØˇŲ„Ø§Øš Ø¯Ų‡ÛŒØ¯ ÚŠŲ‡ باید Ø¯Øą ŲˆØąŲˆØ¯ بؚدی ØąŲ…Ø˛ ØšØ¨ŲˆØą ØŽŲˆØ¯ ØąØ§ ØĒØēÛŒÛŒØą Ø¯Ų‡Ø¯.", @@ -291,6 +362,8 @@ "user_restore_scheduled_removal": "Ø¨Ø§Ø˛ÛŒØ§Ø¨ÛŒ ÚŠØ§ØąØ¨Øą - Ø­Ø°Ų Ø¨ØąŲ†Ø§Ų…Ų‡ ØąÛŒØ˛ÛŒ Ø´Ø¯Ų‡ Ø¯Øą {date, date, long}", "user_settings": "ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØ§ØąØ¨Øą", "user_settings_description": "Ų…Ø¯ÛŒØąÛŒØĒ ØĒŲ†Ø¸ÛŒŲ…Ø§ØĒ ÚŠØ§ØąØ¨Øą", + "user_successfully_removed": "ÚŠØ§ØąØ¨Øą {email} با Ų…ŲˆŲŲ‚ÛŒØĒ Ø­Ø°Ų شد.", + "users_page_description": "ØĩŲØ­Ų‡ Ų…Ø¯ÛŒØąÛŒØĒ ÚŠØ§ØąØ¨ØąØ§Ų†", "version_check_enabled_description": "ŲØšØ§Ų„â€ŒØŗØ§Ø˛ÛŒ Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡", "version_check_implications": "ŲˆÛŒÚ˜Ú¯ÛŒ Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡ Ø¨Ų‡ Ø§ØąØĒØ¨Ø§Øˇ Ø¯ŲˆØąŲ‡ ای با github.com Ų…ØĒÚŠÛŒ Ø§ØŗØĒ", "version_check_settings": "Ø¨ØąØąØŗÛŒ Ų†ØŗØŽŲ‡", @@ -302,6 +375,7 @@ "admin_password": "ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų…Ø¯ÛŒØą", "administration": "Ų…Ø¯ÛŒØąÛŒØĒ", "advanced": "ŲžÛŒØ´ØąŲØĒŲ‡", + "advanced_settings_proxy_headers_title": "Ų‡Ø¯Øą Ų‡Ø§ÛŒ ŲžØąŲˆÚŠØŗÛŒ ØŗŲØ§ØąØ´ÛŒ [ØĸØ˛Ų…Ø§ÛŒØ´ÛŒ]", "album_added": "ØĸŲ„Ø¨ŲˆŲ… اØļØ§ŲŲ‡ شد", "album_cover_updated": "ØŦŲ„Ø¯ ØĸŲ„Ø¨ŲˆŲ… Ø¨Ų‡â€ŒØąŲˆØ˛ØąØŗØ§Ų†ÛŒ شد", "album_info_updated": "Ø§ØˇŲ„Ø§ØšØ§ØĒ ØĸŲ„Ø¨ŲˆŲ… Ø¨Ų‡â€ŒØąŲˆØ˛ØąØŗØ§Ų†ÛŒ شد", @@ -338,6 +412,8 @@ "change_name": "ØĒØēÛŒÛŒØą Ų†Ø§Ų…", "change_name_successfully": "Ų†Ø§Ų… با Ų…ŲˆŲŲ‚ÛŒØĒ ØĒØēÛŒÛŒØą ÛŒØ§ŲØĒ", "change_password": "ØĒØēÛŒÛŒØą ØąŲ…Ø˛ ØšØ¨ŲˆØą", + "change_password_form_password_mismatch": "ØąŲ…Ø˛ ØšØ¨ŲˆØą Ų‡Ø§ Ų…ØˇØ§Ø¨Ų‚ØĒ Ų†Ø¯Ø§ØąŲ†Ø¯", + "change_password_form_reenter_new_password": "ØĒÚŠØąØ§Øą ØąŲ…Ø˛ ØšØ¨ŲˆØą ØŦدید", "change_your_password": "ØąŲ…Ø˛ ØšØ¨ŲˆØą ØŽŲˆØ¯ ØąØ§ ØĒØēÛŒÛŒØą Ø¯Ų‡ÛŒØ¯", "check_logs": "Ø¨ØąØąØŗÛŒ Ų„Ø§Ú¯â€ŒŲ‡Ø§", "city": "Ø´Ų‡Øą", @@ -442,7 +518,6 @@ "external_libraries": "ÚŠØĒØ§Ø¨ØŽØ§Ų†Ų‡â€ŒŲ‡Ø§ÛŒ ØŽØ§ØąØŦی", "favorite": "ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒ", "favorites": "ØšŲ„Ø§Ų‚Ų‡â€ŒŲ…Ų†Ø¯ÛŒâ€ŒŲ‡Ø§", - "file_name": "Ų†Ø§Ų… ŲØ§ÛŒŲ„", "file_name_or_extension": "Ų†Ø§Ų… ŲØ§ÛŒŲ„ یا ŲžØŗŲˆŲ†Ø¯", "filename": "Ų†Ø§Ų… ŲØ§ÛŒŲ„", "filetype": "Ų†ŲˆØš ŲØ§ÛŒŲ„", diff --git a/i18n/fi.json b/i18n/fi.json index 3eab7b3df7..425e7a719e 100644 --- a/i18n/fi.json +++ b/i18n/fi.json @@ -15,9 +15,13 @@ "add_a_location": "Lisää sijainti", "add_a_name": "Lisää nimi", "add_a_title": "Lisää otsikko", + "add_action": "Lisää toiminto", + "add_action_description": "Klikkaa lisätäksesi suoritettava toiminto", "add_birthday": "Lisää syntymäpäivä", "add_endpoint": "Lisää päätepiste", "add_exclusion_pattern": "Lisää poissulkemismalli", + "add_filter": "Lisää suodatin", + "add_filter_description": "Klikkaa lisätäksesi suodatinehto", "add_location": "Lisää sijainti", "add_more_users": "Lisää käyttäjiä", "add_partner": "Lisää kumppani", @@ -36,6 +40,7 @@ "add_to_shared_album": "Lisää jaettuun albumiin", "add_upload_to_stack": "Lisää kuvapinoon", "add_url": "Lisää URL", + "add_workflow_step": "Lisää tyÃļnkulun vaihe", "added_to_archive": "Lisätty arkistoon", "added_to_favorites": "Lisätty suosikkeihin", "added_to_favorites_count": "{count, number} lisätty suosikkeihin", @@ -63,7 +68,7 @@ "cleared_jobs": "TyÃļn {job} tehtävät tyhjennetty", "config_set_by_file": "Asetukset on tällä hetkellä määritelty tiedostosta", "confirm_delete_library": "Haluatko varmasti poistaa kirjaston {library}?", - "confirm_delete_library_assets": "Oletko varma että haluat poistaa tämän kirjaston? Tämä poistaa {count, plural, one {# kohteen} other {# kohdetta}} Immichistä eikä sitä voida perua. Tiedostot jäävät levylle.", + "confirm_delete_library_assets": "Haluatko varmasti poistaa tämän kirjaston? Tämä poistaa {count, plural, one {# kohteen} other {# kohdetta}} Immichistä eikä sitä voida perua. Tiedostot jäävät levylle.", "confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi", "confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myÃļs nimetyt henkilÃļt.", "confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?", @@ -97,6 +102,7 @@ "image_preview_description": "Keskikokoinen kuva, josta metatiedot on poistettu, käytetään yksittäisen resurssin katseluun ja koneoppimiseen", "image_preview_quality_description": "Esikatselulaatu 1-100. Korkeampi arvo on parempi, mutta tuottaa suurempia tiedostoja ja voi heikentää sovelluksen reagointikykyä. Matalan arvon asettaminen voi vaikuttaa koneoppimisen laatuun.", "image_preview_title": "Esikatselun asetukset", + "image_progressive": "Progressiivinen", "image_quality": "Laatu", "image_resolution": "Resoluutio", "image_resolution_description": "Korkeammat resoluutiot voivat säilyttää enemmän yksityiskohtia, mutta niiden koodaus kestää kauemmin, tiedostokoot ovat suurempia ja ne voivat heikentää sovelluksen reagointikykyä.", @@ -181,10 +187,21 @@ "machine_learning_smart_search_enabled": "Ota käyttÃļÃļn älykäs haku", "machine_learning_smart_search_enabled_description": "Jos ei käytÃļssä, kuvia ei koodata älykkäälle etsinnälle.", "machine_learning_url_description": "Koneoppimispalvelimen URL-osoite. Jos lisätään useampi kuin yksi URL-osoite, kutakin osoitetta kohden yritetään kerran, kunnes yksi niistä vastaa. Yritykset tehdään järjestyksessä ensimmäisestä viimeiseen. Palvelimet, jotka eivät vastaa, ohitetaan tilapäisesti, kunnes ne ovat taas tavoitettavissa.", + "maintenance_delete_backup": "Poista varmuuskopio", + "maintenance_delete_backup_description": "Tämä tiedosto poistetaan pysyvästi.", + "maintenance_delete_error": "Varmuuskopion poistaminen epäonnistui.", + "maintenance_restore_backup": "Palauta varmuuskopio", + "maintenance_restore_backup_description": "Immich tyhjennetään ja palautetaan valitusta varmuuskopiosta. Ennen jatkamista luodaan varmuuskopio.", + "maintenance_restore_backup_different_version": "Tämä varmuuskopio luotiin Immichin eri versiolla!", + "maintenance_restore_backup_unknown_version": "Varmuuskopion versiota ei voitu määrittää.", + "maintenance_restore_database_backup": "Palauta tietokannan varmuuskopio", + "maintenance_restore_database_backup_description": "Palaa takaisin tietokannan aiempaan tilaan käyttäen varmuuskopiotiedostoa", "maintenance_settings": "Ylläpito", "maintenance_settings_description": "Laita Immich ylläpitotilaan.", - "maintenance_start": "Käynnistä ylläpitotila", + "maintenance_start": "Vaihda ylläpitotilaan", "maintenance_start_error": "Ylläpitotilan käynnistys epäonnistui.", + "maintenance_upload_backup": "Lähetä tietokannan varmuuskopiotiedosto", + "maintenance_upload_backup_error": "Varmuuskopiota ei voitu lähettää, onhan se .sql-/.sql.gz-tiedosto?", "manage_concurrency": "Hallitse yhtäaikaisia toimintoja", "manage_concurrency_description": "Mene tÃļiden sivulle muuttamaan tÃļiden yhtäaikaisuutta", "manage_log_settings": "Hallitse lokien asetuksia", @@ -277,8 +294,8 @@ "paths_validated_successfully": "Kaikki polut validoitu", "person_cleanup_job": "HenkilÃļpuhdistus", "queue_details": "Jonon tiedot", - "queues": "TÃļiden jonot", - "queues_page_description": "Ylläpitäjän tÃļiden jonosivu", + "queues": "Tehtäväjonot", + "queues_page_description": "Tehtäväjonojen ylläpitosivu", "quota_size_gib": "KiintiÃļn koko (Gt)", "refreshing_all_libraries": "Virkistetään kaikki kirjastot", "registration": "Pääkäyttäjän rekisterÃļinti", @@ -431,6 +448,9 @@ "admin_password": "Ylläpitäjän salasana", "administration": "Ylläpito", "advanced": "Edistyneet", + "advanced_settings_clear_image_cache": "Tyhjennä kuvien välimuisti", + "advanced_settings_clear_image_cache_error": "Kuvien välimuistin tyhjentäminen epäonnistui", + "advanced_settings_clear_image_cache_success": "Tyhjennettiin onnistuneesti {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Käytä tätä vaihtoehtoa suodattaaksesi mediaa synkronoinnin aikana vaihtoehtoisten kriteerien perusteella. Kokeile tätä vain, jos sovelluksessa on ongelmia kaikkien albumien tunnistamisessa.", "advanced_settings_enable_alternate_media_filter_title": "[KOKEELLINEN] Käytä vaihtoehtoisen laitteen albumin synkronointisuodatinta", "advanced_settings_log_level_title": "Kirjaustaso: {level}", @@ -467,6 +487,7 @@ "album_remove_user": "Poista käyttäjä?", "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_search_not_found": "Haullasi ei lÃļytynyt yhtään albumia", + "album_selected": "Albumi valittu", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_summary": "Albumi tiivistelmä", "album_updated": "Albumi päivitetty", @@ -488,9 +509,11 @@ "albums_default_sort_order_description": "Kohteiden ensisijainen lajittelujärjestys uusia albumeja luotaessa.", "albums_feature_description": "Kokoelma kohteita, jotka voidaan jakaa muille käyttäjille.", "albums_on_device_count": "({count}) albumia laitteella", + "albums_selected": "{count, plural, one {# albumi valittu} other {# albumia valittu}}", "all": "Kaikki", "all_albums": "Kaikki albumit", "all_people": "Kaikki henkilÃļt", + "all_photos": "Kaikki kuvat", "all_videos": "Kaikki videot", "allow_dark_mode": "Salli tumma tila", "allow_edits": "Salli muutokset", @@ -498,6 +521,9 @@ "allow_public_user_to_upload": "Salli julkisten käyttäjien lähettää tiedostoja", "allowed": "Sallittu", "alt_text_qr_code": "QR-koodi", + "always_keep": "Säilytä aina", + "always_keep_photos_hint": "Tilan vapauttaminen säilyttää kaikki kuvat tällä laitteella.", + "always_keep_videos_hint": "Tilan vapauttaminen säilyttää kaikki videot tällä laitteella.", "anti_clockwise": "Vastapäivään", "api_key": "API-avain", "api_key_description": "Tämä arvo näytetään vain kerran. Varmista, että olet kopioinut sen ennen kuin suljet ikkunan.", @@ -524,10 +550,12 @@ "archived_count": "{count, plural, other {Arkistoitu #}}", "are_these_the_same_person": "Ovatko he sama henkilÃļ?", "are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?", + "array_field_not_fully_supported": "Taulukkokentät vaativat JSON:in manuaalista muokkaamista", "asset_action_delete_err_read_only": "Vain luku -tilassa olevia kohteita ei voitu poistaa, ohitetaan", "asset_action_share_err_offline": "Verkottomassa tilassa olevia kohteita ei voitu noutaa, ohitetaan", "asset_added_to_album": "Lisätty albumiin", "asset_adding_to_album": "Lisätään albumiinâ€Ļ", + "asset_created": "Kohde luotu", "asset_description_updated": "Kohteen kuvaus on päivitetty", "asset_filename_is_offline": "Kohde {filename} on offline-tilassa", "asset_has_unassigned_faces": "Kohteella on määrittämättÃļmiä kasvoja", @@ -652,6 +680,7 @@ "backup_options_page_title": "Varmuuskopioinnin asetukset", "backup_setting_subtitle": "Hallinnoi aktiivisia ja taustalla olevia lähetysasetuksia", "backup_settings_subtitle": "Hallitse lähetysasetuksia", + "backup_upload_details_page_more_details": "Paina saadaksesi lisätietoja", "backward": "Taaksepäin", "biometric_auth_enabled": "Biometrinen tunnistautuminen käytÃļssä", "biometric_locked_out": "Sinulta on evätty pääsy biometriseen tunnistautumiseen", @@ -710,6 +739,8 @@ "change_password_form_password_mismatch": "Salasanat eivät täsmää", "change_password_form_reenter_new_password": "Uusi salasana uudelleen", "change_pin_code": "Vaihda PIN-koodi", + "change_trigger": "Vaihda laukaisin", + "change_trigger_prompt": "Haluatko varmasti vaihtaa laukaisimen? Tämä poistaa kaikki olemassa olevat toiminnot ja suodattimet.", "change_your_password": "Vaihda salasanasi", "changed_visibility_successfully": "Näkyvyys vaihdettu", "charging": "Ladataan laitetta", @@ -718,8 +749,17 @@ "check_corrupt_asset_backup_button": "Suorita tarkistus", "check_corrupt_asset_backup_description": "Suorita tämä tarkistus vain Wi-Fi-yhteyden kautta ja vasta, kun kaikki kohteet on varmuuskopioitu. Toimenpide voi kestää muutamia minuutteja.", "check_logs": "Katso lokeja", + "checksum": "Tarkistussumma", "choose_matching_people_to_merge": "Valitse henkilÃļt joka yhdistetään", "city": "Kaupunki", + "cleanup_confirm_description": "Immich lÃļysi {count} turvallisesti palvelimelle varmuuskopioitua kohdetta (luotu ennen {date}). Poistetaanko paikalliset kopiot tästä laitteesta?", + "cleanup_confirm_prompt_title": "Poistetaanko tästä laitteesta?", + "cleanup_deleted_assets": "Siirretty {count} kohdetta laitteen roskakoriin", + "cleanup_deleting": "Siirretään roskakoriin...", + "cleanup_found_assets": "LÃļytyi {count} varmuuskopioitua kohdetta", + "cleanup_icloud_shared_albums_excluded": "Jaettuja iCloud-albumeja ei skannata", + "cleanup_no_assets_found": "Ehtojasi vastaavia varmuuskopioituja kohteita ei lÃļytynyt", + "cleanup_preview_title": "Poistettavia kohteita {count}", "clear": "Tyhjennä", "clear_all": "Tyhjennä kaikki", "clear_all_recent_searches": "Tyhjennä viimeisimmät haut", @@ -785,6 +825,7 @@ "create_album": "Luo albumi", "create_album_page_untitled": "NimetÃļn", "create_api_key": "Luo API-avain", + "create_first_workflow": "Luo ensimmäinen tyÃļnkulku", "create_library": "Luo uusi kirjasto", "create_link": "Luo linkki", "create_link_to_share": "Luo linkki jaettavaksi", @@ -799,14 +840,18 @@ "create_tag": "Luo tunniste", "create_tag_description": "Luo uusi tunniste. Sisäkkäisiä tunnisteita varten syÃļtä tunnisteen täydellinen polku kauttaviivat mukaan luettuna.", "create_user": "Luo käyttäjä", + "create_workflow": "Luo tyÃļnkulku", "created": "Luotu", "created_at": "Luotu", "creating_linked_albums": "Luodaan linkattuja albumeita...", "crop": "Rajaa", + "crop_aspect_ratio_fixed": "Kiinteä", + "crop_aspect_ratio_original": "Alkuperäinen", "curated_object_page_title": "Asiat", "current_device": "Nykyinen laite", "current_pin_code": "Nykyinen PIN-koodi", "current_server_address": "Nykyinen palvelinosoite", + "custom_date": "Mukautettu päivä", "custom_locale": "Muokatut maa-asetukset", "custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen", "custom_url": "Mukautettu URL", @@ -865,6 +910,7 @@ "deselect_all": "Poista valinnat", "details": "Tiedot", "direction": "Suunta", + "disable": "Poista käytÃļstä", "disabled": "Poistettu käytÃļstä", "disallow_edits": "Älä salli muokkauksia", "discord": "Discord", @@ -890,6 +936,7 @@ "download_include_embedded_motion_videos": "Upotetut videot", "download_include_embedded_motion_videos_description": "Sisällytä liikekuviin upotetut videot erillisinä tiedostoina", "download_notfound": "Latausta ei lÃļytynyt", + "download_original": "Lataa alkuperäinen", "download_paused": "Lataus keskeytetty", "download_settings": "Lataukset", "download_settings_description": "Hallitse aineiston lataukseen liittyviä asetuksia", @@ -899,6 +946,7 @@ "download_waiting_to_retry": "Odotetaan uudelleenyritystä", "downloading": "Ladataan", "downloading_asset_filename": "Ladataan mediaa {filename}", + "downloading_from_icloud": "Ladataan iCloudista", "downloading_media": "Median lataaminen", "drop_files_to_upload": "Pudota tiedostot mihin tahansa ladataksesi ne", "duplicates": "Kaksoiskappaleet", @@ -927,11 +975,17 @@ "edit_tag": "Muokkaa tunnistetta", "edit_title": "Muokkaa otsikkoa", "edit_user": "Muokkaa käyttäjää", + "edit_workflow": "Muokkaa tyÃļnkulkua", "editor": "Muokkaaja", "editor_close_without_save_prompt": "Muutoksia ei tallenneta", "editor_close_without_save_title": "Suljetaanko editori?", - "editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet", - "editor_crop_tool_h2_rotation": "Rotaatio", + "editor_confirm_reset_all_changes": "Haluatko varmasti nollata kaikki muutokset?", + "editor_flip_horizontal": "Käännä vaakatasossa", + "editor_flip_vertical": "Käännä pystytasossa", + "editor_orientation": "Suunta", + "editor_reset_all_changes": "Nollaa muutokset", + "editor_rotate_left": "Kierrä 90° vastapäivään", + "editor_rotate_right": "Kierrä 90° myÃļtäpäivään", "email": "SähkÃļposti", "email_notifications": "SähkÃļposti-ilmoitukset", "empty_folder": "Kansio on tyhjä", @@ -950,6 +1004,7 @@ "error_change_sort_album": "Albumin lajittelujärjestyksen muuttaminen epäonnistui", "error_delete_face": "Virhe kasvojen poistamisessa kohteesta", "error_getting_places": "Ongelma paikkojen haussa", + "error_loading_albums": "Virhe albumeita ladatessa", "error_loading_image": "Kuvan lataus ei onnistunut", "error_loading_partners": "Ongelma partnerin haussa: {error}", "error_saving_image": "Virhe: {error}", @@ -1012,6 +1067,7 @@ "unable_to_complete_oauth_login": "OAuth-kirjautumista ei voitu suorittaa loppuun", "unable_to_connect": "Yhteyttä ei voitu muodostaa", "unable_to_copy_to_clipboard": "LeikepÃļydälle ei voitu kopioida, varmista että käytät sivua https-yhteyden kautta", + "unable_to_create": "TyÃļnkulun luominen ei onnistunut", "unable_to_create_admin_account": "Pääkäyttäjän luominen epäonnistui", "unable_to_create_api_key": "Uuden API-avaimen luominen epäonnistui", "unable_to_create_library": "Kirjaston luominen epäonnistui", @@ -1022,6 +1078,7 @@ "unable_to_delete_exclusion_pattern": "Ei voida poistaa poissulkemismallia", "unable_to_delete_shared_link": "Jaetun linkin poistaminen epäonnistui", "unable_to_delete_user": "Käyttäjän poistaminen epäonnistui", + "unable_to_delete_workflow": "TyÃļnkulun poistaminen ei onnistunut", "unable_to_download_files": "Tiedostojen lataaminen epäonnistui", "unable_to_edit_exclusion_pattern": "Ei voida muokata poissulkemismallia", "unable_to_empty_trash": "Roskakorin tyhjentäminen epäonnistui", @@ -1072,8 +1129,10 @@ "unable_to_update_settings": "Asetusten päivitys epäonnistui", "unable_to_update_timeline_display_status": "Aikajanalla näyttämisen asetusta ei voitu tallettaa", "unable_to_update_user": "Käyttäjän muokkaus epäonnistui", + "unable_to_update_workflow": "TyÃļnkulun päivittäminen ei onnistunut", "unable_to_upload_file": "Tiedostoa ei voitu ladata" }, + "errors_text": "Virheet", "exclusion_pattern": "Poissulkemismenetelmä", "exif": "Exif", "exif_bottom_sheet_description": "Lisää kuvausâ€Ļ", @@ -1118,7 +1177,6 @@ "features": "Ominaisuudet", "features_in_development": "Kehityksessä olevat ominaisuudet", "features_setting_description": "Hallitse sovelluksen ominaisuuksia", - "file_name": "Tiedoston nimi", "file_name_or_extension": "Tiedostonimi tai tiedostopääte", "file_size": "Tiedostokoko", "filename": "Tiedostonimi", @@ -1126,6 +1184,7 @@ "filter": "Suodatin", "filter_people": "Suodata henkilÃļt", "filter_places": "Suodata paikkoja", + "filters": "Suodattimet", "find_them_fast": "LÃļydä nopeasti hakemalla nimellä", "first": "Ensimmäinen", "fix_incorrect_match": "Korjaa virheellinen osuma", @@ -1166,12 +1225,14 @@ "header_settings_header_name_input": "Otsikon nimi", "header_settings_header_value_input": "Otsikon arvo", "headers_settings_tile_title": "Mukautettu proxy headers", + "height": "Korkeus", "hi_user": "Hei {name} ({email})", "hide_all_people": "Piilota kaikki henkilÃļt", "hide_gallery": "Piilota galleria", "hide_named_person": "Piilota henkilÃļn {name}", "hide_password": "Piilota salasana", "hide_person": "Piilota henkilÃļ", + "hide_schema": "Piilota skeema", "hide_text_recognition": "Piilota tekstin tunnistus", "hide_unnamed_people": "Piilota nimeämättÃļmät henkilÃļt", "home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.", @@ -1244,9 +1305,17 @@ "ios_debug_info_processing_ran_at": "Prosessi valmistui {dateTime}", "items_count": "{count, plural, one {# kpl} other {# kpl}}", "jobs": "Taustatehtävät", + "json_editor": "JSON-muokkain", + "json_error": "JSON-virhe", "keep": "Säilytä", + "keep_albums": "Säilytä albumit", "keep_all": "Säilytä kaikki", + "keep_description": "Valitse, mitä laitteella säilytetään tilan vapautuksen yhteydessä.", + "keep_favorites": "Säilytä suosikit", + "keep_on_device": "Säilytä laitteella", + "keep_on_device_hint": "Valitse laitteella säilytettävät kohteet", "keep_this_delete_others": "Säilytä tämä, poista muut", + "keeping": "Säilytetään: {items}", "kept_this_deleted_others": "Tämä kohde säilytettiin. {count, plural, one {# asset} other {# assets}} poistettiin", "keyboard_shortcuts": "Pikanäppäimet", "language": "Kieli", @@ -1339,10 +1408,24 @@ "loop_videos_description": "Ota käyttÃļÃļn jatkuva videotoisto tarkemmassa näkymässä.", "main_branch_warning": "Käytät kehitysversiota; suosittelemme vahvasti käyttämään julkaisuversiota!", "main_menu": "Päävalikko", + "maintenance_action_restore": "Palautetaan tietokanta", "maintenance_description": "Immich on asetettu ylläpitotilaan.", "maintenance_end": "Poistu ylläpitotilasta", "maintenance_end_error": "Poistuminen ylläpitotilasta epäonnistui.", "maintenance_logged_in_as": "Kirjautuneena käyttäjänä {user}", + "maintenance_restore_from_backup": "Palauta varmuuskopiosta", + "maintenance_restore_library": "Palauta kirjastosta", + "maintenance_restore_library_confirm": "Jos tämä vaikuttaa oikealta, jatka varmuuskopion palauttamista!", + "maintenance_restore_library_folder_pass": "luettavissa ja kirjoitettavissa", + "maintenance_restore_library_folder_read_fail": "ei luettavissa", + "maintenance_restore_library_folder_write_fail": "ei kirjoitettavissa", + "maintenance_restore_library_hint_missing_files": "Sinulta saattaa puuttua tärkeitä tiedostoja", + "maintenance_restore_library_hint_regenerate_later": "Voit luoda ne uudelleen myÃļhemmin asetuksissa", + "maintenance_restore_library_loading": "Ladataan eheystarkistuksia ja heurestiikkaaâ€Ļ", + "maintenance_task_backup": "Luodaan varmuuskopiota olemassa olevasta tietokannastaâ€Ļ", + "maintenance_task_migrations": "Suoritetaan tietokantamigraatioitaâ€Ļ", + "maintenance_task_restore": "Palautetaan valittu varmuuskopioâ€Ļ", + "maintenance_task_rollback": "Palauttaminen epäonnistui, palataan takaisin palautuspisteeseenâ€Ļ", "maintenance_title": "Tilapäisesti ei saatavilla", "make": "Valmistaja", "manage_geolocation": "Muokkaa sijaintia", @@ -1426,6 +1509,7 @@ "my_albums": "Omat albumit", "name": "Nimi", "name_or_nickname": "Nimi tai lempinimi", + "name_required": "Nimi on pakollinen", "navigate": "Navigoi", "navigate_to_time": "Navigoi aikaan", "network_requirement_photos_upload": "Käytä mobiiliverkkoa kuvien varmuuskopioimiseksi", @@ -1450,11 +1534,13 @@ "next": "Seuraava", "next_memory": "Seuraava muisto", "no": "Ei", + "no_actions_added": "Toimintoja ei ole vielä lisätty", + "no_albums_found": "Albumeja ei lÃļytynyt", "no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä", "no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.", "no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.", "no_archived_assets_message": "Arkistoi kuvia ja videoita piilottaaksesi ne kuvat näkymästä", - "no_assets_message": "NAPAUTA LADATAKSESI ENSIMMÄINEN KUVASI", + "no_assets_message": "Napsauta lähettääksesi ensimmäisen kuvasi", "no_assets_to_show": "Ei näytettäviä kohteita", "no_cast_devices_found": "Cast-laitteita ei lÃļytynyt", "no_checksum_local": "Ei tarkistussummaa - paikallista sisältÃļä ei voida hakea", @@ -1464,6 +1550,7 @@ "no_exif_info_available": "EXIF-tietoa ei saatavilla", "no_explore_results_message": "Lataa lisää kuvia tutkiaksesi kokoelmaasi.", "no_favorites_message": "Lisää suosikkeja lÃļytääksesi nopeasti parhaat kuvasi ja videosi", + "no_filters_added": "Suodattimia ei ole vielä lisätty", "no_libraries_message": "Luo ulkoinen kirjasto nähdäksesi valokuvasi ja videot", "no_local_assets_found": "Paikallista sisältÃļä ei lÃļytynyt tällä tarkistussummalla", "no_location_set": "Ei sijaintia asetettuna", @@ -1481,7 +1568,6 @@ "not_available": "N/A", "not_in_any_album": "Ei yhdessäkään albumissa", "not_selected": "Ei valittu", - "note_apply_storage_label_to_previously_uploaded assets": "Huom: Jotta voit soveltaa tallennustunnistetta aiemmin ladattuihin kohteisiin, suorita", "notes": "Muistiinpanot", "nothing_here_yet": "Ei vielä mitään", "notification_permission_dialog_content": "Ottaaksesi ilmoitukset käyttÃļÃļn, siirry asetuksiin ja valitse 'salli'.", @@ -1583,11 +1669,14 @@ "person_age_years": "{years, plural, other {# vuotta}} vanha", "person_birthdate": "Syntynyt {date}", "person_hidden": "{name}{hidden, select, true { (piilotettu)} other {}}", + "person_recognized": "HenkilÃļ tunnistettu", + "person_selected": "HenkilÃļ valittu", "photo_shared_all_users": "Näyttää että olet jakanut kuvasi kaikkien käyttäjien kanssa, tai sinulla ei ole käyttäjää kenelle jakaa.", "photos": "Kuvat", "photos_and_videos": "Kuvat ja videot", "photos_count": "{count, plural, one {{count, number} Kuva} other {{count, number} kuvaa}}", "photos_from_previous_years": "Kuvia edellisiltä vuosilta", + "photos_only": "Vain kuvat", "pick_a_location": "Valitse sijainti", "pick_custom_range": "Mukautettu väli", "pick_date_range": "Valitse päivämäärien väli", @@ -1677,7 +1766,7 @@ "reassigned_assets_to_new_person": "Määritetty {count, plural, one {# media} other {# mediaa}} uudelle henkilÃļlle", "reassing_hint": "Määritä valitut mediat käyttäjälle", "recent": "Viimeisin", - "recent-albums": "Viimeisimmät albumit", + "recent_albums": "Viimeisimmät albumit", "recent_searches": "Edelliset haut", "recently_added": "Viimeksi lisätty", "recently_added_page_title": "Viimeksi lisätyt", @@ -1832,7 +1921,9 @@ "second": "Toinen", "see_all_people": "Näytä kaikki henkilÃļt", "select": "Valitse", + "select_album": "Valitse albumi", "select_album_cover": "Valitse albumin kansi", + "select_albums": "Valitse albumit", "select_all": "Valitse kaikki", "select_all_duplicates": "Valitse kaikki kaksoiskappaleet", "select_all_in": "Valitse kaikki {group}", @@ -1843,6 +1934,7 @@ "select_keep_all": "Valitse pidä kaikki", "select_library_owner": "Valitse kirjaston omistaja", "select_new_face": "Valitse uudet kasvot", + "select_person": "Valitse henkilÃļ", "select_person_to_tag": "Valitse henkilÃļ, jonka haluat merkitä", "select_photos": "Valitse kuvat", "select_trash_all": "Valitse kaikki roskakoriin", @@ -1978,6 +2070,7 @@ "show_password": "Näytä salasana", "show_person_options": "Näytä henkilÃļasetukset", "show_progress_bar": "Näytä eteneminen", + "show_schema": "Näytä skeema", "show_search_options": "Näytä hakuvaihtoehdot", "show_shared_links": "Näytä jaetut linkit", "show_slideshow_transition": "Näytä diaesitys siirtymä", @@ -1995,6 +2088,8 @@ "skip_to_folders": "Siirry kansioihin", "skip_to_tags": "Siirry tunnisteisiin", "slideshow": "Diaesitys", + "slideshow_repeat": "Kertaa diaesitys", + "slideshow_repeat_description": "Palaa takaisin alkuun diaesityksen päättyessä", "slideshow_settings": "Diaesityksen asetukset", "sort_albums_by": "Järjestä albumit...", "sort_created": "Luontipäivä", @@ -2105,6 +2200,11 @@ "trash_page_select_assets_btn": "Valitse kohteet", "trash_page_title": "Roskakori ({count})", "trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.", + "trigger": "Laukaisin", + "trigger_description": "TyÃļnkulun aloittava tapahtuma", + "trigger_person_recognized": "HenkilÃļ tunnistettu", + "trigger_person_recognized_description": "Laukaistaan kun henkilÃļ tunnistetaan", + "trigger_type": "Laukaisimen tyyppi", "troubleshoot": "Vianetsintä", "type": "Tyyppi", "unable_to_change_pin_code": "PIN-koodin vaihtaminen epäonnistui", @@ -2119,6 +2219,7 @@ "unhide_person": "Poista henkilÃļ piilosta", "unknown": "Tuntematon", "unknown_country": "Tuntematon maa", + "unknown_date": "Tuntematon päiväys", "unknown_year": "Tuntematon vuosi", "unlimited": "Rajoittamaton", "unlink_motion_video": "Poista liikevideon linkitys", @@ -2135,13 +2236,14 @@ "unstack": "Pura pino", "unstack_action_prompt": "{count} purettu pinosta", "unstacked_assets_count": "Poistettu pinosta {count, plural, one {# kohde} other {# kohdetta}}", + "unsupported_field_type": "Ei-tuettu kentän tyyppi", "untagged": "Ilman tunnistetta", + "untitled_workflow": "NimetÃļn tyÃļnkulku", "up_next": "Seuraavaksi", "update_location_action_prompt": "Päivitä {count} kohteen sijaintia:", "updated_at": "Päivitetty", "updated_password": "Salasana päivitetty", "upload": "Siirrä palvelimelle", - "upload_action_prompt": "{count} jonossa lähetystä varten", "upload_concurrency": "Latausten samanaikaisuus", "upload_details": "Lähetyksen tiedot", "upload_dialog_info": "Haluatko varmuuskopioida valitut kohteet palvelimelle?", @@ -2160,7 +2262,7 @@ "url": "URL", "usage": "KäyttÃļ", "use_biometric": "Käytä biometriikkaa", - "use_current_connection": "käytä nykyistä yhteyttä", + "use_current_connection": "Käytä nykyistä yhteyttä", "use_custom_date_range": "Käytä omaa aikaväliä", "user": "Käyttäjä", "user_has_been_deleted": "Käyttäjä on poistettu.", @@ -2181,6 +2283,7 @@ "utilities": "Apuohjelmat", "validate": "Validoi", "validate_endpoint_error": "Anna kelvollinen URL-osoite", + "validation_error": "Validointivirhe", "variables": "Muuttujat", "version": "Versio", "version_announcement_closing": "Ystäväsi Alex", @@ -2192,6 +2295,7 @@ "video_hover_setting_description": "Toista videon esikatselukuva kun kursori on kuvan päällä. Vaikka toiminto on pois käytÃļstä, toiston voi aloittaa viemällä kursori toistokuvakkeen päälle.", "videos": "Videot", "videos_count": "{count, plural, one {# video} other {# videota}}", + "videos_only": "Vain videot", "view": "Katso", "view_album": "Näytä albumi", "view_all": "Näytä kaikki", @@ -2212,14 +2316,28 @@ "viewer_stack_use_as_main_asset": "Käytä pääkohteena", "viewer_unstack": "Pura pino", "visibility_changed": "{count, plural, one {# henkilÃļn} other {# henkilÃļiden}} näkyvyys vaihdettu", + "visual": "Visuaalinen", + "visual_builder": "Visuaalinen koostaja", "waiting": "Odottaa", "waiting_count": "Odottaa: {count}", "warning": "Varoitus", "week": "Viikko", "welcome": "Tervetuloa", "welcome_to_immich": "Tervetuloa Immichiin", + "width": "Leveys", "wifi_name": "Wi-Fi-verkon nimi", - "workflow": "TyÃļnkulku", + "workflow_delete_prompt": "Haluatko varmasti poistaa tämän tyÃļnkulun?", + "workflow_deleted": "TyÃļnkulku poistettu", + "workflow_description": "TyÃļnkulun kuvaus", + "workflow_info": "TyÃļnkulut tiedot", + "workflow_json": "TyÃļnkulun JSON", + "workflow_json_help": "Muokkaa tyÃļnkulun kokoonpanoa JSON-muodossa. Muutokset synkronoidaan visuaaliseen koostajaan.", + "workflow_name": "TyÃļnkulun nimi", + "workflow_navigation_prompt": "Haluatko varmasti poistua tallentamatta muutoksia?", + "workflow_summary": "TyÃļnkulun yhteenveto", + "workflow_update_success": "TyÃļnkulku päivitetty onnistuneesti", + "workflow_updated": "TyÃļnkulku päivitetty", + "workflows": "TyÃļnkulut", "wrong_pin_code": "Väärä PIN-koodi", "year": "Vuosi", "years_ago": "{years, plural, one {# vuosi} other {# vuotta}} sitten", diff --git a/i18n/fil.json b/i18n/fil.json index 413ed85828..c3340f2c8f 100644 --- a/i18n/fil.json +++ b/i18n/fil.json @@ -14,6 +14,7 @@ "add_a_location": "Dagdagan ng lugar", "add_a_name": "Dagdagan ng pangalan", "add_a_title": "Dagdagan ng pamagat", + "add_birthday": "Maglagay ng kaarawan", "add_endpoint": "Dagdagan ng dulo", "add_location": "Magdagdag ng lugar", "add_more_users": "Magdagdag ng mga user", diff --git a/i18n/fr.json b/i18n/fr.json index 4c871c1c84..7dc9e80e21 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -5,6 +5,7 @@ "acknowledge": "Compris", "action": "Action", "action_common_update": "Mettre à jour", + "action_description": "Un ensemble d'actions applicables sur des mÊdias filtrÊs", "actions": "Actions", "active": "En cours", "active_count": "Actif : {count}", @@ -15,9 +16,14 @@ "add_a_location": "Ajouter une localisation", "add_a_name": "Ajouter un nom", "add_a_title": "Ajouter un titre", + "add_action": "Ajouter une action", + "add_action_description": "Cliquez pour ajouter une action à rÊaliser", + "add_assets": "Ajouter des mÊdias", "add_birthday": "Ajouter un anniversaire", "add_endpoint": "Ajouter une adresse", "add_exclusion_pattern": "Ajouter un schÊma d'exclusion", + "add_filter": "Ajouter un filtre", + "add_filter_description": "Cliquez pour ajouter une condition au filtre", "add_location": "Ajouter une localisation", "add_more_users": "Ajouter plus d'utilisateurs", "add_partner": "Ajouter un partenaire", @@ -36,6 +42,7 @@ "add_to_shared_album": "Ajouter à l'album partagÊ", "add_upload_to_stack": "Ajouter les ÊlÊments tÊlÊversÊs à la pile", "add_url": "Ajouter l'URL", + "add_workflow_step": "Ajouter une Êtape de flux de traitement", "added_to_archive": "AjoutÊ à l'archive", "added_to_favorites": "AjoutÊ aux favoris", "added_to_favorites_count": "{count, number} ajoutÊ(s) aux favoris", @@ -79,7 +86,7 @@ "export_config_as_json_description": "TÊlÊcharger la configuration actuelle du système en tant que fichier JSON", "external_libraries_page_description": "Page d'administration des bibliothèques externes", "face_detection": "DÊtection des visages", - "face_detection_description": "DÊtection des visages dans les mÊdias à l'aide de l'apprentissage automatique. Pour les vidÊos, seule la miniature est prise en compte. ÂĢ Actualiser Âģ (re)traite tous les mÊdias. ÂĢ RÊinitialiser Âģ retraite tous les visages en repartant de zÊro. ÂĢ Manquant Âģ met en file d'attente les mÊdias qui n'ont pas encore ÊtÊ traitÊs. Lorsque la dÊtection est terminÊe, les visages dÊtectÊs seront mis en file d'attente pour la reconnaissance faciale.", + "face_detection_description": "DÊtecte les visages dans les mÊdias à l'aide de l'apprentissage automatique. Pour les vidÊos, seule la miniature est prise en compte. ÂĢ Actualiser Âģ (re)traite tous les mÊdias. ÂĢ RÊinitialiser Âģ retraite tous les visages en repartant de zÊro. ÂĢ Manquant Âģ met en file d'attente les mÊdias qui n'ont pas encore ÊtÊ traitÊs. Lorsque la dÊtection est terminÊe, les visages dÊtectÊs seront mis en file d'attente pour la reconnaissance faciale, les regroupant en personnes existantes ou nouvelles.", "facial_recognition_job_description": "Regrouper les visages dÊtectÊs en personnes. Cette Êtape est exÊcutÊe une fois la dÊtection des visages terminÊe. ÂĢ RÊinitialiser Âģ (re)regroupe tous les visages. ÂĢ Manquant Âģ met en file d'attente les visages auxquels aucune personne n'a ÊtÊ attribuÊe.", "failed_job_command": "La commande {command} a ÊchouÊ pour la tÃĸche : {job}", "force_delete_user_warning": "ATTENTION : Cette opÊration entraÃŽne la suppression immÊdiate de l'utilisateur et de tous ses mÊdias. Cette opÊration ne peut ÃĒtre annulÊe et les fichiers ne peuvent ÃĒtre rÊcupÊrÊs.", @@ -97,6 +104,8 @@ "image_preview_description": "Image de taille moyenne avec mÊtadonnÊes retirÊes, utilisÊe lors de la visualisation d'un seul mÊdia et pour l'apprentissage automatique", "image_preview_quality_description": "QualitÊ de l'aperçu : de 1 à 100. Une valeur plus ÊlevÊe produit de meilleurs rÊsultats, mais elle produit des fichiers plus volumineux et peut rÊduire la rÊactivitÊ de l'application. Une valeur trop basse peut affecter la qualitÊ de l'apprentissage automatique.", "image_preview_title": "Paramètres de prÊvisualisation", + "image_progressive": "Progressif", + "image_progressive_description": "Encode les images JPEG de manière progressive pour un affichage graduel. Cela n'a pas d'effet sur les images en WebP.", "image_quality": "QualitÊ", "image_resolution": "RÊsolution", "image_resolution_description": "Les rÊsolutions plus ÊlevÊes permettent de prÊserver davantage de dÊtails, mais l'encodage est plus long, les fichiers sont plus volumineux et la rÊactivitÊ de l'application peut s'en trouver rÊduite.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Activer la recherche intelligente", "machine_learning_smart_search_enabled_description": "Si cette option est dÊsactivÊe, les images ne seront pas encodÊes pour la recherche intelligente.", "machine_learning_url_description": "L’URL du serveur d'apprentissage automatique. Si plusieurs URL sont fournies, chaque serveur sera essayÊ un par un jusqu’à ce que l’un d’eux rÊponde avec succès, dans l’ordre de la première à la dernière. Les serveurs ne rÊpondant pas seront temporairement ignorÊs jusqu'à ce qu'ils soient de nouveau opÊrationnels.", + "maintenance_delete_backup": "Supprimer la sauvegarde", + "maintenance_delete_backup_description": "Ce fichier sera dÊfinitivement supprimÊ.", + "maintenance_delete_error": "Échec de la suppression de la sauvegarde.", + "maintenance_restore_backup": "Restaurer la sauvegarde", + "maintenance_restore_backup_description": "Immich sera effacÊ et restaurÊ à partir de la sauvegarde choisie. Une sauvegarde sera crÊÊe avant de continuer.", + "maintenance_restore_backup_different_version": "Cette sauvegarde a ÊtÊ crÊÊe avec une version diffÊrente de Immich !", + "maintenance_restore_backup_unknown_version": "Impossible de dÊterminer la version de sauvegarde.", + "maintenance_restore_database_backup": "Restaurer la sauvegarde de la base de donnÊes", + "maintenance_restore_database_backup_description": "Revenir à un Êtat antÊrieur de la base de donnÊes à l'aide d'un fichier de sauvegarde", "maintenance_settings": "Maintenance", "maintenance_settings_description": "Mettre Immich en mode maintenance.", - "maintenance_start": "DÊmarrer le mode maintenance", + "maintenance_start": "Passer en mode maintenance", "maintenance_start_error": "Échec du dÊmarrage du mode maintenance.", + "maintenance_upload_backup": "TÊlÊcharger le fichier de sauvegarde de la base de donnÊes", + "maintenance_upload_backup_error": "Impossible de tÊlÊcharger la sauvegarde, s'agit-il d'un fichier .sql/.sql.gz ?", "manage_concurrency": "GÊrer du multitÃĸche", "manage_concurrency_description": "Naviguer vers la pages des tÃĸches pour gÊrer le multitÃĸche", "manage_log_settings": "GÊrer les paramètres de journalisation", @@ -252,7 +272,7 @@ "oauth_auto_register": "Inscription automatique", "oauth_auto_register_description": "Inscrire automatiquement de nouveaux utilisateurs après leur connexion avec OAuth", "oauth_button_text": "Texte du bouton", - "oauth_client_secret_description": "NÊcessaire si le protocole PKCE (Proof Key for Code Exchange) n'est pas supportÊ mar le fournisseur d'authentification OAuth", + "oauth_client_secret_description": "NÊcessaire pour un client confidentiel, ou si le protocole PKCE (Proof Key for Code Exchange) n'est pas supportÊ par le client public.", "oauth_enable_description": "Connexion avec OAuth", "oauth_mobile_redirect_uri": "URI de redirection mobile", "oauth_mobile_redirect_uri_override": "Remplacer l'URI de redirection mobile", @@ -291,7 +311,7 @@ "search_jobs": "Recherche des tÃĸchesâ€Ļ", "send_welcome_email": "Envoyer un courriel de bienvenue", "server_external_domain_settings": "Domaine externe", - "server_external_domain_settings_description": "Nom de domaine pour les liens partagÊs publics, y compris http(s)://", + "server_external_domain_settings_description": "Nom de domaine utilisÊ pour les liens externes", "server_public_users": "Utilisateurs publics", "server_public_users_description": "Tous les utilisateurs (nom et courriel) sont listÊs lors de l'ajout d'un utilisateur à des albums partagÊs. Quand cela est dÊsactivÊ, la liste des utilisateurs est uniquement disponible pour les comptes administrateurs.", "server_settings": "Paramètres du serveur", @@ -331,7 +351,7 @@ "template_settings": "Modèles de notifications", "template_settings_description": "GÊrer les modèles personnalisÊs pour les notifications", "theme_custom_css_settings": "CSS personnalisÊ", - "theme_custom_css_settings_description": "Les feuilles de style en cascade (CSS) permettent de personnaliser l'apparence d'Immich.", + "theme_custom_css_settings_description": "Les feuilles de style (CSS) permettent de personnaliser l'apparence d'Immich.", "theme_settings": "Paramètres du thème", "theme_settings_description": "GÊrer la personnalisation de l'interface web d'Immich", "thumbnail_generation_job": "GÊnÊration des miniatures", @@ -431,6 +451,9 @@ "admin_password": "Mot de passe Admin", "administration": "Administration", "advanced": "AvancÊ", + "advanced_settings_clear_image_cache": "Vider le cache des images", + "advanced_settings_clear_image_cache_error": "Erreur au vidage du cache des images", + "advanced_settings_clear_image_cache_success": "Vidage avec succès de {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilisez cette option pour filtrer les mÊdia durant la synchronisation avec des critères alternatifs. N'utilisez cela que lorsque l'application n'arrive pas à dÊtecter tous les albums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPÉRIMENTAL] Utiliser le filtre de synchronisation d'album alternatif", "advanced_settings_log_level_title": "Niveau de journalisation : {level}", @@ -467,10 +490,12 @@ "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_selected": "Album sÊlectionnÊ", "album_share_no_users": "Il semble que vous ayez partagÊ cet album avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec lequel le partager.", "album_summary": "RÊsumÊ de l'album", "album_updated": "Album mis à jour", "album_updated_setting_description": "Recevoir une notification par courriel lorsqu'un album partagÊ a de nouveaux mÊdias", + "album_upload_assets": "TÊlÊchargez des fichiers depuis votre ordinateur et ajoutez-les à l'album", "album_user_left": "{album} quittÊ", "album_user_removed": "{user} supprimÊ", "album_viewer_appbar_delete_confirm": "Êtes-vous sur de vouloir supprimer cet album de votre compte ?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# album sÊlectionnÊ} other {# albums sÊlectionnÊs}}", "all": "Tout", "all_albums": "Tous les albums", "all_people": "Toutes les personnes", + "all_photos": "Toutes les photos", "all_videos": "Toutes les vidÊos", "allow_dark_mode": "Autoriser le mode sombre", "allow_edits": "Autoriser les modifications", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permettre l'envoi par des utilisateurs non connectÊs", "allowed": "AutorisÊ", "alt_text_qr_code": "Image du code QR", + "always_keep": "Toujours conserver", + "always_keep_photos_hint": "LibÊrer de l'espace va conserver toutes les photos sur cet appareil.", + "always_keep_videos_hint": "LibÊrer de l'espace va conserver toutes les vidÊos sur cet appareil.", "anti_clockwise": "Sens anti-horaire", "api_key": "ClÊ API", "api_key_description": "Cette valeur ne sera affichÊe qu'une seule fois. Assurez-vous de la copier avant de fermer la fenÃĒtre.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {# archivÊ} other {# archivÊs}}", "are_these_the_same_person": "Est-ce la mÃĒme personne ?", "are_you_sure_to_do_this": "Êtes-vous sÃģr de vouloir faire ceci ?", + "array_field_not_fully_supported": "Les champs du tableau nÊcessitent la modification manuelle du JSON", "asset_action_delete_err_read_only": "Impossible de supprimer le(s) mÊdia(s) en lecture seule, ils sont ignorÊs", "asset_action_share_err_offline": "Impossible de rÊcupÊrer le(s) mÊdia(s) hors ligne, ils sont ignorÊs", "asset_added_to_album": "AjoutÊ à l'album", "asset_adding_to_album": "Ajout à l'albumâ€Ļ", + "asset_created": "MÊdia crÊÊ", "asset_description_updated": "La description du mÊdia a ÊtÊ mise à jour", "asset_filename_is_offline": "Le mÊdia {filename} est hors ligne", "asset_has_unassigned_faces": "Le mÊdia a des visages non attribuÊs", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Disposition", "asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos", "asset_list_settings_title": "Grille de photos", + "asset_not_found_on_device_android": "MÊdia introuvable sur l'appareil", + "asset_not_found_on_device_ios": "MÊdia introuvable sur l'appareil. Si vous utilisez iCloud, le mÊdia peut ÃĒtre inaccessible en raison d'un fichier corrompu stockÊ sur iCloud", + "asset_not_found_on_icloud": "MÊdia introuvable sur iCloud. Le mÊdia est peut-ÃĒtre inaccessible en raison d'un fichier corrompu stockÊ sur iCloud", "asset_offline": "MÊdia hors ligne", "asset_offline_description": "Ce mÊdia externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.", "asset_restored_successfully": "ÉlÊment restaurÊ avec succès", @@ -575,7 +610,7 @@ "assets_were_part_of_album_count": "{count, plural, one {Un mÊdia est} other {Des mÊdias sont}} dÊjà dans l'album", "assets_were_part_of_albums_count": "{count, plural, one {Le mÊdia Êtait dÊjà prÊsent} other {Les mÊdias Êtaient dÊjà prÊsents}} dans les albums", "authorized_devices": "Appareils autorisÊs", - "automatic_endpoint_switching_subtitle": "Se connecter localement lorsque connectÊ au WI-FI spÊcifiÊ mais utiliser une adresse alternative lorsque connectÊ à un autre rÊseau", + "automatic_endpoint_switching_subtitle": "Se connecter localement via le rÊseau Wi-Fi dÊsignÊ lorsqu'il est disponible et utiliser d'autres connexions ailleurs", "automatic_endpoint_switching_title": "Changement automatique d'adresse", "autoplay_slideshow": "Lecture automatique d'un diaporama", "back": "Retour", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "SÊlectionner les albums", "backup_album_selection_page_selection_info": "Informations sur la sÊlection", "backup_album_selection_page_total_assets": "Total des ÊlÊments uniques", - "backup_albums_sync": "Sauvegarde de la synchronisation des albums", + "backup_albums_sync": "Sauvegarde de la Synchronisation des Albums", "backup_all": "Tout", "backup_background_service_backup_failed_message": "Échec de la sauvegarde des mÊdias. Nouvelle tentativeâ€Ļ", "backup_background_service_complete_notification": "Sauvegarde du mÊdia terminÊe", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", "change_pin_code": "Changer le code PIN", + "change_trigger": "Changer le dÊclencheur", + "change_trigger_prompt": "Êtes-vous sÃģr de vouloir changer le dÊclencheur ? Cela va supprimer toutes les actions et filtres existants.", "change_your_password": "Changer votre mot de passe", "changed_visibility_successfully": "VisibilitÊ modifiÊe avec succès", "charging": "En charge", @@ -722,6 +759,18 @@ "checksum": "Somme de contrôle", "choose_matching_people_to_merge": "Choisir les personnes à fusionner", "city": "Ville", + "cleanup_confirm_description": "Immich a trouvÊ {count} ÊlÊments (crÊÊs avant {date}) sauvegardÊs en toute sÊcuritÊ sur le serveur. Supprimer les copies locales de cet appareil ?", + "cleanup_confirm_prompt_title": "Supprimer de cet appareil ?", + "cleanup_deleted_assets": "{count} ÊlÊments ont ÊtÊ dÊplacÊs vers la corbeille de l'appareil", + "cleanup_deleting": "DÊplacement vers la corbeille...", + "cleanup_found_assets": "{count} ÊlÊments trouvÊs et sauvegardÊs", + "cleanup_found_assets_with_size": "{count} mÊdias sauvegardÊs trouvÊs ({size})", + "cleanup_icloud_shared_albums_excluded": "Les albums partagÊs iCloud sont exclus de l'analyse", + "cleanup_no_assets_found": "Aucun ÊlÊment correspondant aux critères ci-dessus n'a ÊtÊ trouvÊ. LibÊrer de l'espace peut seulement supprimer les mÊdias qui ont ÊtÊ sauvegardÊs sur le serveur", + "cleanup_preview_title": "ÉlÊments à supprimer ({count})", + "cleanup_step3_description": "Rechercher des mÊdias sauvegardÊs qui correspondent à vos dates et aux paramètres de conservation.", + "cleanup_step4_summary": "{count} ÊlÊments crÊÊs avant le {date} à supprimer localement sur votre appareil. Les photos resteront accessibles depuis l'appli Immich.", + "cleanup_trash_hint": "Pour libÊrer complètement l’espace de stockage, ouvrez l’application Galerie du système et videz la corbeille", "clear": "Effacer", "clear_all": "Effacer tout", "clear_all_recent_searches": "Supprimer les recherches rÊcentes", @@ -733,6 +782,8 @@ "client_cert_import": "Importer", "client_cert_import_success_msg": "Certificat importÊ", "client_cert_invalid_msg": "Fichier de certificat invalide ou mot de passe incorrect", + "client_cert_password_message": "Renseignez le mot de passe de ce certificat", + "client_cert_password_title": "Mot de passe du certificat", "client_cert_remove_msg": "Certificat supprimÊ", "client_cert_subtitle": "Prend en charge uniquement le format PKCS12 (.p12, .pfx). L'importation/suppression de certificats n'est possible qu'avant la connexion", "client_cert_title": "Certificat SSL [EXPÉRIMENTAL]", @@ -743,6 +794,11 @@ "color": "Couleur", "color_theme": "Thème de couleur", "command": "Commande", + "command_palette_prompt": "Trouver rapidement des pages, actions ou commandes", + "command_palette_to_close": "pour fermer", + "command_palette_to_navigate": "pour entrer", + "command_palette_to_select": "pour sÊlectionner", + "command_palette_to_show_all": "pour tout afficher", "comment_deleted": "Commentaire supprimÊ", "comment_options": "Options des commentaires", "comments_and_likes": "Commentaires et \"J'aime\"", @@ -787,6 +843,7 @@ "create_album": "CrÊer un album", "create_album_page_untitled": "Sans titre", "create_api_key": "CrÊer une clÊ d'API", + "create_first_workflow": "CrÊer le premier flux de traitement", "create_library": "CrÊer une bibliothèque", "create_link": "CrÊer le lien", "create_link_to_share": "CrÊer un lien pour partager", @@ -801,17 +858,25 @@ "create_tag": "CrÊer une Êtiquette", "create_tag_description": "CrÊer une nouvelle Êtiquette. Pour les Êtiquettes imbriquÊes, veuillez entrer le chemin complet de l'Êtiquette, y compris les caractères \"/\".", "create_user": "CrÊer un utilisateur", + "create_workflow": "CrÊer un flux de traitement", "created": "CrÊÊ", "created_at": "CrÊÊ à", "creating_linked_albums": "CrÊation des albums liÊs...", "crop": "Recadrer", + "crop_aspect_ratio_fixed": "FigÊ", + "crop_aspect_ratio_free": "Libre", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Objets", "current_device": "Appareil actuel", "current_pin_code": "Code PIN actuel", "current_server_address": "Adresse actuelle du serveur", + "custom_date": "Date personnalisÊe", "custom_locale": "Paramètres rÊgionaux personnalisÊs", "custom_locale_description": "Afficher les dates et nombres en fonction des paramètres rÊgionaux", "custom_url": "URL personnalisÊe", + "cutoff_date_description": "Conservez les photos depuis les derniersâ€Ļ", + "cutoff_day": "{count, plural, one {jour} other {jours}}", + "cutoff_year": "{count, plural, one {annÊe} other {annÊes}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Sombre", @@ -867,6 +932,7 @@ "deselect_all": "Tout dÊsÊlectionner", "details": "DÊtails", "direction": "Ordre", + "disable": "DÊsactiver", "disabled": "DÊsactivÊ", "disallow_edits": "Ne pas autoriser les modifications", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "VidÊos intÊgrÊes", "download_include_embedded_motion_videos_description": "Inclure des vidÊos intÊgrÊes dans les photos de mouvement comme un fichier sÊparÊ", "download_notfound": "TÊlÊchargement non trouvÊ", + "download_original": "TÊlÊcharger l'original", "download_paused": "TÊlÊchargement en pause", "download_settings": "TÊlÊcharger", "download_settings_description": "GÊrer les paramètres de tÊlÊchargement des mÊdias", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "TÊlÊchargement en attente du prochain essai", "downloading": "TÊlÊchargement", "downloading_asset_filename": "TÊlÊchargement du mÊdia {filename}", + "downloading_from_icloud": "TÊlÊchargement depuis iCloud", "downloading_media": "TÊlÊchargement du mÊdia", "drop_files_to_upload": "DÊposez les fichiers n'importe oÚ pour envoyer", "duplicates": "Doublons", @@ -929,11 +997,22 @@ "edit_tag": "Modifier l'Êtiquette", "edit_title": "Modifier le titre", "edit_user": "Modifier l'utilisateur", + "edit_workflow": "Modifier le flux de traitement", "editor": "Editeur", "editor_close_without_save_prompt": "Les changements ne seront pas enregistrÊs", "editor_close_without_save_title": "Fermer l'Êditeur ?", - "editor_crop_tool_h2_aspect_ratios": "Rapports hauteur/largeur", - "editor_crop_tool_h2_rotation": "Rotation", + "editor_confirm_reset_all_changes": "Êtes-vous sÃģr de vouloir rÊinitialiser toutes les modifications ?", + "editor_discard_edits_confirm": "Annuler les Êditions", + "editor_discard_edits_prompt": "Vous avez des modifications non sauvegardÊes. Etes-vous sÃģr de vouloir les perdre ?", + "editor_discard_edits_title": "Annuler les Êditions ?", + "editor_edits_applied_error": "Echec d'application des Êditions", + "editor_edits_applied_success": "Editions appliquÊes avec succès", + "editor_flip_horizontal": "Retourner horizontalement", + "editor_flip_vertical": "Retourner verticalement", + "editor_orientation": "Orientation", + "editor_reset_all_changes": "RÊinitialiser les modifications", + "editor_rotate_left": "Rotation de 90° dans le sens inverse des aiguilles d'une montre", + "editor_rotate_right": "Rotation de 90° dans le sens des aiguilles d'une montre", "email": "Courriel", "email_notifications": "Notifications email", "empty_folder": "Ce dossier est vide", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Impossible de modifier l'ordre de tri des albums", "error_delete_face": "Erreur lors de la suppression du visage pour le mÊdia", "error_getting_places": "Erreur à la rÊcupÊration des lieux", + "error_loading_albums": "Erreur au chargement des albums", "error_loading_image": "Erreur de chargement de l'image", "error_loading_partners": "Erreur de rÊcupÊration des partenaires : {error}", + "error_retrieving_asset_information": "Erreur à la rÊcupÊration des informations du mÊdia", "error_saving_image": "Erreur : {error}", "error_tag_face_bounding_box": "Erreur lors de l'identification de visage - impossible de rÊcupÊrer les coordonnÊes du cadre entourant le visage", "error_title": "Erreur - Quelque chose s'est mal passÊ", + "error_while_navigating": "Erreur lors de la navigation vers le mÊdia", "errors": { "cannot_navigate_next_asset": "Impossible de naviguer jusqu'au prochain mÊdia", "cannot_navigate_previous_asset": "Impossible de naviguer jusqu'au prÊcÊdent mÊdia", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "Impossible de terminer la connexion OAuth", "unable_to_connect": "Impossible de se connecter", "unable_to_copy_to_clipboard": "Impossible de copier dans le presse-papiers, assurez-vous que vous accÊdez à la page via https", + "unable_to_create": "Impossible de crÊer le flux de traitement", "unable_to_create_admin_account": "Impossible de crÊer le compte administrateur", "unable_to_create_api_key": "Impossible de crÊer une nouvelle clÊ API", "unable_to_create_library": "Impossible de crÊer la bibliothèque", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Impossible de supprimer le modèle d'exclusion", "unable_to_delete_shared_link": "Impossible de supprimer le lien de partage", "unable_to_delete_user": "Impossible de supprimer l'utilisateur", + "unable_to_delete_workflow": "Impossible de supprimer le flux de traitement", "unable_to_download_files": "Impossible de tÊlÊcharger les fichiers", "unable_to_edit_exclusion_pattern": "Impossible de modifier le modèle d'exclusion", "unable_to_empty_trash": "Impossible de vider la corbeille", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "Impossible de scanner la bibliothèque", "unable_to_set_feature_photo": "Impossible de dÊfinir la photo de la personne", "unable_to_set_profile_picture": "Impossible d'enregistrer la photo de profil", + "unable_to_set_rating": "Impossible de dÊfinir une note", "unable_to_submit_job": "Impossible d'exÊcuter la tÃĸche", "unable_to_trash_asset": "Impossible de mettre le mÊdia à la corbeille", "unable_to_unlink_account": "Impossible de dÊtacher le compte", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Impossible de mettre à jour les paramètres", "unable_to_update_timeline_display_status": "Impossible de mettre à jour le statut d'affichage de la vue chronologique", "unable_to_update_user": "Impossible de mettre à jour l'utilisateur", + "unable_to_update_workflow": "Impossible de mettre à jour le flux de traitement", "unable_to_upload_file": "Impossible d'envoyer le fichier" }, + "errors_text": "Erreurs", "exclusion_pattern": "SchÊma d'exclusion", "exif": "Exif", "exif_bottom_sheet_description": "Ajouter une description...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONNES", "exif_bottom_sheet_person_add_person": "Ajouter un nom", "exit_slideshow": "Quitter le diaporama", + "expand": "DÊvelopper", "expand_all": "Tout dÊvelopper", "experimental_settings_new_asset_list_subtitle": "En cours de dÊveloppement", "experimental_settings_new_asset_list_title": "Activer la grille de photos expÊrimentale", @@ -1120,14 +1208,17 @@ "features": "FonctionnalitÊs", "features_in_development": "FonctionnalitÊs en dÊveloppement", "features_setting_description": "GÊrer les fonctionnalitÊs de l'application", - "file_name": "Nom du fichier", "file_name_or_extension": "Nom du fichier ou extension", + "file_name_text": "Nom du fichier", + "file_name_with_value": "Nom du fichier : {file_name}", "file_size": "Taille du fichier", "filename": "Nom du fichier", "filetype": "Type de fichier", - "filter": "Filtres", + "filter": "Filtrer", + "filter_description": "Conditions pour filtrer les mÊdias ciblÊs", "filter_people": "Filtrer les personnes", "filter_places": "Filtrer par lieu", + "filters": "Filtres", "find_them_fast": "Pour les retrouver rapidement par leur nom", "first": "Premier", "fix_incorrect_match": "Corriger une association incorrecte", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Parcourir l'affichage par dossiers pour les photos et les vidÊos sur le système de fichiers", "forgot_pin_code_question": "Code PIN oubliÊ ?", "forward": "Avant", + "free_up_space": "LibÊrer de l'espace", + "free_up_space_description": "DÊplacer les photos et vidÊos sauvegardÊes vers la corbeille de votre appareil pour libÊrer de l'espace. Vos copies sur le serveur restent en sÊcuritÊ.", + "free_up_space_settings_subtitle": "LibÊrer l'espace de votre appareil", "full_path": "Chemin complet : {path}", "gcast_enabled": "Diffusion Google Cast", "gcast_enabled_description": "Cette fonctionnalitÊ charge des ressources externes depuis Google pour fonctionner.", "general": "GÊnÊral", "geolocation_instruction_location": "Cliquez sur un mÊdia avec des coordonnÊes GPS pour utiliser sa localisation, ou bien sÊlectionnez une localisation directement sur la carte", "get_help": "Obtenir de l'aide", + "get_people_error": "Erreur de rÊcupÊration des personnes", "get_wifiname_error": "Impossible d'obtenir le nom du rÊseau wifi. Assurez-vous d'avoir donnÊ les permissions nÊcessaires à l'application et que vous ÃĒtes connectÊ à un rÊseau wifi", "getting_started": "Commencer", "go_back": "Retour", @@ -1175,6 +1270,7 @@ "hide_named_person": "Masquer {name}", "hide_password": "Masquer le mot de passe", "hide_person": "Masquer la personne", + "hide_schema": "Masquer le schÊma", "hide_text_recognition": "Cacher la reconnaissance de texte", "hide_unnamed_people": "Cacher les personnes non nommÊes", "home_page_add_to_album_conflicts": "{added} ÊlÊments ajoutÊs à l'album {album}. {failed} ÊlÊments sont dÊjà dans l'album.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "Le traitement a ÊtÊ lancÊ {dateTime}", "items_count": "{count, plural, one {# ÊlÊment} other {# ÊlÊments}}", "jobs": "TÃĸches", + "json_editor": "Éditeur JSON", + "json_error": "Erreur JSON", "keep": "Conserver", + "keep_albums": "Conserver les albums", + "keep_albums_count": "Conserver {count} {count, plural, one {album} other {albums}}", "keep_all": "Les conserver tous", + "keep_description": "Choisissez ce qui reste sur votre appareil quand vous libÊrez de l'espace.", + "keep_favorites": "Garder les favoris", + "keep_on_device": "Conserver sur l'appareil", + "keep_on_device_hint": "SÊlectionnez les ÊlÊments à conserver sur cet appareil", "keep_this_delete_others": "Conserver celui-ci, supprimer les autres", + "keeping": "ConservÊ : {items}", "kept_this_deleted_others": "Ce mÊdia a ÊtÊ conservÊ, et {count, plural, one {un autre a ÊtÊ supprimÊ} other {# autres ont ÊtÊ supprimÊs}}", "keyboard_shortcuts": "Raccourcis clavier", "language": "Langue", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Activer pour voir la vidÊo en boucle dans le lecteur dÊtaillÊ.", "main_branch_warning": "Vous utilisez une version de dÊveloppement. Nous vous recommandons fortement d'utiliser une version stable !", "main_menu": "Menu principal", + "maintenance_action_restore": "Restauration de la base de donnÊes", "maintenance_description": "Immich a ÊtÊ mis en mode maintenance.", "maintenance_end": "ArrÃĒter le mode maintenance", "maintenance_end_error": "Échec de l'arrÃĒt du mode maintenance.", "maintenance_logged_in_as": "Actuellement connectÊ en tant que {user}", + "maintenance_restore_from_backup": "Restaurer à partir d'une sauvegarde", + "maintenance_restore_library": "Restaurer votre bibliothèque", + "maintenance_restore_library_confirm": "Si cela vous semble correct, continuez à restaurer une sauvegarde !", + "maintenance_restore_library_description": "Restauration de la base de donnÊes", + "maintenance_restore_library_folder_has_files": "Le dossier {folder} contient {count} dossier(s)", + "maintenance_restore_library_folder_no_files": "Il manque des fichiers dans {folder}  !", + "maintenance_restore_library_folder_pass": "lecture et Êcriture", + "maintenance_restore_library_folder_read_fail": "lecture impossible", + "maintenance_restore_library_folder_write_fail": "Êcriture impossible", + "maintenance_restore_library_hint_missing_files": "Vous risquez de perdre des fichiers importants", + "maintenance_restore_library_hint_regenerate_later": "Vous pouvez les rÊgÊnÊrer ultÊrieurement dans les paramètres", + "maintenance_restore_library_hint_storage_template_missing_files": "Vous utilisez un modèle de stockage ? Il se peut que certains fichiers soient manquants", + "maintenance_restore_library_loading": "Chargement des contrôles d'intÊgritÊ et des heuristiquesâ€Ļ", + "maintenance_task_backup": "CrÊation d'une sauvegarde de la base de donnÊes existanteâ€Ļ", + "maintenance_task_migrations": "ExÊcution des migrations de base de donnÊesâ€Ļ", + "maintenance_task_restore": "Restauration de la sauvegarde sÊlectionnÊeâ€Ļ", + "maintenance_task_rollback": "La restauration a ÊchouÊ, retour au point de restaurationâ€Ļ", "maintenance_title": "Temporairement non disponible", "make": "Marque", "manage_geolocation": "GÊrer la localisation", @@ -1408,19 +1531,24 @@ "minimize": "RÊduire", "minute": "Minute", "minutes": "Minutes", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Manquant", "mobile_app": "Appli mobile", "mobile_app_download_onboarding_note": "TÊlÊchargez l'application mobile compagnon via les options suivantes", "model": "Modèle", "month": "Mois", - "monthly_title_text_date_format": "MMMM y", + "monthly_title_text_date_format": "MMMM a", "more": "Plus", "move": "DÊplacer", + "move_down": "Descendre", "move_off_locked_folder": "DÊplacer en dehors du dossier verrouillÊ", "move_to": "DÊplacer vers", + "move_to_device_trash": "DÊplacer vers la corbeille de l'appareil", "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Êes de tous les albums et ne seront visibles que dans le dossier verrouillÊ", + "move_up": "Monter", "moved_to_archive": "{count, plural, one {# ÊlÊment dÊplacÊ} other {# ÊlÊments dÊplacÊs}} vers les archives", "moved_to_library": "{count, plural, one {# ÊlÊment dÊplacÊ} other {# ÊlÊments dÊplacÊs}} vers la bibliothèque", "moved_to_trash": "DÊplacÊ dans la corbeille", @@ -1430,6 +1558,7 @@ "my_albums": "Mes albums", "name": "Nom", "name_or_nickname": "Nom ou surnom", + "name_required": "Le nom est nÊcessaire", "navigate": "Naviguer vers", "navigate_to_time": "Naviguer vers Date/Heure", "network_requirement_photos_upload": "Utiliser les donnÊes mobile pour sauvegarder les photos", @@ -1454,20 +1583,24 @@ "next": "Suivant", "next_memory": "Souvenir suivant", "no": "Non", + "no_actions_added": "Aucune action ajoutÊe pour le moment", + "no_albums_found": "Aucun album trouvÊ", "no_albums_message": "CrÊer un album pour organiser vos photos et vidÊos", "no_albums_with_name_yet": "Il semble que vous n'ayez pas encore d'albums avec ce nom.", "no_albums_yet": "Il semble que vous n'ayez pas encore d'album.", "no_archived_assets_message": "Archiver des photos et vidÊos pour les masquer dans votre bibliothèque", - "no_assets_message": "CLIQUEZ POUR ENVOYER VOTRE PREMIÈRE PHOTO", + "no_assets_message": "Cliquez pour envoyer votre première photo", "no_assets_to_show": "Aucun ÊlÊment à afficher", "no_cast_devices_found": "Aucun appareil de diffusion trouvÊ", "no_checksum_local": "Aucune empreinte numerique disponible - impossible de rÊcupÊrer les mÊdias locaux", "no_checksum_remote": "Aucune empreinte numÊrique disponible - impossible de rÊcupÊrer les mÊdias distants", + "no_configuration_needed": "Aucune configuration nÊcessaire", "no_devices": "Aucun appareil autorisÊ", "no_duplicates_found": "Aucun doublon n'a ÊtÊ trouvÊ.", "no_exif_info_available": "Aucune information exif disponible", "no_explore_results_message": "Envoyez plus de photos pour explorer votre bibliothèque.", "no_favorites_message": "Ajouter des photos et vidÊos à vos favoris pour les retrouver plus rapidement", + "no_filters_added": "Aucun filtre ajoutÊ pour le moment", "no_libraries_message": "CrÊer une bibliothèque externe pour voir vos photos et vidÊos dans un autre espace de stockage", "no_local_assets_found": "Aucun mÊdia local trouvÊ avec cette empreinte numerique", "no_location_set": "Aucune localisation definie", @@ -1481,11 +1614,11 @@ "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", + "none": "Aucun", "not_allowed": "Non autorisÊ", "not_available": "N/A", "not_in_any_album": "Dans aucun album", "not_selected": "Non sÊlectionnÊ", - "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'Êtiquette de stockage aux mÊdias prÊcÊdemment envoyÊs, exÊcutez", "notes": "Notes", "nothing_here_yet": "Rien pour le moment", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sÊlectionnez Autoriser.", @@ -1515,6 +1648,7 @@ "online": "En ligne", "only_favorites": "Uniquement les favoris", "open": "Ouvrir", + "open_calendar": "Ouvrir le calendrier", "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", @@ -1563,6 +1697,7 @@ "people": "Personnes", "people_edits_count": "{count, plural, one {# personne ÊditÊe} other {# personnes ÊditÊes}}", "people_feature_description": "Parcourir les photos et vidÊos groupÊes par personnes", + "people_selected": "{count, plural, one {# personne sÊlectionnÊe} other {# personnes sÊlectionnÊes}}", "people_sidebar_description": "Afficher le menu Personnes dans la barre latÊrale", "permanent_deletion_warning": "Avertissement avant suppression dÊfinitive", "permanent_deletion_warning_setting_description": "Afficher un avertissement avant la suppression dÊfinitive d'un mÊdia", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# ans}}", "person_birthdate": "NÊ(e) le {date}", "person_hidden": "{name}{hidden, select, true { (cachÊ)} other {}}", + "person_recognized": "Personne reconnue", + "person_selected": "Personne sÊlectionnÊe", "photo_shared_all_users": "Il semble que vous ayez partagÊ vos photos avec tous les utilisateurs ou que vous n'ayez aucun utilisateur avec qui les partager.", "photos": "Photos", "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", + "photos_only": "Photos uniquement", "pick_a_location": "Choisissez une localisation", "pick_custom_range": "PÊriode personnalisÊe", "pick_date_range": "SÊlectionner une pÊriode de dates", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "La clÊ du produit pour le Serveur est gÊrÊe par l'administrateur", "query_asset_id": "Obtenir l'ID du mÊdia", "queue_status": "{count}/{total} en file d'attente", + "rate_asset": "Évaluer un mÊdia", "rating": "Étoile d'Êvaluation", "rating_clear": "Effacer l'Êvaluation", "rating_count": "{count, plural, one {# Êtoile} other {# Êtoiles}}", "rating_description": "Afficher l'Êvaluation EXIF dans le panneau d'information", + "rating_set": "Note dÊfinie sur {rating, plural, one {# Êtoile} other {# Êtoiles}}", "reaction_options": "Options de rÊaction", "read_changelog": "Lire les changements", "readonly_mode_disabled": "Mode lecture seule dÊsactivÊ", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# mÊdia rÊattribuÊ} other {# mÊdias rÊattribuÊs}} à une nouvelle personne", "reassing_hint": "Attribuer ces mÊdias à une personne existante", "recent": "RÊcent", - "recent-albums": "Albums rÊcents", + "recent_albums": "Albums rÊcents", "recent_searches": "Recherches rÊcentes", "recently_added": "RÊcemment ajoutÊ", "recently_added_page_title": "RÊcemment ajoutÊ", @@ -1770,9 +1910,11 @@ "saved_settings": "Paramètres enregistrÊs", "say_something": "RÊagir", "scaffold_body_error_occurred": "Une erreur s'est produite", + "scan": "Analyse", "scan_all_libraries": "Analyser toutes les bibliothèques", "scan_library": "Analyser", "scan_settings": "Paramètres d'analyse", + "scanning": "Analyse en cours", "scanning_for_album": "Recherche d'albums en cours...", "search": "Recherche", "search_albums": "Rechercher des albums", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "SÊlectionner type de mÊdia", "search_filter_ocr": "Recherche par OCR", "search_filter_people_title": "SÊlectionner une personne", + "search_filter_star_rating": "Note par Êtoiles", "search_for": "Chercher", "search_for_existing_person": "Rechercher une personne existante", "search_no_more_result": "Plus de rÊsultats", @@ -1836,17 +1979,23 @@ "second": "Seconde", "see_all_people": "Voir toutes les personnes", "select": "SÊlectionner", + "select_album": "SÊlectionnez un album", "select_album_cover": "SÊlectionner la couverture d'album", + "select_albums": "SÊlectionnez des albums", "select_all": "Tout sÊlectionner", "select_all_duplicates": "SÊlectionner tous les doublons", "select_all_in": "Tout sÊlectionner dans {group}", "select_avatar_color": "SÊlectionner la couleur de l'avatar", + "select_count": "{count, plural, one {SÊlectionner #} other {SÊlectionner #}}", + "select_cutoff_date": "SÊlectionnez la date limite", "select_face": "SÊlectionner le visage", "select_featured_photo": "SÊlectionner la photo de profil de cette personne", "select_from_computer": "SÊlectionner à partir de l'ordinateur", "select_keep_all": "Choisir de tout garder", "select_library_owner": "SÊlectionner le propriÊtaire de la bibliothèque", "select_new_face": "SÊlectionner un nouveau visage", + "select_people": "SÊlectionnez des personnes", + "select_person": "SÊlectionnez une personne", "select_person_to_tag": "SÊlectionner une personne à identifier", "select_photos": "SÊlectionner les photos", "select_trash_all": "Choisir de tout supprimer", @@ -1982,6 +2131,7 @@ "show_password": "Afficher le mot de passe", "show_person_options": "Afficher les options de personnes", "show_progress_bar": "Afficher la barre de progression", + "show_schema": "Afficher le schÊma", "show_search_options": "Afficher les options de recherche", "show_shared_links": "Afficher les liens partagÊs", "show_slideshow_transition": "Afficher la transition du diaporama", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Passer vers les dossiers", "skip_to_tags": "Passer vers les Êtiquettes", "slideshow": "Diaporama", + "slideshow_repeat": "RÊpÊter le diaporama", + "slideshow_repeat_description": "Reboucler au dÊbut lorsque le diaporama se termine", "slideshow_settings": "Paramètres du diaporama", "sort_albums_by": "Trier les albums par...", "sort_created": "Date de crÊation", @@ -2038,6 +2190,7 @@ "support": "Soutenir", "support_and_feedback": "Support & Retours", "support_third_party_description": "Votre installation d'Immich est packagÊe via une application tierce. Si vous rencontrez des anomalies, elles peuvent venir de ce packaging tiers, merci de crÊer les anomalies avec ces tiers en premier lieu en utilisant les liens ci-dessous.", + "supporter": "Contributeur", "swap_merge_direction": "Inverser la direction de fusion", "sync": "Synchroniser", "sync_albums": "Synchroniser dans des albums", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Choisissez le thème de l'application", "theme_setting_three_stage_loading_subtitle": "Le chargement en trois Êtapes peut amÊliorer les performances de chargement, mais entraÃŽne une augmentation significative de la charge du rÊseau", "theme_setting_three_stage_loading_title": "Activer le chargement en trois Êtapes", + "then": "Ensuite", "they_will_be_merged_together": "Elles seront fusionnÊes ensemble", "third_party_resources": "Ressources tierces", "time": "Horaire", @@ -2090,7 +2244,7 @@ "to_parent": "Aller au dossier parent", "to_select": "pour faire une sÊlection", "to_trash": "Corbeille", - "toggle_settings": "Inverser les paramètres", + "toggle_settings": "Afficher/masquer les paramètres", "toggle_theme_description": "Changer le thème", "total": "Total", "total_usage": "Utilisation globale", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "SÊlectionner les ÊlÊments", "trash_page_title": "Corbeille ({count})", "trashed_items_will_be_permanently_deleted_after": "Les ÊlÊments dans la corbeille seront supprimÊs dÊfinitivement après {days, plural, one {# jour} other {# jours}}.", + "trigger": "DÊclencheur", + "trigger_asset_uploaded": "MÊdia tÊlÊversÊ", + "trigger_asset_uploaded_description": "DÊclenchÊ lorsqu'un nouveau mÊdia est tÊlÊversÊ", + "trigger_description": "Un ÊvÊnement qui active le flux de traitement", + "trigger_person_recognized": "Personne reconnue", + "trigger_person_recognized_description": "DÊclenchÊ lorsqu'une personne est dÊtectÊe", + "trigger_type": "Type de dÊclencheur", "troubleshoot": "DÊpannage", "type": "Type", "unable_to_change_pin_code": "Impossible de changer le code PIN", @@ -2123,6 +2284,7 @@ "unhide_person": "Afficher la personne", "unknown": "Inconnu", "unknown_country": "Pays non connu", + "unknown_date": "Date inconnue", "unknown_year": "AnnÊe inconnue", "unlimited": "IllimitÊ", "unlink_motion_video": "DÊtacher la photo animÊe", @@ -2139,17 +2301,19 @@ "unstack": "DÊpiler", "unstack_action_prompt": "{count} dÊpilÊ(s)", "unstacked_assets_count": "{count, plural, one {# mÊdia dÊpilÊ} other {# mÊdias dÊpilÊs}}", + "unsupported_field_type": "Type de champ non supportÊ", "untagged": "Sans Êtiquette", + "untitled_workflow": "Flux de traitement sans titre", "up_next": "Suite", "update_location_action_prompt": "Mettre à jour la localisation des {count} mÊdias sÊlectionnÊs avec :", "updated_at": "Mis à jour à", "updated_password": "Mot de passe mis à jour", "upload": "Envoyer", - "upload_action_prompt": "{count} en attente d'envoi", "upload_concurrency": "Envois simultanÊs", "upload_details": "DÊtails des envois", "upload_dialog_info": "Voulez-vous sauvegarder la sÊlection vers le serveur ?", "upload_dialog_title": "Envoyer le mÊdia", + "upload_error_with_count": "Erreur de chargement pour {count, plural, one {# mÊdia} other {# mÊdias}}", "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.", "upload_finished": "Envoi fini", "upload_progress": "{remaining, number} restant(s) - {processed, number} traitÊ(s)/{total, number}", @@ -2185,6 +2349,7 @@ "utilities": "Utilitaires", "validate": "Valider", "validate_endpoint_error": "Merci d'entrer un lien valide", + "validation_error": "Erreur de validation", "variables": "Variables", "version": "Version", "version_announcement_closing": "Ton ami, Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Lancer la prÊvisualisation vidÊo au survol. Si dÊsactivÊ, la lecture peut quand mÃĒme ÃĒtre dÊmarrÊe en survolant le bouton Play.", "videos": "VidÊos", "videos_count": "{count, plural, one {# VidÊo} other {# VidÊos}}", + "videos_only": "VidÊos uniquement", "view": "Voir", "view_album": "Afficher l'album", "view_all": "Voir tout", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "Utiliser comme ÊlÊment principal", "viewer_unstack": "DÊpiler", "visibility_changed": "VisibilitÊ changÊe pour {count, plural, one {# personne} other {# personnes}}", + "visual": "Visuel", + "visual_builder": "Constructeur visuel", "waiting": "En attente", "waiting_count": "En attente : {count}", "warning": "Attention", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Bienvenue sur Immich", "width": "Largeur", "wifi_name": "Nom du rÊseau wifi", - "workflow": "Flux de travail", + "workflow_delete_prompt": "Êtes-vous sÃģr de vouloir supprimer ce flux de traitement ?", + "workflow_deleted": "Flux de traitement supprimÊ", + "workflow_description": "Description du flux de traitement", + "workflow_info": "Informations du flux de traitement", + "workflow_json": "JSON du flux de traitement", + "workflow_json_help": "Modifier la configuration du flux de traitement dans un format JSON. Les changements se synchroniseront avec le constructeur visuel.", + "workflow_name": "Nom du flux de traitement", + "workflow_navigation_prompt": "Êtes-vous sÃģr de vouloir quitter sans enregistrer vos changements ?", + "workflow_summary": "RÊsumÊ du flux de traitement", + "workflow_update_success": "Flux de traitement mis à jour avec succès", + "workflow_updated": "Flux de traitement mis à jour", + "workflows": "Flux de traitement", + "workflows_help_text": "Les flux de traitement automatisent des actions sur vos mÊdias, en se basant sur des dÊclencheurs et des filtres", "wrong_pin_code": "Code PIN erronÊ", "year": "AnnÊe", "years_ago": "Il y a {years, plural, one {# an} other {# ans}}", "yes": "Oui", "you_dont_have_any_shared_links": "Vous n'avez aucun lien partagÊ", "your_wifi_name": "Nom du rÊseau wifi", + "zero_to_clear_rating": "Appuyez sur 0 pour effacer la notation du mÊdia", "zoom_image": "Zoomer", "zoom_to_bounds": "Zoom sur la zone" } diff --git a/i18n/ga.json b/i18n/ga.json index 63f8fee42b..409cc293d6 100644 --- a/i18n/ga.json +++ b/i18n/ga.json @@ -5,6 +5,7 @@ "acknowledge": "AdmhÃĄil", "action": "Gníomh", "action_common_update": "NuashonrÃē", + "action_description": "Sraith gníomhartha le dÊanamh ar na sÃŗcmhainní scagtha", "actions": "Gníomhartha", "active": "Gníomhach", "active_count": "Gníomhach: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Cuir suíomh leis", "add_a_name": "Cuir ainm leis", "add_a_title": "Cuir teideal leis", + "add_action": "Cuir gníomh leis", + "add_action_description": "CliceÃĄil chun gníomh a chur leis le dÊanamh", + "add_assets": "Cuir sÃŗcmhainní leis", "add_birthday": "Cuir breithlÃĄ leis", "add_endpoint": "Cuir críochphointe leis", "add_exclusion_pattern": "Cuir patrÃēn eisiaimh leis", + "add_filter": "Cuir scagaire leis", + "add_filter_description": "CliceÃĄil chun coinníoll scagaire a chur leis", "add_location": "Cuir suíomh leis", "add_more_users": "Cuir níos mÃŗ ÃēsÃĄideoirí leis", "add_partner": "Cuir comhphÃĄirtí leis", @@ -36,6 +42,7 @@ "add_to_shared_album": "Cuir le halbam comhroinnte", "add_upload_to_stack": "Cuir uaslÃŗdÃĄil leis an gcruach", "add_url": "Cuir URL leis", + "add_workflow_step": "Cuir cÊim sreabha oibre leis", "added_to_archive": "Curtha leis an gcartlann", "added_to_favorites": "Curtha le rogha pearsanta", "added_to_favorites_count": "Cuireadh {count, number} le mo rogha pearsanta", @@ -97,6 +104,8 @@ "image_preview_description": "ÍomhÃĄ meÃĄnmhÊide le meiteashonraí strÃŗicthe, a ÃēsÃĄidtear agus sÃŗcmhainn aonair ÃĄ breathnÃē agus le haghaidh foghlama meaisín", "image_preview_quality_description": "CÃĄilíocht rÊamhamhairc Ãŗ 1-100. Is airde is fearr, ach cruthaíonn sÊ comhaid níos mÃŗ agus d'fhÊadfadh sÊ freagrÃēlacht aipeanna a laghdÃē. D'fhÊadfadh tionchar a bheith ag luach íseal ar chÃĄilíocht na foghlama meaisín.", "image_preview_title": "Socruithe RÊamhamhairc", + "image_progressive": "ForÃĄsach", + "image_progressive_description": "ÍomhÃĄnna JPEG ÃĄ n-ionchÃŗdÃē de rÊir a chÊile le haghaidh taispeÃĄntais luchtaithe de rÊir a chÊile. Níl aon Êifeacht aige seo ar íomhÃĄnna WebP.", "image_quality": "CÃĄilíocht", "image_resolution": "Taifeach", "image_resolution_description": "Is fÊidir le taifeach níos airde níos mÃŗ sonraí a chaomhnÃē ach tÃŗgann sÊ níos faide iad a ionchÃŗdÃē, bíonn mÊideanna comhaid níos mÃŗ acu agus fÊadann siad freagrÃēlacht aipeanna a laghdÃē.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Cumasaigh cuardach cliste", "machine_learning_smart_search_enabled_description": "Mura bhfuil sÊ sin ar fÃĄil, ní dhÊanfar íomhÃĄnna a ionchÃŗdÃē le haghaidh cuardaigh chliste.", "machine_learning_url_description": "URL an fhreastalaí foghlama meaisín. MÃĄ chuirtear níos mÃŗ nÃĄ URL amhÃĄin ar fÃĄil, dÊanfar iarracht ar gach freastalaí ceann ag an am go dtí go bhfreagrÃŗidh ceann acu go rathÃēil, in ord Ãŗn gcÊad cheann go dtí an ceann deireanach. DÊanfar neamhaird shealadach ar fhreastalaithe nach bhfreagrÃŗidh go dtí go mbeidh siad ar líne arís.", + "maintenance_delete_backup": "Scrios CÃēltaca", + "maintenance_delete_backup_description": "Scriosfar an comhad seo go neamh-inchÃēlghairthe.", + "maintenance_delete_error": "Theip ar an gcÃēltaca a scriosadh.", + "maintenance_restore_backup": "AthchÃŗirigh CÃēltaca", + "maintenance_restore_backup_description": "Scriosfar agus athchÃŗireofar Immich Ãŗn gcÃēltaca roghnaithe. CruthÃŗfar cÃēltaca sula leanfar ar aghaidh.", + "maintenance_restore_backup_different_version": "Cruthaíodh an cÃēltaca seo le leagan difriÃēil de Immich!", + "maintenance_restore_backup_unknown_version": "Níorbh fhÊidir an leagan cÃēltaca a chinneadh.", + "maintenance_restore_database_backup": "AthchÃŗirigh cÃēltaca bunachar sonraí", + "maintenance_restore_database_backup_description": "Rolladh ar ais go staid bhunachar sonraí níos luaithe ag baint ÃēsÃĄide as comhad cÃēltaca", "maintenance_settings": "CothabhÃĄil", "maintenance_settings_description": "Cuir Immich i mÃŗd cothabhÃĄla.", - "maintenance_start": "Tosaigh mÃŗd cothabhÃĄla", + "maintenance_start": "Athraigh go mÃŗd cothabhÃĄla", "maintenance_start_error": "Theip ar an modh cothabhÃĄla a thosÃē.", + "maintenance_upload_backup": "UaslÃŗdÃĄil comhad cÃēltaca bunachar sonraí", + "maintenance_upload_backup_error": "Níorbh fhÊidir an cÃēltaca a uaslÃŗdÃĄil, an comhad .sql/.sql.gz Ê?", "manage_concurrency": "Bainistigh ComhthrÃĄthacht", "manage_concurrency_description": "TÊigh chuig leathanach na bpost chun comhthrÃĄthacht poist a bhainistiÃē", "manage_log_settings": "Bainistigh socruithe loga", @@ -252,7 +272,7 @@ "oauth_auto_register": "ClÃĄrÃē uathoibríoch", "oauth_auto_register_description": "ClÃĄraigh ÃēsÃĄideoirí nua go huathoibríoch tar Êis síniÃē isteach le OAuth", "oauth_button_text": "TÊacs cnaipe", - "oauth_client_secret_description": "Riachtanach mura dtacaíonn an solÃĄthraí OAuth le PKCE (Eochair ChruthÃēnais le haghaidh MalartÃē CÃŗd)", + "oauth_client_secret_description": "Riachtanach do chliant rÃēnda, nÃŗ mura dtacaítear le PKCE (Eochair ChruthÃēnais le haghaidh MalartÃē CÃŗd) do chliant poiblí.", "oauth_enable_description": "LogÃĄil isteach le OAuth", "oauth_mobile_redirect_uri": "URI atreoraithe soghluaiste", "oauth_mobile_redirect_uri_override": "SÃĄrÃē URI atreoraithe soghluaiste", @@ -291,7 +311,7 @@ "search_jobs": "Cuardaigh poistâ€Ļ", "send_welcome_email": "Seol ríomhphost fÃĄilte", "server_external_domain_settings": "Fearann seachtrach", - "server_external_domain_settings_description": "Fearann le haghaidh naisc chomhroinnte poiblí, lena n-ÃĄirítear http(s)://", + "server_external_domain_settings_description": "Fearann a ÃēsÃĄidtear le haghaidh naisc sheachtracha", "server_public_users": "ÚsÃĄideoirí Poiblí", "server_public_users_description": "Liostaítear gach ÃēsÃĄideoir (ainm agus ríomhphost) nuair a chuirtear ÃēsÃĄideoir le halbaim chomhroinnte. Nuair a bhíonn sÊ díchumasaithe, ní bheidh an liosta ÃēsÃĄideoirí ar fÃĄil ach d’ÃēsÃĄideoirí riarthÃŗra.", "server_settings": "Socruithe Freastalaí", @@ -431,6 +451,9 @@ "admin_password": "Pasfhocal RiarthÃŗra", "administration": "RiarachÃĄn", "advanced": "ArdleibhÊil", + "advanced_settings_clear_image_cache": "Glan an Taisce ÍomhÃĄ", + "advanced_settings_clear_image_cache_error": "Theip ar an taisce íomhÃĄ a ghlanadh", + "advanced_settings_clear_image_cache_success": "Glanadh {size} go rathÃēil", "advanced_settings_enable_alternate_media_filter_subtitle": "ÚsÃĄid an rogha seo chun meÃĄin a scagadh le linn sioncrÃŗnaithe bunaithe ar chritÊir mhalartacha. NÃĄ dÊan iarracht air seo ach amhÃĄin mÃĄ bhíonn fadhbanna agat leis an aip ag braith gach albam.", "advanced_settings_enable_alternate_media_filter_title": "[TURGNAMHACH] ÚsÃĄid scagaire sioncrÃŗnaithe albam glÊas malartach", "advanced_settings_log_level_title": "LeibhÊal loga: {level}", @@ -467,10 +490,12 @@ "album_remove_user": "Bain an t-ÃēsÃĄideoir?", "album_remove_user_confirmation": "An bhfuil tÃē cinnte gur mian leat {user} a bhaint?", "album_search_not_found": "Ní bhfuarthas aon albaim a mheaitseÃĄlann do chuardach", + "album_selected": "Albam roghnaithe", "album_share_no_users": "Is cosÃēil gur roinn tÃē an t-albam seo le gach ÃēsÃĄideoir nÃŗ nach bhfuil aon ÃēsÃĄideoir agat le roinnt leis.", "album_summary": "Achoimre ar an albam", "album_updated": "Albam nuashonraithe", "album_updated_setting_description": "Faigh fÃŗgra ríomhphoist nuair a bhíonn sÃŗcmhainní nua i albam comhroinnte", + "album_upload_assets": "UaslÃŗdÃĄil sÃŗcmhainní Ãŗ do ríomhaire agus cuir le halbam iad", "album_user_left": "D'fhÃĄg {album}", "album_user_removed": "Baineadh {user}", "album_viewer_appbar_delete_confirm": "An bhfuil tÃē cinnte gur mian leat an t-albam seo a scriosadh Ãŗ do chuntas?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Ord sÃŗrtÃĄla sÃŗcmhainní tosaigh agus albaim nua ÃĄ gcruthÃē.", "albums_feature_description": "BailiÃēchÃĄin sÃŗcmhainní is fÊidir a roinnt le hÃēsÃĄideoirí eile.", "albums_on_device_count": "Albaim ar an nglÊas ({count})", + "albums_selected": "{count, plural, one {# albam roghnaithe} other {# albam roghnaithe}}", "all": "Gach", "all_albums": "Gach albam", "all_people": "Gach duine", + "all_photos": "Gach grianghraf", "all_videos": "Gach físeÃĄn", "allow_dark_mode": "Ceadaigh mÃŗd dorcha", "allow_edits": "Ceadaigh eagarthÃŗireachtaí", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Ceadaigh d'ÃēsÃĄideoirí poiblí uaslÃŗdÃĄil", "allowed": "Ceadaithe", "alt_text_qr_code": "ÍomhÃĄ cÃŗd QR", + "always_keep": "Coinnigh i gcÃŗnaí", + "always_keep_photos_hint": "Coinneoidh Saoradh SpÃĄis na grianghraif go lÊir ar an nglÊas seo.", + "always_keep_videos_hint": "Coinneoidh Saoradh SpÃĄis na físeÃĄin go lÊir ar an nglÊas seo.", "anti_clockwise": "Tuathalach", "api_key": "Eochair API", "api_key_description": "Ní thaispeÃĄnfar an luach seo ach uair amhÃĄin. Bí cinnte Ê a chÃŗipeÃĄil sula ndÃēnann tÃē an fhuinneog.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {Cartlannaithe #}}", "are_these_the_same_person": "An iad seo an duine cÊanna?", "are_you_sure_to_do_this": "An bhfuil tÃē cinnte gur mian leat Ê seo a dhÊanamh?", + "array_field_not_fully_supported": "Éilíonn rÊimsí eagar eagarthÃŗireacht JSON de lÃĄimh", "asset_action_delete_err_read_only": "Ní fÊidir sÃŗcmhainn(í) lÊite amhÃĄin a scriosadh, ag scipeÃĄil", "asset_action_share_err_offline": "Ní fÊidir sÃŗcmhainn(í) as líne a fhÃĄil, ag scipeÃĄil", "asset_added_to_album": "Curtha leis an albam", "asset_adding_to_album": "Ag cur leis an albamâ€Ļ", + "asset_created": "SÃŗcmhainn cruthaithe", "asset_description_updated": "TÃĄ cur síos na sÃŗcmhainne nuashonraithe", "asset_filename_is_offline": "TÃĄ an tsÃŗcmhainn {filename} as líne", "asset_has_unassigned_faces": "TÃĄ aghaidheanna neamhshannta ag an tsÃŗcmhainn", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Leagan Amach", "asset_list_settings_subtitle": "Socruithe leagan amach eangach grianghraf", "asset_list_settings_title": "Eangach Grianghraf", + "asset_not_found_on_device_android": "Níor aimsíodh an tsÃŗcmhainn ar an nglÊas", + "asset_not_found_on_device_ios": "SÃŗcmhainn gan teacht ar an nglÊas. MÃĄ tÃĄ iCloud in ÃēsÃĄid agat, b'fhÊidir nach bhfuil an tsÃŗcmhainn inrochtana mar gheall ar chomhad lochtach atÃĄ stÃŗrÃĄilte ar iCloud", + "asset_not_found_on_icloud": "SÃŗcmhainn gan teacht ar iCloud. B’fhÊidir nach bhfuil an tsÃŗcmhainn inrochtana mar gheall ar chomhad lochtach atÃĄ stÃŗrÃĄilte ar iCloud", "asset_offline": "SÃŗcmhainn As Líne", "asset_offline_description": "Níl an tsÃŗcmhainn sheachtrach seo le fÃĄil ar dhiosca a thuilleadh. TÊigh i dteagmhÃĄil le riarthÃŗir do Immich le haghaidh cabhrach.", "asset_restored_successfully": "AthchÃŗiríodh an tsÃŗcmhainn go rathÃēil", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Roghnaigh albaim", "backup_album_selection_page_selection_info": "Eolas RoghnÃēchÃĄin", "backup_album_selection_page_total_assets": "IomlÃĄn na sÃŗcmhainní uathÃēla", - "backup_albums_sync": "SioncrÃŗnÃē albam cÃēltaca", + "backup_albums_sync": "SioncrÃŗnÃē Albam CÃēltaca", "backup_all": "Gach", "backup_background_service_backup_failed_message": "Theip ar chÃēltaca sÃŗcmhainní. Ag iarraidh arísâ€Ļ", "backup_background_service_complete_notification": "CÃēltaca sÃŗcmhainní críochnaithe", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Ní hionann na pasfhocail", "change_password_form_reenter_new_password": "Ath-iontrÃĄil Pasfhocal Nua", "change_pin_code": "Athraigh an cÃŗd PIN", + "change_trigger": "Athraigh an spreagadh", + "change_trigger_prompt": "An bhfuil tÃē cinnte gur mian leat an spreagthÃŗir a athrÃē? Bainfear gach gníomh agus scagaire atÃĄ ann cheana leis seo.", "change_your_password": "Athraigh do phasfhocal", "changed_visibility_successfully": "Athraíodh an infheictheacht go rathÃēil", "charging": "MuirearÃē", @@ -722,6 +759,18 @@ "checksum": "Suim sheiceÃĄla", "choose_matching_people_to_merge": "Roghnaigh daoine comhoiriÃēnacha le cumasc", "city": "Cathair", + "cleanup_confirm_description": "Fuair Immich {count} sÃŗcmhainní (cruthaithe roimh {date}) cÃēltaca sÃĄbhÃĄilte chuig an bhfreastalaí. Bain na cÃŗipeanna ÃĄitiÃēla den ghlÊas seo?", + "cleanup_confirm_prompt_title": "Bain den ghlÊas seo?", + "cleanup_deleted_assets": "Bogadh {count} sÃŗcmhainní chuig bruscar an ghlÊis", + "cleanup_deleting": "Ag bogadh go dtí an bruscar...", + "cleanup_found_assets": "Fuarthas {count} sÃŗcmhainní cÃēltaca", + "cleanup_found_assets_with_size": "Fuarthas {count} sÃŗcmhainní cÃēltaca ({size})", + "cleanup_icloud_shared_albums_excluded": "Níl Albaim Chomhroinnte iCloud san ÃĄireamh sa scanadh", + "cleanup_no_assets_found": "Ní bhfuarthas aon sÃŗcmhainní a chomhlíonann na critÊir thuas. Ní fÊidir le SpÃĄs Saor a Bhaint ach sÃŗcmhainní a bhaint atÃĄ cÃēltaca dÊanta díobh chuig an bhfreastalaí", + "cleanup_preview_title": "SÃŗcmhainní le baint ({count})", + "cleanup_step3_description": "ScanÃĄil le haghaidh sÃŗcmhainní cÃēltaca a mheaitseÃĄlann do dhÃĄta agus coinnigh socruithe.", + "cleanup_step4_summary": "{count} sÃŗcmhainní (cruthaithe roimh {date}) le baint de do ghlÊas ÃĄitiÃēil. Beidh rochtain ar ghrianghraif Ãŗn aip Immich i gcÃŗnaí.", + "cleanup_trash_hint": "Chun spÃĄs stÃŗrÃĄla a athghabhÃĄil go hiomlÃĄn, oscail aip gailearaí an chÃŗrais agus folmhaigh an bruscar", "clear": "Glan", "clear_all": "Glan gach rud", "clear_all_recent_searches": "Glan gach cuardach le dÊanaí", @@ -733,6 +782,8 @@ "client_cert_import": "IompÃŗrtÃĄil", "client_cert_import_success_msg": "TÃĄ deimhniÃē cliant allmhairithe", "client_cert_invalid_msg": "Comhad teastais neamhbhailí nÃŗ pasfhocal mícheart", + "client_cert_password_message": "Cuir isteach an focal faire don deimhniÃē seo", + "client_cert_password_title": "Pasfhocal an Teastais", "client_cert_remove_msg": "Baineadh teastas an chliaint", "client_cert_subtitle": "Tacaíonn sÊ le formÃĄid PKCS12 (.p12, .pfx) amhÃĄin. Ní fÊidir teastais a allmhairiÃē/a bhaint ach amhÃĄin roimh logÃĄil isteach", "client_cert_title": "Teastas cliant SSL [TURGHAINNEACH]", @@ -743,6 +794,11 @@ "color": "Dath", "color_theme": "TÊama datha", "command": "OrdÃē", + "command_palette_prompt": "Aimsigh leathanaigh, gníomhartha nÃŗ orduithe go tapa", + "command_palette_to_close": "a dhÃēnadh", + "command_palette_to_navigate": "dul isteach", + "command_palette_to_select": "a roghnÃē", + "command_palette_to_show_all": "chun gach rud a thaispeÃĄint", "comment_deleted": "TrÃĄcht scriosta", "comment_options": "Roghanna trÃĄchta", "comments_and_likes": "TrÃĄchtanna & Is maith liom", @@ -787,6 +843,7 @@ "create_album": "Cruthaigh albam", "create_album_page_untitled": "Gan Teideal", "create_api_key": "Cruthaigh eochair API", + "create_first_workflow": "Cruthaigh an chÊad sreabhadh oibre", "create_library": "Cruthaigh Leabharlann", "create_link": "Cruthaigh nasc", "create_link_to_share": "Cruthaigh nasc le roinnt", @@ -801,17 +858,25 @@ "create_tag": "Cruthaigh clib", "create_tag_description": "Cruthaigh clib nua. I gcÃĄs clibeanna neadaithe, cuir isteach cosÃĄn iomlÃĄn an chlib, lena n-ÃĄirítear slaiseanna ar aghaidh.", "create_user": "Cruthaigh ÃēsÃĄideoir", + "create_workflow": "Cruthaigh sreabhadh oibre", "created": "Cruthaithe", "created_at": "Cruthaithe", "creating_linked_albums": "Ag cruthÃē albaim nasctha...", "crop": "Barr", + "crop_aspect_ratio_fixed": "Seasta", + "crop_aspect_ratio_free": "Saor in aisce", + "crop_aspect_ratio_original": "Bunaidh", "curated_object_page_title": "Rudaí", "current_device": "GlÊas reatha", "current_pin_code": "CÃŗd PIN reatha", "current_server_address": "Seoladh reatha an fhreastalaí", + "custom_date": "DÃĄta saincheaptha", "custom_locale": "LogÃĄn Saincheaptha", "custom_locale_description": "FormÃĄidigh dÃĄtaí agus uimhreacha bunaithe ar an teanga agus ar an rÊigiÃēn", "custom_url": "URL Saincheaptha", + "cutoff_date_description": "Coinnigh grianghraif Ãŗn uair dheireanachâ€Ļ", + "cutoff_day": "{count, plural, one {lÃĄ} other {laethanta}}", + "cutoff_year": "{count, plural, one {bliain} other {blianta}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Dorcha", @@ -867,6 +932,7 @@ "deselect_all": "Díroghnaigh Gach Rud", "details": "Sonraí", "direction": "Treo", + "disable": "Díchumasaigh", "disabled": "Míchumasaithe", "disallow_edits": "Dícheadaigh eagarthÃŗireachtaí", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "FíseÃĄin leabaithe", "download_include_embedded_motion_videos_description": "Cuir físeÃĄin atÃĄ leabaithe i ngrianghraif ghluaiste san ÃĄireamh mar chomhad ar leithligh", "download_notfound": "ÍoslÃŗdÃĄil gan aimsiÃē", + "download_original": "ÍoslÃŗdÃĄil an bunleagan", "download_paused": "ÍoslÃŗdÃĄil curtha ar sos", "download_settings": "ÍoslÃŗdÃĄil", "download_settings_description": "Bainistigh socruithe a bhaineann le híoslÃŗdÃĄil sÃŗcmhainní", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "Ag fanacht le hathiarracht", "downloading": "Ag íoslÃŗdÃĄil", "downloading_asset_filename": "Ag íoslÃŗdÃĄil sÃŗcmhainn {filename}", + "downloading_from_icloud": "Ag íoslÃŗdÃĄil Ãŗ iCloud", "downloading_media": "Ag íoslÃŗdÃĄil na meÃĄn", "drop_files_to_upload": "Scaoil comhaid ÃĄit ar bith le huaslÃŗdÃĄil", "duplicates": "DÃēblaigh", @@ -929,11 +997,22 @@ "edit_tag": "Cuir an clib in eagar", "edit_title": "Cuir Teideal in Eagar", "edit_user": "Cuir ÃēsÃĄideoir in eagar", + "edit_workflow": "Sreabhadh oibre a chur in eagar", "editor": "EagarthÃŗir", "editor_close_without_save_prompt": "Ní shÃĄbhÃĄlfar na hathruithe", "editor_close_without_save_title": "DÃēn an t-eagarthÃŗir?", - "editor_crop_tool_h2_aspect_ratios": "CÃŗimheasa gnÊ", - "editor_crop_tool_h2_rotation": "RothlÃē", + "editor_confirm_reset_all_changes": "An bhfuil tÃē cinnte gur mian leat na hathruithe go lÊir a athshocrÃē?", + "editor_discard_edits_confirm": "Scrios na heagarthÃŗireachtaí", + "editor_discard_edits_prompt": "TÃĄ eagarthÃŗireachtaí neamhshÃĄbhÃĄilte agat. An bhfuil tÃē cinnte gur mhaith leat iad a chaitheamh amach?", + "editor_discard_edits_title": "Scrios na heagarthÃŗireachtaí?", + "editor_edits_applied_error": "Theip ar na heagarthÃŗireachtaí a chur i bhfeidhm", + "editor_edits_applied_success": "Cuireadh na heagarthÃŗireachtaí i bhfeidhm go rathÃēil", + "editor_flip_horizontal": "Fillte go cothromÃĄnach", + "editor_flip_vertical": "Smeach ingearach", + "editor_orientation": "Treoshuíomh", + "editor_reset_all_changes": "Athshocraigh athruithe", + "editor_rotate_left": "Rothlaigh 90° tuathalach", + "editor_rotate_right": "Rothlaigh 90° deiseal", "email": "Ríomhphost", "email_notifications": "FÃŗgraí ríomhphoist", "empty_folder": "TÃĄ an fillteÃĄn seo folamh", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Theip ar ord sÃŗrtÃĄla an albaim a athrÃē", "error_delete_face": "EarrÃĄid ag scriosadh aghaidhe Ãŗn tsÃŗcmhainn", "error_getting_places": "EarrÃĄid ag fÃĄil ÃĄiteanna", + "error_loading_albums": "EarrÃĄid ag luchtÃē albaim", "error_loading_image": "EarrÃĄid ag luchtÃē íomhÃĄ", "error_loading_partners": "EarrÃĄid ag luchtÃē comhphÃĄirtithe: {error}", + "error_retrieving_asset_information": "EarrÃĄid ag aisghabhÃĄil faisnÊise sÃŗcmhainne", "error_saving_image": "EarrÃĄid: {error}", "error_tag_face_bounding_box": "EarrÃĄid ag clibeÃĄil aghaidhe - ní fÊidir comhordanÃĄidí bosca teorann a fhÃĄil", "error_title": "EarrÃĄid - Chuaigh rud Êigin mícheart", + "error_while_navigating": "EarrÃĄid agus nascleanÃēint ÃĄ dÊanamh chuig an tsÃŗcmhainn", "errors": { "cannot_navigate_next_asset": "Ní fÊidir nascleanÃēint a dhÊanamh chuig an gcÊad tsÃŗcmhainn eile", "cannot_navigate_previous_asset": "Ní fÊidir nascleanÃēint a dhÊanamh chuig an tsÃŗcmhainn roimhe seo", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "Ní fÊidir logÃĄil isteach OAuth a chríochnÃē", "unable_to_connect": "Ní fÊidir ceangal", "unable_to_copy_to_clipboard": "Ní fÊidir cÃŗip a dhÊanamh chuig an ghearrthaisce, dÊan cinnte go bhfuil tÃē ag rochtain an leathanaigh trí https", + "unable_to_create": "Ní fÊidir sreabhadh oibre a chruthÃē", "unable_to_create_admin_account": "Ní fÊidir cuntas riarthÃŗra a chruthÃē", "unable_to_create_api_key": "Ní fÊidir eochair API nua a chruthÃē", "unable_to_create_library": "Ní fÊidir leabharlann a chruthÃē", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Ní fÊidir patrÃēn eisiaimh a scriosadh", "unable_to_delete_shared_link": "Ní fÊidir nasc comhroinnte a scriosadh", "unable_to_delete_user": "Ní fÊidir an t-ÃēsÃĄideoir a scriosadh", + "unable_to_delete_workflow": "Ní fÊidir an sreabhadh oibre a scriosadh", "unable_to_download_files": "Ní fÊidir comhaid a íoslÃŗdÃĄil", "unable_to_edit_exclusion_pattern": "Ní fÊidir patrÃēn eisiaimh a chur in eagar", "unable_to_empty_trash": "Ní fÊidir an bruscar a fholmhÃē", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "Ní fÊidir an leabharlann a scanadh", "unable_to_set_feature_photo": "Ní fÊidir grianghraf gnÊ a shocrÃē", "unable_to_set_profile_picture": "Ní fÊidir pictiÃēr prÃŗifíle a shocrÃē", + "unable_to_set_rating": "Ní fÊidir rÃĄtÃĄil a shocrÃē", "unable_to_submit_job": "Ní fÊidir an post a chur isteach", "unable_to_trash_asset": "Ní fÊidir an tsÃŗcmhainn a chur sa bhruscar", "unable_to_unlink_account": "Ní fÊidir an cuntas a dhícheangal", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Ní fÊidir socruithe a nuashonrÃē", "unable_to_update_timeline_display_status": "Ní fÊidir stÃĄdas taispeÃĄna an amlíne a nuashonrÃē", "unable_to_update_user": "Ní fÊidir an t-ÃēsÃĄideoir a nuashonrÃē", + "unable_to_update_workflow": "Ní fÊidir an sreabhadh oibre a nuashonrÃē", "unable_to_upload_file": "Ní fÊidir an comhad a uaslÃŗdÃĄil" }, + "errors_text": "EarrÃĄidí", "exclusion_pattern": "PatrÃēn eisiaimh", "exif": "Exif", "exif_bottom_sheet_description": "Cuir Cur Síos leis...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "DAOINE", "exif_bottom_sheet_person_add_person": "Cuir ainm leis", "exit_slideshow": "Scoir an TaispeÃĄntais SleamhnÃĄn", + "expand": "Leathnaigh", "expand_all": "Leathnaigh gach rud", "experimental_settings_new_asset_list_subtitle": "Obair ar siÃēl", "experimental_settings_new_asset_list_title": "Cumasaigh eangach grianghraf turgnamhach", @@ -1120,14 +1208,17 @@ "features": "GnÊithe", "features_in_development": "GnÊithe i bhForbairt", "features_setting_description": "Bainistigh gnÊithe an aip", - "file_name": "Ainm comhaid", "file_name_or_extension": "Ainm comhaid nÃŗ síneadh", + "file_name_text": "Ainm comhaid", + "file_name_with_value": "Ainm comhaid: {file_name}", "file_size": "MÊid comhaid", "filename": "Ainm comhaid", "filetype": "CineÃĄl comhaid", "filter": "Scagaire", + "filter_description": "Coinníollacha chun na sÃŗcmhainní sprice a scagadh", "filter_people": "Scag daoine", "filter_places": "Scag ÃĄiteanna", + "filters": "Scagairí", "find_them_fast": "Aimsigh iad go tapa de rÊir ainm le cuardach", "first": "Ar dtÃēs", "fix_incorrect_match": "Deisigh cluiche mícheart", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Ag brabhsÃĄil an amharc fillteÃĄin le haghaidh na ngrianghraf agus na bhfíseÃĄn ar an gcÃŗras comhad", "forgot_pin_code_question": "An ndearna tÃē dearmad ar do PIN?", "forward": "Chun tosaigh", + "free_up_space": "SpÃĄs a Shaoradh", + "free_up_space_description": "Bog grianghraif agus físeÃĄin chÃēltaca chuig bruscar do ghlÊis chun spÃĄs a shaoradh. Fanann do chÃŗipeanna ar an bhfreastalaí slÃĄn.", + "free_up_space_settings_subtitle": "Saor stÃŗrÃĄil glÊis", "full_path": "CosÃĄn iomlÃĄn: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "LÃŗdÃĄlann an ghnÊ seo acmhainní seachtracha Ãŗ Google chun go n-oibreoidh sí.", "general": "GinearÃĄlta", "geolocation_instruction_location": "CliceÃĄil ar shÃŗcmhainn le comhordanÃĄidí GPS chun a suíomh a ÃēsÃĄid, nÃŗ roghnaigh suíomh go díreach Ãŗn lÊarscÃĄil", "get_help": "Faigh Cabhair", + "get_people_error": "EarrÃĄid ag fÃĄil daoine", "get_wifiname_error": "Níorbh fhÊidir ainm Wi-Fi a fhÃĄil. Cinntigh gur dheonaigh tÃē na ceadanna riachtanacha agus go bhfuil tÃē ceangailte le líonra Wi-Fi", "getting_started": "Ag TosÃē", "go_back": "TÊigh ar ais", @@ -1175,6 +1270,7 @@ "hide_named_person": "Folaigh duine {name}", "hide_password": "Folaigh an focal faire", "hide_person": "Folaigh duine", + "hide_schema": "Folaigh an scÊim", "hide_text_recognition": "Folaigh aitheantas tÊacs", "hide_unnamed_people": "Folaigh daoine gan ainm", "home_page_add_to_album_conflicts": "Cuireadh sÃŗcmhainní {added} leis an albam {album}. TÃĄ sÃŗcmhainní {failed} san albam cheana fÊin.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "Rith an phrÃŗiseÃĄil {dateTime}", "items_count": "{count, plural, one {# mír} other {# míreanna}}", "jobs": "Poist", + "json_editor": "EagarthÃŗir JSON", + "json_error": "EarrÃĄid JSON", "keep": "CoimeÃĄd", + "keep_albums": "Coinnigh albaim", + "keep_albums_count": "Ag coinneÃĄil {count} {count, plural, one {album} other {albums}}", "keep_all": "Coinnigh Gach Rud", + "keep_description": "Roghnaigh cad a fhanann ar do ghlÊas agus spÃĄs ÃĄ shaoradh.", + "keep_favorites": "Coinnigh na cinn is fearr leat", + "keep_on_device": "Coinnigh ar an nglÊas", + "keep_on_device_hint": "Roghnaigh míreanna le coinneÃĄil ar an nglÊas seo", "keep_this_delete_others": "Coinnigh seo, scrios cinn eile", + "keeping": "Ag coinneÃĄil: {items}", "kept_this_deleted_others": "Choinnigh an tsÃŗcmhainn seo agus scriosadh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}}", "keyboard_shortcuts": "Aicearraí mÊarchlÃĄir", "language": "Teanga", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Cumasaigh físeÃĄn a lÃēbadh go huathoibríoch san amharcÃŗir sonraí.", "main_branch_warning": "TÃĄ leagan forbartha in ÃēsÃĄid agat; molaimid go lÃĄidir leagan scaoilte a ÃēsÃĄid!", "main_menu": "Príomh-roghchlÃĄr", + "maintenance_action_restore": "Bunachar Sonraí ÃĄ AthchÃŗiriÃē", "maintenance_description": "TÃĄ Immich curtha i mÃŗd cothabhÃĄla.", "maintenance_end": "Deireadh a chur leis an modh cothabhÃĄla", "maintenance_end_error": "Theip ar an modh cothabhÃĄla a chríochnÃē.", "maintenance_logged_in_as": "LogÃĄilte isteach faoi lÃĄthair mar {user}", + "maintenance_restore_from_backup": "AthchÃŗirigh Ãŗ ChÃēltaca", + "maintenance_restore_library": "AthchÃŗirigh Do Leabharlann", + "maintenance_restore_library_confirm": "MÃĄs cosÃēil go bhfuil sÊ seo ceart, lean ar aghaidh le cÃēltaca a athchÃŗiriÃē!", + "maintenance_restore_library_description": "Bunachar Sonraí ÃĄ AthchÃŗiriÃē", + "maintenance_restore_library_folder_has_files": "TÃĄ {count} fillteÃĄn(anna) i {folder}", + "maintenance_restore_library_folder_no_files": "TÃĄ comhaid ar iarraidh i {folder}!", + "maintenance_restore_library_folder_pass": "inlÊite agus inscríofa", + "maintenance_restore_library_folder_read_fail": "ní fÊidir a lÊamh", + "maintenance_restore_library_folder_write_fail": "ní fÊidir a scríobh", + "maintenance_restore_library_hint_missing_files": "B’fhÊidir go bhfuil comhaid thÃĄbhachtacha ar iarraidh ort", + "maintenance_restore_library_hint_regenerate_later": "Is fÊidir leat iad seo a athghiniÃēint níos dÊanaí sna socruithe", + "maintenance_restore_library_hint_storage_template_missing_files": "Ag baint ÃēsÃĄide as teimplÊad stÃŗrÃĄla? B’fhÊidir go bhfuil comhaid ar iarraidh ort", + "maintenance_restore_library_loading": "Ag lÃŗdÃĄil seiceÃĄlacha slÃĄine agus heorasticíâ€Ļ", + "maintenance_task_backup": "Ag cruthÃē cÃēltaca den bhunachar sonraí atÃĄ ann cheana fÊinâ€Ļ", + "maintenance_task_migrations": "Imircí bunachar sonraí ÃĄ reÃĄchtÃĄilâ€Ļ", + "maintenance_task_restore": "Ag athchÃŗiriÃē an chÃēltaca roghnaitheâ€Ļ", + "maintenance_task_rollback": "Theip ar an athchÃŗiriÃē, ag rolladh ar ais go dtí an pointe athchÃŗiritheâ€Ļ", "maintenance_title": "Gan FÃĄil go Sealadach", "make": "DÊan", "manage_geolocation": "Bainistigh suíomh", @@ -1408,6 +1531,8 @@ "minimize": "Íoslaghdaigh", "minute": "NÃŗimÊad", "minutes": "NÃŗimÊid", + "mirror_horizontal": "CothromÃĄnach", + "mirror_vertical": "Ingearach", "missing": "Ar iarraidh", "mobile_app": "Aip Shoghluaiste", "mobile_app_download_onboarding_note": "ÍoslÃŗdÃĄil an aip shoghluaiste tionlacain ag baint ÃēsÃĄide as na roghanna seo a leanas", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Tuilleadh", "move": "Bog", + "move_down": "Bog síos", "move_off_locked_folder": "Bog amach as fillteÃĄn faoi ghlas", "move_to": "Bog go", + "move_to_device_trash": "Bog go dtí bruscar an ghlÊis", "move_to_lock_folder_action_prompt": "{count} curtha leis an bhfillteÃĄn faoi ghlas", "move_to_locked_folder": "Bog go fillteÃĄn faoi ghlas", "move_to_locked_folder_confirmation": "Bainfear na grianghraif agus na físeÃĄin seo as na halbaim uile, agus ní bheidh siad le feiceÃĄil ach amhÃĄin Ãŗn bhfillteÃĄn faoi ghlas", + "move_up": "Bog suas", "moved_to_archive": "Bogadh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}} chuig an gcartlann", "moved_to_library": "Bogadh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}} chuig an leabharlann", "moved_to_trash": "Bogtha chuig an mbruscar", @@ -1430,6 +1558,7 @@ "my_albums": "Mo chuid albaim", "name": "Ainm", "name_or_nickname": "Ainm nÃŗ leasainm", + "name_required": "TÃĄ ainm ag teastÃĄil", "navigate": "Loingseoireacht", "navigate_to_time": "NascleanÃēint chuig Am", "network_requirement_photos_upload": "ÚsÃĄid sonraí ceallacha chun grianghraif a chÃēltaca", @@ -1454,20 +1583,24 @@ "next": "Ar Aghaidh", "next_memory": "An chÊad chuimhne eile", "no": "Níl", + "no_actions_added": "Níl aon ghníomhartha curtha leis fÃŗs", + "no_albums_found": "Níor aimsíodh aon albaim", "no_albums_message": "Cruthaigh albam chun do ghrianghraif agus do fhíseÃĄin a eagrÃē", "no_albums_with_name_yet": "Is cosÃēil nach bhfuil aon albaim agat leis an ainm seo go fÃŗill.", "no_albums_yet": "Is cosÃēil nach bhfuil aon albaim agat fÃŗs.", "no_archived_assets_message": "Cartlannaigh grianghraif agus físeÃĄin chun iad a cheilt Ãŗ d’amharc Grianghraf", - "no_assets_message": "CLICEÁIL CHUN DO CHÉAD GHRIANGHRAF A UASLÓDÁIL", + "no_assets_message": "CliceÃĄil chun do chÊad ghrianghraf a uaslÃŗdÃĄil", "no_assets_to_show": "Gan aon sÃŗcmhainní le taispeÃĄint", "no_cast_devices_found": "Ní bhfuarthas aon ghlÊasanna teilgthe", "no_checksum_local": "Níl aon suim seiceÃĄla ar fÃĄil - ní fÊidir sÃŗcmhainní ÃĄitiÃēla a aisghabhÃĄil", "no_checksum_remote": "Níl aon suim seiceÃĄla ar fÃĄil - ní fÊidir sÃŗcmhainn iargÃēlta a aisghabhÃĄil", + "no_configuration_needed": "Níl aon chumraíocht ag teastÃĄil", "no_devices": "Gan aon fheistí Ãēdaraithe", "no_duplicates_found": "Ní bhfuarthas aon dÃēblaigh.", "no_exif_info_available": "Níl aon fhaisnÊis exif ar fÃĄil", "no_explore_results_message": "UaslÃŗdÃĄil tuilleadh grianghraf chun do bhailiÃēchÃĄn a iniÃēchadh.", "no_favorites_message": "Cuir na cinn is fearr leat leis chun do phictiÃēir agus do fhíseÃĄin is fearr a aimsiÃē go tapa", + "no_filters_added": "Níl aon scagairí curtha leis fÃŗs", "no_libraries_message": "Cruthaigh leabharlann sheachtrach chun do ghrianghraif agus físeÃĄin a fheiceÃĄil", "no_local_assets_found": "Ní bhfuarthas aon sÃŗcmhainní ÃĄitiÃēla leis an tsuim sheiceÃĄla seo", "no_location_set": "Níl aon suíomh socraithe", @@ -1481,11 +1614,11 @@ "no_results_description": "Bain triail as comhchiallach nÃŗ eochairfhocal níos ginearÃĄlta", "no_shared_albums_message": "Cruthaigh albam chun grianghraif agus físeÃĄin a roinnt le daoine i do líonra", "no_uploads_in_progress": "Níl aon uaslÃŗdÃĄlacha ar siÃēl", + "none": "Dada", "not_allowed": "Ní cheadaítear", "not_available": "N/B", "not_in_any_album": "Ní in aon albam", "not_selected": "Níor roghnaíodh", - "note_apply_storage_label_to_previously_uploaded assets": "NÃŗta: Chun an LipÊad StÃŗrÃĄla a chur i bhfeidhm ar shÃŗcmhainní a uaslÃŗdÃĄileadh roimhe seo, rith an", "notes": "NÃŗtaí", "nothing_here_yet": "Níl aon rud anseo fÃŗs", "notification_permission_dialog_content": "Chun fÃŗgraí a chumasÃē, tÊigh go Socruithe agus roghnaigh ceadaigh.", @@ -1515,6 +1648,7 @@ "online": "Ar líne", "only_favorites": "Is fearr leat amhÃĄin", "open": "Oscail", + "open_calendar": "Oscail an fÊilire", "open_in_map_view": "Oscail i radharc lÊarscÃĄile", "open_in_openstreetmap": "Oscail in OpenStreetMap", "open_the_search_filters": "Oscail na scagairí cuardaigh", @@ -1563,6 +1697,7 @@ "people": "Daoine", "people_edits_count": "EagarthÃŗireacht dÊanta {count, plural, one {# duine} other {# daoine}}", "people_feature_description": "Ag brabhsÃĄil grianghraif agus físeÃĄin grÃēpÃĄilte de rÊir daoine", + "people_selected": "{count, plural, one {# duine roghnaithe} other {# duine roghnaithe}}", "people_sidebar_description": "TaispeÃĄin nasc chuig Daoine sa bharra taoibh", "permanent_deletion_warning": "Rabhadh scriosadh buan", "permanent_deletion_warning_setting_description": "TaispeÃĄin rabhadh agus sÃŗcmhainní ÃĄ scriosadh go buan", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# blianta}} d'aois", "person_birthdate": "Rugadh ar {date}", "person_hidden": "{name}{hidden, select, true { (i bhfolach)} other {}}", + "person_recognized": "Duine aitheanta", + "person_selected": "Duine roghnaithe", "photo_shared_all_users": "Is cosÃēil gur roinn tÃē do ghrianghraif le gach ÃēsÃĄideoir nÃŗ nach bhfuil aon ÃēsÃĄideoir agat le roinnt leis.", "photos": "Grianghraif", "photos_and_videos": "Grianghraif & FíseÃĄin", "photos_count": "{count, plural, one {{count, number} Grianghraf} other {{count, number} Grianghraif}}", "photos_from_previous_years": "Grianghraif Ãŗ bhlianta roimhe seo", + "photos_only": "Grianghraif amhÃĄin", "pick_a_location": "Roghnaigh suíomh", "pick_custom_range": "Raon saincheaptha", "pick_date_range": "Roghnaigh raon dÃĄta", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "DÊanann an riarthÃŗir bainistíocht ar eochair tÃĄirge an fhreastalaí", "query_asset_id": "ID SÃŗcmhainne Iarratais", "queue_status": "ScuaineÃĄil {count}/{total}", + "rate_asset": "RÃĄtÃĄil SÃŗcmhainn", "rating": "RÃĄtÃĄil rÊalta", "rating_clear": "Glan rÃĄtÃĄil", "rating_count": "{count, plural, one {# rÊalta} other {# rÊaltaí}}", "rating_description": "TaispeÃĄin an rÃĄtÃĄil EXIF sa phainÊal eolais", + "rating_set": "Socraithe go {rating, plural, one {# rÊalta} other {# rÊalta}}", "reaction_options": "Roghanna imoibrithe", "read_changelog": "LÊigh an Log Athraithe", "readonly_mode_disabled": "MÃŗd lÊite amhÃĄin díchumasaithe", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "Athshannadh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}} do dhuine nua", "reassing_hint": "Sannadh sÃŗcmhainní roghnaithe do dhuine atÃĄ ann cheana fÊin", "recent": "Le dÊanaí", - "recent-albums": "Albaim le dÊanaí", + "recent_albums": "Albaim le dÊanaí", "recent_searches": "Cuardaigh le dÊanaí", "recently_added": "Cuireadh leis le dÊanaí", "recently_added_page_title": "Curtha leis le DÊanaí", @@ -1770,9 +1910,11 @@ "saved_settings": "Socruithe sÃĄbhÃĄilte", "say_something": "Abair rud Êigin", "scaffold_body_error_occurred": "Tharla earrÃĄid", + "scan": "Scanadh", "scan_all_libraries": "ScanÃĄil Gach Leabharlann", "scan_library": "Scanadh", "scan_settings": "Socruithe Scanadh", + "scanning": "Ag scanadh", "scanning_for_album": "Ag scanadh le haghaidh albam...", "search": "Cuardaigh", "search_albums": "Cuardaigh albaim", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Roghnaigh cineÃĄl meÃĄn", "search_filter_ocr": "Cuardaigh de rÊir OCR", "search_filter_people_title": "Roghnaigh daoine", + "search_filter_star_rating": "RÃĄtÃĄil RÊalta", "search_for": "Cuardaigh le haghaidh", "search_for_existing_person": "Cuardaigh duine atÃĄ ann cheana fÊin", "search_no_more_result": "Gan aon torthaí eile", @@ -1836,17 +1979,23 @@ "second": "Dara", "see_all_people": "FÊach ar gach duine", "select": "Roghnaigh", + "select_album": "Roghnaigh albam", "select_album_cover": "Roghnaigh clÃēdach albaim", + "select_albums": "Roghnaigh albaim", "select_all": "Roghnaigh gach rud", "select_all_duplicates": "Roghnaigh na dÃēblaigh go lÊir", "select_all_in": "Roghnaigh gach rud i {group}", "select_avatar_color": "Roghnaigh dath an abhatÃĄr", + "select_count": "{count, plural, one {Roghnaigh #} other {Roghnaigh #}}", + "select_cutoff_date": "Roghnaigh dÃĄta scoir", "select_face": "Roghnaigh aghaidh", "select_featured_photo": "Roghnaigh grianghraf le feiceÃĄil", "select_from_computer": "Roghnaigh Ãŗn ríomhaire", "select_keep_all": "Roghnaigh coinnigh gach rud", "select_library_owner": "Roghnaigh ÃēinÊir leabharlainne", "select_new_face": "Roghnaigh aghaidh nua", + "select_people": "Roghnaigh daoine", + "select_person": "Roghnaigh duine", "select_person_to_tag": "Roghnaigh duine le clibeÃĄil", "select_photos": "Roghnaigh grianghraif", "select_trash_all": "Roghnaigh gach rud sa bhruscar", @@ -1982,6 +2131,7 @@ "show_password": "TaispeÃĄin an focal faire", "show_person_options": "TaispeÃĄin roghanna duine", "show_progress_bar": "TaispeÃĄin an Barra Dul Chun Cinn", + "show_schema": "TaispeÃĄin scÊim", "show_search_options": "TaispeÃĄin roghanna cuardaigh", "show_shared_links": "TaispeÃĄin naisc chomhroinnte", "show_slideshow_transition": "TaispeÃĄin an t-aistriÃē sleamhnÃĄn", @@ -1999,6 +2149,8 @@ "skip_to_folders": "LÊim go dtí na fillteÃĄin", "skip_to_tags": "LÊim go dtí na clibeanna", "slideshow": "SleamhnÃĄn", + "slideshow_repeat": "AthdhÊan an sleamhnÃĄn", + "slideshow_repeat_description": "LÃēb ar ais go dtí an tÃēs nuair a chríochnaíonn an sleamhnÃĄn", "slideshow_settings": "Socruithe sleamhnÃĄn", "sort_albums_by": "SÃŗrtÃĄil albaim de rÊir...", "sort_created": "DÃĄta cruthaithe", @@ -2038,6 +2190,7 @@ "support": "Tacaíocht", "support_and_feedback": "Tacaíocht & Aiseolas", "support_third_party_description": "Rinne tríÃē pÃĄirtí pacÃĄiste de do shuiteÃĄil Immich. D’fhÊadfadh sÊ gur an pacÃĄiste sin ba chÃēis le fadhbanna a bhíonn agat, mar sin tabhair ceisteanna dÃŗibh ar dtÃēs trí na naisc thíos a ÃēsÃĄid.", + "supporter": "Tacaíochtaí", "swap_merge_direction": "Malartaigh treo an chumaisc", "sync": "SioncrÃŗnaigh", "sync_albums": "SioncrÃŗnaigh albaim", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Roghnaigh socrÃē tÊama an aip", "theme_setting_three_stage_loading_subtitle": "D’fhÊadfadh luchtÃē trí chÊim feidhmíocht an luchtaithe a mhÊadÃē ach bíonn ualach líonra i bhfad níos airde mar thoradh air", "theme_setting_three_stage_loading_title": "Cumasaigh luchtÃē trí chÊim", + "then": "Ansin", "they_will_be_merged_together": "Cuirfear le chÊile iad", "third_party_resources": "Acmhainní TríÃē PÃĄirtí", "time": "Am", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Roghnaigh sÃŗcmhainní", "trash_page_title": "Bruscar ({count})", "trashed_items_will_be_permanently_deleted_after": "Scriosfar míreanna atÃĄ curtha sa bhruscar go buan i ndiaidh {days, plural, one {# lÃĄ} other {# laethanta}}.", + "trigger": "SpriocdhíriÃē", + "trigger_asset_uploaded": "SÃŗcmhainn UaslÃŗdÃĄilte", + "trigger_asset_uploaded_description": "Spreagtha nuair a uaslÃŗdÃĄlfar sÃŗcmhainn nua", + "trigger_description": "Imeacht a chuireann tÃēs leis an sreabhadh oibre", + "trigger_person_recognized": "Duine Aitheanta", + "trigger_person_recognized_description": "Spreagtar nuair a bhraitear duine", + "trigger_type": "CineÃĄl spreagthÃŗra", "troubleshoot": "Fabhtcheartaigh", "type": "CineÃĄl", "unable_to_change_pin_code": "Ní fÊidir an cÃŗd PIN a athrÃē", @@ -2123,6 +2284,7 @@ "unhide_person": "Nocht an duine", "unknown": "Anaithnid", "unknown_country": "Tír Anaithnid", + "unknown_date": "DÃĄta anaithnid", "unknown_year": "Bliain Anaithnid", "unlimited": "Gan teorainn", "unlink_motion_video": "Dínasc físeÃĄn gluaisne", @@ -2139,17 +2301,19 @@ "unstack": "Dí-chruachadh", "unstack_action_prompt": "{count} gan chruachadh", "unstacked_assets_count": "Gan chruachadh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}}", + "unsupported_field_type": "CineÃĄl rÊimse nach dtacaítear leis", "untagged": "Gan Chlib", + "untitled_workflow": "Sreabhadh oibre gan teideal", "up_next": "Ar aghaidh", "update_location_action_prompt": "Nuashonraigh suíomh na sÃŗcmhainní roghnaithe {count} le:", "updated_at": "Nuashonraithe", "updated_password": "Pasfhocal nuashonraithe", "upload": "UaslÃŗdÃĄil", - "upload_action_prompt": "{count} i scuaine le haghaidh uaslÃŗdÃĄla", "upload_concurrency": "UaslÃŗdÃĄil comhthrÃĄthacht", "upload_details": "Sonraí UaslÃŗdÃĄla", "upload_dialog_info": "Ar mhaith leat cÃēltaca den ShÃŗcmhainn/na SÃŗcmhainní roghnaithe a dhÊanamh chuig an bhfreastalaí?", "upload_dialog_title": "UaslÃŗdÃĄil SÃŗcmhainn", + "upload_error_with_count": "EarrÃĄid uaslÃŗdÃĄla le haghaidh {count, plural, one {# sÃŗcmhainn} other {# sÃŗcmhainní}}", "upload_errors": "UaslÃŗdÃĄil críochnaithe le {count, plural, one {# earrÃĄid} other {# earrÃĄidí}}, athnuachan an leathanach chun sÃŗcmhainní uaslÃŗdÃĄla nua a fheiceÃĄil.", "upload_finished": "UaslÃŗdÃĄil críochnaithe", "upload_progress": "FÃĄgtha {remaining, number} - PrÃŗiseÃĄilte {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "ÚsÃĄid", "use_biometric": "ÚsÃĄid bithmhÊadrach", - "use_current_connection": "bain ÃēsÃĄid as an nasc reatha", + "use_current_connection": "ÚsÃĄid an nasc reatha", "use_custom_date_range": "ÚsÃĄid raon dÃĄta saincheaptha ina ionad", "user": "ÚsÃĄideoir", "user_has_been_deleted": "Scriosadh an t-ÃēsÃĄideoir seo.", @@ -2185,6 +2349,7 @@ "utilities": "FÃŗntais", "validate": "BailíochtÃē", "validate_endpoint_error": "Cuir isteach URL bailí le do thoil", + "validation_error": "EarrÃĄid bailíochtaithe", "variables": "AthrÃŗga", "version": "Leagan", "version_announcement_closing": "Do chara, Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Seinn mionsamhail físe nuair a bhíonn an luch ag luascadh thar an mír. FiÃē nuair atÃĄ sÊ díchumasaithe, is fÊidir athsheinm a thosÃē tríd an luch a luascadh thar an deilbhín seinnte.", "videos": "FíseÃĄin", "videos_count": "{count, plural, one {# FíseÃĄn} other {# FíseÃĄin}}", + "videos_only": "FíseÃĄin amhÃĄin", "view": "Amharc", "view_album": "FÊach ar an Albam", "view_all": "FÊach ar Gach Rud", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "ÚsÃĄid mar PhríomhshÃŗcmhainn", "viewer_unstack": "Dí-Chruach", "visibility_changed": "Athraíodh infheictheacht do {count, plural, one {# duine} other {# daoine}}", + "visual": "Amhairc", + "visual_builder": "TÃŗgÃĄlaí amhairc", "waiting": "Ag fanacht", "waiting_count": "Ag fanacht: {count}", "warning": "Rabhadh", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "FÃĄilte go hImmich", "width": "Leithead", "wifi_name": "Ainm Wi-Fi", - "workflow": "Sreabhadh Oibre", + "workflow_delete_prompt": "An bhfuil tÃē cinnte gur mian leat an sreabhadh oibre seo a scriosadh?", + "workflow_deleted": "Sreabhadh oibre scriosta", + "workflow_description": "Cur síos ar an sreabhadh oibre", + "workflow_info": "Eolas faoin sreabhadh oibre", + "workflow_json": "Sreabhadh Oibre JSON", + "workflow_json_help": "Cuir cumraíocht an tsreabha oibre in eagar i bhformÃĄid JSON. DÊanfar athruithe a shioncronÃē leis an tÃŗgÃĄlaí amhairc.", + "workflow_name": "Ainm an tsreafa oibre", + "workflow_navigation_prompt": "An bhfuil tÃē cinnte gur mian leat imeacht gan do chuid athruithe a shÃĄbhÃĄil?", + "workflow_summary": "Achoimre ar an sreabhadh oibre", + "workflow_update_success": "Nuashonraíodh an sreabhadh oibre go rathÃēil", + "workflow_updated": "Sreabhadh oibre nuashonraithe", + "workflows": "Sreafaí oibre", + "workflows_help_text": "Uathoibríonn sreafaí oibre gníomhartha ar do shÃŗcmhainní bunaithe ar spreagthÃŗirí agus scagairí", "wrong_pin_code": "CÃŗd PIN mícheart", "year": "Bliain", "years_ago": "{years, plural, one {# bliain} other {# blianta}} Ãŗ shin", "yes": "TÃĄ", "you_dont_have_any_shared_links": "Níl aon naisc chomhroinnte agat", "your_wifi_name": "Ainm do Wi-Fi", + "zero_to_clear_rating": "brÃēigh 0 chun rÃĄtÃĄil sÃŗcmhainne a ghlanadh", "zoom_image": "ÍomhÃĄ ZÃēmÃĄil", "zoom_to_bounds": "ZÃēmÃĄil go dtí na teorainneacha" } diff --git a/i18n/gl.json b/i18n/gl.json index 3891577065..135e3f64cd 100644 --- a/i18n/gl.json +++ b/i18n/gl.json @@ -5,8 +5,10 @@ "acknowledge": "De acordo", "action": "AcciÃŗn", "action_common_update": "Actualizar", + "action_description": "Un conxunto de acciÃŗns a levar a cabo nos recursos filtrados", "actions": "AcciÃŗns", "active": "Activo", + "active_count": "Activo:{count}", "activity": "Actividade", "activity_changed": "A actividade estÃĄ {enabled, select, true {activada} other {desactivada}}", "add": "Engadir", @@ -14,9 +16,14 @@ "add_a_location": "Engadir unha localizaciÃŗn", "add_a_name": "Engadir un nome", "add_a_title": "Engadir un título", + "add_action": "Engadir acciÃŗn", + "add_action_description": "Faga click para engadir unha acciÃŗn a realizar", + "add_assets": "Engadir activos", "add_birthday": "Engadir aniversario", "add_endpoint": "Engadir punto final", "add_exclusion_pattern": "Engadir patrÃŗn de exclusiÃŗn", + "add_filter": "Engadir filtro", + "add_filter_description": "Faga click para engadir unha condiciÃŗn de filtrado", "add_location": "Engadir localizaciÃŗn", "add_more_users": "Engadir mÃĄis usuarios", "add_partner": "Engadir compaÃąeiro/a", @@ -35,6 +42,7 @@ "add_to_shared_album": "Engadir ao ÃĄlbum compartido", "add_upload_to_stack": "Engade cargar ÃĄ pila", "add_url": "Engadir URL", + "add_workflow_step": "Engadir paso de fluxo de traballo", "added_to_archive": "Engadido ao arquivo", "added_to_favorites": "Engadido a favoritos", "added_to_favorites_count": "Engadíronse {count, number} a favoritos", @@ -67,6 +75,7 @@ "confirm_reprocess_all_faces": "EstÃĄ seguro de que quere reprocesar todas as caras? Isto tamÊn borrarÃĄ as persoas nomeadas.", "confirm_user_password_reset": "EstÃĄ seguro de que quere restablecer o contrasinal de {user}?", "confirm_user_pin_code_reset": "EstÃĄ seguro de que quere restablecer o PIN de {user}?", + "copy_config_to_clipboard_description": "Copiar a configuraciÃŗn actual do sistema coma un obxecto JSON ao portapapeis", "create_job": "Crear traballo", "cron_expression": "ExpresiÃŗn Cron", "cron_expression_description": "Estableza o intervalo de escaneo usando o formato cron. Para obter mÃĄis informaciÃŗn, consulte por exemplo Crontab Guru", @@ -74,6 +83,8 @@ "disable_login": "Desactivar inicio de sesiÃŗn", "duplicate_detection_job_description": "Executar aprendizaxe automÃĄtica nos activos para detectar imaxes similares. Depende da Busca Intelixente", "exclusion_pattern_description": "Os patrÃŗns de exclusiÃŗn permítenlle ignorar ficheiros e cartafoles ao escanear a sÃēa biblioteca. Isto Ê Ãētil se ten cartafoles que conteÃąen ficheiros que non quere importar, como ficheiros RAW.", + "export_config_as_json_description": "Descarga a configuraciÃŗn actual coma un arquivo JSON", + "external_libraries_page_description": "PÃĄxina da librería externa do administrador", "face_detection": "DetecciÃŗn de caras", "face_detection_description": "Detectar as caras nos activos usando aprendizaxe automÃĄtica. Para vídeos, sÃŗ se considera a miniatura. \"Actualizar\" (re)procesa todos os activos. \"Restablecer\" ademais borra todos os datos de caras actuais. \"Faltantes\" pon en cola os activos que aínda non foron procesados. As caras detectadas poranse en cola para o RecoÃąecemento Facial despois de completar a DetecciÃŗn de Caras, agrupÃĄndoas en persoas existentes ou novas.", "facial_recognition_job_description": "Agrupar caras detectadas en persoas. Este paso execÃētase despois de completar a DetecciÃŗn de Caras. \"Restablecer\" (re)agrupa todas as caras. \"Faltantes\" pon en cola as caras que non teÃąen unha persoa asignada.", @@ -93,6 +104,8 @@ "image_preview_description": "Imaxe de tamaÃąo medio con metadatos eliminados, usada ao ver un Ãēnico activo e para aprendizaxe automÃĄtica", "image_preview_quality_description": "Calidade da vista previa de 1 a 100. Canto mÃĄis alto, mellor, pero produce ficheiros mÃĄis grandes e pode reducir a capacidade de resposta da aplicaciÃŗn. Establecer un valor baixo pode afectar ÃĄ calidade da aprendizaxe automÃĄtica.", "image_preview_title": "ConfiguraciÃŗn da vista previa", + "image_progressive": "Progresivo", + "image_progressive_description": "Codifica imaxes JPEG progresivamente para unha visualizaciÃŗn con carga gradual. Isto non ten ningÃēn efecto en imaxes WebP.", "image_quality": "Calidade", "image_resolution": "ResoluciÃŗn", "image_resolution_description": "ResoluciÃŗns mÃĄis altas poden preservar mÃĄis detalles pero tardan mÃĄis en codificarse, teÃąen tamaÃąos de ficheiro mÃĄis grandes e poden reducir a capacidade de resposta da aplicaciÃŗn.", @@ -101,6 +114,7 @@ "image_thumbnail_description": "Miniatura pequena con metadatos eliminados, usada ao ver grupos de fotos como a liÃąa de tempo principal", "image_thumbnail_quality_description": "Calidade da miniatura de 1 a 100. Canto mÃĄis alto, mellor, pero produce ficheiros mÃĄis grandes e pode reducir a capacidade de resposta da aplicaciÃŗn.", "image_thumbnail_title": "ConfiguraciÃŗn da miniatura", + "import_config_from_json_description": "Importar a configuraciÃŗn do sistema subindo un arquivo de configuraciÃŗn JSON", "job_concurrency": "concorrencia de {job}", "job_created": "Traballo creado", "job_not_concurrency_safe": "Este traballo non Ê seguro para execuciÃŗn concorrente.", @@ -108,11 +122,13 @@ "job_settings_description": "Xestionar a concorrencia de traballos", "jobs_delayed": "{jobCount, plural, other {# atrasados}}", "jobs_failed": "{jobCount, plural, other {# fallados}}", + "jobs_over_time": "Traballos ao longo do tempo", "library_created": "Biblioteca creada: {library}", "library_deleted": "Biblioteca eliminada", "library_details": "Detalles da biblioteca", "library_folder_description": "Especifique un cartafol para importar. Este cartafol, incluídos os subcartafoles, analizaranse para atopar imaxes e vídeos.", "library_remove_exclusion_pattern_prompt": "EstÃĄ seguro de que quere eliminar este patrÃŗn de exclusiÃŗn?", + "library_remove_folder_prompt": "Seguro que queres eliminar este cartafol importante?", "library_scanning": "Escaneo periÃŗdico", "library_scanning_description": "Configurar o escaneo periÃŗdico da biblioteca", "library_scanning_enable_description": "Activar o escaneo periÃŗdico da biblioteca", @@ -174,8 +190,23 @@ "machine_learning_smart_search_enabled": "Activar busca intelixente", "machine_learning_smart_search_enabled_description": "Se estÃĄ desactivado, as imaxes non se codificarÃĄn para a busca intelixente.", "machine_learning_url_description": "A URL do servidor de aprendizaxe automÃĄtica. Se se proporciona mÃĄis dunha URL, intentarase con cada servidor un por un ata que un responda correctamente, en orde do primeiro ao Ãēltimo. Os servidores que non respondan ignoraranse temporalmente ata que volvan estar en liÃąa.", + "maintenance_delete_backup": "Eliminar copia de seguridade", + "maintenance_delete_backup_description": "Este arquivo serÃĄ borrado permanentemente.", + "maintenance_delete_error": "Erro ao eliminar a copia de seguridade.", + "maintenance_restore_backup": "Recuperar copia de seguridade", + "maintenance_restore_backup_description": "Immich borrarase e restaurarase desde a copia de seguridade escollida. Crearase unha copia de seguridade antes de continuar.", + "maintenance_restore_backup_different_version": "Esta copia de seguridade foi creada cunha versiÃŗn diferente de Immich!", + "maintenance_restore_backup_unknown_version": "Non se puido determinal a versiÃŗn da copia de seguridade.", + "maintenance_restore_database_backup": "Restaurar copia de seguridade da base de datos", + "maintenance_restore_database_backup_description": "Reverter a un estado anterior da base de datos usando unha copia de seguridade", "maintenance_settings": "Mantemento", + "maintenance_settings_description": "PoÃąer Immich en modo mantemento.", + "maintenance_start": "Cambiar ao modo de mantemento", + "maintenance_start_error": "Erro ao iniciar o modo de mantemento.", + "maintenance_upload_backup": "Subir arquivo de copia de seguridade", + "maintenance_upload_backup_error": "Non se puido subir a copia de seguridade, o formato Ê .sql/.sql.gz ?", "manage_concurrency": "Xestionar Concorrencia", + "manage_concurrency_description": "Navegar ÃĄ pÃĄxina de traballos para xestionar a concorrencia de trabalhos", "manage_log_settings": "Xestionar configuraciÃŗn de rexistro", "map_dark_style": "Estilo escuro", "map_enable_description": "Activar funciÃŗns do mapa", @@ -241,7 +272,7 @@ "oauth_auto_register": "Rexistro automÃĄtico", "oauth_auto_register_description": "Rexistrar automaticamente novos usuarios despois de iniciar sesiÃŗn con OAuth", "oauth_button_text": "Texto do botÃŗn", - "oauth_client_secret_description": "Requirido se o provedor OAuth non admite PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Requirido para clientes confidenciais ou se o provedor OAuth non admite PKCE (Proof Key for Code Exchange).", "oauth_enable_description": "Iniciar sesiÃŗn con OAuth", "oauth_mobile_redirect_uri": "URI de redirecciÃŗn mÃŗbil", "oauth_mobile_redirect_uri_override": "SubstituciÃŗn de URI de redirecciÃŗn mÃŗbil", @@ -265,10 +296,14 @@ "password_settings_description": "Xestionar a configuraciÃŗn de inicio de sesiÃŗn con contrasinal", "paths_validated_successfully": "Todas as rutas validadas correctamente", "person_cleanup_job": "Limpeza de persoas", + "queue_details": "Detalles da Cola", + "queues": "Colas de traballos", + "queues_page_description": "PÃĄxina de colas de traballo (admin)", "quota_size_gib": "TamaÃąo da cota (GiB)", "refreshing_all_libraries": "Actualizando todas as bibliotecas", "registration": "Rexistro do administrador", "registration_description": "Dado que vostede Ê o primeiro usuario no sistema, asignarÃĄselle como Administrador e serÃĄ responsable das tarefas administrativas. Os usuarios adicionais serÃĄn creados por vostede.", + "remove_failed_jobs": "Eliminar os traballos con erros", "require_password_change_on_login": "Requirir que o usuario cambie o contrasinal no primeiro inicio de sesiÃŗn", "reset_settings_to_default": "Restablecer a configuraciÃŗn aos valores predeterminados", "reset_settings_to_recent_saved": "Restablecer ÃĄ configuraciÃŗn gardada recentemente", @@ -281,8 +316,10 @@ "server_public_users_description": "Todos os usuarios (nome e correo electrÃŗnico) lístanse ao engadir un usuario a ÃĄlbums compartidos. Cando estÃĄ desactivado, a lista de usuarios sÃŗ estarÃĄ dispoÃąible para os usuarios administradores.", "server_settings": "ConfiguraciÃŗn do servidor", "server_settings_description": "Xestionar a configuraciÃŗn do servidor", + "server_stats_page_description": "PÃĄxina de estatísticas do servidor (admin)", "server_welcome_message": "Mensaxe de benvida", "server_welcome_message_description": "Unha mensaxe que se mostra na pÃĄxina de inicio de sesiÃŗn.", + "settings_page_description": "PÃĄxina de axustes (admin)", "sidecar_job": "Metadatos Sidecar", "sidecar_job_description": "Descubrir ou sincronizar metadatos sidecar desde o sistema de ficheiros", "slideshow_duration_description": "NÃēmero de segundos para mostrar cada imaxe", @@ -314,7 +351,7 @@ "template_settings": "Modelos de NotificaciÃŗn", "template_settings_description": "Xestionar modelos personalizados para notificaciÃŗns", "theme_custom_css_settings": "CSS Personalizado", - "theme_custom_css_settings_description": "As Follas de Estilo en Cascada (CSS) permiten personalizar o deseÃąo de Immich.", + "theme_custom_css_settings_description": "As follas de estilo en cascada (CSS) permiten personalizar o deseÃąo de Immich.", "theme_settings": "ConfiguraciÃŗn do Tema", "theme_settings_description": "Xestionar a personalizaciÃŗn da interface web de Immich", "thumbnail_generation_job": "Xerar Miniaturas", @@ -401,6 +438,8 @@ "user_restore_scheduled_removal": "Restaurar usuario - eliminaciÃŗn programada o {date, date, long}", "user_settings": "ConfiguraciÃŗn do Usuario", "user_settings_description": "Xestionar a configuraciÃŗn do usuario", + "user_successfully_removed": "O usuario {email} foi eliminado satisfactoriamente.", + "users_page_description": "PÃĄxina de usuarios administradores", "version_check_enabled_description": "Activar comprobaciÃŗn de versiÃŗn", "version_check_implications": "A funciÃŗn de comprobaciÃŗn de versiÃŗn depende da comunicaciÃŗn periÃŗdica con github.com", "version_check_settings": "ComprobaciÃŗn de VersiÃŗn", @@ -412,6 +451,9 @@ "admin_password": "Contrasinal do administrador", "administration": "AdministraciÃŗn", "advanced": "Avanzado", + "advanced_settings_clear_image_cache": "Limpar a cachÊ da imaxe", + "advanced_settings_clear_image_cache_error": "Fallo ao limpar a cachÊ da imaxe", + "advanced_settings_clear_image_cache_success": "CachÊ borrada correctamente {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Use esta opciÃŗn para filtrar medios durante a sincronizaciÃŗn baseÃĄndose en criterios alternativos. SÃŗ probe isto se ten problemas coa aplicaciÃŗn para detectar todos os ÃĄlbums.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Usar filtro alternativo de sincronizaciÃŗn de ÃĄlbums do dispositivo", "advanced_settings_log_level_title": "Nivel de rexistro: {level}", @@ -448,10 +490,12 @@ "album_remove_user": "Eliminar usuario?", "album_remove_user_confirmation": "EstÃĄ seguro de que quere eliminar a {user}?", "album_search_not_found": "Non se atoparon ÃĄlbums que coincidan coa sÃēa busca", + "album_selected": "Álbum seleccionado", "album_share_no_users": "Parece que compartiu este ÃĄlbum con todos os usuarios ou non ten ningÃēn usuario co que compartir.", "album_summary": "Resumo do ÃĄlbum", "album_updated": "Álbum actualizado", "album_updated_setting_description": "Recibir unha notificaciÃŗn por correo electrÃŗnico cando un ÃĄlbum compartido teÃąa novos activos", + "album_upload_assets": "Cargar recursos desde o teu ordenador e engadilos ao ÃĄlbum", "album_user_left": "Saíu de {album}", "album_user_removed": "Eliminouse a {user}", "album_viewer_appbar_delete_confirm": "EstÃĄ seguro de que quere eliminar este ÃĄlbum da sÃēa conta?", @@ -469,9 +513,11 @@ "albums_default_sort_order_description": "Orde inicial dos ficheiros ao crear novos ÃĄlbums.", "albums_feature_description": "ColecciÃŗns de ficheiros que se poden compartir con outros usuarios.", "albums_on_device_count": "Álbums no dispositivo ({count})", + "albums_selected": "{count, plural, one {# ÃĄlbum selected} other {# ÃĄlbums selected}}", "all": "Todo", "all_albums": "Todos os ÃĄlbums", "all_people": "Todas as persoas", + "all_photos": "Todas as fotos", "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir ediciÃŗns", @@ -479,6 +525,9 @@ "allow_public_user_to_upload": "Permitir que o usuario pÃēblico cargue", "allowed": "Permitido", "alt_text_qr_code": "Imaxe de cÃŗdigo QR", + "always_keep": "Manter sempre", + "always_keep_photos_hint": "Liberar espazo manterÃĄ todas as fotos neste dispositivo.", + "always_keep_videos_hint": "Liberar espazo manterÃĄ todos os videos neste dispositivo.", "anti_clockwise": "Sentido antihorario", "api_key": "Chave API", "api_key_description": "Este valor sÃŗ se mostrarÃĄ unha vez. AsegÃērese de copialo antes de pechar a xanela.", @@ -505,10 +554,12 @@ "archived_count": "{count, plural, other {Arquivados #}}", "are_these_the_same_person": "Son estas a mesma persoa?", "are_you_sure_to_do_this": "EstÃĄ seguro de que quere facer isto?", + "array_field_not_fully_supported": "Os campos tipo array precisan ediciÃŗn manual no JSON", "asset_action_delete_err_read_only": "Non se poden eliminar activo(s) de sÃŗ lectura, omitindo", "asset_action_share_err_offline": "Non se poden obter activo(s) fÃŗra de liÃąa, omitindo", "asset_added_to_album": "Engadido ao ÃĄlbum", "asset_adding_to_album": "Engadindo ao ÃĄlbumâ€Ļ", + "asset_created": "Recurso creado", "asset_description_updated": "A descriciÃŗn do activo actualizouse", "asset_filename_is_offline": "O activo {filename} estÃĄ fÃŗra de liÃąa", "asset_has_unassigned_faces": "O activo ten caras sen asignar", @@ -521,6 +572,9 @@ "asset_list_layout_sub_title": "DeseÃąo", "asset_list_settings_subtitle": "ConfiguraciÃŗn do deseÃąo da grella de fotos", "asset_list_settings_title": "Grella de Fotos", + "asset_not_found_on_device_android": "Non se atopou o recurso no dispositivo", + "asset_not_found_on_device_ios": "Non se atopou o recurso no dispositivo. Se estÃĄs a usar iCloud, Ê posible que non sexa posible acceder ao recurso debido a un ficheiro incorrecto almacenado en iCloud", + "asset_not_found_on_icloud": "Non se atopou o recurso en iCloud. É posible que non se poida acceder ao recurso debido a un ficheiro incorrecto almacenado en iCloud", "asset_offline": "Activo FÃŗra de LiÃąa", "asset_offline_description": "Este activo externo xa non se atopa no disco. Por favor, contacte co seu administrador de Immich para obter axuda.", "asset_restored_successfully": "Activo restaurado correctamente", @@ -572,7 +626,7 @@ "backup_album_selection_page_select_albums": "Seleccionar ÃĄlbums", "backup_album_selection_page_selection_info": "InformaciÃŗn da selecciÃŗn", "backup_album_selection_page_total_assets": "Total de activos Ãēnicos", - "backup_albums_sync": "SincronizaciÃŗn de ÃĄlbums da copia de seguridade", + "backup_albums_sync": "SincronizaciÃŗn de ÃĄlbums de copia de seguridade", "backup_all": "Todo", "backup_background_service_backup_failed_message": "Erro ao facer copia de seguridade dos activos. Reintentandoâ€Ļ", "backup_background_service_complete_notification": "Copia de seguridade dos recursos completada", @@ -633,6 +687,7 @@ "backup_options_page_title": "OpciÃŗns da copia de seguridade", "backup_setting_subtitle": "Xestionar a configuraciÃŗn de carga en segundo plano e primeiro plano", "backup_settings_subtitle": "Xestionar configuraciÃŗn de subidas", + "backup_upload_details_page_more_details": "Toca para mais detalles", "backward": "AtrÃĄs", "biometric_auth_enabled": "AutenticaciÃŗn biomÊtrica activada", "biometric_locked_out": "EstÃĄ bloqueado da autenticaciÃŗn biomÊtrica", @@ -691,6 +746,8 @@ "change_password_form_password_mismatch": "Os contrasinais non coinciden", "change_password_form_reenter_new_password": "Reintroducir Novo Contrasinal", "change_pin_code": "Cambiar cÃŗdigo PIN", + "change_trigger": "Cambiar o disparador", + "change_trigger_prompt": "Seguro que queres cambiar o disparador? EliminarÃĄ todas as acciÃŗns e filtros existentes.", "change_your_password": "Cambiar o seu contrasinal", "changed_visibility_successfully": "Visibilidade cambiada correctamente", "charging": "Cargando", @@ -699,8 +756,21 @@ "check_corrupt_asset_backup_button": "Realizar comprobaciÃŗn", "check_corrupt_asset_backup_description": "Execute esta comprobaciÃŗn sÃŗ a travÊs da wifi e unha vez que todos os activos teÃąan copia de seguridade. O procedemento pode tardar uns minutos.", "check_logs": "Comprobar Rexistros", + "checksum": "Suma de comprobaciÃŗn", "choose_matching_people_to_merge": "Elixir persoas coincidentes para fusionar", "city": "Cidade", + "cleanup_confirm_description": "Immich atopou {count} recursos (creados antes de {date}) copiados de seguridade no servidor. Queres eliminar as copias locais deste dispositivo?", + "cleanup_confirm_prompt_title": "Eliminar deste dispositivo?", + "cleanup_deleted_assets": "MovÊronse {count} recursos ÃĄ papeleira do dispositivo", + "cleanup_deleting": "Movendo ao lixo...", + "cleanup_found_assets": "AtopÃĄronse {count} arquivo(s) con copias de seguridade", + "cleanup_found_assets_with_size": "AtopÃĄronse {count} recursos con copia de seguridade ({size})", + "cleanup_icloud_shared_albums_excluded": "Os ÃĄlbums compartidos de iCloud estÃĄn excluídos da anÃĄlise", + "cleanup_no_assets_found": "Non se atoparon recursos que coincidan cos criterios anteriores. Liberar espazo sÃŗ pode eliminar recursos dos que se fixo unha copia de seguridade no servidor", + "cleanup_preview_title": "Arquivos que van ser borrados ({count})", + "cleanup_step3_description": "Busca recursos con copia de seguridade que coincidan coa tÃēa data e garda a configuraciÃŗn.", + "cleanup_step4_summary": "{count} arquivos (creados antes do {date}) para eliminar do teu dispositivo local. As fotos seguirÃĄn accesibles dende a aplicaciÃŗn de Immich.", + "cleanup_trash_hint": "Para recuperar todo o espazo de almacenamento, abre a aplicaciÃŗn da galería do sistema e baleira a papeleira", "clear": "Limpar", "clear_all": "Limpar todo", "clear_all_recent_searches": "Limpar todas as buscas recentes", @@ -712,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado de cliente importado", "client_cert_invalid_msg": "Ficheiro de certificado invÃĄlido ou contrasinal incorrecto", + "client_cert_password_message": "Introduza o contrasinal para este certificado", + "client_cert_password_title": "Contrasinal do certificado", "client_cert_remove_msg": "Certificado de cliente eliminado", "client_cert_subtitle": "Soporta sÃŗ o formato PKCS12 (.p12, .pfx). A importaciÃŗn ou eliminaciÃŗn de certificados estÃĄ dispoÃąible sÃŗ antes de iniciar sesiÃŗn", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -721,6 +793,7 @@ "collapse_all": "Contraer todo", "color": "Cor", "color_theme": "Tema de cor", + "command": "Comando", "comment_deleted": "Comentario eliminado", "comment_options": "OpciÃŗns de comentario", "comments_and_likes": "Comentarios e GÃēstames", @@ -765,6 +838,7 @@ "create_album": "Crear ÃĄlbum", "create_album_page_untitled": "Sen título", "create_api_key": "Crear chave API", + "create_first_workflow": "Crear o primeiro fluxo de traballo", "create_library": "Crear Biblioteca", "create_link": "Crear ligazÃŗn", "create_link_to_share": "Crear ligazÃŗn para compartir", @@ -779,17 +853,25 @@ "create_tag": "Crear etiqueta", "create_tag_description": "Crear unha nova etiqueta. Para etiquetas aniÃąadas, introduza a ruta completa da etiqueta incluíndo barras inclinadas.", "create_user": "Crear usuario", + "create_workflow": "Crear fluxo de traballo", "created": "Creado", "created_at": "Creado", "creating_linked_albums": "Creando ÃĄlbums vinculados...", "crop": "Recortar", + "crop_aspect_ratio_fixed": "Fixado", + "crop_aspect_ratio_free": "Libre", + "crop_aspect_ratio_original": "Orixinal", "curated_object_page_title": "Cousas", "current_device": "Dispositivo actual", "current_pin_code": "CÃŗdigo PIN actual", "current_server_address": "Enderezo do servidor actual", + "custom_date": "Data personalizada", "custom_locale": "ConfiguraciÃŗn Rexional Personalizada", "custom_locale_description": "Formatar datas e nÃēmeros baseÃĄndose na lingua e a rexiÃŗn", "custom_url": "URL personalizada", + "cutoff_date_description": "Manter fotos dos Ãēltimosâ€Ļ", + "cutoff_day": "{count, plural, one {day} other {days}}", + "cutoff_year": "{count, plural, one {day} other {days}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", @@ -845,6 +927,7 @@ "deselect_all": "Deseleccionar todo", "details": "Detalles", "direction": "DirecciÃŗn", + "disable": "Desactivar", "disabled": "Desactivado", "disallow_edits": "Non permitir ediciÃŗns", "discord": "Discord", @@ -870,6 +953,7 @@ "download_include_embedded_motion_videos": "Vídeos incrustados", "download_include_embedded_motion_videos_description": "Incluír vídeos incrustados en fotos en movemento como un ficheiro separado", "download_notfound": "Descarga non atopada", + "download_original": "Descargar ­orixinal", "download_paused": "Descarga pausada", "download_settings": "Descarga", "download_settings_description": "Xestionar configuraciÃŗns relacionadas coa descarga de activos", @@ -879,6 +963,7 @@ "download_waiting_to_retry": "Agardando para reintentar", "downloading": "Descargando", "downloading_asset_filename": "Descargando activo {filename}", + "downloading_from_icloud": "Descargando dende iCloud", "downloading_media": "Descargando medios", "drop_files_to_upload": "Solte ficheiros en calquera lugar para cargar", "duplicates": "Duplicados", @@ -907,11 +992,22 @@ "edit_tag": "Editar etiqueta", "edit_title": "Editar Título", "edit_user": "Editar usuario", + "edit_workflow": "Editar fluxo de traballo", "editor": "Editor", "editor_close_without_save_prompt": "Os cambios non se gardarÃĄn", "editor_close_without_save_title": "Pechar editor?", - "editor_crop_tool_h2_aspect_ratios": "ProporciÃŗns de aspecto", - "editor_crop_tool_h2_rotation": "RotaciÃŗn", + "editor_confirm_reset_all_changes": "Seguro que queres restablecer todos os cambios?", + "editor_discard_edits_confirm": "Descartar ediciÃŗns", + "editor_discard_edits_prompt": "Tes cambios sen gardar. EstÃĄs seguro que queres descartalos?", + "editor_discard_edits_title": "Descartar cambios?", + "editor_edits_applied_error": "Non se puideron aplicar as ediciÃŗns", + "editor_edits_applied_success": "As ediciÃŗns aplicÃĄronse correctamente", + "editor_flip_horizontal": "Xirar horizontalmente", + "editor_flip_vertical": "Xirar verticalmente", + "editor_orientation": "OrientaciÃŗn", + "editor_reset_all_changes": "Restablecer os cambios", + "editor_rotate_left": "Xirar 90° en sentido antihorario", + "editor_rotate_right": "Xirar 90° en sentido horario", "email": "Correo electrÃŗnico", "email_notifications": "NotificaciÃŗns por correo electrÃŗnico", "empty_folder": "Este cartafol estÃĄ baleiro", @@ -930,11 +1026,14 @@ "error_change_sort_album": "Erro ao cambiar a orde de clasificaciÃŗn do ÃĄlbum", "error_delete_face": "Erro ao eliminar a cara do activo", "error_getting_places": "Erro ao obter lugares", + "error_loading_albums": "Produciuse un erro ao cargar os ÃĄlbums", "error_loading_image": "Erro ao cargar a imaxe", "error_loading_partners": "Erro cargando compaÃąeiros/as: {error}", + "error_retrieving_asset_information": "Produciuse un erro ao recuperar a informaciÃŗn do recurso", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao etiquetar cara - non se poden obter as coordenadas da caixa delimitadora", "error_title": "Erro - Algo saíu mal", + "error_while_navigating": "Produciuse un erro ao navegar ata o recurso", "errors": { "cannot_navigate_next_asset": "Non se pode navegar ao seguinte activo", "cannot_navigate_previous_asset": "Non se pode navegar ao activo anterior", @@ -969,6 +1068,7 @@ "failed_to_unstack_assets": "Erro ao desapilar activos", "failed_to_update_notification_status": "Erro ao actualizar o estado das notificaciÃŗns", "incorrect_email_or_password": "Correo electrÃŗnico ou contrasinal incorrectos", + "library_folder_already_exists": "Esta ruta de importaciÃŗn xa existe.", "paths_validation_failed": "{paths, plural, one {# ruta fallou} other {# rutas fallaron}} na validaciÃŗn", "profile_picture_transparent_pixels": "As imaxes de perfil non poden ter píxeles transparentes. Por favor, faga zoom e/ou mova a imaxe.", "quota_higher_than_disk_size": "Estableceu unha cota superior ao tamaÃąo do disco", @@ -991,6 +1091,7 @@ "unable_to_complete_oauth_login": "Non se puido completar o inicio de sesiÃŗn OAuth", "unable_to_connect": "Non se puido conectar", "unable_to_copy_to_clipboard": "Non se puido copiar ao portapapeis, asegÃērese de acceder ÃĄ pÃĄxina a travÊs de https", + "unable_to_create": "Non se pode crear o fluxo de traballo", "unable_to_create_admin_account": "Non se puido crear a conta de administrador", "unable_to_create_api_key": "Non se puido crear unha nova Chave API", "unable_to_create_library": "Non se puido crear a biblioteca", @@ -1001,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Non se puido eliminar o patrÃŗn de exclusiÃŗn", "unable_to_delete_shared_link": "Non se puido eliminar a ligazÃŗn compartida", "unable_to_delete_user": "Non se puido eliminar o usuario", + "unable_to_delete_workflow": "Non se pode eliminar o fluxo de traballo", "unable_to_download_files": "Non se puideron descargar os ficheiros", "unable_to_edit_exclusion_pattern": "Non se puido editar o patrÃŗn de exclusiÃŗn", "unable_to_empty_trash": "Non se puido baleirar o lixo", @@ -1040,6 +1142,7 @@ "unable_to_scan_library": "Non se puido escanear a biblioteca", "unable_to_set_feature_photo": "Non se puido establecer a foto destacada", "unable_to_set_profile_picture": "Non se puido establecer a imaxe de perfil", + "unable_to_set_rating": "Non se pode definir a clasificaciÃŗn", "unable_to_submit_job": "Non se puido enviar o traballo", "unable_to_trash_asset": "Non se puido mover o activo ao lixo", "unable_to_unlink_account": "Non se puido desvincular a conta", @@ -1051,8 +1154,11 @@ "unable_to_update_settings": "Non se puido actualizar a configuraciÃŗn", "unable_to_update_timeline_display_status": "Non se puido actualizar o estado de visualizaciÃŗn da liÃąa de tempo", "unable_to_update_user": "Non se puido actualizar o usuario", + "unable_to_update_workflow": "Non se pode actualizar o fluxo de traballo", "unable_to_upload_file": "Non se puido cargar o ficheiro" }, + "errors_text": "Erros", + "exclusion_pattern": "PatrÃŗn de exclusiÃŗn", "exif": "Exif", "exif_bottom_sheet_description": "Engadir DescriciÃŗn...", "exif_bottom_sheet_description_error": "Erro ao actualizar a descriciÃŗn", @@ -1083,6 +1189,7 @@ "external_network_sheet_info": "Cando non estea na rede wifi preferida, a aplicaciÃŗn conectarase ao servidor a travÊs da primeira das seguintes URLs que poida alcanzar, comezando de arriba a abaixo", "face_unassigned": "Sen asignar", "failed": "Fallado", + "failed_count": "Fallou: {count}", "failed_to_authenticate": "Fallou a autenticaciÃŗn", "failed_to_load_assets": "Erro ao cargar activos", "failed_to_load_folder": "Erro ao cargar o cartafol", @@ -1095,14 +1202,17 @@ "features": "FunciÃŗns", "features_in_development": "Funcionalidades en Desenvolvemento", "features_setting_description": "Xestionar as funciÃŗns da aplicaciÃŗn", - "file_name": "Nome do ficheiro", "file_name_or_extension": "Nome do ficheiro ou extensiÃŗn", + "file_name_text": "Nome do arquivo", + "file_name_with_value": "Nome do arquivo: {file_name}", "file_size": "TamaÃąo do arquivo", "filename": "Nome do ficheiro", "filetype": "Tipo de ficheiro", "filter": "Filtro", + "filter_description": "CondiciÃŗns para filtrar os activos obxectivo", "filter_people": "Filtrar persoas", "filter_places": "Filtrar lugares", + "filters": "Filtros", "find_them_fast": "AtÃŗpeos rÃĄpido por nome coa busca", "first": "Primeiro/a", "fix_incorrect_match": "Corrixir coincidencia incorrecta", @@ -1112,11 +1222,16 @@ "folders_feature_description": "Navegar pola vista de cartafoles para as fotos e vídeos no sistema de ficheiros", "forgot_pin_code_question": "Esqueceu o seu PIN?", "forward": "Adiante", + "free_up_space": "Liberar espazo", + "free_up_space_description": "Move as fotos e os vídeos dos que fixeches unha copia de seguridade ÃĄ papeleira do teu dispositivo para liberar espazo. As tÃēas copias no servidor permanecen seguras.", + "free_up_space_settings_subtitle": "Liberar almacenamento do dispositivo", + "full_path": "Ruta completa: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carga recursos externos de Google para poder funcionar.", "general": "Xeral", "geolocation_instruction_location": "Prema nun recurso con coordenadas GPS para usar a sÃēa localizaciÃŗn, ou seleccione unha localizaciÃŗn directamente no mapa", "get_help": "Obter Axuda", + "get_people_error": "Erro ao obter xente", "get_wifiname_error": "Non se puido obter o nome da wifi. AsegÃērese de que concedeu os permisos necesarios e estÃĄ conectado a unha rede wifi", "getting_started": "Primeiros Pasos", "go_back": "Volver", @@ -1142,12 +1257,15 @@ "header_settings_header_name_input": "Nome da cabeceira", "header_settings_header_value_input": "Valor da cabeceira", "headers_settings_tile_title": "Cabeceiras de proxy personalizadas", + "height": "Altura", "hi_user": "Ola {name} ({email})", "hide_all_people": "Ocultar todas as persoas", "hide_gallery": "Ocultar galería", "hide_named_person": "Ocultar a persoa {name}", "hide_password": "Ocultar contrasinal", "hide_person": "Ocultar persoa", + "hide_schema": "Ocultar esquema", + "hide_text_recognition": "Ocultar recoÃąecemento de texto", "hide_unnamed_people": "Ocultar persoas sen nome", "home_page_add_to_album_conflicts": "Engadidos {added} activos ao ÃĄlbum {album}. {failed} activos xa estÃĄn no ÃĄlbum.", "home_page_add_to_album_err_local": "Non se poden engadir activos locais a ÃĄlbums aínda, omitindo", @@ -1219,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "O procesamento executouse ÃĄs {dateTime}", "items_count": "{count, plural, one {# elemento} other {# elementos}}", "jobs": "Traballos", + "json_editor": "Editor JSON", + "json_error": "Erro JSON", "keep": "Conservar", + "keep_albums": "Gardar ÃĄlbums", + "keep_albums_count": "Manter {count} {count, plural, one {album} other {albums}}", "keep_all": "Conservar Todo", + "keep_description": "Escolle o que permanece no teu dispositivo ao liberar espazo.", + "keep_favorites": "Manter favoritos", + "keep_on_device": "Manter no dispositivo", + "keep_on_device_hint": "Seleccionar elementos a manter neste dispositivo", "keep_this_delete_others": "Conservar este, eliminar outros", + "keeping": "Mantendo: {items}", "kept_this_deleted_others": "Conservouse este activo e eliminÃĄronse {count, plural, one {# activo} other {# activos}}", "keyboard_shortcuts": "Atallos de teclado", "language": "Lingua", @@ -1241,6 +1368,8 @@ "let_others_respond": "Permitir que outros respondan", "level": "Nivel", "library": "Biblioteca", + "library_add_folder": "Engadir carpeta", + "library_edit_folder": "Editar carpeta", "library_options": "OpciÃŗns da biblioteca", "library_page_device_albums": "Álbums no Dispositivo", "library_page_new_album": "Novo ÃĄlbum", @@ -1261,6 +1390,7 @@ "local": "Local", "local_asset_cast_failed": "Non Ê posíbel proxectar un recurso que non estÃĄ cargado no servidor", "local_assets": "Recursos Locais", + "local_id": "ID local", "local_media_summary": "Resumo de Contido Local", "local_network": "Rede local", "local_network_sheet_info": "A aplicaciÃŗn conectarase ao servidor a travÊs desta URL cando use a rede wifi especificada", @@ -1312,6 +1442,29 @@ "loop_videos_description": "Activar para reproducir automaticamente un vídeo en bucle no visor de detalles.", "main_branch_warning": "EstÃĄ a usar unha versiÃŗn de desenvolvemento; recomendamos encarecidamente usar unha versiÃŗn de lanzamento!", "main_menu": "MenÃē principal", + "maintenance_action_restore": "Restablecendo base de datos", + "maintenance_description": "Immich foi posto en modo de mantemento.", + "maintenance_end": "Finalizar o modo de mantemento", + "maintenance_end_error": "Erro ao finalizar o modo de mantemento.", + "maintenance_logged_in_as": "SesiÃŗn iniciada actualmente como {user}", + "maintenance_restore_from_backup": "Restaurar dende unha copia de seguridade", + "maintenance_restore_library": "Restaura a tÃēa librería", + "maintenance_restore_library_confirm": "Se isto parece correcto, continÃēa restaurando unha copia de seguridade!", + "maintenance_restore_library_description": "Restaurando copia de seguridade", + "maintenance_restore_library_folder_has_files": "{folder} ten {count} carpeta(s)", + "maintenance_restore_library_folder_no_files": "Faltan arquivos en {folder}!", + "maintenance_restore_library_folder_pass": "lexible e escribible", + "maintenance_restore_library_folder_read_fail": "non lexible", + "maintenance_restore_library_folder_write_fail": "non escribible", + "maintenance_restore_library_hint_missing_files": "Pode que che falten ficheiros importantes", + "maintenance_restore_library_hint_regenerate_later": "Podes rexeneralos mÃĄis tarde na configuraciÃŗn", + "maintenance_restore_library_hint_storage_template_missing_files": "Usas un modelo de almacenamento? Pode que che falten arquivos", + "maintenance_restore_library_loading": "Cargando comprobaciÃŗns de integridade e heurísticasâ€Ļ", + "maintenance_task_backup": "Creando unha copia de seguridade da base de datos existenteâ€Ļ", + "maintenance_task_migrations": "Executando migraciÃŗns de bases de datosâ€Ļ", + "maintenance_task_restore": "Restaurando a copia de seguridade escollidaâ€Ļ", + "maintenance_task_rollback": "Fallou a restauraciÃŗn, volvendo ao punto de restauraciÃŗnâ€Ļ", + "maintenance_title": "Non dispoÃąible temporalmente", "make": "Marca", "manage_geolocation": "Xestionar a localizaciÃŗn", "manage_media_access_rationale": "Requírese este permiso para xestionar correctamente o traslado dos recursos ao lixo e a sÃēa restauraciÃŗn desde el.īģŋ", @@ -1372,6 +1525,8 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Faltantes", "mobile_app": "AplicaciÃŗn MÃŗbil", "mobile_app_download_onboarding_note": "Descarga a aplicaciÃŗn mÃŗbil complementaria usando as seguintes opciÃŗns", @@ -1380,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM a", "more": "MÃĄis", "move": "Mover", + "move_down": "Baixar", "move_off_locked_folder": "Mover fÃŗra do cartafol bloqueado", "move_to": "Mover a", + "move_to_device_trash": "Mover ÃĄ papeleira do dispositivo", "move_to_lock_folder_action_prompt": "{count} engadido/a ao cartafol bloqueado", "move_to_locked_folder": "Mover ao cartafol bloqueado", "move_to_locked_folder_confirmation": "Estas fotos e vídeo eliminaranse de todos os ÃĄlbums e sÃŗ serÃĄn visíbeis dende o cartafol bloqueado", + "move_up": "Subir", "moved_to_archive": "Moveuse {count, plural, one {# recurso} other {# recursos}} ao arquivo", "moved_to_library": "Moveuse {count, plural, one {# recurso} other {# recursos}} ÃĄ biblioteca", "moved_to_trash": "Movido ao lixo", @@ -1394,6 +1552,7 @@ "my_albums": "Os meus ÃĄlbums", "name": "Nome", "name_or_nickname": "Nome ou alcume", + "name_required": "O nome Ê obligatorio", "navigate": "Navegar", "navigate_to_time": "Navegar ata Hora", "network_requirement_photos_upload": "Usar datos mÃŗbiles para facer copia de seguridade das fotos", @@ -1418,22 +1577,27 @@ "next": "Seguinte", "next_memory": "Seguinte recordo", "no": "Non", + "no_actions_added": "Non hai acciÃŗns engadidas polo momento", + "no_albums_found": "Non se atoparon ÃĄlbums", "no_albums_message": "Cree un ÃĄlbum para organizar as sÃēas fotos e vídeos", "no_albums_with_name_yet": "Parece que aínda non ten ningÃēn ÃĄlbum con este nome.", "no_albums_yet": "Parece que aínda non ten ningÃēn ÃĄlbum.", "no_archived_assets_message": "Arquive fotos e vídeos para ocultalos da sÃēa vista de Fotos", - "no_assets_message": "PREMA PARA CARGAR A SÚA PRIMEIRA FOTO", + "no_assets_message": "Fai clic para subir a tÃēa primeira foto", "no_assets_to_show": "Non hai activos para mostrar", "no_cast_devices_found": "Non se atoparon dispositivos de transmisiÃŗn", "no_checksum_local": "Non hai suma de verificaciÃŗn dispoÃąible - non se poden obter os activos locais", "no_checksum_remote": "Non hai suma de verificaciÃŗn dispoÃąible - non se pode obter o activo remoto", + "no_configuration_needed": "Non se precisa configuraciÃŗn", "no_devices": "Dispositivos non autorizados", "no_duplicates_found": "Non se atoparon duplicados.", "no_exif_info_available": "Non hai informaciÃŗn EXIF dispoÃąible", "no_explore_results_message": "Suba mÃĄis fotos para explorar a sÃēa colecciÃŗn.", "no_favorites_message": "Engada favoritos para atopar rapidamente as sÃēas mellores fotos e vídeos", + "no_filters_added": "Aínda non se engadiron filtros", "no_libraries_message": "Cree unha biblioteca externa para ver as sÃēas fotos e vídeos", "no_local_assets_found": "Non se atoparon elementos locais con esta suma de comprobaciÃŗn", + "no_location_set": "Non se estableceu a localizaciÃŗn", "no_locked_photos_message": "As fotos e vídeos no cartafol con chave estÃĄn ocultos e non aparecerÃĄn mentres navegas ou buscas na tÃēa biblioteca.", "no_name": "Sen Nome", "no_notifications": "Sen notificaciÃŗns", @@ -1444,11 +1608,11 @@ "no_results_description": "Probe cun sinÃŗnimo ou palabra chave mÃĄis xeral", "no_shared_albums_message": "Cree un ÃĄlbum para compartir fotos e vídeos con persoas na sÃēa rede", "no_uploads_in_progress": "Non hai cargas en curso", + "none": "Nada", "not_allowed": "Non permitido", "not_available": "Non dispoÃąible", "not_in_any_album": "Non estÃĄ en ningÃēn ÃĄlbum", "not_selected": "Non seleccionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar a Etiqueta de Almacenamento a activos cargados previamente, execute o", "notes": "Notas", "nothing_here_yet": "Aínda nada por aquí", "notification_permission_dialog_content": "Para activar as notificaciÃŗns, vaia a Axustes e seleccione permitir.", @@ -1493,6 +1657,7 @@ "other_variables": "Outras variables", "owned": "Propio", "owner": "Propietario", + "page": "PÃĄxina", "partner": "CompaÃąeiro/a", "partner_can_access": "{partner} pode acceder a", "partner_can_access_assets": "Todas as sÃēas fotos e vídeos excepto os de Arquivo e Eliminados", @@ -1525,6 +1690,7 @@ "people": "Persoas", "people_edits_count": "Editadas {count, plural, one {# persoa} other {# persoas}}", "people_feature_description": "Navegar por fotos e vídeos agrupados por persoas", + "people_selected": "{count, plural, one {# persoa seleccionada} other {# persoas seleccionadas}}", "people_sidebar_description": "Mostrar unha ligazÃŗn a Persoas na barra lateral", "permanent_deletion_warning": "Aviso de eliminaciÃŗn permanente", "permanent_deletion_warning_setting_description": "Mostrar un aviso ao eliminar permanentemente activos", @@ -1549,11 +1715,14 @@ "person_age_years": "{years, plural, one {# ano} other {# anos}} de idade", "person_birthdate": "Nacido/a o {date}", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "person_recognized": "Persoa recoÃąecida", + "person_selected": "Persoa seleccionada", "photo_shared_all_users": "Parece que compartiu as sÃēas fotos con todos os usuarios ou non ten ningÃēn usuario co que compartir.", "photos": "Fotos", "photos_and_videos": "Fotos e Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", + "photos_only": "SÃŗ fotos", "pick_a_location": "Elixir unha localizaciÃŗn", "pick_custom_range": "Rango personalizado", "pick_date_range": "Seleccionar un rango de datas", @@ -1629,10 +1798,12 @@ "purchase_settings_server_activated": "A chave do produto do servidor Ê xestionada polo administrador", "query_asset_id": "Consultar o ID do activo", "queue_status": "Pondo en cola {count}/{total}", - "rating": "ClasificaciÃŗn por estrelas", + "rate_asset": "Clasificar activo", + "rating": "ValoraciÃŗn", "rating_clear": "Borrar clasificaciÃŗn", "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", "rating_description": "Mostrar a clasificaciÃŗn EXIF no panel de informaciÃŗn", + "rating_set": "ClasificaciÃŗn definida en {rating, plural, one {# star} other {# stars}}", "reaction_options": "OpciÃŗns de reacciÃŗn", "read_changelog": "Ler Rexistro de Cambios", "readonly_mode_disabled": "Modo sÃŗ lectura desactivado", @@ -1643,7 +1814,7 @@ "reassigned_assets_to_new_person": "Reasignados {count, plural, one {# activo} other {# activos}} a unha nova persoa", "reassing_hint": "Asignar activos seleccionados a unha persoa existente", "recent": "Recente", - "recent-albums": "Álbums recentes", + "recent_albums": "Álbums recentes", "recent_searches": "Buscas recentes", "recently_added": "Engadido recentemente", "recently_added_page_title": "Engadido Recentemente", @@ -1732,9 +1903,11 @@ "saved_settings": "ConfiguraciÃŗn gardada", "say_something": "Dicir algo", "scaffold_body_error_occurred": "Ocorreu un erro", + "scan": "Escanear", "scan_all_libraries": "Escanear Todas as Bibliotecas", "scan_library": "Escanear", "scan_settings": "ConfiguraciÃŗn de Escaneo", + "scanning": "Escaneando", "scanning_for_album": "Escaneando ÃĄlbum...", "search": "Buscar", "search_albums": "Buscar ÃĄlbums", @@ -1764,6 +1937,7 @@ "search_filter_media_type_title": "Seleccionar tipo de medio", "search_filter_ocr": "Buscar por OCR", "search_filter_people_title": "Seleccionar persoas", + "search_filter_star_rating": "ClasificaciÃŗn por estrelas", "search_for": "Buscar por", "search_for_existing_person": "Buscar persoa existente", "search_no_more_result": "Non hai mÃĄis resultados", @@ -1798,17 +1972,23 @@ "second": "Segundo", "see_all_people": "Ver todas as persoas", "select": "Seleccionar", + "select_album": "Seleccionar ÃĄlbume", "select_album_cover": "Seleccionar portada do ÃĄlbum", + "select_albums": "Seleccionar ÃĄlbumes", "select_all": "Seleccionar todo", "select_all_duplicates": "Seleccionar todos os duplicados", "select_all_in": "Seleccionar todo en {group}", "select_avatar_color": "Seleccionar cor do avatar", + "select_count": "{count, plural, one {Seleccionar #} other {Seleccionar #}}", + "select_cutoff_date": "Seleccionar data límite", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto destacada", "select_from_computer": "Seleccionar do ordenador", "select_keep_all": "Seleccionar conservar todo", "select_library_owner": "Seleccionar propietario da biblioteca", "select_new_face": "Seleccionar nova cara", + "select_people": "Seleccionar xente", + "select_person": "Seleccionar persoa", "select_person_to_tag": "Seleccionar unha persoa para etiquetar", "select_photos": "Seleccionar fotos", "select_trash_all": "Seleccionar mover todo ao lixo", @@ -1824,6 +2004,8 @@ "server_offline": "Servidor FÃŗra de LiÃąa", "server_online": "Servidor En LiÃąa", "server_privacy": "Privacidade do Servidor", + "server_restarting_description": "Esta pÃĄxina actualizarase en breve.", + "server_restarting_title": "O servidor estase reiniciando", "server_stats": "Estatísticas do Servidor", "server_update_available": "Hai unha actualizaciÃŗn do servidor dispoÃąible", "server_version": "VersiÃŗn do Servidor", @@ -1942,11 +2124,13 @@ "show_password": "Mostrar contrasinal", "show_person_options": "Mostrar opciÃŗns da persoa", "show_progress_bar": "Mostrar Barra de Progreso", + "show_schema": "Mostrar esquema", "show_search_options": "Mostrar opciÃŗns de busca", "show_shared_links": "Mostrar ligazÃŗns compartidas", "show_slideshow_transition": "Mostrar transiciÃŗn da presentaciÃŗn", "show_supporter_badge": "Insignia de seguidor/a", "show_supporter_badge_description": "Mostrar unha insignia de seguidor/a", + "show_text_recognition": "Mostrar recoÃąecemento de texto", "show_text_search_menu": "Mostrar o menÃē de busca de texto", "shuffle": "Aleatorio", "sidebar": "Barra lateral", @@ -1958,6 +2142,8 @@ "skip_to_folders": "Saltar a cartafoles", "skip_to_tags": "Saltar a etiquetas", "slideshow": "PresentaciÃŗn", + "slideshow_repeat": "Repetir presentaciÃŗn de diapositivas", + "slideshow_repeat_description": "Volver ao principio ao rematar a presentaciÃŗn de diapositivas", "slideshow_settings": "ConfiguraciÃŗn da presentaciÃŗn", "sort_albums_by": "Ordenar ÃĄlbums por...", "sort_created": "Data de creaciÃŗn", @@ -2017,6 +2203,7 @@ "tags": "Etiquetas", "tap_to_run_job": "Tocar para executar tarefa", "template": "Modelo", + "text_recognition": "RecoÃąecemento de texto", "theme": "Tema", "theme_selection": "SelecciÃŗn de tema", "theme_selection_description": "Establecer automaticamente o tema a claro ou escuro baseÃĄndose na preferencia do sistema do seu navegador", @@ -2033,6 +2220,7 @@ "theme_setting_theme_subtitle": "Elixir a configuraciÃŗn do tema da aplicaciÃŗn", "theme_setting_three_stage_loading_subtitle": "A carga en tres etapas pode aumentar o rendemento da carga pero causa unha carga de rede significativamente maior", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", + "then": "EntÃŗn", "they_will_be_merged_together": "Fusionaranse xuntos", "third_party_resources": "Recursos de Terceiros", "time": "Hora", @@ -2049,6 +2237,7 @@ "to_select": "Para seleccionar", "to_trash": "Lixo", "toggle_settings": "Alternar configuraciÃŗn", + "toggle_theme_description": "Cambiar tema", "total": "Total", "total_usage": "Uso total", "trash": "Lixo", @@ -2066,6 +2255,13 @@ "trash_page_select_assets_btn": "Seleccionar activos", "trash_page_title": "Lixo ({count})", "trashed_items_will_be_permanently_deleted_after": "Os elementos no lixo eliminaranse permanentemente despois de {days, plural, one {# día} other {# días}}.", + "trigger": "Disparador", + "trigger_asset_uploaded": "Activo subido", + "trigger_asset_uploaded_description": "Actívase cando se carga un activo novo", + "trigger_description": "Un evento que inicia o fluxo de traballo", + "trigger_person_recognized": "Persoa recoÃąecida", + "trigger_person_recognized_description": "Actívase cando se detecta a unha persoa", + "trigger_type": "TIpo de disparador", "troubleshoot": "Solucionar problemas", "type": "Tipo", "unable_to_change_pin_code": "Non Ê posible cambiar o cÃŗdigo PIN", @@ -2080,6 +2276,7 @@ "unhide_person": "Mostrar persoa", "unknown": "DescoÃąecido", "unknown_country": "País DescoÃąecido", + "unknown_date": "Data descoÃąecida", "unknown_year": "Ano DescoÃąecido", "unlimited": "Ilimitado", "unlink_motion_video": "Desvincular vídeo en movemento", @@ -2096,17 +2293,19 @@ "unstack": "Desapilar", "unstack_action_prompt": "{count} desapilados", "unstacked_assets_count": "Desapilados {count, plural, one {# activo} other {# activos}}", + "unsupported_field_type": "Tipo de campo non soportado", "untagged": "Sen etiquetar", + "untitled_workflow": "Fluxo de traballo sen título", "up_next": "A continuaciÃŗn", "update_location_action_prompt": "Actualizar a localizaciÃŗn de {count} elementos seleccionados con:", "updated_at": "Actualizado", "updated_password": "Contrasinal actualizado", "upload": "Subir", - "upload_action_prompt": "{count} en cola de espera para cargar", "upload_concurrency": "Concorrencia de subida", "upload_details": "Detalles da Carga", "upload_dialog_info": "Quere facer copia de seguridade do(s) Activo(s) seleccionado(s) no servidor?", "upload_dialog_title": "Subir Activo", + "upload_error_with_count": "Erro de subida para {count, plural, one {# asset} other {# assets}}", "upload_errors": "Subida completada con {count, plural, one {# erro} other {# erros}}. Actualice a pÃĄxina para ver os novos activos subidos.", "upload_finished": "Carga finalizada", "upload_progress": "Restantes {remaining, number} - Procesados {processed, number}/{total, number}", @@ -2121,7 +2320,7 @@ "url": "URL", "usage": "Uso", "use_biometric": "Usar biometría", - "use_current_connection": "usar conexiÃŗn actual", + "use_current_connection": "Empregar conexiÃŗn actual", "use_custom_date_range": "Usar rango de datas personalizado no seu lugar", "user": "Usuario", "user_has_been_deleted": "Este usuario foi eliminado.", @@ -2142,6 +2341,7 @@ "utilities": "Utilidades", "validate": "Validar", "validate_endpoint_error": "Por favor, introduza unha URL vÃĄlida", + "validation_error": "Erro de validaciÃŗn", "variables": "Variables", "version": "VersiÃŗn", "version_announcement_closing": "O seu amigo, Alex", @@ -2153,10 +2353,12 @@ "video_hover_setting_description": "Reproducir miniatura do vídeo cando o rato estÃĄ sobre o elemento. Mesmo cando estÃĄ desactivado, a reproduciÃŗn pode iniciarse pasando o rato sobre a icona de reproduciÃŗn.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "videos_only": "SÃŗ vídeos", "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver Todo", "view_all_users": "Ver todos os usuarios", + "view_asset_owners": "Ver os propietarios", "view_details": "Ver detalles", "view_in_timeline": "Ver na liÃąa de tempo", "view_link": "Ver ligazÃŗn", @@ -2172,19 +2374,36 @@ "viewer_stack_use_as_main_asset": "Usar como Activo Principal", "viewer_unstack": "Desapilar", "visibility_changed": "Visibilidade cambiada para {count, plural, one {# persoa} other {# persoas}}", + "visual": "Visual", + "visual_builder": "Construtor visual", "waiting": "Agardando", + "waiting_count": "Esperando: {count}", "warning": "Aviso", "week": "Semana", "welcome": "Benvido/a", "welcome_to_immich": "Benvido/a a Immich", + "width": "Ancho", "wifi_name": "Nome da wifi", - "workflow": "Fluxo de traballo", + "workflow_delete_prompt": "EstÃĄs seguro que queres eliminar este fluxo de traballo?", + "workflow_deleted": "Fluxo de traballo eliminado", + "workflow_description": "DescriciÃŗn do fluxo de traballo", + "workflow_info": "InformaciÃŗn do fluxo de traballo", + "workflow_json": "JSON do fluxo de traballo", + "workflow_json_help": "Edita a configuraciÃŗn do fluxo de traballo en formato JSON. Os cambios sincronizaranse co creador visual.", + "workflow_name": "Nome do fluxo de traballo", + "workflow_navigation_prompt": "EstÃĄs seguro que desexar saír sen gardar os cambios?", + "workflow_summary": "Resumo do fluxo de traballo", + "workflow_update_success": "Fluxo de traballo actualizado con Êxito", + "workflow_updated": "Fluxo de traballo actualizado", + "workflows": "Fluxos de traballo", + "workflows_help_text": "Os fluxos de traballo automatizan acciÃŗns nos teus recursos en funciÃŗn de disparadores e filtros", "wrong_pin_code": "CÃŗdigo PIN incorrecto", "year": "Ano", "years_ago": "Hai {years, plural, one {# ano} other {# anos}}", "yes": "Si", "you_dont_have_any_shared_links": "Non ten ningunha ligazÃŗn compartida", "your_wifi_name": "O nome da sÃēa wifi", + "zero_to_clear_rating": "preme 0 para borrar a cualificaciÃŗn do activo", "zoom_image": "Ampliar Imaxe", "zoom_to_bounds": "Axustar ao perímetro" } diff --git a/i18n/gsw.json b/i18n/gsw.json index b9a0ebcab7..17f8171c60 100644 --- a/i18n/gsw.json +++ b/i18n/gsw.json @@ -718,8 +718,13 @@ "check_corrupt_asset_backup_button": "ÜberprÃŧefig durrefÃŧehrä", "check_corrupt_asset_backup_description": "FÃŧhr die PrÃŧefig nume mit aktiviertem WLAN dur, nachdem alli Dateie gsiichert worde sind. Dä Vorgang cha e paar Minute duurä.", "check_logs": "Logs prÃŧafä", + "checksum": "PrÃŧefsumme", "choose_matching_people_to_merge": "Wähl passendi Persone zum ZämmezfÃŧehre", "city": "Stadt", + "cleanup_confirm_description": "Immich hed {count} Dateie (vorem {date} erstellt) sicher ufem Server gfunde. SÃļlled die lokale Kopie vo dem Grät glÃļscht werde?", + "cleanup_confirm_prompt_title": "Vo dem Grät entferne?", + "cleanup_deleted_assets": "{count} Dateie i de lokali Papierchorb verschobe", + "cleanup_icloud_shared_albums_excluded": "Teilti iCloud Albe sind vom Scan usgschlosse", "clear": "Lääre", "clear_all": "Alles lääre", "clear_all_recent_searches": "Alli letschte Suechvorgäng lÃļsche", @@ -785,6 +790,7 @@ "create_album": "Album erstellä", "create_album_page_untitled": "Unbenennt", "create_api_key": "API Key erstellä", + "create_first_workflow": "Erste Workflow erstelle", "create_library": "Bibliothek erstellä", "create_link": "Link erstellä", "create_link_to_share": "Link zum Teile erstellä", @@ -799,10 +805,14 @@ "create_tag": "Tag erstellä", "create_tag_description": "Erstell en neue Tag. FÃŧr verschachtleti Tags gib dr ganze Pfad inklusiv Schrägstrich aa.", "create_user": "Nutzer erstellä", + "create_workflow": "Workflow erstelle", "created": "Erstellt", "created_at": "Erstellt", "creating_linked_albums": "Erstelle verknÃŧpfti Albene...", "crop": "Zueschniidä", + "crop_aspect_ratio_fixed": "Fixiert", + "crop_aspect_ratio_free": "Frei", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Sachä", "current_device": "Aktuells Grät", "current_pin_code": "Aktuelle PIN Code", @@ -865,6 +875,7 @@ "deselect_all": "Alli abwähle", "details": "Details", "direction": "Richtig", + "disable": "Deaktiviere", "disabled": "Deaktiviert", "disallow_edits": "Bearbeitig verbÃŧÃŧtä", "discord": "Discord", @@ -890,6 +901,7 @@ "download_include_embedded_motion_videos": "Iigbetteti Videos", "download_include_embedded_motion_videos_description": "Videos, wo i Bewegigsfotos iigbettet sind, als separate Datei iifÃŧege", "download_notfound": "Download nÃļd gfundä", + "download_original": "Original abelade", "download_paused": "Download pausiert", "download_settings": "Download", "download_settings_description": "Iihstelligä fÃŧrs Abeladä vo Dateie verwalte", @@ -927,11 +939,10 @@ "edit_tag": "Tag bearbeite", "edit_title": "Titel bearbeite", "edit_user": "Nutzer bearbeite", + "edit_workflow": "Workflow bearbeite", "editor": "Bearbeiter", "editor_close_without_save_prompt": "D’Änderige werden nÃļd gspeichert", "editor_close_without_save_title": "Editor schlÃŧssä?", - "editor_crop_tool_h2_aspect_ratios": "Siiteverhältniss", - "editor_crop_tool_h2_rotation": "Drehig", "email": "E-Mail", "email_notifications": "E-Mail Benochrichtigunge", "empty_folder": "Dä Ordner isch leer", @@ -1118,7 +1129,6 @@ "features": "Funktione", "features_in_development": "Feature isch in Entwicklig", "features_setting_description": "Funkione i de App verwalte", - "file_name": "Dateiname", "file_name_or_extension": "Dateiname oder -erwiiterig", "file_size": "DateigrÃļssi", "filename": "Dateiname", @@ -1481,7 +1491,6 @@ "not_available": "N/A", "not_in_any_album": "I keinem Album", "not_selected": "NÃļd usgwählt", - "note_apply_storage_label_to_previously_uploaded assets": "Hiwiis: Zum e Spycherpfad-Bezeichnig aawehde, start de", "notes": "Notize", "nothing_here_yet": "No nÃŧt do", "notification_permission_dialog_content": "Zum Benachrichtige aktiviere, navigier zu Iistellige und drÃŧck \"Erlaube\".", diff --git a/i18n/he.json b/i18n/he.json index 29acf7a029..e4d534693b 100644 --- a/i18n/he.json +++ b/i18n/he.json @@ -5,6 +5,7 @@ "acknowledge": "הבנ×Ēי", "action": "פ×ĸולה", "action_common_update": "×ĸדכון", + "action_description": "ץט פ×ĸולו×Ē ×œ×‘×™×Ļו×ĸ ×ĸל נכסים מסוננים", "actions": "פ×ĸולו×Ē", "active": "פ×ĸיל", "active_count": "פ×ĸיל: {count}", @@ -15,9 +16,13 @@ "add_a_location": "הוספ×Ē ×ž×™×§×•×", "add_a_name": "הוספ×Ē ×Š×", "add_a_title": "הוספ×Ē ×›×•×Ēר×Ē", + "add_action": "×”×•×Ą×Ŗ פ×ĸולה", + "add_action_description": "לח×Ĩ כדי ×œ×”×•×Ą×™×Ŗ פ×ĸולה לבי×Ļו×ĸ", "add_birthday": "הוספ×Ē ×™×•× הולד×Ē", "add_endpoint": "×”×•×Ą×Ŗ כ×Ēוב×Ē URL", "add_exclusion_pattern": "הוספ×Ē ×“×¤×•×Ą החרגה", + "add_filter": "×”×•×Ą×Ŗ סינון", + "add_filter_description": "לח×Ĩ כדי ×œ×”×•×Ą×™×Ŗ ×Ēנאי לסינון", "add_location": "הוספ×Ē ×ž×™×§×•×", "add_more_users": "הוספ×Ē ×ĸוד מ׊×Ēמשים", "add_partner": "הוספ×Ē ×Š×•×Ē×Ŗ", @@ -36,6 +41,7 @@ "add_to_shared_album": "הוספה לאלבום משו×Ē×Ŗ", "add_upload_to_stack": "×”×•×Ą×Ŗ א×Ē ×”×”×ĸלאה ל×ĸרימה", "add_url": "הוספ×Ē ×§×™×Š×•×¨", + "add_workflow_step": "×”×•×Ą×Ŗ שלב בסדר פ×ĸולו×Ē", "added_to_archive": "× ×•×Ą×Ŗ לארכיון", "added_to_favorites": "× ×•×Ą×Ŗ למו×ĸדפים", "added_to_favorites_count": "{count, number} נוספו למו×ĸדפים", @@ -68,6 +74,7 @@ "confirm_reprocess_all_faces": "האם באמ×Ē ×‘×¨×Ļונך ל×ĸבד מחדש א×Ē ×›×œ הפנים? זה גם ינקה אנשים ב×ĸלי ׊ם.", "confirm_user_password_reset": "האם באמ×Ē ×‘×¨×Ļונך לאפס א×Ē ×”×Ą×™×Ą×ž×” של המש×Ēמ׊ {user}?", "confirm_user_pin_code_reset": "האם א×Ēה בטוח שבר×Ļונך לאפס א×Ē ×§×•×“ ה PIN של {user}?", + "copy_config_to_clipboard_description": "ה×ĸ×Ē×§ א×Ē ×Ē×Ļור×Ē ×”×ž×ĸרכ×Ē ×”× ×•×›×—×™×Ē ×›××•×‘×™×™×§×˜ JSON ללוח", "create_job": "×Ļור ×ĸבודה", "cron_expression": "ביטוי cron", "cron_expression_description": "הגדר א×Ē ×ž×¨×•×•×— הסריקה באמ×Ļ×ĸו×Ē ×Ēבני×Ē ×”- cron. למיד×ĸ × ×•×Ą×Ŗ נא לפנו×Ē ×œ×ž×Š×œ אל Crontab Guru", @@ -76,6 +83,7 @@ "duplicate_detection_job_description": "הפ×ĸל למיד×Ē ×ž×›×•× ×” ×ĸל ×Ēמונו×Ē ×›×“×™ לזהו×Ē ×Ēמונו×Ē ×“×•×ž×•×Ē. נ׊×ĸן ×ĸל חיפוש חכם", "exclusion_pattern_description": "דפוסי החרגה מאפשרים לך לה×Ē×ĸלם מקב×Ļים ומ×Ēיקיו×Ē ×‘×ĸ×Ē ×Ą×¨×™×§×Ē ×”×Ą×¤×¨×™×™×” שלך. זה שימושי אם יש לך ×Ēיקיו×Ē ×”×ž×›×™×œ×•×Ē ×§×‘×Ļים שאינך רו×Ļה לייבא, כגון קוב×Ļי RAW.", "export_config_as_json_description": "הורד×Ē ×”×’×“×¨×•×Ē ×”×ž×ĸרכ×Ē ×”× ×•×›×—×™×•×Ē ×›×§×•×‘×Ĩ JSON", + "external_libraries_page_description": "דת ספרייה חי×Ļוני של מנהל מ×ĸרכ×Ē", "face_detection": "אי×Ēור פנים", "face_detection_description": "א×Ēר א×Ē ×”×¤× ×™× ב×Ēמונו×Ē ×‘××ž×Ļ×ĸו×Ē ×œ×ž×™×“×Ē ×ž×›×•× ×”. ×ĸבור סרטונים, רק ה×Ēמונה הממוז×ĸר×Ē × ×œ×§×—×Ē ×‘×—×Š×‘×•×Ÿ. \"ר×ĸנון\" מ×ĸבד (מחדש) א×Ē ×›×œ ה×Ēמונו×Ē. \"איפוס\" מנקה ×‘× ×•×Ą×Ŗ א×Ē ×›×œ × ×Ēוני הפנים הנוכחיים. \"חסרים\" ×ž×•×Ą×™×Ŗ ל×Ēור ×Ēמונו×Ē ×Š×œ× ×ĸובדו ×ĸדיין. לאחר שאי×Ēור הפנים הושלם, פנים שאו×Ēרו י×ĸמדו ב×Ēור לזיהוי פנים המשייך או×Ēן לאנשים קיימים או חדשים.", "facial_recognition_job_description": "קב×Ĩ פנים שאו×Ēרו ל×Ēוך אנשים. שלב זה מור×Ĩ לאחר השלמ×Ē ××™×Ēור פנים. \"איפוס\" מקב×Ĩ (מחדש) א×Ē ×›×œ הפר×Ļופים. \"חסרים\" ×ž×•×Ą×™×Ŗ ל×Ēור פנים שלא הוק×Ļה להם אדם.", @@ -103,17 +111,21 @@ "image_thumbnail_description": "×Ēמונה ממוז×ĸר×Ē ×§×˜× ×” ×ĸם מטא-× ×Ēונים שהוסרו, מ׊מ׊×Ē ×‘×ĸ×Ē ×Ļפייה בקבו×Ļו×Ē ×Š×œ ×Ēמונו×Ē ×›×ž×• ×Ļיר הזמן הראשי", "image_thumbnail_quality_description": "איכו×Ē ×Ēמונה ממוז×ĸר×Ē ×ž-1 ×ĸד 100. איכו×Ē ×’×‘×•×”×” יו×Ēר היא טובה יו×Ēר, אבל מיי×Ļר×Ē ×§×‘×Ļים גדולים יו×Ēר ויכולה להפחי×Ē ××Ē ×Ēגוב×Ēיו×Ē ×”×™×™×Š×•×.", "image_thumbnail_title": "הגדרו×Ē ×Ēמונה ממוז×ĸר×Ē", + "import_config_from_json_description": "ייבוא ×Ē×Ļור×Ē ×ž×ĸרכ×Ē ×‘××ž×Ļ×ĸו×Ē ×§×•×‘×Ĩ ×Ē×Ļורה JSON", "job_concurrency": "בו-זמניו×Ē ×Š×œ {job}", "job_created": "×ĸבודה נו×Ļרה", - "job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.", + "job_not_concurrency_safe": "×ĸבודה זו אינה בטוחה להר×Ļה במקביל.", "job_settings": "הגדרו×Ē ×ž×Š×™×ž×”", - "job_settings_description": "ניהול בו-זמניו×Ē ×Š×œ משימה", + "job_settings_description": "נהל א×Ē ×ž×§×‘×™×œ×™×•×Ē ×”×ĸבודו×Ē", "jobs_delayed": "{jobCount, plural, other {# ×ĸוכבו}}", "jobs_failed": "{jobCount, plural, other {# נכשלו}}", "jobs_over_time": "משימו×Ē ×œ××•×¨×š זמן", "library_created": "נו×Ļרה ספרייה: {library}", "library_deleted": "ספרייה נמחקה", "library_details": "פרטי ספריה", + "library_folder_description": "×Ļיין ×Ēיקייה לייבוא. ×Ēיקייה זו, כולל ×Ēיקיו×Ē ×ž×Š× ×”, ×Ēיסרק לאי×Ēור ×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™×.", + "library_remove_exclusion_pattern_prompt": "האם א×Ēה בטוח שבר×Ļונך להסיר א×Ē ×“×¤×•×Ą ההחרגה הזה?", + "library_remove_folder_prompt": "האם א×Ēה בטוח שבר×Ļונך להסיר א×Ē ×Ēיקיי×Ē ×”×™×™×‘×•× הזו?", "library_scanning": "סריקה ×Ēקופ×Ēי×Ē", "library_scanning_description": "הגדר סריק×Ē ×Ą×¤×¨×™×™×” ×Ēקופ×Ēי×Ē", "library_scanning_enable_description": "אפ׊ר סריק×Ē ×Ą×¤×¨×™×™×” ×Ēקופ×Ēי×Ē", @@ -176,10 +188,11 @@ "machine_learning_smart_search_enabled_description": "אם מושב×Ē, ×Ēמונו×Ē ×œ× יקודדו לחיפוש חכם.", "machine_learning_url_description": "כ×Ēוב×Ē ×”-URL של ׊ר×Ē ×œ×ž×™×“×Ē ×”×ž×›×•× ×”. אם ני×Ē× ×Ē ×™×•×Ēר מכ×Ēוב×Ē URL אח×Ē, כל ׊ר×Ē ×™× ×•×Ą×” ניסיון אחד בכל פ×ĸם ×ĸד שאחד מהם יגיב בה×Ļלחה, לפי הסדר מהראשון ×ĸד האחרון. ׊ר×Ēים שלא מגיבים יוזנחו זמני×Ē ×ĸד שיחזרו להיו×Ē ×ž×§×•×•× ×™×.", "maintenance_settings": "×Ēחזוקה", - "maintenance_settings_description": "ה×ĸבר×Ē Immich למ×Ļב ×Ēחזוקה.", + "maintenance_settings_description": "ה×ĸבר א×Ē Immich למ×Ļב ×Ēחזוקה.", "maintenance_start": "ה×Ēחל×Ē ×ž×Ļב ×Ēחזוקה", "maintenance_start_error": "ה×Ēחל×Ē ×ž×Ļב ×Ēחזוקה נכשלה.", - "manage_concurrency": "ניהול בו-זמניו×Ē", + "manage_concurrency": "ניהול מקביליו×Ē", + "manage_concurrency_description": "×ĸבור ×œ×“×Ŗ ה×ĸבודו×Ē ×›×“×™ לנהל הר×Ļ×Ē ×ĸבודו×Ē ×‘×ž×§×‘×™×œ", "manage_log_settings": "ניהול הגדרו×Ē ×¨×™×Š×•× ביומן", "map_dark_style": "×ĸי×Ļוב כהה", "map_enable_description": "אפ׊ר ×Ēכונו×Ē ×ž×¤×”", @@ -460,6 +473,7 @@ "album_remove_user": "להסיר מ׊×Ēמ׊?", "album_remove_user_confirmation": "האם באמ×Ē ×‘×¨×Ļונך להסיר א×Ē {user}?", "album_search_not_found": "לא נמ×Ļאו אלבומים ה×Ēואמים לחיפוש שלך", + "album_selected": "אלבום נבחר", "album_share_no_users": "נראה ששי×Ēפ×Ē ××Ē ×”××œ×‘×•× הזה ×ĸם כל המש×Ēמשים או שאין לך את מ׊×Ēמ׊ לש×Ē×Ŗ אי×Ēו.", "album_summary": "×Ē×§×Ļיר אלבום", "album_updated": "אלבום ×ĸודכן", @@ -517,10 +531,12 @@ "archived_count": "{count, plural, other {# הו×ĸברו לארכיון}}", "are_these_the_same_person": "האם אלה או×Ēו האדם?", "are_you_sure_to_do_this": "האם באמ×Ē ×‘×¨×Ļונך ל×ĸשו×Ē ××Ē ×–×”?", + "array_field_not_fully_supported": "שדו×Ē ×”×ž×ĸרך דורשים ×ĸריכה ידני×Ē ×Š×œ ה-JSON", "asset_action_delete_err_read_only": "לא ני×Ēן למחוק ×Ēמונו×Ē ×œ×§×¨×™××” בלבד, מדלג", "asset_action_share_err_offline": "לא ני×Ēן להשיג ×Ēמונו×Ē ×œ× מקוונו×Ē, מדלג", "asset_added_to_album": "× ×•×Ą×Ŗ לאלבום", "asset_adding_to_album": "×ž×•×Ą×™×Ŗ לאלבוםâ€Ļ", + "asset_created": "×Ēמונה נו×Ļרה", "asset_description_updated": "×Ēיאור ה×Ēמונה ×ĸודכן", "asset_filename_is_offline": "ה×Ēמונה {filename} אינה מקוונ×Ē", "asset_has_unassigned_faces": "ל×Ēמונה יש פנים שלא הוק×Ļו", @@ -645,6 +661,7 @@ "backup_options_page_title": "אפשרויו×Ē ×’×™×‘×•×™", "backup_setting_subtitle": "ניהול הגדרו×Ē ×”×ĸלא×Ē ×¨×§×ĸ וחזי×Ē", "backup_settings_subtitle": "נהל הגדרו×Ē ×”×ĸלאה", + "backup_upload_details_page_more_details": "הקש לפרטים נוספים", "backward": "אחורה", "biometric_auth_enabled": "אימו×Ē ×‘×™×•×ž×˜×¨×™ הופ×ĸל", "biometric_locked_out": "גישה לאימו×Ē ×”×‘×™×•×ž×˜×¨×™ נחסמה", @@ -924,8 +941,6 @@ "editor": "×ĸורך", "editor_close_without_save_prompt": "השינויים לא יישמרו", "editor_close_without_save_title": "לסגור א×Ē ×”×ĸורך?", - "editor_crop_tool_h2_aspect_ratios": "יחסי רוחב גובה", - "editor_crop_tool_h2_rotation": "סיבוב", "email": "דוא\"ל", "email_notifications": "ה×Ēראו×Ē ×‘××™×ž×™×™×œ", "empty_folder": "×Ēיקיה זו ריקה", @@ -983,7 +998,7 @@ "failed_to_unstack_assets": "ביטול ×ĸרימ×Ē ×Ēמונו×Ē × ×›×Š×œ×”", "failed_to_update_notification_status": "שגיאה ב×ĸדכון הה×Ēראה", "incorrect_email_or_password": "דוא\"ל או סיסמה שגויים", - "library_folder_already_exists": "מסלול הייבוא כבר מוגדר.", + "library_folder_already_exists": "× ×Ēיב הייבוא כבר מוגדר.", "paths_validation_failed": "{paths, plural, one {× ×Ēיב # נכשל} other {# × ×Ēיבים נכשלו}} אימו×Ē", "profile_picture_transparent_pixels": "×Ēמונו×Ē ×¤×¨×•×¤×™×œ אינן יכולו×Ē ×œ×›×œ×•×œ פיקסלים שקופים. נא להגדיל ו/או להזיז א×Ē ×”×Ēמונה.", "quota_higher_than_disk_size": "הגדר×Ē ×ž×›×Ą×” גבוהה יו×Ēר מגודל הדיסק", @@ -1068,6 +1083,7 @@ "unable_to_update_user": "לא ני×Ēן ל×ĸדכן מ׊×Ēמ׊", "unable_to_upload_file": "לא ני×Ēן לה×ĸלו×Ē ×§×•×‘×Ĩ" }, + "exclusion_pattern": "דפוס אי הכללה", "exif": "Exif", "exif_bottom_sheet_description": "×”×•×Ą×Ŗ ×Ēיאור...", "exif_bottom_sheet_description_error": "שגיאה ב×ĸדכון ה×Ēיאור", @@ -1111,7 +1127,6 @@ "features": "×Ēכונו×Ē", "features_in_development": "×Ēכונו×Ē ×‘×¤×™×Ēוח", "features_setting_description": "ניהול ×Ēכונו×Ē ×”×™×™×Š×•×", - "file_name": "׊ם הקוב×Ĩ", "file_name_or_extension": "׊ם קוב×Ĩ או סיומ×Ē", "file_size": "גודל קוב×Ĩ", "filename": "׊ם קוב×Ĩ", @@ -1128,7 +1143,7 @@ "folders_feature_description": "×ĸיון ב×Ē×Ļוג×Ē ×”×Ēיקייה ×ĸבור ה×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× שבמ×ĸרכ×Ē ×”×§×‘×Ļים", "forgot_pin_code_question": "שחכ×Ē ××Ē ×”-PIN שלך?", "forward": "קדימה", - "full_path": "מסלול מלא: {path}", + "full_path": "× ×Ēיב מלא: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "×Ēכונה זא×Ē ×˜×•×ĸ× ×Ē ×ž×Š××‘×™× חי×Ļוניים מגוגל בכדי לפ×ĸול.", "general": "כללי", @@ -1213,6 +1228,7 @@ "in_albums": "ב{count, plural, one {אלבום #} other {# אלבומים}}", "in_archive": "בארכיון", "in_year": "ב×ĸוד {year}", + "in_year_selector": "ב", "include_archived": "כלול ארכיון", "include_shared_albums": "כלול אלבומים משו×Ēפים", "include_shared_partner_assets": "כלול ×Ēמונו×Ē ×Š×Š×•×Ēפו ×ĸ\"י השו×Ē×Ŗ", @@ -1249,6 +1265,7 @@ "language_setting_description": "בחר א×Ē ×”×Š×¤×” המו×ĸדפ×Ē ×ĸליך", "large_files": "קב×Ļים גדולים", "last": "אחרון", + "last_months": "{count, plural, one {החודש האחרון} other {# החודשים האחרונים}}", "last_seen": "נראה לאחרונה", "latest_version": "גרסה ×ĸדכני×Ē ×‘×™×•×Ēר", "latitude": "קו רוחב", @@ -1280,6 +1297,7 @@ "local": "מקומי", "local_asset_cast_failed": "לא ני×Ēן לשדר ×Ēמונה שלא הו×ĸל×Ēה לשר×Ē", "local_assets": "×Ēמונו×Ē ×ž×§×•×ž×™×•×Ē", + "local_id": "ID מקומי", "local_media_summary": "סיכום של מדיה מקומי×Ē", "local_network": "ר׊×Ē ×ž×§×•×ž×™×Ē", "local_network_sheet_info": "היישום י×Ēחבר לשר×Ē ×“×¨×š הכ×Ēוב×Ē ×”×–××Ē ×›××Š×¨ מ׊×Ēמשים ברש×Ē ×”××™× ×˜×¨× ×˜ האלחוטי ׊מ×Ļוינ×Ē", @@ -1331,10 +1349,17 @@ "loop_videos_description": "אפ׊ר הפ×ĸלה חוזר×Ē ××•×˜×•×ž×˜×™×Ē ×Š×œ סרטון במ×Ļיג הפרטים.", "main_branch_warning": "הגרסה המו×Ē×§× ×Ē ×”×™× גרס×Ē ×¤×™×Ēוח; אנחנו ממלי×Ļים בחום להש×Ēמ׊ בגרסה י×Ļיבה!", "main_menu": "×Ēפריט ראשי", + "maintenance_description": "Immich הו×ĸבר למ×Ļב ×Ēחזוקה.", + "maintenance_end": "סיום מ×Ļב ×Ēחזוקה", + "maintenance_end_error": "כשל בסיום מ×Ļב ×Ēחזוקה.", + "maintenance_logged_in_as": "מחובר כרג×ĸ ב×Ēור {user}", + "maintenance_title": "לא זמין באופן זמני", "make": "×Ēו×Ļר×Ē", "manage_geolocation": "נהל מיקום", + "manage_media_access_rationale": "הרשאה זא×Ē × ×“×¨×Š×Ē ×œ×˜×™×¤×•×œ ×Ēקין בה×ĸבר×Ē × ×›×Ą×™× לאשפה ושחזורם ממנה.", "manage_media_access_settings": "פ×Ēח הגדרו×Ē", "manage_media_access_subtitle": "אפ׊ר לאפליק×Ļיי×Ē Immich לנהל ולהזיז קב×Ļי מדיה.", + "manage_media_access_title": "גישה לניהול מדיה", "manage_shared_links": "ניהול קישורים משו×Ēפים", "manage_sharing_with_partners": "ניהול שי×Ēות ×ĸם שו×Ēפים", "manage_the_app_settings": "ניהול הגדרו×Ē ×”××¤×œ×™×§×Ļיה", @@ -1398,6 +1423,7 @@ "more": "×ĸוד", "move": "ה×ĸבר", "move_off_locked_folder": "הו×Ļאה מה×Ēיקייה הנ×ĸולה", + "move_to": "ה×ĸבר ל", "move_to_lock_folder_action_prompt": "{count} נוספו ל×Ēיקייה הנ×ĸולה", "move_to_locked_folder": "ה×ĸבר ל×Ēיקיה הנ×ĸולה", "move_to_locked_folder_confirmation": "ה×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× האלו יוסרו מכל האלבומים, ויהיו מו×Ļגים רק ב×Ēיקיה הנ×ĸולה", @@ -1443,12 +1469,14 @@ "no_cast_devices_found": "לא נמ×Ļאו מכשירי שידור", "no_checksum_local": "אין Checksum זמין - לא ני×Ēן לאחזר ×Ēמונו×Ē ×ž×§×•×ž×™×•×Ē", "no_checksum_remote": "אין Checksum זמין - לא ני×Ēן לאחזר ×Ēמונו×Ē ×ž×”×Š×¨×Ē", + "no_devices": "אין מכשירים מורשים", "no_duplicates_found": "לא נמ×Ļאו כפילויו×Ē.", "no_exif_info_available": "אין מיד×ĸ זמין ×ĸל מטא-× ×Ēונים (exif)", "no_explore_results_message": "ה×ĸלה ×Ēמונו×Ē × ×•×Ą×¤×•×Ē ×›×“×™ לחקור א×Ē ×”××•×Ą×Ŗ שלך.", "no_favorites_message": "×”×•×Ą×Ŗ מו×ĸדפים כדי למ×Ļוא במהירו×Ē ××Ē ×”×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× הכי טובים שלך", "no_libraries_message": "×Ļור ספרייה חי×Ļוני×Ē ×›×“×™ לראו×Ē ××Ē ×”×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× שלך", "no_local_assets_found": "לא נמ×Ļאו ×Ēמונו×Ē ×ĸם Checksum זהה", + "no_location_set": "לא הוגדר מיקום", "no_locked_photos_message": "×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× ב×Ēיקייה הנ×ĸולה מוס×Ēרים ולא יופי×ĸו בזמן הגלישה או החיפוש בספרייה שלך.", "no_name": "אין ׊ם", "no_notifications": "אין ה×Ēראו×Ē", @@ -1459,10 +1487,10 @@ "no_results_description": "נסה להש×Ēמ׊ במילה נרדפ×Ē ××• במיל×Ē ×ž×¤×Ēח יו×Ēר כללי×Ē", "no_shared_albums_message": "×Ļור אלבום כדי לש×Ē×Ŗ ×Ēמונו×Ē ×•×Ą×¨×˜×•× ×™× ×ĸם אנשים ברש×Ē ×Š×œ×š", "no_uploads_in_progress": "אין ה×ĸלאו×Ē ×‘×Ēהליך", + "not_allowed": "לא מורשה", "not_available": "לא רלוונטי", "not_in_any_album": "לא בשום אלבום", "not_selected": "לא נבחרו", - "note_apply_storage_label_to_previously_uploaded assets": "ה×ĸרה: כדי להחיל א×Ē ×Ēווי×Ē ×”××—×Ą×•×Ÿ ×ĸל ×Ēמונו×Ē ×Š×”×•×ĸלו ב×ĸבר, הפ×ĸל א×Ē", "notes": "ה×ĸרו×Ē", "nothing_here_yet": "אין כאן כלום ×ĸדיין", "notification_permission_dialog_content": "כדי לאפשר ה×Ēראו×Ē, לך להגדרו×Ē ×”×ž×›×Š×™×¨ ובחר אפ׊ר.", @@ -1507,6 +1535,7 @@ "other_variables": "מ׊×Ēנים אחרים", "owned": "בב×ĸלו×Ē", "owner": "ב×ĸלים", + "page": "דת", "partner": "שו×Ē×Ŗ", "partner_can_access": "{partner} יכול/ה לגש×Ē", "partner_can_access_assets": "כל ה×Ēמונו×Ē ×•×”×Ą×¨×˜×•× ×™× שלך פרט לאלו שבארכיון ושנמחקו", @@ -1569,6 +1598,8 @@ "photos_count": "{count, plural, one {×Ēמונה {count, number}} other {{count, number} ×Ēמונו×Ē}}", "photos_from_previous_years": "×Ēמונו×Ē ×ž×Š× ×™× קודמו×Ē", "pick_a_location": "בחר מיקום", + "pick_custom_range": "טווח מו×Ēאם אישי×Ē", + "pick_date_range": "בחר טווח ×Ēאריכים", "pin_code_changed_successfully": "קוד ה PIN שונה בה×Ļלחה", "pin_code_reset_successfully": "קוד PIN אופס בה×Ļלחה", "pin_code_setup_successfully": "קוד PIN הוגדר בה×Ļלחה", @@ -1655,7 +1686,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {×Ēמונה # הוק×Ļ×Ēה} other {# ×Ēמונו×Ē ×”×•×§×Ļו}} מחדש לאדם חדש", "reassing_hint": "הק×Ļא×Ē ×Ēמונו×Ē ×Š× ×‘×—×¨×• לאדם קיים", "recent": "חדש", - "recent-albums": "אלבומים אחרונים", + "recent_albums": "אלבומים אחרונים", "recent_searches": "חיפושים אחרונים", "recently_added": "× ×•×Ą×Ŗ לאחרונה", "recently_added_page_title": "× ×•×Ą×Ŗ לאחרונה", @@ -1836,6 +1867,8 @@ "server_offline": "השר×Ē ×ž× ×•×Ē×§", "server_online": "החיבור לשר×Ē ×¤×ĸיל", "server_privacy": "פרטיו×Ē ×”×Š×¨×Ē", + "server_restarting_description": "הדת י×Ēר×ĸנן ב×ĸוד רג×ĸ.", + "server_restarting_title": "השר×Ē ×ž×•×¤×ĸל מחדש", "server_stats": "סטטיסטיקו×Ē ×Š×¨×Ē", "server_update_available": "×ĸדכון ׊ר×Ē ×–×ž×™×Ÿ", "server_version": "גרס×Ē ×Š×¨×Ē", @@ -1959,6 +1992,7 @@ "show_slideshow_transition": "ה×Ļג מ×ĸבר מ×Ļג×Ē", "show_supporter_badge": "×Ēג ×Ēומך", "show_supporter_badge_description": "ה×Ļג ×Ēג ×Ēומך", + "show_text_recognition": "ה×Ļג זיהוי טקץט", "show_text_search_menu": "ה×Ļג ×Ēפריט חיפוש טקץט", "shuffle": "×ĸרבוב", "sidebar": "סרגל ×Ļד", @@ -2029,6 +2063,7 @@ "tags": "×Ēגים", "tap_to_run_job": "לח×Ĩ ×ĸל מנ×Ē ×œ×”×¤×ĸיל משימה", "template": "×Ēבני×Ē", + "text_recognition": "זיהוי טקץט", "theme": "×ĸרכ×Ē × ×•×Š×", "theme_selection": "בחיר×Ē ×ĸרכ×Ē × ×•×Š×", "theme_selection_description": "הגדר אוטומטי×Ē ××Ē ×ĸרכ×Ē ×”× ×•×Š× לבהיר או כהה בה×Ēבסס ×ĸל ה×ĸדפ×Ē ×”×ž×ĸרכ×Ē ×Š×œ הדפדפן שלך", @@ -2049,6 +2084,7 @@ "third_party_resources": "משאבי ×Ļד שלישי", "time": "זמן", "time_based_memories": "זכרונו×Ē ×ž×‘×•×Ą×Ą×™ זמן", + "time_based_memories_duration": "מספר השניו×Ē ×œ×”×Ļג×Ē ×›×œ ×Ēמונה.", "timeline": "×Ļיר זמן", "timezone": "אזור זמן", "to_archive": "ה×ĸבר לארכיון", @@ -2060,6 +2096,7 @@ "to_select": "לבחור", "to_trash": "אשפה", "toggle_settings": "×”×—×œ×Ŗ מ×Ļב הגדרו×Ē", + "toggle_theme_description": "הפ×ĸלה/כיבוי של ×ĸרכ×Ē × ×•×Š×", "total": "סה\"כ", "total_usage": "שימוש כולל", "trash": "אשפה", @@ -2113,8 +2150,7 @@ "updated_at": "×ĸודכן", "updated_password": "סיסמה ×ĸודכנה", "upload": "ה×ĸלאה", - "upload_action_prompt": "{count} נוספו ל×Ēור לה×ĸלאה", - "upload_concurrency": "בו-זמניו×Ē ×Š×œ ה×ĸלאה", + "upload_concurrency": "מספר ה×ĸלאו×Ē ×‘×ž×§×‘×™×œ", "upload_details": "פרטי ה×ĸלאה", "upload_dialog_info": "האם בר×Ļונך לגבו×Ē ××Ē ×”×Ēמונו×Ē ×Š× ×‘×—×¨×• לשר×Ē?", "upload_dialog_title": "ה×ĸלא×Ē ×Ēמונה", @@ -2168,6 +2204,7 @@ "view_album": "ה×Ļג אלבום", "view_all": "ה×Ļג הכל", "view_all_users": "ה×Ļג א×Ē ×›×œ המש×Ēמשים", + "view_asset_owners": "ה×Ļג א×Ē ×‘×ĸלי ה×Ēמונו×Ē", "view_details": "ה×Ļג פרטים", "view_in_timeline": "ראה ב×Ļיר הזמן", "view_link": "ה×Ļג קישור", @@ -2184,10 +2221,12 @@ "viewer_unstack": "ביטול ×ĸרימה", "visibility_changed": "הנראו×Ē ×”×Š×Ē× ×Ēה ×ĸבור {count, plural, one {אדם #} other {# אנשים}}", "waiting": "ממ×Ēין", + "waiting_count": "ממ×Ēין: {count}", "warning": "אזהרה", "week": "שבו×ĸ", "welcome": "ברוכים הבאים", "welcome_to_immich": "ברוכים הבאים אל immich", + "width": "רוחב", "wifi_name": "׊ם הרש×Ē ×”××œ×—×•×˜×™×Ē", "wrong_pin_code": "קוד PIN שגוי", "year": "שנה", diff --git a/i18n/hi.json b/i18n/hi.json index 97c5443bd4..ff05291cef 100644 --- a/i18n/hi.json +++ b/i18n/hi.json @@ -5,8 +5,10 @@ "acknowledge": "⤏āĨā¤ĩāĨ€ā¤•ā¤žā¤° ⤕⤰āĨ‡ā¤‚", "action": "ā¤•ā¤žā¤°āĨā¤°ā¤ĩā¤žā¤ˆ", "action_common_update": "⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨", + "action_description": "ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ⤕ā¤ŋā¤ ā¤—ā¤ ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ā¤Ē⤰ ⤕ā¤ŋā¤ ā¤œā¤žā¤¨āĨ‡ ā¤ĩā¤žā¤˛āĨ‡ ā¤ā¤•āĨā¤ļ⤍ ā¤•ā¤ž ⤏āĨ‡ā¤Ÿ", "actions": "ā¤•ā¤žā¤°āĨā¤¯ā¤ĩā¤žā¤šā¤ŋā¤¯ā¤žā¤‚", "active": "⤏⤕āĨā¤°ā¤ŋ⤝", + "active_count": "⤏⤕āĨā¤°ā¤ŋ⤝: {count}", "activity": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ", "activity_changed": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ {enabled, select, true {enabled} other {disabled}}", "add": "ā¤Ąā¤žā¤˛āĨ‡ā¤‚", @@ -14,9 +16,14 @@ "add_a_location": "ā¤ā¤• ⤏āĨā¤Ĩā¤žā¤¨ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_a_name": "ā¤¨ā¤žā¤Ž ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_a_title": "ā¤ā¤• ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_action": "ā¤•ā¤žā¤°āĨā¤°ā¤ĩā¤žā¤ˆ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_action_description": "⤕āĨ‹ā¤ˆ ā¤ā¤•āĨā¤ļ⤍ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨā¤˛ā¤ŋ⤕ ⤕⤰āĨ‡ā¤‚", + "add_assets": "ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", "add_birthday": "⤅ā¤Ē⤍āĨ‡ ⤜⤍āĨā¤Žā¤Ļā¤ŋ⤍ ā¤•ā¤ž ⤉⤞āĨā¤˛āĨ‡ā¤– ⤕⤰āĨ‡ā¤‚", "add_endpoint": "endpoint ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_exclusion_pattern": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ⤉ā¤Ļā¤žā¤šā¤°ā¤Ŗ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_filter": "āĨžā¤ŋ⤞āĨā¤Ÿā¤° ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_filter_description": "ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ā¤•ā¤‚ā¤ĄāĨ€ā¤ļ⤍ ⤜āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨā¤˛ā¤ŋ⤕ ⤕⤰āĨ‡ā¤‚", "add_location": "⤏āĨā¤Ĩā¤žā¤¨ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_more_users": "⤅⤧ā¤ŋ⤕ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_partner": "⤜āĨ‹ā¤Ąā¤ŧāĨ€ā¤Ļā¤žā¤° ā¤Ąā¤žā¤˛āĨ‡ā¤‚", @@ -35,6 +42,7 @@ "add_to_shared_album": "ā¤ļāĨ‡ā¤¯ā¤° ⤕ā¤ŋā¤ ā¤—ā¤ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛āĨ‡ā¤‚", "add_upload_to_stack": "⤏āĨā¤ŸāĨˆā¤• ā¤ŽāĨ‡ā¤‚ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", "add_url": "URL ā¤Ąā¤žā¤˛āĨ‡ā¤‚", + "add_workflow_step": "ā¤ĩ⤰āĨā¤•ā¤Ģā¤ŧāĨā¤˛āĨ‹ ⤏āĨā¤ŸāĨ‡ā¤Ē ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", "added_to_archive": "⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", "added_to_favorites": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "added_to_favorites_count": "ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤ŽāĨ‡ā¤‚ {count, number} ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", @@ -48,7 +56,7 @@ "authentication_settings_reenable": "ā¤ĒāĨā¤¨ā¤ƒ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, Server Command ā¤•ā¤ž ā¤ĒāĨā¤°ā¤¯āĨ‹ā¤— ⤕⤰āĨ‡āĨ¤", "background_task_job": "ā¤ĒāĨƒā¤ˇāĨā¤ ā¤­āĨ‚ā¤Žā¤ŋ ā¤•ā¤žā¤°āĨā¤¯", "backup_database": "ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ā¤Ąā¤‚ā¤Ē ā¤Ŧā¤¨ā¤žā¤ā¤‚", - "backup_database_enable_description": "Enable database dumps", + "backup_database_enable_description": "ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ā¤Ąā¤‚ā¤Ē ā¤šā¤žā¤˛āĨ‚ ⤕⤰āĨ‡ā¤‚", "backup_keep_last_amount": "⤰⤖⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Ēā¤ŋ⤛⤞āĨ‡ ā¤Ąā¤‚ā¤Ē ⤕āĨ€ ā¤Žā¤žā¤¤āĨā¤°ā¤ž", "backup_onboarding_1_description": "⤕āĨā¤˛ā¤žā¤‰ā¤Ą ā¤ŽāĨ‡ā¤‚ ā¤¯ā¤ž ⤕ā¤ŋ⤏āĨ€ ⤅⤍āĨā¤¯ ⤭āĨŒā¤¤ā¤ŋ⤕ ⤏āĨā¤Ĩā¤žā¤¨ ā¤Ē⤰ ⤑ā¤Ģā¤¸ā¤žā¤‡ā¤Ÿ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤞ā¤ŋā¤Ēā¤ŋāĨ¤", "backup_onboarding_2_description": "ā¤ĩā¤ŋ⤭ā¤ŋ⤍āĨā¤¨ ⤉ā¤Ē⤕⤰⤪āĨ‹ā¤‚ ā¤Ē⤰ ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤¯ā¤žā¤āĨ¤ ā¤‡ā¤¸ā¤ŽāĨ‡ā¤‚ ā¤ŽāĨā¤–āĨā¤¯ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‡ā¤‚ ⤔⤰ ⤉⤍ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‹ā¤‚ ā¤•ā¤ž ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ļā¤žā¤Žā¤ŋ⤞ ā¤šāĨˆāĨ¤", @@ -67,6 +75,7 @@ "confirm_reprocess_all_faces": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤏⤭āĨ€ ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ⤕āĨ‹ ā¤ĻāĨ‹ā¤Ŧā¤žā¤°ā¤ž ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤ŋ⤤ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ⤇⤏⤏āĨ‡ ā¤¨ā¤žā¤Žā¤ŋ⤤ ⤞āĨ‹ā¤— ⤭āĨ€ ā¤¸ā¤žā¤Ģ ā¤šāĨ‹ ā¤œā¤žā¤¯āĨ‡ā¤‚⤗āĨ‡āĨ¤", "confirm_user_password_reset": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ {user} ā¤•ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "confirm_user_pin_code_reset": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ {user} ā¤•ā¤ž ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", + "copy_config_to_clipboard_description": "ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰āĨ‡ā¤ļ⤍ ⤕āĨ‹ JSON ⤑ā¤ŦāĨā¤œāĨ‡ā¤•āĨā¤Ÿ ⤕āĨ‡ ⤰āĨ‚ā¤Ē ā¤ŽāĨ‡ā¤‚ ⤕āĨā¤˛ā¤ŋā¤Ēā¤ŦāĨ‹ā¤°āĨā¤Ą ā¤Ē⤰ ⤕āĨ‰ā¤ĒāĨ€ ⤕⤰āĨ‡ā¤‚", "create_job": "⤜āĨ‰ā¤Ŧ ā¤Ŧā¤¨ā¤žā¤ā¤", "cron_expression": "⤕āĨā¤°āĨ‰ā¤¨ ⤅⤭ā¤ŋā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ", "cron_expression_description": "⤕āĨā¤°āĨ‰ā¤¨ ā¤ĒāĨā¤°ā¤žā¤°āĨ‚ā¤Ē ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤕āĨ‡ ⤏āĨā¤•āĨˆā¤¨ā¤ŋ⤂⤗ ā¤…ā¤‚ā¤¤ā¤°ā¤žā¤˛ ⤏āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚āĨ¤ ⤅⤧ā¤ŋ⤕ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨƒā¤Ēā¤¯ā¤ž ⤕āĨā¤°āĨ‹ā¤¨ā¤ŸāĨˆā¤Ŧ ⤗āĨā¤°āĨ ā¤ĻāĨ‡ā¤–āĨ‡ā¤‚", @@ -74,6 +83,8 @@ "disable_login": "⤞āĨ‰ā¤—ā¤ŋ⤍ ⤅⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", "duplicate_detection_job_description": "ā¤¸ā¤Žā¤žā¤¨ ⤛ā¤ĩā¤ŋ⤝āĨ‹ā¤‚ ā¤•ā¤ž ā¤Ēā¤¤ā¤ž ā¤˛ā¤—ā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤Ē⤰ ā¤Žā¤ļāĨ€ā¤¨ ⤞⤰āĨā¤¨ā¤ŋ⤂⤗ ā¤šā¤˛ā¤žā¤ā¤‚āĨ¤ ā¤¯ā¤š ā¤•ā¤žā¤°āĨā¤¯ā¤•āĨā¤ˇā¤Žā¤¤ā¤ž ⤏āĨā¤Žā¤žā¤°āĨā¤Ÿ ⤖āĨ‹ā¤œ ā¤Ē⤰ ⤍ā¤ŋ⤰āĨā¤­ā¤° ⤕⤰⤤āĨ€ ā¤šāĨˆ", "exclusion_pattern_description": "Exclusion ā¤ĒāĨˆā¤Ÿā¤°āĨā¤¨ ⤆ā¤Ē⤕āĨ‹ ⤅ā¤Ē⤍āĨ€ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ⤕āĨ‹ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰⤤āĨ‡ ā¤¸ā¤Žā¤¯ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‹ā¤‚ ⤔⤰ ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤°āĨ‹ā¤‚ ⤕āĨ‹ ⤅⤍ā¤ĻāĨ‡ā¤–ā¤ž ⤕⤰⤍āĨ‡ ā¤ĻāĨ‡ā¤¤ā¤ž ā¤šāĨˆāĨ¤ ā¤¯ā¤š ⤉ā¤Ē⤝āĨ‹ā¤—āĨ€ ā¤šāĨˆ ⤝ā¤Ļā¤ŋ ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ⤐⤏āĨ‡ ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤šāĨˆā¤‚ ⤜ā¤ŋā¤¨ā¤ŽāĨ‡ā¤‚ ⤐⤏āĨ€ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‡ā¤‚ ā¤šāĨˆā¤‚ ⤜ā¤ŋ⤍āĨā¤šāĨ‡ā¤‚ ⤆ā¤Ē ā¤†ā¤¯ā¤žā¤¤ ā¤¨ā¤šāĨ€ā¤‚ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚, ⤜āĨˆā¤¸āĨ‡ RAW ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‡ā¤‚āĨ¤", + "export_config_as_json_description": "ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰āĨ‡ā¤ļ⤍ ⤕āĨ‹ JSON ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤕āĨ‡ ⤰āĨ‚ā¤Ē ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤‰ā¤¨ā¤˛āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", + "external_libraries_page_description": "ā¤ā¤Ąā¤Žā¤ŋ⤍ ā¤Ŧā¤žā¤šā¤°āĨ€ ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤ĒāĨ‡ā¤œ", "face_detection": "ā¤ŽāĨā¤– ⤏⤂ā¤ļāĨ‹ā¤§ā¤¨", "face_detection_description": "ā¤Žā¤ļāĨ€ā¤¨ ⤞⤰āĨā¤¨ā¤ŋ⤂⤗ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤕āĨ‡ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ā¤•ā¤ž ā¤Ēā¤¤ā¤ž ā¤˛ā¤—ā¤žā¤ā¤‚āĨ¤ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ⤕āĨ‡ ⤞ā¤ŋā¤, ⤕āĨ‡ā¤ĩ⤞ ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ā¤Ē⤰ ā¤ĩā¤ŋā¤šā¤žā¤° ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤¤ā¤ž ā¤šāĨˆāĨ¤ \"⤏⤭āĨ€\" ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ (ā¤ĒāĨā¤¨ā¤ƒ) ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤ŋ⤤ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆāĨ¤ \"ā¤˛ā¤žā¤Ēā¤¤ā¤ž\" ⤉⤍ ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤•ā¤¤ā¤žā¤°ā¤Ŧā¤ĻāĨā¤§ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆ ⤜ā¤ŋ⤍āĨā¤šāĨ‡ā¤‚ ⤅⤭āĨ€ ⤤⤕ ā¤¸ā¤‚ā¤¸ā¤žā¤§ā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤ ā¤ĢāĨ‡ā¤¸ ā¤Ąā¤ŋ⤟āĨ‡ā¤•āĨā¤ļ⤍ ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨ‹ā¤¨āĨ‡ ⤕āĨ‡ ā¤Ŧā¤žā¤Ļ ā¤Ēā¤šā¤šā¤žā¤¨āĨ‡ ā¤—ā¤ ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ⤕āĨ‹ ⤚āĨ‡ā¤šā¤°āĨ‡ ⤕āĨ€ ā¤Ēā¤šā¤šā¤žā¤¨ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤•ā¤¤ā¤žā¤°ā¤Ŧā¤ĻāĨā¤§ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž, ⤉⤍āĨā¤šāĨ‡ā¤‚ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ā¤¯ā¤ž ā¤¨ā¤ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤ŽāĨ‚ā¤šā¤ŋ⤤ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤žāĨ¤", "facial_recognition_job_description": "ā¤¸ā¤ŽāĨ‚ā¤š ⤍āĨ‡ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ā¤•ā¤ž ā¤Ēā¤¤ā¤ž ā¤˛ā¤—ā¤žā¤¯ā¤žāĨ¤ ā¤¯ā¤š ⤚⤰⤪ ā¤ĢāĨ‡ā¤¸ ā¤Ąā¤ŋ⤟āĨ‡ā¤•āĨā¤ļ⤍ ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨ‹ā¤¨āĨ‡ ⤕āĨ‡ ā¤Ŧā¤žā¤Ļ ā¤šā¤˛ā¤¤ā¤ž ā¤šāĨˆāĨ¤ \"⤏⤭āĨ€\" ⤚āĨ‡ā¤šā¤°āĨ‹ā¤‚ ⤕āĨ‹ (ā¤ĒāĨā¤¨ā¤ƒ) ā¤¸ā¤ŽāĨ‚ā¤šā¤ŋ⤤ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆāĨ¤ \"ā¤˛ā¤žā¤Ēā¤¤ā¤ž\" ā¤•ā¤¤ā¤žā¤° ā¤ŽāĨ‡ā¤‚ ā¤ĩāĨ‡ ⤚āĨ‡ā¤šā¤°āĨ‡ ā¤šāĨˆā¤‚ ⤜ā¤ŋ⤍⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤍ā¤ŋ⤝āĨā¤•āĨā¤¤ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", @@ -93,6 +104,8 @@ "image_preview_description": "ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž ā¤°ā¤šā¤ŋ⤤ ā¤Žā¤§āĨā¤¯ā¤Ž ā¤†ā¤•ā¤žā¤° ⤕āĨ€ ⤛ā¤ĩā¤ŋ, ⤜ā¤ŋā¤¸ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ā¤ā¤•ā¤˛ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ⤔⤰ ā¤Žā¤ļāĨ€ā¤¨ ⤞⤰āĨā¤¨ā¤ŋ⤂⤗ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤šāĨ‹ā¤¤ā¤ž ā¤šāĨˆ", "image_preview_quality_description": "ā¤ĒāĨ‚⤰āĨā¤ĩā¤žā¤ĩ⤞āĨ‹ā¤•⤍ ⤕āĨ€ ⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž (1 ⤏āĨ‡ 100 ⤤⤕)āĨ¤ ⤅⤧ā¤ŋ⤕ ā¤Žā¤žā¤¨ ā¤ŦāĨ‡ā¤šā¤¤ā¤° ⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž ā¤ĻāĨ‡ā¤¤ā¤ž ā¤šāĨˆ, ⤞āĨ‡ā¤•ā¤ŋ⤍ ⤇⤏⤏āĨ‡ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤†ā¤•ā¤žā¤° ā¤Ŧā¤ĸā¤ŧā¤¤ā¤ž ā¤šāĨˆ ⤔⤰ ⤐ā¤Ē ⤕āĨ€ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ⤕āĨā¤ˇā¤Žā¤¤ā¤ž ā¤•ā¤Ž ā¤šāĨ‹ ⤏⤕⤤āĨ€ ā¤šāĨˆāĨ¤ ā¤Ŧā¤šāĨā¤¤ ā¤•ā¤Ž ā¤Žā¤žā¤¨ ā¤Žā¤ļāĨ€ā¤¨ ⤞⤰āĨā¤¨ā¤ŋ⤂⤗ ⤕āĨ€ ⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž ⤕āĨ‹ ā¤ĒāĨā¤°ā¤­ā¤žā¤ĩā¤ŋ⤤ ⤕⤰ ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆāĨ¤", "image_preview_title": "ā¤ĒāĨ‚⤰āĨā¤ĩā¤Ļ⤰āĨā¤ļ⤍ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", + "image_progressive": "ā¤ĒāĨā¤°ā¤—⤤ā¤ŋā¤ļāĨ€ā¤˛", + "image_progressive_description": "JPEG ⤛ā¤ĩā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤕āĨā¤°ā¤Žā¤ŋ⤕ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤉⤍āĨā¤šāĨ‡ā¤‚ ā¤ĒāĨā¤°āĨ‹ā¤—āĨā¤°āĨ‡ā¤¸ā¤ŋā¤ĩ⤞āĨ€ ā¤ā¤¨ā¤•āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚āĨ¤ ā¤‡ā¤¸ā¤•ā¤ž WebP ⤛ā¤ĩā¤ŋ⤝āĨ‹ā¤‚ ā¤Ē⤰ ⤕āĨ‹ā¤ˆ ā¤ĒāĨā¤°ā¤­ā¤žā¤ĩ ā¤¨ā¤šāĨ€ā¤‚ ā¤Ēā¤Ąā¤ŧā¤¤ā¤ž ā¤šāĨˆāĨ¤", "image_quality": "⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž", "image_resolution": "⤰ā¤ŋ⤜ā¤ŧāĨ‰ā¤˛āĨā¤¯āĨ‚ā¤ļ⤍", "image_resolution_description": "ā¤‰ā¤šāĨā¤šā¤¤ā¤° ⤰ā¤ŋ⤜ā¤ŧāĨ‰ā¤˛āĨā¤¯āĨ‚ā¤ļ⤍ ⤅⤧ā¤ŋ⤕ ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤏āĨā¤°ā¤•āĨā¤ˇā¤ŋ⤤ ⤰⤖ ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ, ⤞āĨ‡ā¤•ā¤ŋ⤍ ā¤ā¤¨āĨā¤•āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤅⤧ā¤ŋ⤕ ā¤¸ā¤Žā¤¯ ⤞āĨ‡ā¤¤ā¤ž ā¤šāĨˆ, ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤†ā¤•ā¤žā¤° ā¤Ŧā¤Ąā¤ŧā¤ž ā¤šāĨ‹ā¤¤ā¤ž ā¤šāĨˆ ⤔⤰ ⤐ā¤Ē ⤕āĨ€ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤕āĨā¤°ā¤ŋā¤¯ā¤žā¤ļāĨ€ā¤˛ā¤¤ā¤ž ā¤•ā¤Ž ā¤šāĨ‹ ⤏⤕⤤āĨ€ ā¤šāĨˆāĨ¤", @@ -101,6 +114,7 @@ "image_thumbnail_description": "ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž ā¤šā¤Ÿā¤žā¤ˆ ā¤—ā¤ˆ ⤛āĨ‹ā¤ŸāĨ€ ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛, ⤜ā¤ŋā¤¸ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ā¤ĢāĨ‹ā¤ŸāĨ‹ ā¤¸ā¤ŽāĨ‚ā¤šāĨ‹ā¤‚ ⤕āĨ‹ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤜āĨˆā¤¸āĨ‡ ā¤ŽāĨā¤–āĨā¤¯ ā¤Ÿā¤žā¤‡ā¤Žā¤˛ā¤žā¤‡ā¤¨ ā¤ŽāĨ‡ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤¤ā¤ž ā¤šāĨˆ", "image_thumbnail_quality_description": "ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ⤕āĨ€ ⤗āĨā¤Ŗā¤ĩ⤤āĨā¤¤ā¤ž 1-100 ⤤⤕āĨ¤ ā¤‰ā¤šāĨā¤šā¤¤ā¤° ā¤ŦāĨ‡ā¤šā¤¤ā¤° ā¤šāĨˆ, ⤞āĨ‡ā¤•ā¤ŋ⤍ ā¤Ŧā¤Ąā¤ŧāĨ€ ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‡ā¤‚ ā¤Ŧā¤¨ā¤žā¤¤ā¤ž ā¤šāĨˆ ⤔⤰ ⤐ā¤Ē ⤕āĨ€ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤕āĨā¤°ā¤ŋā¤¯ā¤žā¤ļāĨ€ā¤˛ā¤¤ā¤ž ⤕āĨ‹ ā¤•ā¤Ž ⤕⤰ ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆāĨ¤", "image_thumbnail_title": "ā¤Ĩ⤂ā¤Ŧ⤍āĨ‡ā¤˛ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", + "import_config_from_json_description": "JSON ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰āĨ‡ā¤ļ⤍ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤕āĨ‡ ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤕āĨ‰ā¤¨āĨā¤Ģā¤ŧā¤ŋ⤗⤰āĨ‡ā¤ļ⤍ ⤇⤂ā¤ĒāĨ‹ā¤°āĨā¤Ÿ ⤕⤰āĨ‡ā¤‚", "job_concurrency": "{job} ā¤¸ā¤Žā¤°āĨ‚ā¤Ēā¤¤ā¤ž", "job_created": "⤍āĨŒā¤•⤰āĨ€ ā¤Ŧā¤¨ā¤žā¤ˆ ā¤—ā¤ˆ", "job_not_concurrency_safe": "ā¤¯ā¤š ā¤•ā¤žā¤°āĨā¤¯ (⤜āĨ‰ā¤Ŧ) ā¤¸ā¤Žā¤ĩ⤰āĨā¤¤āĨ€-⤏āĨā¤°ā¤•āĨā¤ˇā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", @@ -108,6 +122,7 @@ "job_settings_description": "ā¤•ā¤žā¤°āĨā¤¯ (⤜āĨ‰ā¤Ŧ) ā¤¸ā¤Žā¤ĩ⤰āĨā¤¤āĨ€ā¤¤ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "jobs_delayed": "{jobCount, plural, other {# ā¤ĩā¤ŋ⤞⤂ā¤Ŧā¤ŋ⤤}}", "jobs_failed": "{jobCount, plural, other {# ⤅⤏ā¤Ģ⤞}}", + "jobs_over_time": "ā¤¸ā¤Žā¤¯ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ⤍āĨŒā¤•⤰ā¤ŋā¤¯ā¤žā¤‚", "library_created": "⤍ā¤ŋ⤰āĨā¤Žā¤ŋ⤤ ⤏⤂⤗āĨā¤°ā¤š: {library}", "library_deleted": "⤏⤂⤗āĨā¤°ā¤š ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "library_details": "⤏⤂⤗āĨā¤°ā¤š ā¤ĩā¤ŋā¤ĩ⤰⤪", @@ -175,11 +190,23 @@ "machine_learning_smart_search_enabled": "⤏āĨā¤Žā¤žā¤°āĨā¤Ÿ ⤖āĨ‹ā¤œ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", "machine_learning_smart_search_enabled_description": "⤝ā¤Ļā¤ŋ ⤅⤕āĨā¤ˇā¤Ž ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ, ⤤āĨ‹ ⤏āĨā¤Žā¤žā¤°āĨā¤Ÿ ⤖āĨ‹ā¤œ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤛ā¤ĩā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ā¤ā¤¨āĨā¤•āĨ‹ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤žāĨ¤", "machine_learning_url_description": "ā¤Žā¤ļāĨ€ā¤¨ ⤞⤰āĨā¤¨ā¤ŋ⤂⤗ ⤏⤰āĨā¤ĩ⤰ ā¤•ā¤ž URLāĨ¤ ⤝ā¤Ļā¤ŋ ā¤ā¤• ⤏āĨ‡ ⤅⤧ā¤ŋ⤕ URL ā¤Ļā¤ŋā¤ ā¤—ā¤ ā¤šāĨˆā¤‚, ⤤āĨ‹ ā¤ĒāĨā¤°ā¤¤āĨā¤¯āĨ‡ā¤• ⤏⤰āĨā¤ĩ⤰ ⤕āĨ‹ ā¤ā¤•-ā¤ā¤• ⤕⤰⤕āĨ‡ ⤕āĨ‹ā¤ļā¤ŋā¤ļ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž, ā¤Ēā¤šā¤˛āĨ‡ ⤏āĨ‡ ⤆⤖ā¤ŋ⤰āĨ€ ⤤⤕, ⤜ā¤Ŧ ⤤⤕ ⤕āĨ‹ā¤ˆ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ⤍ ā¤ĻāĨ‡āĨ¤ ⤜āĨ‹ ⤏⤰āĨā¤ĩ⤰ ā¤ĒāĨā¤°ā¤¤ā¤ŋ⤕āĨā¤°ā¤ŋā¤¯ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤ĻāĨ‡ā¤¤āĨ‡, ⤉⤍āĨā¤šāĨ‡ā¤‚ ⤅⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤¨ā¤œā¤°ā¤…ā¤‚ā¤Ļā¤žā¤œ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž ⤜ā¤Ŧ ⤤⤕ ā¤ĩāĨ‡ ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ā¤‘ā¤¨ā¤˛ā¤žā¤‡ā¤¨ ⤍ ā¤šāĨ‹ā¤‚āĨ¤", + "maintenance_delete_backup": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤Ąā¤ŋ⤞āĨ€ā¤Ÿ ⤕⤰āĨ‡ā¤‚", + "maintenance_delete_backup_description": "ā¤¯ā¤š ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤏āĨā¤Ĩā¤žā¤¯āĨ€ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤Žā¤ŋā¤Ÿā¤ž ā¤ĻāĨ€ ā¤œā¤žā¤ā¤—āĨ€āĨ¤ ⤇⤏āĨ‡ ā¤ĩā¤žā¤Ē⤏ ā¤¨ā¤šāĨ€ā¤‚ ā¤˛ā¤žā¤¯ā¤ž ā¤œā¤ž ⤏⤕āĨ‡ā¤—ā¤žāĨ¤", + "maintenance_delete_error": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤Žā¤ŋā¤Ÿā¤žā¤¯ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤žāĨ¤", + "maintenance_restore_backup": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ĩā¤žā¤Ē⤏ ā¤˛ā¤žā¤ā¤", + "maintenance_restore_backup_description": "Immich ā¤•ā¤ž ā¤¸ā¤žā¤°ā¤ž ā¤ĄāĨ‡ā¤Ÿā¤ž ā¤ĒāĨ‚⤰āĨ€ ā¤¤ā¤°ā¤š ā¤Žā¤ŋā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž ⤔⤰ ⤚āĨā¤¨āĨ‡ ā¤—ā¤ ā¤ŦāĨˆā¤•⤅ā¤Ē ⤏āĨ‡ ā¤ĄāĨ‡ā¤Ÿā¤ž ā¤ĩā¤žā¤Ē⤏ ā¤˛ā¤žā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤žāĨ¤ ⤆⤗āĨ‡ ā¤Ŧā¤ĸā¤ŧ⤍āĨ‡ ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤ā¤• ā¤¨ā¤¯ā¤ž ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤Ŧā¤¨ā¤žā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤žāĨ¤", + "maintenance_restore_backup_different_version": "ā¤¯ā¤š ā¤ŦāĨˆā¤•⤅ā¤Ē Immich ⤕āĨ‡ ⤕ā¤ŋ⤏āĨ€ ⤅⤞⤗ version ā¤ŽāĨ‡ā¤‚ ā¤Ŧā¤¨ā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤Ĩā¤ž!", + "maintenance_restore_backup_unknown_version": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤•ā¤ž version ⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤žāĨ¤", + "maintenance_restore_database_backup": "ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ĩā¤žā¤Ē⤏ ā¤˛ā¤žā¤ā¤", + "maintenance_restore_database_backup_description": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤕āĨ‡ ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ⤕āĨ‹ ā¤Ēā¤šā¤˛āĨ‡ ⤕āĨ€ ⤏āĨā¤Ĩā¤ŋ⤤ā¤ŋ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤žā¤Ē⤏ ā¤˛ā¤žā¤ā¤", "maintenance_settings": "ā¤°ā¤–ā¤°ā¤–ā¤žā¤ĩ", "maintenance_settings_description": "Immich ⤕āĨ‹ ā¤ŽāĨ‡ā¤‚ā¤ŸāĨ‡ā¤¨āĨ‡ā¤‚⤏ ā¤ŽāĨ‹ā¤Ą ā¤ŽāĨ‡ā¤‚ ⤰⤖āĨ‡ā¤‚āĨ¤", - "maintenance_start": "ā¤°ā¤–ā¤°ā¤–ā¤žā¤ĩ ā¤ŽāĨ‹ā¤Ą ā¤ļāĨā¤°āĨ‚ ⤕⤰āĨ‡ā¤‚", + "maintenance_start": "ā¤°ā¤–ā¤°ā¤–ā¤žā¤ĩ ā¤ŽāĨ‹ā¤Ą ā¤Ē⤰ ⤏āĨā¤ĩā¤ŋ⤚ ⤕⤰āĨ‡ā¤‚", "maintenance_start_error": "ā¤ŽāĨ‡ā¤‚ā¤ŸāĨ‡ā¤¨āĨ‡ā¤‚⤏ ā¤ŽāĨ‹ā¤Ą ā¤ļāĨā¤°āĨ‚ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨ‹ ā¤¸ā¤•ā¤žāĨ¤", + "maintenance_upload_backup": "ā¤ĄāĨ‡ā¤Ÿā¤žā¤ŦāĨ‡ā¤¸ ⤕āĨ€ ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", + "maintenance_upload_backup_error": "ā¤ŦāĨˆā¤•⤅ā¤Ē ⤅ā¤Ē⤞āĨ‹ā¤Ą ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤žāĨ¤ ⤕āĨā¤¯ā¤ž ā¤¯ā¤š .sql ā¤¯ā¤ž .sql.gz ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤šāĨˆ?", "manage_concurrency": "ā¤¸ā¤Žā¤ĩ⤰āĨā¤¤āĨ€ā¤¤ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", + "manage_concurrency_description": "ā¤ā¤• ā¤¸ā¤žā¤Ĩ ⤚⤞⤍āĨ‡ ā¤ĩā¤žā¤˛āĨ‡ ⤜āĨ‰ā¤ŦāĨā¤¸ ā¤•ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤜āĨ‰ā¤ŦāĨā¤¸ ā¤ĒāĨ‡ā¤œ ā¤Ē⤰ ā¤œā¤žā¤ā¤", "manage_log_settings": "⤞āĨ‰ā¤— ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "map_dark_style": "ā¤Ąā¤žā¤°āĨā¤• ā¤ļāĨˆā¤˛āĨ€", "map_enable_description": "ā¤Žā¤žā¤¨ā¤šā¤ŋ⤤āĨā¤° ⤏āĨā¤ĩā¤ŋā¤§ā¤žā¤ā¤ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", @@ -245,7 +272,7 @@ "oauth_auto_register": "ā¤‘ā¤ŸāĨ‹ ⤰⤜ā¤ŋ⤏āĨā¤Ÿā¤°", "oauth_auto_register_description": "OAuth ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤‡ā¤¨ ⤇⤍ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ā¤Ŧā¤žā¤Ļ ⤏āĨā¤ĩā¤šā¤žā¤˛ā¤ŋ⤤ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤¨ā¤ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‹ ā¤Ēā¤‚ā¤œāĨ€ā¤•āĨƒā¤¤ ⤕⤰āĨ‡ā¤‚", "oauth_button_text": "⤟āĨ‡ā¤•āĨā¤¸āĨā¤Ÿ ā¤Ŧ⤟⤍", - "oauth_client_secret_description": "⤝ā¤Ļā¤ŋ PKCE (⤕āĨ‹ā¤Ą ā¤ā¤•āĨā¤¸ā¤šāĨ‡ā¤‚ā¤œ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ĒāĨā¤°āĨ‚ā¤Ģā¤ŧ ⤕āĨā¤‚ā¤œāĨ€) OAuth ā¤ĒāĨā¤°ā¤Ļā¤žā¤¤ā¤ž ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤¸ā¤Žā¤°āĨā¤Ĩā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆ ⤤āĨ‹ ā¤¯ā¤š ⤆ā¤ĩā¤ļāĨā¤¯ā¤• ā¤šāĨˆ", + "oauth_client_secret_description": "ā¤¯ā¤š Confidential (⤗āĨ‹ā¤Ē⤍āĨ€ā¤¯) ⤕āĨā¤˛ā¤žā¤‡ā¤‚ā¤Ÿ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤆ā¤ĩā¤ļāĨā¤¯ā¤• ā¤šāĨˆ, ā¤¯ā¤ž ⤝ā¤Ļā¤ŋ Public ⤕āĨā¤˛ā¤žā¤‡ā¤‚ā¤Ÿ ā¤ŽāĨ‡ā¤‚ PKCE (Proof Key for Code Exchange) ā¤¸ā¤Žā¤°āĨā¤Ĩā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "oauth_enable_description": "OAuth ⤏āĨ‡ ⤞āĨ‰ā¤—ā¤ŋ⤍ ⤕⤰āĨ‡ā¤‚", "oauth_mobile_redirect_uri": "ā¤ŽāĨ‹ā¤Ŧā¤žā¤‡ā¤˛ ⤰āĨ€ā¤Ąā¤žā¤¯ā¤°āĨ‡ā¤•āĨā¤Ÿ ⤝āĨ‚ā¤†ā¤°ā¤†ā¤ˆ", "oauth_mobile_redirect_uri_override": "ā¤ŽāĨ‹ā¤Ŧā¤žā¤‡ā¤˛ ⤰āĨ€ā¤Ąā¤žā¤¯ā¤°āĨ‡ā¤•āĨā¤Ÿ ⤝āĨ‚ā¤†ā¤°ā¤†ā¤ˆ ⤓ā¤ĩā¤°ā¤°ā¤žā¤‡ā¤Ą", @@ -269,10 +296,14 @@ "password_settings_description": "ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ⤞āĨ‰ā¤—ā¤ŋ⤍ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "paths_validated_successfully": "⤏⤭āĨ€ ā¤Ēā¤Ĩ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤Žā¤žā¤¨āĨā¤¯ ⤕ā¤ŋā¤ ā¤—ā¤", "person_cleanup_job": "ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤏ā¤Ģā¤ŧā¤žā¤ˆ", + "queue_details": "ā¤ĒāĨā¤°ā¤•āĨā¤°ā¤ŋā¤¯ā¤ž ā¤•ā¤¤ā¤žā¤° ā¤•ā¤ž ā¤ĩā¤ŋā¤ĩ⤰⤪", + "queues": "ā¤•ā¤žā¤°āĨā¤¯ ā¤•ā¤¤ā¤žā¤°", + "queues_page_description": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• ā¤•ā¤žā¤°āĨā¤¯ ā¤•ā¤¤ā¤žā¤° ā¤ĒāĨ‡ā¤œ", "quota_size_gib": "⤕āĨ‹ā¤Ÿā¤ž ā¤†ā¤•ā¤žā¤° (GiB)", "refreshing_all_libraries": "⤏⤭āĨ€ ā¤ĒāĨā¤¸āĨā¤¤ā¤•ā¤žā¤˛ā¤¯āĨ‹ā¤‚ ⤕āĨ‹ ā¤¤ā¤žā¤œā¤ŧā¤ž ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ", - "registration": "ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ ā¤Ēā¤‚ā¤œāĨ€ā¤•⤰⤪", + "registration": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• ā¤Ēā¤‚ā¤œāĨ€ā¤•⤰⤪", "registration_description": "⤚āĨ‚⤂⤕ā¤ŋ ⤆ā¤Ē ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ā¤Ē⤰ ā¤Ēā¤šā¤˛āĨ‡ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤šāĨˆā¤‚, ⤇⤏⤞ā¤ŋā¤ ⤆ā¤Ē⤕āĨ‹ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ ⤕āĨ‡ ⤰āĨ‚ā¤Ē ā¤ŽāĨ‡ā¤‚ ⤍ā¤ŋ⤝āĨā¤•āĨā¤¤ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤žā¤ā¤—ā¤ž ⤔⤰ ⤆ā¤Ē ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤¨ā¤ŋ⤕ ā¤•ā¤žā¤°āĨā¤¯āĨ‹ā¤‚ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤜ā¤ŋā¤ŽāĨā¤ŽāĨ‡ā¤Ļā¤žā¤° ā¤šāĨ‹ā¤‚⤗āĨ‡, ⤔⤰ ⤅⤤ā¤ŋ⤰ā¤ŋ⤕āĨā¤¤ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤆ā¤Ē⤕āĨ‡ ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ā¤Ŧā¤¨ā¤žā¤ ā¤œā¤žā¤ā¤‚ā¤—āĨ‡āĨ¤", + "remove_failed_jobs": "⤅⤏ā¤Ģ⤞ ā¤•ā¤žā¤°āĨā¤¯ ā¤šā¤Ÿā¤žā¤ā¤", "require_password_change_on_login": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤕āĨ‹ ā¤Ēā¤šā¤˛āĨ‡ ⤞āĨ‰ā¤—ā¤ŋ⤍ ā¤Ē⤰ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ŧā¤Ļ⤞⤍āĨ‡ ⤕āĨ€ ⤆ā¤ĩā¤ļāĨā¤¯ā¤•ā¤¤ā¤ž ā¤šāĨˆ", "reset_settings_to_default": "⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ⤕āĨ‹ ā¤Ąā¤ŋā¤Ģā¤ŧāĨ‰ā¤˛āĨā¤Ÿ ā¤Ē⤰ ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", "reset_settings_to_recent_saved": "⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ⤕āĨ‹ ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤šāĨ‡ā¤œāĨ€ ā¤—ā¤ˆ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤Ē⤰ ⤰āĨ€ā¤¸āĨ‡ā¤Ÿ ⤕⤰āĨ‡ā¤‚", @@ -285,8 +316,10 @@ "server_public_users_description": "ā¤¸ā¤žā¤ā¤ž ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤜āĨ‹ā¤Ąā¤ŧ⤤āĨ‡ ā¤¸ā¤Žā¤¯ ⤏⤭āĨ€ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ (ā¤¨ā¤žā¤Ž ⤔⤰ ā¤ˆā¤ŽāĨ‡ā¤˛) ⤕āĨ€ ⤏āĨ‚ā¤šāĨ€ ā¤Ļā¤ŋā¤–ā¤žā¤ˆ ā¤œā¤žā¤¤āĨ€ ā¤šāĨˆāĨ¤ ⤝ā¤Ļā¤ŋ ā¤¯ā¤š ā¤ĩā¤ŋ⤕⤞āĨā¤Ē ⤅⤕āĨā¤ˇā¤Ž ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ, ⤤āĨ‹ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤏āĨ‚ā¤šāĨ€ ⤕āĨ‡ā¤ĩ⤞ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ (ā¤ā¤Ąā¤Žā¤ŋ⤍) ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤šāĨ‹ā¤—āĨ€āĨ¤", "server_settings": "⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", "server_settings_description": "⤏⤰āĨā¤ĩ⤰ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", + "server_stats_page_description": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• (Admin) ⤏⤰āĨā¤ĩ⤰ ā¤†ā¤ā¤•ā¤Ąā¤ŧāĨ‡ ā¤ĒāĨ‡ā¤œ", "server_welcome_message": "⤏āĨā¤ĩā¤žā¤—ā¤¤ ⤏⤂ā¤ĻāĨ‡ā¤ļ", "server_welcome_message_description": "ā¤ā¤• ⤏⤂ā¤ĻāĨ‡ā¤ļ ⤜āĨ‹ ⤞āĨ‰ā¤—ā¤ŋ⤍ ā¤ĒāĨƒā¤ˇāĨā¤  ā¤Ē⤰ ā¤ĒāĨā¤°ā¤Ļ⤰āĨā¤ļā¤ŋ⤤ ā¤šāĨ‹ā¤¤ā¤ž ā¤šāĨˆāĨ¤", + "settings_page_description": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• (Admin) ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤ĒāĨ‡ā¤œ", "sidecar_job": "ā¤¸ā¤žā¤‡ā¤Ąā¤•ā¤žā¤° ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž", "sidecar_job_description": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤏āĨ‡ ā¤¸ā¤žā¤‡ā¤Ąā¤•ā¤žā¤° ā¤ŽāĨ‡ā¤Ÿā¤žā¤ĄāĨ‡ā¤Ÿā¤ž ⤖āĨ‹ā¤œāĨ‡ā¤‚ ā¤¯ā¤ž ⤏ā¤ŋ⤂⤕āĨā¤°ā¤¨ā¤žā¤‡ā¤œā¤ŧ ⤕⤰āĨ‡ā¤‚", "slideshow_duration_description": "ā¤ĒāĨā¤°ā¤¤āĨā¤¯āĨ‡ā¤• ⤛ā¤ĩā¤ŋ ⤕āĨ‹ ā¤ĒāĨā¤°ā¤Ļ⤰āĨā¤ļā¤ŋ⤤ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏āĨ‡ā¤•ā¤‚ā¤Ą ⤕āĨ€ ⤏⤂⤖āĨā¤¯ā¤ž", @@ -405,6 +438,8 @@ "user_restore_scheduled_removal": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤕āĨ‹ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚ - {date, date, long} ā¤Ē⤰ ā¤šā¤Ÿā¤žā¤¯ā¤ž ā¤œā¤žā¤¨ā¤ž ⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ā¤šāĨˆ", "user_settings": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗", "user_settings_description": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", + "user_successfully_removed": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž {email} ⤕āĨ‹ ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆāĨ¤", + "users_page_description": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤• (Admin) ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤ĒāĨ‡ā¤œ", "version_check_enabled_description": "⤍⤈ ⤰ā¤ŋ⤞āĨ€ā¤œā¤ŧ ⤕āĨ€ ā¤œā¤žā¤ā¤š ⤕āĨ‡ ⤞ā¤ŋā¤ GitHub ā¤Ē⤰ ⤆ā¤ĩ⤧ā¤ŋ⤕ ⤅⤍āĨā¤°āĨ‹ā¤§ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰āĨ‡ā¤‚", "version_check_implications": "⤏⤂⤏āĨā¤•⤰⤪ ā¤œā¤žā¤ā¤š ⤏āĨā¤ĩā¤ŋā¤§ā¤ž github.com ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ⤆ā¤ĩ⤧ā¤ŋ⤕ ā¤¸ā¤‚ā¤šā¤žā¤° ā¤Ē⤰ ⤍ā¤ŋ⤰āĨā¤­ā¤° ⤕⤰⤤āĨ€ ā¤šāĨˆ", "version_check_settings": "⤏⤂⤏āĨā¤•⤰⤪ ⤚āĨ‡ā¤•", @@ -416,6 +451,9 @@ "admin_password": "ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ē⤕ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "administration": "ā¤ĒāĨā¤°ā¤ļā¤žā¤¸ā¤¨", "advanced": "ā¤ĩā¤ŋ⤕⤏ā¤ŋ⤤", + "advanced_settings_clear_image_cache": "ā¤‡ā¤ŽāĨ‡ā¤œ ⤕āĨˆā¤ļ (cache) ā¤¸ā¤žā¤Ģā¤ŧ ⤕⤰āĨ‡ā¤‚", + "advanced_settings_clear_image_cache_error": "ā¤‡ā¤ŽāĨ‡ā¤œ ⤕āĨˆā¤ļ (cache) ā¤¸ā¤žā¤Ģā¤ŧ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤ž", + "advanced_settings_clear_image_cache_success": "{size} ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤¸ā¤žā¤Ģā¤ŧ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "advanced_settings_enable_alternate_media_filter_subtitle": "⤏ā¤ŋ⤂⤕ ⤕āĨ‡ ā¤ĻāĨŒā¤°ā¤žā¤¨ ā¤ĩāĨˆā¤•⤞āĨā¤Ēā¤ŋ⤕ ā¤Žā¤žā¤¨ā¤Ļā¤‚ā¤ĄāĨ‹ā¤‚ ⤕āĨ‡ ā¤†ā¤§ā¤žā¤° ā¤Ē⤰ ā¤ŽāĨ€ā¤Ąā¤ŋā¤¯ā¤ž ⤕āĨ‹ ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤇⤏ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚āĨ¤ ⤇⤏āĨ‡ ⤕āĨ‡ā¤ĩ⤞ ⤤⤭āĨ€ ā¤†ā¤œā¤ŧā¤Žā¤žā¤ā¤ ⤜ā¤Ŧ ⤆ā¤Ē⤕āĨ‹ ⤐ā¤Ē ā¤ĻāĨā¤ĩā¤žā¤°ā¤ž ⤏⤭āĨ€ ā¤ā¤˛āĨā¤Ŧā¤ŽāĨ‹ā¤‚ ā¤•ā¤ž ā¤Ēā¤¤ā¤ž ā¤˛ā¤—ā¤žā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤Žā¤¸āĨā¤¯ā¤ž ā¤šāĨ‹āĨ¤", "advanced_settings_enable_alternate_media_filter_title": "[ā¤ĒāĨā¤°ā¤¯āĨ‹ā¤—ā¤žā¤¤āĨā¤Žā¤•] ā¤ĩāĨˆā¤•⤞āĨā¤Ēā¤ŋ⤕ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏ā¤ŋ⤂⤕ ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚", "advanced_settings_log_level_title": "⤞āĨ‰ā¤— ⤏āĨā¤¤ā¤°:{level}", @@ -452,10 +490,12 @@ "album_remove_user": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤šā¤Ÿā¤žā¤ā¤‚?", "album_remove_user_confirmation": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ {user} ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", "album_search_not_found": "⤆ā¤Ē⤕āĨ€ ⤖āĨ‹ā¤œ ⤏āĨ‡ ā¤ŽāĨ‡ā¤˛ ā¤–ā¤žā¤¤ā¤ž ⤕āĨ‹ā¤ˆ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤ž", + "album_selected": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤚āĨā¤¨ā¤ž ā¤—ā¤¯ā¤ž", "album_share_no_users": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤍āĨ‡ ā¤¯ā¤š ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏⤭āĨ€ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤ā¤ž ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤šāĨˆ ā¤¯ā¤ž ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ā¤¸ā¤žā¤ā¤ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "album_summary": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤¸ā¤žā¤°ā¤žā¤‚ā¤ļ", "album_updated": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "album_updated_setting_description": "⤜ā¤Ŧ ⤕ā¤ŋ⤏āĨ€ ā¤¸ā¤žā¤ā¤ž ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤍⤈ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤ ā¤šāĨ‹ā¤‚ ⤤āĨ‹ ā¤ā¤• ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤ž ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ⤕⤰āĨ‡ā¤‚", + "album_upload_assets": "⤅ā¤Ē⤍āĨ‡ ⤕⤂ā¤ĒāĨā¤¯āĨ‚ā¤Ÿā¤° ⤏āĨ‡ ā¤ŽāĨ€ā¤Ąā¤ŋā¤¯ā¤ž ā¤Ģā¤ŧā¤žā¤‡ā¤˛āĨ‡ā¤‚ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚ ⤔⤰ ⤉⤍āĨā¤šāĨ‡ā¤‚ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", "album_user_left": "ā¤Ŧā¤žā¤¯ā¤žā¤ {album}", "album_user_removed": "{user} ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "album_viewer_appbar_delete_confirm": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤇⤏ ā¤ā¤˛āĨā¤Ŧā¤Ž ⤕āĨ‹ ⤅ā¤Ē⤍āĨ‡ ā¤–ā¤žā¤¤āĨ‡ ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", @@ -468,14 +508,16 @@ "album_viewer_page_share_add_users": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤜āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", "album_with_link_access": "⤞ā¤ŋ⤂⤕ ā¤ĩā¤žā¤˛āĨ‡ ⤕ā¤ŋ⤏āĨ€ ⤭āĨ€ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤇⤏ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ā¤ĻāĨ‡ā¤–⤍āĨ‡ ā¤ĻāĨ‡ā¤‚āĨ¤", "albums": "ā¤ā¤˛ā¤Ŧā¤Ž", - "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albums}}", + "albums_count": "{count, plural, one {{count, number} ā¤ā¤˛āĨā¤Ŧā¤Ž} other {{count, number} ā¤ā¤˛āĨā¤Ŧā¤Ž}}", "albums_default_sort_order": "ā¤Ąā¤ŋā¤Ģā¤ŧāĨ‰ā¤˛āĨā¤Ÿ ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏āĨ‰ā¤°āĨā¤Ÿ ⤕āĨā¤°ā¤Ž", "albums_default_sort_order_description": "⤍⤝āĨ‡ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ŧā¤¨ā¤žā¤¤āĨ‡ ā¤¸ā¤Žā¤¯ ⤆⤰⤂⤭ā¤ŋ⤕ ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤏āĨ‰ā¤°āĨā¤Ÿ ⤕āĨā¤°ā¤ŽāĨ¤", "albums_feature_description": "ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤•ā¤ž ⤏⤂⤗āĨā¤°ā¤š ⤜ā¤ŋ⤏āĨ‡ ⤅⤍āĨā¤¯ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤žā¤“⤂ ⤕āĨ‡ ā¤¸ā¤žā¤Ĩ ā¤¸ā¤žā¤ā¤ž ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆāĨ¤", "albums_on_device_count": "ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤Ē⤰ ā¤ā¤˛āĨā¤Ŧā¤Ž ({count})", + "albums_selected": "{count, plural, one {# ā¤ā¤˛āĨā¤Ŧā¤Ž ⤚āĨā¤¨ā¤ž ā¤—ā¤¯ā¤ž} other {# ā¤ā¤˛āĨā¤Ŧā¤Ž ⤚āĨā¤¨āĨ‡ ā¤—ā¤}}", "all": "⤏⤭āĨ€", "all_albums": "⤏⤭āĨ€ ā¤ā¤˛ā¤Ŧā¤Ž", "all_people": "⤏⤭āĨ€ ⤞āĨ‹ā¤—", + "all_photos": "⤏⤭āĨ€ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹", "all_videos": "⤏⤭āĨ€ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹", "allow_dark_mode": "ā¤Ąā¤žā¤°āĨā¤• ā¤ŽāĨ‹ā¤Ą ⤕āĨ€ ⤅⤍āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚", "allow_edits": "⤏⤂ā¤Ēā¤žā¤Ļ⤍ ⤕āĨ€ ⤅⤍āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚", @@ -483,6 +525,9 @@ "allow_public_user_to_upload": "ā¤¸ā¤žā¤°āĨā¤ĩ⤜⤍ā¤ŋ⤕ ⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ⤕āĨ‹ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ⤕āĨ€ ⤅⤍āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚", "allowed": "⤅⤍āĨā¤Žā¤¤", "alt_text_qr_code": "⤕āĨā¤¯āĨ‚⤆⤰ ⤕āĨ‹ā¤Ą ⤛ā¤ĩā¤ŋ", + "always_keep": "ā¤šā¤ŽāĨ‡ā¤ļā¤ž ⤰⤖āĨ‡ā¤‚", + "always_keep_photos_hint": "“ā¤ĢāĨā¤°āĨ€ ⤅ā¤Ē ⤏āĨā¤ĒāĨ‡ā¤¸â€ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤍āĨ‡ ā¤Ē⤰ ⤇⤏ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤕āĨ€ ⤏⤭āĨ€ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤Ŧ⤍āĨ€ ā¤°ā¤šāĨ‡ā¤‚⤗āĨ€āĨ¤", + "always_keep_videos_hint": "“ā¤ĢāĨā¤°āĨ€ ⤅ā¤Ē ⤏āĨā¤ĒāĨ‡ā¤¸â€ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰⤍āĨ‡ ā¤Ē⤰ ⤇⤏ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤕āĨ€ ⤏⤭āĨ€ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤Ŧ⤍āĨ€ ā¤°ā¤šāĨ‡ā¤‚⤗āĨ€āĨ¤", "anti_clockwise": "ā¤ĩā¤žā¤Žā¤žā¤ĩ⤰āĨā¤¤", "api_key": "ā¤ā¤ĒāĨ€ā¤†ā¤ˆ ⤕āĨ€", "api_key_description": "ā¤¯ā¤š ⤕āĨ€ ⤕āĨ‡ā¤ĩ⤞ ā¤ā¤• ā¤Ŧā¤žā¤° ā¤Ļā¤ŋā¤–ā¤žā¤ˆ ā¤œā¤žā¤ā¤—āĨ€āĨ¤ ā¤ĩā¤ŋā¤‚ā¤ĄāĨ‹ ā¤Ŧ⤂ā¤Ļ ⤕⤰⤍āĨ‡ ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ⤕āĨƒā¤Ēā¤¯ā¤ž ⤇⤏āĨ‡ ⤕āĨ‰ā¤ĒāĨ€ ā¤•ā¤°ā¤¨ā¤ž ⤏āĨā¤¨ā¤ŋā¤ļāĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚āĨ¤āĨ¤", @@ -509,10 +554,12 @@ "archived_count": "{count, plural, other {# ⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤕ā¤ŋā¤ ā¤—ā¤}}", "are_these_the_same_person": "⤕āĨā¤¯ā¤ž ⤝āĨ‡ ā¤ĩā¤šāĨ€ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ā¤šāĨˆā¤‚?", "are_you_sure_to_do_this": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤¸āĨā¤¤ā¤ĩ ā¤ŽāĨ‡ā¤‚ ⤇⤏āĨ‡ ā¤•ā¤°ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", + "array_field_not_fully_supported": "Array ā¤Ģā¤ŧāĨ€ā¤˛āĨā¤Ą ⤕āĨ‡ ⤞ā¤ŋā¤ JSON ⤕āĨ‹ ā¤ŽāĨˆā¤¨āĨā¤¯āĨā¤…⤞ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ⤏⤂ā¤Ēā¤žā¤Ļā¤ŋ⤤ ā¤•ā¤°ā¤¨ā¤ž ⤆ā¤ĩā¤ļāĨā¤¯ā¤• ā¤šāĨˆ", "asset_action_delete_err_read_only": "⤕āĨ‡ā¤ĩ⤞ ā¤Ēā¤ĸā¤ŧ⤍āĨ‡ ⤝āĨ‹ā¤—āĨā¤¯ ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(⤓⤂) ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¯ā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž, ⤛āĨ‹ā¤Ąā¤ŧā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ", "asset_action_share_err_offline": "⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨ ā¤Ē⤰ā¤ŋ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤ā¤) ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ā¤¨ā¤šāĨ€ā¤‚ ⤕āĨ€ ā¤œā¤ž ⤏⤕⤤āĨ€, ⤛āĨ‹ā¤Ąā¤ŧāĨ€ ā¤œā¤ž ā¤°ā¤šāĨ€ ā¤šāĨˆ", "asset_added_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "asset_adding_to_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆâ€Ļ", + "asset_created": "ā¤ā¤¸āĨ‡ā¤Ÿ ā¤Ŧā¤¨ā¤žā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "asset_description_updated": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨ ⤕⤰ ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", "asset_filename_is_offline": "ā¤ā¤¸āĨ‡ā¤Ÿ {filename} ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨ ā¤šāĨˆ", "asset_has_unassigned_faces": "ā¤ā¤¸āĨ‡ā¤Ÿ ā¤ŽāĨ‡ā¤‚ ⤅⤍ā¤ŋ⤰āĨā¤§ā¤žā¤°ā¤ŋ⤤ ⤚āĨ‡ā¤šā¤°āĨ‡ ā¤šāĨˆā¤‚", @@ -525,6 +572,9 @@ "asset_list_layout_sub_title": "⤞āĨ‡ā¤†ā¤‰ā¤Ÿ", "asset_list_settings_subtitle": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤗āĨā¤°ā¤ŋā¤Ą ⤞āĨ‡ā¤†ā¤‰ā¤Ÿ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸", "asset_list_settings_title": "⤚ā¤ŋ⤤āĨā¤° ⤕āĨ€ ā¤œā¤žā¤˛āĨ€", + "asset_not_found_on_device_android": "ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤Ē⤰ ā¤ā¤¸āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤ž", + "asset_not_found_on_device_ios": "ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤Ē⤰ ā¤ā¤¸āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤žāĨ¤ ⤝ā¤Ļā¤ŋ ⤆ā¤Ē iCloud ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚, ⤤āĨ‹ iCloud ā¤ŽāĨ‡ā¤‚ ā¤–ā¤°ā¤žā¤Ŧ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤šāĨ‹ā¤¨āĨ‡ ⤕āĨ‡ ā¤•ā¤žā¤°ā¤Ŗ ā¤ā¤¸āĨ‡ā¤Ÿ ⤤⤕ ā¤Ēā¤šāĨā¤ā¤šā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž", + "asset_not_found_on_icloud": "iCloud ā¤Ē⤰ ā¤ā¤¸āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤žāĨ¤ iCloud ā¤ŽāĨ‡ā¤‚ ā¤–ā¤°ā¤žā¤Ŧ ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤šāĨ‹ā¤¨āĨ‡ ⤕āĨ‡ ā¤•ā¤žā¤°ā¤Ŗ ā¤ā¤¸āĨ‡ā¤Ÿ ⤤⤕ ā¤Ēā¤šāĨā¤ā¤šā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž", "asset_offline": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨", "asset_offline_description": "ā¤¯ā¤š ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ⤑ā¤Ģā¤ŧā¤˛ā¤žā¤‡ā¤¨ ā¤šāĨˆāĨ¤", "asset_restored_successfully": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ(ā¤¯ā¤žā¤) ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤ĒāĨā¤¨ā¤°āĨā¤¸āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ⤕āĨ€ ā¤—ā¤ˆā¤‚", @@ -576,7 +626,7 @@ "backup_album_selection_page_select_albums": "ā¤ā¤˛āĨā¤Ŧā¤Ž ⤚āĨā¤¨āĨ‡ā¤‚", "backup_album_selection_page_selection_info": "⤚⤝⤍ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€", "backup_album_selection_page_total_assets": "⤕āĨā¤˛ ⤅ā¤ĻāĨā¤ĩā¤ŋ⤤āĨ€ā¤¯ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤", - "backup_albums_sync": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤•ā¤ž ⤤āĨā¤˛āĨā¤¯ā¤•ā¤žā¤˛ā¤¨", + "backup_albums_sync": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤•ā¤ž ⤏ā¤ŋ⤂⤕āĨā¤°āĨ‹ā¤¨ā¤žā¤‡ā¤œā¤ŧāĨ‡ā¤ļ⤍", "backup_all": "⤏⤭āĨ€", "backup_background_service_backup_failed_message": "⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤•ā¤ž ā¤ŦāĨˆā¤•⤅ā¤Ē ⤞āĨ‡ā¤¨āĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤ĩā¤ŋā¤Ģ⤞. ā¤ĒāĨā¤¨ā¤ƒ ā¤ĒāĨā¤°ā¤¯ā¤žā¤¸ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆâ€Ļ", "backup_background_service_complete_notification": "ā¤ā¤¸āĨ‡ā¤Ÿ ā¤•ā¤ž ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ĒāĨ‚ā¤°ā¤ž ā¤šāĨā¤†", @@ -637,6 +687,7 @@ "backup_options_page_title": "ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", "backup_setting_subtitle": "ā¤ĒāĨƒā¤ˇāĨā¤ ā¤­āĨ‚ā¤Žā¤ŋ ⤔⤰ ⤅⤗āĨā¤°ā¤­āĨ‚ā¤Žā¤ŋ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ā¤ĒāĨā¤°ā¤Ŧ⤂⤧ā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "backup_settings_subtitle": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤¸ā¤‚ā¤­ā¤žā¤˛āĨ‡ā¤‚", + "backup_upload_details_page_more_details": "⤅⤧ā¤ŋ⤕ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤟āĨˆā¤Ē ⤕⤰āĨ‡ā¤‚", "backward": "ā¤Ēā¤ŋā¤›ā¤˛ā¤ž", "biometric_auth_enabled": "ā¤Ŧā¤žā¤¯āĨ‹ā¤ŽāĨ‡ā¤ŸāĨā¤°ā¤ŋ⤕ ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ⤏⤕āĨā¤ˇā¤Ž", "biometric_locked_out": "⤆ā¤Ē ā¤Ŧā¤žā¤¯āĨ‹ā¤ŽāĨ‡ā¤ŸāĨā¤°ā¤ŋ⤕ ā¤ĒāĨā¤°ā¤Žā¤žā¤ŖāĨ€ā¤•⤰⤪ ⤏āĨ‡ ā¤Ŧā¤žā¤šā¤° ā¤šāĨˆā¤‚", @@ -695,6 +746,8 @@ "change_password_form_password_mismatch": "ā¤¸ā¤žā¤‚ā¤•āĨ‡ā¤¤ā¤ŋ⤕ ā¤ļā¤ŦāĨā¤Ļ ā¤ŽāĨ‡ā¤˛ ā¤¨ā¤šāĨ€ā¤‚ ā¤–ā¤žā¤¤āĨ‡", "change_password_form_reenter_new_password": "ā¤¨ā¤¯ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤ĒāĨā¤¨ā¤ƒ ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", "change_pin_code": "ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", + "change_trigger": "⤟āĨā¤°ā¤ŋ⤗⤰ ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", + "change_trigger_prompt": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ā¤ĩā¤žā¤•ā¤ˆ ⤟āĨā¤°ā¤ŋ⤗⤰ ā¤Ŧā¤Ļā¤˛ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚? ⤇⤏⤏āĨ‡ ⤏⤭āĨ€ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ā¤ā¤•āĨā¤ļ⤍ ⤔⤰ ā¤Ģā¤ŧā¤ŋ⤞āĨā¤Ÿā¤° ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤ ā¤œā¤žā¤ā¤ā¤—āĨ‡āĨ¤", "change_your_password": "⤅ā¤Ēā¤¨ā¤ž ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ŧā¤Ļ⤞āĨ‡ā¤‚", "changed_visibility_successfully": "ā¤ĻāĨƒā¤ļāĨā¤¯ā¤¤ā¤ž ⤏ā¤Ģā¤˛ā¤¤ā¤žā¤ĒāĨ‚⤰āĨā¤ĩ⤕ ā¤Ē⤰ā¤ŋā¤ĩ⤰āĨā¤¤ā¤ŋ⤤", "charging": "ā¤šā¤žā¤°āĨā¤œā¤ŋ⤂⤗", @@ -703,8 +756,21 @@ "check_corrupt_asset_backup_button": "ā¤œā¤žā¤ā¤š ⤕⤰āĨ‡ā¤‚", "check_corrupt_asset_backup_description": "ā¤¯ā¤š ā¤œā¤žā¤ā¤š ⤕āĨ‡ā¤ĩ⤞ ā¤ĩā¤žā¤ˆ-ā¤Ģā¤ŧā¤žā¤ˆ ā¤Ē⤰ ā¤šāĨ€ ⤕⤰āĨ‡ā¤‚ ⤔⤰ ⤏⤭āĨ€ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤•ā¤ž ā¤ŦāĨˆā¤•⤅ā¤Ē ⤞āĨ‡ā¤¨āĨ‡ ⤕āĨ‡ ā¤Ŧā¤žā¤Ļ ā¤šāĨ€ ⤕⤰āĨ‡ā¤‚āĨ¤ ⤇⤏ ā¤ĒāĨā¤°ā¤•āĨā¤°ā¤ŋā¤¯ā¤ž ā¤ŽāĨ‡ā¤‚ ⤕āĨā¤› ā¤Žā¤ŋ⤍⤟ ⤞⤗ ⤏⤕⤤āĨ‡ ā¤šāĨˆā¤‚āĨ¤", "check_logs": "⤞āĨ‰ā¤— ā¤œā¤žā¤‚ā¤šāĨ‡ā¤‚", + "checksum": "⤚āĨ‡ā¤•ā¤¸ā¤Ž (checksum)", "choose_matching_people_to_merge": "ā¤Žā¤°āĨā¤œ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Žā¤ŋ⤞⤤āĨ‡-⤜āĨā¤˛ā¤¤āĨ‡ ⤞āĨ‹ā¤—āĨ‹ā¤‚ ⤕āĨ‹ ⤚āĨā¤¨āĨ‡ā¤‚", "city": "ā¤ļā¤šā¤°", + "cleanup_confirm_description": "Immich ⤍āĨ‡ {date} ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤Ŧā¤¨ā¤žā¤ ā¤—ā¤ {count} ā¤ā¤¸āĨ‡ā¤Ÿ ⤏⤰āĨā¤ĩ⤰ ā¤Ē⤰ ⤏āĨā¤°ā¤•āĨā¤ˇā¤ŋ⤤ ⤰āĨ‚ā¤Ē ⤏āĨ‡ ā¤ŦāĨˆā¤•⤅ā¤Ē ⤕ā¤ŋā¤ ā¤šāĨā¤ ā¤Ēā¤žā¤ ā¤šāĨˆā¤‚āĨ¤ ⤕āĨā¤¯ā¤ž ⤇⤏ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ⤉⤍⤕āĨ€ ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ā¤ĒāĨā¤°ā¤¤ā¤ŋā¤¯ā¤žā¤ ā¤šā¤Ÿā¤žā¤ˆ ā¤œā¤žā¤ā¤?", + "cleanup_confirm_prompt_title": "⤕āĨā¤¯ā¤ž ⤇⤏ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤ā¤?", + "cleanup_deleted_assets": "ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤕āĨ‡ ⤟āĨā¤°āĨˆā¤ļ ā¤ŽāĨ‡ā¤‚ {count} ā¤ā¤¸āĨ‡ā¤Ÿ ⤭āĨ‡ā¤œ ā¤Ļā¤ŋā¤ ā¤—ā¤", + "cleanup_deleting": "⤟āĨā¤°āĨˆā¤ļ ā¤ŽāĨ‡ā¤‚ ⤭āĨ‡ā¤œā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆâ€Ļ", + "cleanup_found_assets": "{count} ā¤ŦāĨˆā¤•⤅ā¤Ē ⤕ā¤ŋā¤ ā¤—ā¤ ⤐⤏āĨ‡ā¤Ÿ ā¤Žā¤ŋ⤞āĨ‡", + "cleanup_found_assets_with_size": "{count} ā¤ŦāĨˆā¤•⤅ā¤Ē ⤕ā¤ŋā¤ ā¤—ā¤ ⤐⤏āĨ‡ā¤Ÿ ā¤Žā¤ŋ⤞āĨ‡ ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud ⤕āĨ‡ ā¤ļāĨ‡ā¤¯ā¤° ⤕ā¤ŋā¤ ā¤—ā¤ ā¤ā¤˛āĨā¤Ŧā¤Ž ⤏āĨā¤•āĨˆā¤¨ ā¤ŽāĨ‡ā¤‚ ā¤ļā¤žā¤Žā¤ŋ⤞ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆā¤‚", + "cleanup_no_assets_found": "⤊ā¤Ē⤰ ā¤Ļā¤ŋā¤ ā¤—ā¤ ā¤Žā¤žā¤¨ā¤Ļā¤‚ā¤ĄāĨ‹ā¤‚ ⤏āĨ‡ ā¤ŽāĨ‡ā¤˛ ā¤–ā¤žā¤¨āĨ‡ ā¤ĩā¤žā¤˛āĨ‡ ⤕āĨ‹ā¤ˆ ⤐⤏āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋ⤞āĨ‡āĨ¤ ‘ā¤ĢāĨā¤°āĨ€ ⤉ā¤Ē ⤏āĨā¤ĒāĨ‡ā¤¸â€™ ⤕āĨ‡ā¤ĩ⤞ ⤉⤍āĨā¤šāĨ€ā¤‚ ⤐⤏āĨ‡ā¤Ÿ ⤕āĨ‹ ā¤šā¤Ÿā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ ⤜ā¤ŋā¤¨ā¤•ā¤ž ā¤ŦāĨˆā¤•⤅ā¤Ē ⤏⤰āĨā¤ĩ⤰ ā¤Ē⤰ ⤞ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", + "cleanup_preview_title": "ā¤šā¤Ÿā¤žā¤ ā¤œā¤žā¤¨āĨ‡ ā¤ĩā¤žā¤˛āĨ‡ ⤐⤏āĨ‡ā¤Ÿ ({count})", + "cleanup_step3_description": "⤤ā¤ŋā¤Ĩā¤ŋ ⤔⤰ ⤏āĨā¤°ā¤•āĨā¤ˇā¤ŋ⤤ ⤰⤖⤍āĨ‡ ⤕āĨ€ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ⤕āĨ‡ ⤅⤍āĨā¤¸ā¤žā¤° ā¤ŦāĨˆā¤•⤅ā¤Ē ⤐⤏āĨ‡ā¤Ÿ ⤏āĨā¤•āĨˆā¤¨ ⤕⤰āĨ‡ā¤‚āĨ¤", + "cleanup_step4_summary": "⤆ā¤Ē⤕āĨ‡ ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ⤏āĨ‡ ā¤šā¤Ÿā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ {date} ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤Ŧā¤¨ā¤žā¤ ā¤—ā¤ {count} ⤐⤏āĨ‡ā¤ŸāĨ¤ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ Immich ⤐ā¤Ē ā¤ŽāĨ‡ā¤‚ ā¤ĻāĨ‡ā¤–āĨ‡ ā¤œā¤ž ⤏⤕āĨ‡ā¤‚⤗āĨ‡āĨ¤", + "cleanup_trash_hint": "⤏āĨā¤ŸāĨ‹ā¤°āĨ‡ā¤œ ⤏āĨā¤ĒāĨ‡ā¤¸ ā¤ĒāĨ‚⤰āĨ€ ā¤¤ā¤°ā¤š ā¤ĩā¤žā¤Ē⤏ ā¤Ēā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤗āĨˆā¤˛ā¤°āĨ€ ⤐ā¤Ē ⤖āĨ‹ā¤˛āĨ‡ā¤‚ ⤔⤰ ⤟āĨā¤°āĨˆā¤ļ ā¤–ā¤žā¤˛āĨ€ ⤕⤰āĨ‡ā¤‚", "clear": "⤏āĨā¤Ē⤎āĨā¤Ÿ", "clear_all": "⤏⤭āĨ€ ā¤¸ā¤žā¤Ģ ⤕⤰āĨ‡ā¤‚", "clear_all_recent_searches": "⤏⤭āĨ€ ā¤šā¤žā¤˛ā¤ŋā¤¯ā¤ž ⤖āĨ‹ā¤œāĨ‡ā¤‚ ā¤¸ā¤žā¤Ģā¤ŧ ⤕⤰āĨ‡ā¤‚", @@ -716,8 +782,10 @@ "client_cert_import": "ā¤†ā¤¯ā¤žā¤¤", "client_cert_import_success_msg": "⤕āĨā¤˛ā¤žā¤‡ā¤‚ā¤Ÿ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤†ā¤¯ā¤žā¤¤ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", "client_cert_invalid_msg": "ā¤…ā¤Žā¤žā¤¨āĨā¤¯ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤¯ā¤ž ⤗⤞⤤ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", + "client_cert_password_message": "⤇⤏ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", + "client_cert_password_title": "ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "client_cert_remove_msg": "⤕āĨā¤˛ā¤žā¤‡ā¤‚ā¤Ÿ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤šā¤Ÿā¤ž ā¤Ļā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž ā¤šāĨˆ", - "client_cert_subtitle": "⤕āĨ‡ā¤ĩ⤞ PKCS12 (.p12, .pfx) ā¤ĒāĨā¤°ā¤žā¤°āĨ‚ā¤Ē ā¤•ā¤ž ā¤¸ā¤Žā¤°āĨā¤Ĩ⤍ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆāĨ¤ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤†ā¤¯ā¤žā¤¤/⤍ā¤ŋā¤•ā¤žā¤˛ā¤¨ā¤ž ⤕āĨ‡ā¤ĩ⤞ ⤞āĨ‰ā¤—ā¤ŋ⤍ ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤šāĨ€ ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤šāĨˆāĨ¤", + "client_cert_subtitle": "⤕āĨ‡ā¤ĩ⤞ PKCS12 (.p12, .pfx) ā¤ĒāĨā¤°ā¤žā¤°āĨ‚ā¤Ē ā¤•ā¤ž ā¤¸ā¤Žā¤°āĨā¤Ĩ⤍ ā¤•ā¤°ā¤¤ā¤ž ā¤šāĨˆāĨ¤ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° ā¤†ā¤¯ā¤žā¤¤/⤍ā¤ŋā¤•ā¤žā¤˛ā¤¨ā¤ž ⤕āĨ‡ā¤ĩ⤞ ⤞āĨ‰ā¤—ā¤ŋ⤍ ⤏āĨ‡ ā¤Ēā¤šā¤˛āĨ‡ ā¤šāĨ€ ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤šāĨˆ", "client_cert_title": "SSL ⤕āĨā¤˛ā¤žā¤‡ā¤‚ā¤Ÿ ā¤ĒāĨā¤°ā¤Žā¤žā¤Ŗā¤Ē⤤āĨā¤° [ā¤ĒāĨā¤°ā¤žā¤¯āĨ‹ā¤—ā¤ŋ⤕]", "clockwise": "ā¤Ļ⤕āĨā¤ˇā¤ŋā¤Ŗā¤žā¤ĩ⤰āĨā¤¤", "close": "ā¤Ŧ⤂ā¤Ļ ⤕⤰āĨ‡ā¤‚", @@ -725,6 +793,7 @@ "collapse_all": "⤏⤭āĨ€ ⤕āĨ‹ ⤏⤂⤕āĨā¤šā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "color": "⤰⤂⤗", "color_theme": "⤰⤂⤗ ā¤ĨāĨ€ā¤Ž", + "command": "⤆ā¤ĻāĨ‡ā¤ļ", "comment_deleted": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪āĨ€ ā¤šā¤Ÿā¤ž ā¤ĻāĨ€ ā¤—ā¤ˆ", "comment_options": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪āĨ€ ā¤ĩā¤ŋ⤕⤞āĨā¤Ē", "comments_and_likes": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋā¤¯ā¤žā¤ ⤔⤰ ā¤Ē⤏⤂ā¤Ļ", @@ -769,6 +838,7 @@ "create_album": "ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ŧā¤¨ā¤žā¤“", "create_album_page_untitled": "ā¤ļāĨ€ā¤°āĨā¤ˇā¤•ā¤šāĨ€ā¤¨", "create_api_key": "⤐.ā¤ĒāĨ€.ā¤†ā¤ˆ. ā¤šā¤žā¤­āĨ€ ā¤Ŧā¤¨ā¤žā¤ā¤‚", + "create_first_workflow": "ā¤Ēā¤šā¤˛ā¤ž ā¤ĩ⤰āĨā¤•ā¤Ģā¤ŧāĨā¤˛āĨ‹ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_library": "ā¤˛ā¤žā¤‡ā¤ŦāĨā¤°āĨ‡ā¤°āĨ€ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_link": "⤞ā¤ŋ⤂⤕ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "create_link_to_share": "ā¤ļāĨ‡ā¤¯ā¤° ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤞ā¤ŋ⤂⤕ ā¤Ŧā¤¨ā¤žā¤ā¤‚", @@ -783,14 +853,18 @@ "create_tag": "⤟āĨˆā¤— ā¤Ŧā¤¨ā¤žā¤ā¤", "create_tag_description": "ā¤ā¤• ā¤¨ā¤¯ā¤ž ⤟āĨˆā¤— ā¤Ŧā¤¨ā¤žā¤ā¤āĨ¤ ⤍āĨ‡ā¤¸āĨā¤ŸāĨ‡ā¤Ą ⤟āĨˆā¤— ⤕āĨ‡ ⤞ā¤ŋā¤, ⤕āĨƒā¤Ēā¤¯ā¤ž ā¤Ģā¤ŧāĨ‰ā¤°ā¤ĩ⤰āĨā¤Ą ⤏āĨā¤˛āĨˆā¤ļ ā¤¸ā¤šā¤ŋ⤤ ⤟āĨˆā¤— ā¤•ā¤ž ā¤ĒāĨ‚ā¤°ā¤ž ā¤Ēā¤Ĩ ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚āĨ¤", "create_user": "⤉ā¤Ē⤝āĨ‹ā¤—⤕⤰āĨā¤¤ā¤ž ā¤Ŧā¤¨ā¤žā¤‡ā¤¯āĨ‡", + "create_workflow": "ā¤ĩ⤰āĨā¤•ā¤Ģā¤ŧāĨā¤˛āĨ‹ ā¤Ŧā¤¨ā¤žā¤ā¤‚", "created": "ā¤Ŧā¤¨ā¤žā¤¯ā¤ž", "created_at": "ā¤Ŧā¤¨ā¤žā¤¯ā¤ž ā¤Ĩā¤ž", "creating_linked_albums": "⤜āĨāĨœāĨ‡ ā¤šāĨā¤ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤Ŧā¤¨ā¤žā¤ ā¤œā¤ž ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚..āĨ¤", "crop": "ā¤›ā¤žā¤ā¤ŸāĨ‡ā¤‚", + "crop_aspect_ratio_free": "⤏āĨā¤ĩ⤤⤂⤤āĨā¤°", + "crop_aspect_ratio_original": "ā¤ŽāĨ‚⤞ ⤅⤍āĨā¤Ēā¤žā¤¤", "curated_object_page_title": "⤚āĨ€ā¤œā¤ŧāĨ‡ā¤‚", "current_device": "ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ⤉ā¤Ē⤕⤰⤪", "current_pin_code": "ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą", "current_server_address": "ā¤ĩ⤰āĨā¤¤ā¤Žā¤žā¤¨ ⤏⤰āĨā¤ĩ⤰ ā¤Ēā¤¤ā¤ž", + "custom_date": "ā¤Žā¤¨ā¤šā¤žā¤šāĨ€ ⤤ā¤ŋā¤Ĩā¤ŋ", "custom_locale": "⤕⤏āĨā¤Ÿā¤Ž ⤞āĨ‹ā¤•āĨ‡ā¤˛", "custom_locale_description": "ā¤­ā¤žā¤ˇā¤ž ⤔⤰ ⤕āĨā¤ˇāĨ‡ā¤¤āĨā¤° ⤕āĨ‡ ā¤†ā¤§ā¤žā¤° ā¤Ē⤰ ā¤Ļā¤ŋā¤¨ā¤žā¤‚ā¤• ⤔⤰ ⤏⤂⤖āĨā¤¯ā¤žā¤ā¤ ā¤ĒāĨā¤°ā¤žā¤°āĨ‚ā¤Ēā¤ŋ⤤ ⤕⤰āĨ‡ā¤‚", "custom_url": "⤕⤏āĨā¤Ÿā¤Ž URL", @@ -914,8 +988,6 @@ "editor": "⤏⤂ā¤Ēā¤žā¤Ļ⤕", "editor_close_without_save_prompt": "ā¤Ē⤰ā¤ŋā¤ĩ⤰āĨā¤¤ā¤¨ ā¤¸ā¤šāĨ‡ā¤œāĨ‡ ā¤¨ā¤šāĨ€ā¤‚ ā¤œā¤žā¤ā¤ā¤—āĨ‡", "editor_close_without_save_title": "⤏⤂ā¤Ēā¤žā¤Ļ⤕ ā¤Ŧ⤂ā¤Ļ ⤕⤰āĨ‡ā¤‚?", - "editor_crop_tool_h2_aspect_ratios": "⤆⤏āĨā¤ĒāĨ‡ā¤•āĨā¤Ÿ ⤅⤍āĨā¤Ēā¤žā¤¤", - "editor_crop_tool_h2_rotation": "⤰āĨ‹ā¤ŸāĨ‡ā¤ļ⤍", "email": "ā¤ˆā¤ŽāĨ‡ā¤˛", "email_notifications": "ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤", "empty_folder": "ā¤¯ā¤š ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤–ā¤žā¤˛āĨ€ ā¤šāĨˆ", @@ -1101,7 +1173,6 @@ "features": "ā¤ĩā¤ŋā¤ļāĨ‡ā¤ˇā¤¤ā¤žā¤ā¤", "features_in_development": "ā¤ĩā¤ŋā¤•ā¤žā¤¸ ā¤ŽāĨ‡ā¤‚ ⤏āĨā¤ĩā¤ŋā¤§ā¤žā¤ā¤", "features_setting_description": "⤐ā¤Ē ⤏āĨā¤ĩā¤ŋā¤§ā¤žā¤“ā¤‚ ā¤•ā¤ž ā¤ĒāĨā¤°ā¤Ŧ⤂⤧⤍ ⤕⤰āĨ‡ā¤‚", - "file_name": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¨ā¤žā¤Ž", "file_name_or_extension": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¨ā¤žā¤Ž ā¤¯ā¤ž ā¤ā¤•āĨā¤¸ā¤ŸāĨ‡ā¤‚ā¤ļ⤍", "file_size": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¸ā¤žā¤‡ā¤œā¤ŧ", "filename": "ā¤Ģā¤ŧā¤žā¤‡ā¤˛ ā¤•ā¤ž ā¤¨ā¤žā¤Ž", @@ -1168,7 +1239,7 @@ "home_page_delete_remote_err_local": "ā¤ĻāĨ‚⤰⤏āĨā¤Ĩ ⤚⤝⤍ ⤕āĨ‹ ā¤šā¤Ÿā¤žā¤¨āĨ‡, ⤛āĨ‹ā¤Ąā¤ŧ⤍āĨ‡ ā¤ŽāĨ‡ā¤‚ ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤", "home_page_favorite_err_local": "⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤅⤭āĨ€ ⤤⤕ ā¤Ē⤏⤂ā¤ĻāĨ€ā¤Ļā¤ž ā¤¨ā¤šāĨ€ā¤‚ ā¤Ŧā¤¨ā¤žā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤ž, ⤛āĨ‹ā¤Ąā¤ŧā¤ž ā¤œā¤ž ā¤°ā¤šā¤ž ā¤šāĨˆ", "home_page_favorite_err_partner": "⤅ā¤Ŧ ⤤⤕ ā¤Ēā¤žā¤°āĨā¤Ÿā¤¨ā¤° ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ā¤ĢāĨ‡ā¤ĩ⤰āĨ‡ā¤Ÿ ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_first_time_notice": "⤝ā¤Ļā¤ŋ ⤆ā¤Ē ā¤Ēā¤šā¤˛āĨ€ ā¤Ŧā¤žā¤° ⤇⤏ ⤐ā¤Ē ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚, ⤤āĨ‹ ⤕āĨƒā¤Ēā¤¯ā¤ž ā¤ā¤• ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ā¤˛āĨā¤Ŧā¤Ž ⤚āĨā¤¨āĨ‡ā¤‚, ā¤¤ā¤žā¤•ā¤ŋ ā¤Ÿā¤žā¤‡ā¤Žā¤˛ā¤žā¤‡ā¤¨ ā¤ŽāĨ‡ā¤‚ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ā¤Ļā¤ŋā¤–ā¤žā¤ˆ ā¤ĻāĨ‡ ⤏⤕āĨ‡ā¤‚", "home_page_locked_error_local": "⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤞āĨ‰ā¤• ⤕ā¤ŋā¤ ā¤—ā¤ ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤šāĨ€ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž, ⤛āĨ‹ā¤Ąā¤ŧā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž ā¤šāĨˆ", "home_page_locked_error_partner": "ā¤¸ā¤žā¤āĨ‡ā¤Ļā¤žā¤° ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤞āĨ‰ā¤• ⤕ā¤ŋā¤ ā¤—ā¤ ā¤Ģā¤ŧāĨ‹ā¤˛āĨā¤Ąā¤° ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤šāĨ€ā¤‚ ⤞āĨ‡ ā¤œā¤žā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤¤ā¤ž, ⤛āĨ‹ā¤Ąā¤ŧāĨ‡ā¤‚", "home_page_share_err_local": "⤞āĨ‹ā¤•⤞ ā¤ā¤¸āĨ‡ā¤ŸāĨā¤¸ ⤕āĨ‹ ⤞ā¤ŋ⤂⤕ ⤕āĨ‡ ⤜⤰ā¤ŋā¤ ā¤ļāĨ‡ā¤¯ā¤° ā¤¨ā¤šāĨ€ā¤‚ ⤕⤰ ⤏⤕⤤āĨ‡, ⤏āĨā¤•ā¤ŋā¤Ē ⤕⤰ ā¤°ā¤šāĨ‡ ā¤šāĨˆā¤‚", @@ -1437,7 +1508,7 @@ "no_albums_with_name_yet": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ⤅⤭āĨ€ ⤤⤕ ⤇⤏ ā¤¨ā¤žā¤Ž ā¤•ā¤ž ⤕āĨ‹ā¤ˆ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "no_albums_yet": "ā¤ā¤¸ā¤ž ā¤˛ā¤—ā¤¤ā¤ž ā¤šāĨˆ ⤕ā¤ŋ ⤆ā¤Ē⤕āĨ‡ ā¤Ēā¤žā¤¸ ⤅⤭āĨ€ ⤤⤕ ⤕āĨ‹ā¤ˆ ā¤ā¤˛āĨā¤Ŧā¤Ž ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆāĨ¤", "no_archived_assets_message": "ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤔⤰ ā¤ĩāĨ€ā¤Ąā¤ŋ⤝āĨ‹ ⤕āĨ‹ ⤅ā¤Ē⤍āĨ‡ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ā¤ĻāĨƒā¤ļāĨā¤¯ ⤏āĨ‡ ⤛ā¤ŋā¤Ēā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤉⤍āĨā¤šāĨ‡ā¤‚ ⤏⤂⤗āĨā¤°ā¤šāĨ€ā¤¤ ⤕⤰āĨ‡ā¤‚", - "no_assets_message": "⤅ā¤Ēā¤¨ā¤ž ā¤Ēā¤šā¤˛ā¤ž ā¤ĢāĨ‹ā¤ŸāĨ‹ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨā¤˛ā¤ŋ⤕ ⤕⤰āĨ‡ā¤‚", + "no_assets_message": "⤅ā¤Ē⤍āĨ€ ā¤Ēā¤šā¤˛āĨ€ ā¤Ģā¤ŧāĨ‹ā¤ŸāĨ‹ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨā¤˛ā¤ŋ⤕ ⤕⤰āĨ‡ā¤‚", "no_assets_to_show": "ā¤Ļā¤ŋā¤–ā¤žā¤¨āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤕āĨ‹ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ ā¤¨ā¤šāĨ€ā¤‚", "no_cast_devices_found": "⤕āĨ‹ā¤ˆ ā¤•ā¤žā¤¸āĨā¤Ÿ ā¤Ąā¤ŋā¤ĩā¤žā¤‡ā¤¸ ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋā¤˛ā¤ž", "no_checksum_local": "⤕āĨ‹ā¤ˆ ⤚āĨ‡ā¤•ā¤¸ā¤Ž ⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤¨ā¤šāĨ€ā¤‚ ā¤šāĨˆ - ⤏āĨā¤Ĩā¤žā¤¨āĨ€ā¤¯ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋā¤¯ā¤žā¤‚ ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ā¤¨ā¤šāĨ€ā¤‚ ⤕āĨ€ ā¤œā¤ž ⤏⤕⤤āĨ€ā¤‚", @@ -1464,7 +1535,6 @@ "not_available": "ā¤˛ā¤žā¤—āĨ‚ ā¤¨ā¤šāĨ€ā¤‚", "not_in_any_album": "⤕ā¤ŋ⤏āĨ€ ā¤ā¤˛ā¤Ŧā¤Ž ā¤ŽāĨ‡ā¤‚ ā¤¨ā¤šāĨ€ā¤‚", "not_selected": "⤚⤝⤍ā¤ŋ⤤ ā¤¨ā¤šāĨ€ā¤‚", - "note_apply_storage_label_to_previously_uploaded assets": "⤍āĨ‹ā¤Ÿ: ā¤Ēā¤šā¤˛āĨ‡ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ€ ā¤—ā¤ˆ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ā¤Ē⤰ ⤏āĨā¤ŸāĨ‹ā¤°āĨ‡ā¤œ ⤞āĨ‡ā¤Ŧ⤞ ā¤˛ā¤žā¤—āĨ‚ ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤, ā¤šā¤˛ā¤žā¤ā¤", "notes": "⤟ā¤ŋā¤ĒāĨā¤Ē⤪ā¤ŋā¤¯ā¤žā¤", "nothing_here_yet": "ā¤¯ā¤šā¤žā¤ ⤅⤭āĨ€ ⤤⤕ ⤕āĨā¤› ā¤¨ā¤šāĨ€ā¤‚", "notification_permission_dialog_content": "⤏āĨ‚ā¤šā¤¨ā¤žā¤ā¤‚ ⤏⤕āĨā¤ˇā¤Ž ⤕⤰⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤¸ ā¤ŽāĨ‡ā¤‚ ā¤œā¤žā¤ā¤‚ ⤔⤰ ⤅⤍āĨā¤Žā¤¤ā¤ŋ ā¤ĻāĨ‡ā¤‚ ⤚āĨā¤¨āĨ‡ā¤‚āĨ¤", @@ -1655,11 +1725,11 @@ "readonly_mode_enabled": "⤕āĨ‡ā¤ĩ⤞-ā¤Ēā¤ĸā¤ŧ⤍āĨ‡ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ŽāĨ‹ā¤Ą ⤏⤕āĨā¤ˇā¤Ž", "ready_for_upload": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ‡ ⤞ā¤ŋā¤ ⤤āĨˆā¤¯ā¤žā¤°", "reassign": "ā¤ĒāĨā¤¨ā¤ƒ ā¤…ā¤¸ā¤žā¤‡ā¤¨", - "reassigned_assets_to_existing_person": "{count, plural, one {# asset} other {# assets}} ⤕āĨ‹ {name, select, null {an existing person} other {{name}}}⤕āĨ‹ ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ā¤…ā¤¸ā¤žā¤‡ā¤¨ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", + "reassigned_assets_to_existing_person": "{count, plural, one {# asset} other {# assets}} ⤕āĨ‹ {name, select, null {an existing person} other {{name}}}⤕āĨ‹ ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ā¤…ā¤¸ā¤žā¤‡ā¤¨ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "reassigned_assets_to_new_person": "{count, plural, one {# asset} other {# assets}} ⤕āĨ‹ ā¤ā¤• ā¤¨ā¤ ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ā¤Ģā¤ŋ⤰ ⤏āĨ‡ ā¤…ā¤¸ā¤žā¤‡ā¤¨ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "reassing_hint": "⤚⤝⤍ā¤ŋ⤤ ⤏⤂ā¤Ē⤤āĨā¤¤ā¤ŋ⤝āĨ‹ā¤‚ ⤕āĨ‹ ⤕ā¤ŋ⤏āĨ€ ā¤ŽāĨŒā¤œāĨ‚ā¤Ļā¤ž ā¤ĩāĨā¤¯ā¤•āĨā¤¤ā¤ŋ ⤕āĨ‹ ⤏āĨŒā¤‚ā¤ĒāĨ‡ā¤‚", "recent": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤•ā¤ž", - "recent-albums": "ā¤šā¤žā¤˛ ⤕āĨ‡ ā¤ā¤˛āĨā¤Ŧā¤Ž", + "recent_albums": "ā¤šā¤žā¤˛ ⤕āĨ‡ ā¤ā¤˛āĨā¤Ŧā¤Ž", "recent_searches": "ā¤šā¤žā¤˛ ⤕āĨ€ ⤖āĨ‹ā¤œāĨ‡ā¤‚", "recently_added": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", "recently_added_page_title": "ā¤šā¤žā¤˛ ā¤šāĨ€ ā¤ŽāĨ‡ā¤‚ ā¤Ąā¤žā¤˛ā¤ž ā¤—ā¤¯ā¤ž", @@ -1864,7 +1934,7 @@ "setting_notifications_notify_failures_grace_period": "ā¤ŦāĨˆā¤•⤗āĨā¤°ā¤žā¤‰ā¤‚ā¤Ą ā¤ŦāĨˆā¤•⤅ā¤Ē ā¤ĢāĨ‡ā¤˛ā¤ŋ⤝⤰ ⤕āĨ€ ⤏āĨ‚ā¤šā¤¨ā¤ž ā¤ĻāĨ‡ā¤‚: {duration}", "setting_notifications_notify_hours": "{count} ā¤˜ā¤‚ā¤ŸāĨ‡", "setting_notifications_notify_immediately": "⤤āĨā¤°ā¤‚⤤", - "setting_notifications_notify_minutes": "{count} ā¤Žā¤ŋ⤍⤟", + "setting_notifications_notify_minutes": "{count} ā¤Žā¤ŋ⤍⤟", "setting_notifications_notify_never": "⤕⤭āĨ€ ā¤¨ā¤šāĨ€ā¤‚", "setting_notifications_notify_seconds": "{count} ⤏āĨ‡ā¤•ā¤‚ā¤Ą", "setting_notifications_single_progress_subtitle": "ā¤šā¤° ā¤ā¤¸āĨ‡ā¤Ÿ ⤕āĨ‡ ⤞ā¤ŋā¤ ⤅ā¤Ē⤞āĨ‹ā¤Ą ā¤ĒāĨā¤°āĨ‹ā¤—āĨā¤°āĨ‡ā¤¸ ⤕āĨ€ ā¤ĒāĨ‚⤰āĨ€ ā¤œā¤žā¤¨ā¤•ā¤žā¤°āĨ€", @@ -1907,7 +1977,7 @@ "shared_link_custom_url_description": "⤕⤏āĨā¤Ÿā¤Ž URL ⤏āĨ‡ ⤇⤏ ā¤ļāĨ‡ā¤¯ā¤°āĨā¤Ą ⤞ā¤ŋ⤂⤕ ⤕āĨ‹ ā¤ā¤•āĨā¤¸āĨ‡ā¤¸ ⤕⤰āĨ‡ā¤‚", "shared_link_edit_description_hint": "ā¤ļāĨ‡ā¤¯ā¤° ā¤ĩā¤ŋā¤ĩ⤰⤪ ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", "shared_link_edit_expire_after_option_day": "1 ā¤Ļā¤ŋ⤍", - "shared_link_edit_expire_after_option_days": "{count} ā¤Ļā¤ŋ⤍", + "shared_link_edit_expire_after_option_days": "{count} ā¤Ļā¤ŋ⤍", "shared_link_edit_expire_after_option_hour": "1 ā¤˜ā¤‚ā¤Ÿā¤ž", "shared_link_edit_expire_after_option_hours": "{count} ā¤˜ā¤‚ā¤ŸāĨ‡", "shared_link_edit_expire_after_option_minute": "1 ā¤Žā¤ŋ⤍⤟", @@ -1916,8 +1986,8 @@ "shared_link_edit_expire_after_option_year": "{count} ā¤ĩ⤰āĨā¤ˇ", "shared_link_edit_password_hint": "ā¤ļāĨ‡ā¤¯ā¤° ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą ā¤Ļ⤰āĨā¤œ ⤕⤰āĨ‡ā¤‚", "shared_link_edit_submit_button": "⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤞ā¤ŋ⤂⤕", - "shared_link_error_server_url_fetch": "⤏⤰āĨā¤ĩ⤰ URL ā¤¨ā¤šāĨ€ā¤‚ ā¤Žā¤ŋ⤞ ā¤°ā¤šā¤ž ā¤šāĨˆ", - "shared_link_expires_day": "{count} ā¤Ļā¤ŋ⤍ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ ā¤°ā¤šā¤ž ā¤šāĨˆ", + "shared_link_error_server_url_fetch": "⤏⤰āĨā¤ĩ⤰ URL ā¤ĒāĨā¤°ā¤žā¤ĒāĨā¤¤ ā¤¨ā¤šāĨ€ā¤‚ ⤕ā¤ŋā¤¯ā¤ž ā¤œā¤ž ā¤¸ā¤•ā¤ž", + "shared_link_expires_day": "{count} ā¤Ļā¤ŋ⤍ ā¤ŽāĨ‡ā¤‚ ⤇⤏⤕āĨ€ ā¤ĩāĨˆā¤§ā¤¤ā¤ž ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ ā¤œā¤žā¤ā¤—āĨ€", "shared_link_expires_days": "{count} ā¤Ļā¤ŋ⤍āĨ‹ā¤‚ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ ā¤œā¤žā¤ā¤—ā¤ž", "shared_link_expires_hour": "{count} ā¤˜ā¤‚ā¤ŸāĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ ā¤œā¤žā¤ā¤—ā¤ž", "shared_link_expires_hours": "{count} ā¤˜ā¤‚ā¤ŸāĨ‡ ā¤ŽāĨ‡ā¤‚ ā¤¸ā¤Žā¤žā¤ĒāĨā¤¤ ā¤šāĨ‹ ā¤œā¤žā¤ā¤—ā¤ž", @@ -2042,11 +2112,11 @@ "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_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_subtitle": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕ ⤕āĨā¤°ā¤ŋā¤¯ā¤žā¤“ā¤‚ ⤔⤰ ā¤‰ā¤šāĨā¤šā¤žā¤°ā¤ŖāĨ‹ā¤‚ ⤕āĨ‡ ⤞ā¤ŋā¤ ā¤ā¤• ⤰⤂⤗ ⤚āĨā¤¨āĨ‡ā¤‚āĨ¤", "theme_setting_primary_color_title": "ā¤ĒāĨā¤°ā¤žā¤Ĩā¤Žā¤ŋ⤕ ⤰⤂⤗", "theme_setting_system_primary_color_title": "⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤰⤂⤗ ā¤•ā¤ž ⤉ā¤Ē⤝āĨ‹ā¤— ⤕⤰āĨ‡ā¤‚", "theme_setting_system_theme_switch": "ā¤‘ā¤ŸāĨ‹ā¤ŽāĨˆā¤Ÿā¤ŋ⤕ (⤏ā¤ŋ⤏āĨā¤Ÿā¤Ž ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗ ā¤Ģā¤ŧāĨ‰ā¤˛āĨ‹ ⤕⤰āĨ‡ā¤‚)", @@ -2122,7 +2192,6 @@ "updated_at": "⤅ā¤Ēā¤ĄāĨ‡ā¤Ÿ ⤕ā¤ŋā¤¯ā¤ž ā¤—ā¤¯ā¤ž", "updated_password": "⤅ā¤ĻāĨā¤¯ā¤¤ā¤¨ ā¤Ēā¤žā¤¸ā¤ĩ⤰āĨā¤Ą", "upload": "ā¤Ąā¤žā¤˛ā¤¨ā¤ž", - "upload_action_prompt": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ‡ ⤞ā¤ŋā¤ {count} ā¤•ā¤¤ā¤žā¤° ā¤ŽāĨ‡ā¤‚", "upload_concurrency": "ā¤¸ā¤Žā¤ĩ⤰āĨā¤¤āĨ€ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", "upload_details": "ā¤ĩā¤ŋā¤ĩ⤰⤪ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕⤰āĨ‡ā¤‚", "upload_dialog_info": "⤕āĨā¤¯ā¤ž ⤆ā¤Ē ⤚āĨā¤¨āĨ‡ ā¤šāĨā¤ ā¤ā¤¸āĨ‡ā¤Ÿ ā¤•ā¤ž ⤏⤰āĨā¤ĩ⤰ ā¤Ē⤰ ā¤ŦāĨˆā¤•⤅ā¤Ē ⤞āĨ‡ā¤¨ā¤ž ā¤šā¤žā¤šā¤¤āĨ‡ ā¤šāĨˆā¤‚?", @@ -2198,7 +2267,6 @@ "welcome": "⤏āĨā¤ĩā¤žā¤—ā¤¤", "welcome_to_immich": "Immich ā¤ŽāĨ‡ā¤‚ ⤆ā¤Ēā¤•ā¤ž ⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤šāĨˆ", "wifi_name": "ā¤ĩā¤žā¤ˆ-ā¤Ģā¤žā¤ˆ ā¤•ā¤ž ā¤¨ā¤žā¤Ž", - "workflow": "ā¤•ā¤žā¤°āĨā¤¯ā¤ĒāĨā¤°ā¤ĩā¤žā¤š", "wrong_pin_code": "⤗⤞⤤ ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą", "year": "ā¤ĩ⤰āĨā¤ˇ", "years_ago": "{years, plural, one {# year} other {# years}} ā¤Ēā¤šā¤˛āĨ‡", diff --git a/i18n/hr.json b/i18n/hr.json index f6fb458ce5..88fa57e230 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -5,6 +5,7 @@ "acknowledge": "Potvrdi", "action": "Akcija", "action_common_update": "AÅžuriranje", + "action_description": "Skup radnji koje se izvrÅĄavaju nad filtriran", "actions": "Akcije", "active": "Aktivno", "active_count": "Aktivno:{count}", @@ -15,9 +16,14 @@ "add_a_location": "Dodaj lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_action": "Dodaj akciju", + "add_action_description": "Kliknite za dodavanje radnje koju treba izvrÅĄiti", + "add_assets": "Dodaj stavke", "add_birthday": "Dodaj rođendan", "add_endpoint": "Dodaj krajnju točku", "add_exclusion_pattern": "Dodaj uzorak izuzimanja", + "add_filter": "Dodaj filter", + "add_filter_description": "Klikni za dodavanje uvjetnog filtriranja", "add_location": "Dodaj lokaciju", "add_more_users": "Dodaj joÅĄ korisnika", "add_partner": "Dodaj partnera", @@ -36,6 +42,7 @@ "add_to_shared_album": "Dodaj u dijeljeni album", "add_upload_to_stack": "Dodaj preneseno u skup", "add_url": "Dodaj URL", + "add_workflow_step": "Dodaj korak radnog procesa", "added_to_archive": "Dodano u arhivu", "added_to_favorites": "Dodano u omiljeno", "added_to_favorites_count": "Dodano {count, number} u omiljeno", @@ -97,6 +104,8 @@ "image_preview_description": "Slika srednje veličine s uklonjenim metapodacima, koristi se prilikom pregledavanja jedne stavke i za strojno učenje", "image_preview_quality_description": "Kvaliteta pregleda od 1-100. ViÅĄe je bolje, ali proizvodi veće datoteke i moÅže smanjiti odziv aplikacije. Postavljanje niske vrijednosti moÅže utjecati na kvalitetu strojnog učenja.", "image_preview_title": "Postavke pregleda", + "image_progressive": "Progresivno", + "image_progressive_description": "Kodiraj JPEG slike progresivno za postupno učitavanje i prikaz. Ovo nema utjecaja na WebP slike.", "image_quality": "Kvaliteta", "image_resolution": "Rezolucija", "image_resolution_description": "Veće razlučivosti mogu sačuvati viÅĄe detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", @@ -169,6 +178,11 @@ "machine_learning_ocr_max_resolution": "Maksimalna razlučivost", "machine_learning_ocr_max_resolution_description": "Pregledi preko ove razlučivosti će promijeniti veličinu poÅĄtujući omjer slike. Veće vrijednosti su točnije, ali trebaju viÅĄe vremena za obradu i koriste viÅĄe memorije.", "machine_learning_ocr_min_detection_score": "Minimalna ocjena prepoznavanja", + "machine_learning_ocr_min_detection_score_description": "Minimalni prag pouzdanosti za detekciju teksta (0–1). NiÅže vrijednosti otkrit će viÅĄe teksta, ali mogu dovesti do laÅžno pozitivnih rezultata.", + "machine_learning_ocr_min_recognition_score": "Minimalni prag prepoznavanja", + "machine_learning_ocr_min_score_recognition_description": "Minimalni prag pouzdanosti za prepoznavanje detektiranog teksta (0–1). NiÅže vrijednosti prepoznat će viÅĄe teksta, ali mogu dovesti do laÅžno pozitivnih rezultata.", + "machine_learning_ocr_model": "Model za prepoznavanje teksta (OCR)", + "machine_learning_ocr_model_description": "Serverski modeli su precizniji od mobilnih modela, ali im treba viÅĄe vremena za obradu i koriste viÅĄe memorije.", "machine_learning_settings": "Postavke strojnog učenja", "machine_learning_settings_description": "Upravljajte značajkama i postavkama strojnog učenja", "machine_learning_smart_search": "Pametna pretraga", @@ -176,6 +190,19 @@ "machine_learning_smart_search_enabled": "Omogući pametno pretraÅživanje", "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraÅživanje.", "machine_learning_url_description": "URL posluÅžitelja strojnog učenja. Ako ste dodali viÅĄe od jednog URLa, svaki server će biti kontaktiraj jedanput dok jedan ne odgovori uspjeÅĄno, u redu od prvog do zadnjeg. Serveri koji ne odgovore će privremeno biti ignorirani dok ponovo ne postanu dostupni.", + "maintenance_delete_backup": "IzbriÅĄi sigurnosnu kopiju", + "maintenance_delete_backup_description": "Ova datoteka će biti trajno obrisana.", + "maintenance_delete_error": "Brisanje sigurnosne kopije nije uspjelo.", + "maintenance_restore_backup": "Vrati sigurnosnu kopiju", + "maintenance_restore_backup_description": "Immich će biti obrisan i vraćen iz odabrane sigurnosne kopije. Prije nastavka izradit će se nova sigurnosna kopija.", + "maintenance_restore_backup_different_version": "Ova sigurnosna kopija izrađena je s drugom verzijom Immicha!", + "maintenance_restore_backup_unknown_version": "Nije moguće odrediti verziju sigurnosne kopije.", + "maintenance_restore_database_backup": "Vrati sigurnosnu kopiju baze podataka", + "maintenance_restore_database_backup_description": "Vrati bazu podataka na ranije stanje pomoću sigurnosne kopije", + "maintenance_settings": "OdrÅžavanje", + "maintenance_settings_description": "Stavi Immich u način odrÅžavanja.", + "maintenance_start": "Prebaci se u način odrÅžavanja", + "maintenance_start_error": "Neuspjelo pokretanje načina odrÅžavanja.", "manage_concurrency": "Upravljanje IstovremenoÅĄÄ‡u", "manage_log_settings": "Upravljanje postavkama zapisivanje", "map_dark_style": "Tamni stil", @@ -908,8 +935,6 @@ "editor": "Urednik", "editor_close_without_save_prompt": "Promjene neće biti spremljene", "editor_close_without_save_title": "Zatvoriti uređivač?", - "editor_crop_tool_h2_aspect_ratios": "Omjeri stranica", - "editor_crop_tool_h2_rotation": "Rotacija", "email": "E-poÅĄta", "email_notifications": "Obavijesti putem e-maila", "empty_folder": "Ova mapa je prazna", @@ -1096,7 +1121,6 @@ "features": "Značajke", "features_in_development": "Značajke u razvoju", "features_setting_description": "Upravljajte značajkama aplikacije", - "file_name": "Naziv datoteke", "file_name_or_extension": "Naziv ili ekstenzija datoteke", "file_size": "Veličina datoteke", "filename": "Naziv datoteke", @@ -1435,7 +1459,6 @@ "not_available": "N/A", "not_in_any_album": "Ni u jednom albumu", "not_selected": "Nije odabrano", - "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primijenili oznaku pohrane na prethodno prenesene stavke, pokrenite", "notes": "BiljeÅĄke", "nothing_here_yet": "Ovdje joÅĄ nema ničega", "notification_permission_dialog_content": "Da biste omogućili obavijesti, idite u Postavke i odaberite dopusti.", @@ -1622,7 +1645,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# stavka ponovno dodijeljena} few {# stavke ponovno dodijeljene} other {# stavki ponovno dodijeljeno}} novoj osobi", "reassing_hint": "Dodijelite odabrane stavke postojećoj osobi", "recent": "Nedavno", - "recent-albums": "Nedavni albumi", + "recent_albums": "Nedavni albumi", "recent_searches": "Nedavne pretrage", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", @@ -2080,7 +2103,6 @@ "updated_at": "AÅžurirano", "updated_password": "Lozinka aÅžurirana", "upload": "Prijenos", - "upload_action_prompt": "{count} u redu za prijenos", "upload_concurrency": "Istovremeni prijenosi", "upload_details": "Detalji prijenosa", "upload_dialog_info": "ÅŊelite li sigurnosno kopirati odabrane stavke na posluÅžitelj?", @@ -2158,7 +2180,6 @@ "welcome": "DobrodoÅĄli", "welcome_to_immich": "DobrodoÅĄli u Immich", "wifi_name": "Naziv Wi-Fi mreÅže", - "workflow": "Način rada", "wrong_pin_code": "Krivi PIN kod", "year": "Godina", "years_ago": "prije {years, plural, =1 {# godinu} few {# godine} other {# godina}}", diff --git a/i18n/hu.json b/i18n/hu.json index 5a93b4085b..c2f3362e18 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -1,12 +1,13 @@ { "about": "Az Immich-ről", "account": "FiÃŗk", - "account_settings": "FiÃŗk BeÃĄllítÃĄsok", + "account_settings": "FiÃŗkbeÃĄllítÃĄsok", "acknowledge": "MegÊrtettem", "action": "MÅąvelet", "action_common_update": "FrissítÊs", + "action_description": "A szÅąrt elemeken vÊgrehajtandÃŗ mÅąveletek", "actions": "MÅąveletek", - "active": "FeldolgozÃĄs alatt", + "active": "Aktív", "active_count": "Aktív: {count}", "activity": "TevÊkenysÊg", "activity_changed": "A tevÊkenysÊg {enabled, select, true {bekapcsolva} other {kikapcsolva}}", @@ -15,9 +16,14 @@ "add_a_location": "Helyszín hozzÃĄadÃĄsa", "add_a_name": "NÊv megadÃĄsa", "add_a_title": "Cím megadÃĄsa", + "add_action": "MÅąvelet hozzÃĄadÃĄsa", + "add_action_description": "Kattints ide egy vÊgrehajtandÃŗ mÅąvelet hozzÃĄadÃĄsÃĄhoz", + "add_assets": "Elemek hozzÃĄadÃĄsa", "add_birthday": "SzÃŧletÊsnap hozzÃĄadÃĄsa", "add_endpoint": "VÊgpont megadÃĄsa", "add_exclusion_pattern": "KihagyÃĄsi minta (pattern) hozzÃĄadÃĄsa", + "add_filter": "SzÅąrő hozzÃĄadÃĄsa", + "add_filter_description": "Kattints ide egy szÅąrÊsi feltÊtel hozzÃĄadÃĄsÃĄhoz", "add_location": "Helyszín megadÃĄsa", "add_more_users": "TovÃĄbbi felhasznÃĄlÃŗk hozzÃĄadÃĄsa", "add_partner": "Partner hozzÃĄadÃĄsa", @@ -36,6 +42,7 @@ "add_to_shared_album": "FelvÊtel megosztott albumba", "add_upload_to_stack": "FeltÃļltÊs hozzÃĄadÃĄsa csoporthoz", "add_url": "URL hozzÃĄadÃĄsa", + "add_workflow_step": "Folyamat lÊpÊs hozzÃĄadÃĄsa", "added_to_archive": "HozzÃĄadva az archívumhoz", "added_to_favorites": "HozzÃĄadva a kedvencekhez", "added_to_favorites_count": "{count, number} hozzÃĄadva a kedvencekhez", @@ -46,7 +53,7 @@ "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", "authentication_settings_disable_all": "Biztosan letiltod az Ãļsszes bejelentkezÊsi mÃŗdot? A bejelentkezÊs teljesen le lesz tiltva.", - "authentication_settings_reenable": "Az ÃējbÃŗli engedÊlyezÊshez hasznÃĄlj egy Szerver Parancsot.", + "authentication_settings_reenable": "Az ÃējbÃŗli engedÊlyezÊshez hasznÃĄlj egy szerver parancsot.", "background_task_job": "HÃĄttÊrfeladatok", "backup_database": "AdatbÃĄzis lementÊse", "backup_database_enable_description": "AdatbÃĄzis mentÊsek engedÊlyezÊse", @@ -60,29 +67,29 @@ "backup_onboarding_title": "BiztonsÃĄgi mentÊsek", "backup_settings": "AdatbÃĄzis mentÊs beÃĄllítÃĄsai", "backup_settings_description": "AdatbÃĄzis mentÊs beÃĄllítÃĄsainak kezelÊse.", - "cleared_jobs": "{job}: feladatai tÃļrÃļlve", + "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?", - "confirm_delete_library_assets": "Biztosan kitÃļrlÃļd ezt a kÊptÃĄrat? Ez kitÃļrli az Immich-ből a benne lÊvő {count, plural, one {#} other {#}} elemet is, Ês ez nem visszavonhatÃŗ. A fÃĄjlok fizikailag a lemezen maradnak.", + "confirm_delete_library": "Biztosan tÃļrÃļlni szeretnÊd a(z) {library} kÊptÃĄrat?", + "confirm_delete_library_assets": "Biztosan tÃļrlÃļd ezt a kÊptÃĄrat? Ez nem visszavonhatÃŗ mÃŗdon tÃļrli az Immich-ből a benne lÊvő {count, plural, one {#} other {#}} elemet is. A fÃĄjlok fizikailag a lemezen maradnak.", "confirm_email_below": "A megerősítÊshez írd be, hogy \"{email}\"", "confirm_reprocess_all_faces": "Biztos vagy benne, hogy Ãējra fel szeretnÊd dolgozni az Ãļsszes arcot? Ez a mÃĄr elnevezett szemÊlyeket is tÃļrli.", "confirm_user_password_reset": "Biztosan vissza szeretnÊd ÃĄllítani {user} jelszavÃĄt?", - "confirm_user_pin_code_reset": "Biztos, hogy vissza akarod ÃĄllítani {user} PIN-kÃŗdjÃĄt?", + "confirm_user_pin_code_reset": "Biztosan vissza akarod ÃĄllítani {user} PIN-kÃŗdjÃĄt?", "copy_config_to_clipboard_description": "Jelenlegi rendszer konfigurÃĄciÃŗ mÃĄsolÃĄsa a vÃĄgÃŗlapra JSON objektumkÊnt", "create_job": "Feladat lÊtrehozÃĄsa", "cron_expression": "Cron kifejezÊs", "cron_expression_description": "A beolvasÃĄsi időkÃļz beÃĄllítÃĄsa a cron formÃĄtummal. TovÃĄbbi informÃĄciÃŗÃŠrt lÃĄsd pl. Crontab Guru", - "cron_expression_presets": "Cron kifejezÊs előbeÃĄllítÃĄsok", - "disable_login": "BelÊpÊs letiltÃĄsa", - "duplicate_detection_job_description": "GÊpi tanulÃĄs futtatÃĄsa a hasonlÃŗ elemek megtalÃĄlÃĄsa cÊljÃĄbÃŗl. Ez az Okos KeresÊs funkciÃŗt hasznÃĄlja", + "cron_expression_presets": "Cron kifejezÊs előbeÃĄllítÃĄs", + "disable_login": "BejelentkezÊs letiltÃĄsa", + "duplicate_detection_job_description": "GÊpi tanulÃĄs futtatÃĄsa a hasonlÃŗ elemek megtalÃĄlÃĄsa cÊljÃĄbÃŗl. Ez az Okos keresÊs funkciÃŗt hasznÃĄlja", "exclusion_pattern_description": "A kihagyÃĄsi mintÃĄk (pattern) hasznÃĄlatakor a mintÃĄnak megfelelő fÃĄjlok vagy mappÃĄk ÃĄt lesznek ugorva a kÊptÃĄr ÃĄtfÊsÃŧlÊsekor. Akkor hasznos, ha a mappÃĄkban vannak olyan fÃĄjlok is, amelyeket nem szeretnÊl importÃĄlni, pl. nyers (RAW) fÃĄjlok.", - "export_config_as_json_description": "Jelenlegi rendszer konfigurÃĄciÃŗ mentÊse JSON fÃĄjlkÊnt", + "export_config_as_json_description": "Jelenlegi rendszer konfigurÃĄciÃŗ letÃļltÊse JSON fÃĄjlkÊnt", "external_libraries_page_description": "Admin kÃŧlső kÃļnyvtÃĄr oldala", "face_detection": "ArckeresÊs", - "face_detection_description": "GÊpi tanulÃĄs segítsÊgÊvel megkeresi, hogy hol talÃĄlhatÃŗak arcok az elemeken. VideÃŗk esetÊben csak a bÊlyegkÊpeken keres. \"FrissítÊs\" (Ãējra) feldolgozza az Ãļsszes elemet. \"VisszaÃĄllítÃĄs\" ezen felÃŧl tÃļrli az Ãļsszes aktuÃĄlis arcadatot. \"HiÃĄnyzÃŗk\" sorba ÃĄllítja azokat az elemeket, amelyek eddig mÊg nem lettek feldolgozva. A megtalÃĄlt arcok ezutÃĄn sorba lesznek ÃĄllítva az ArcfelismerÊshez, ami ezutÃĄn az arcokat csoportosítja Ês meglevő vagy Ãēj szemÊlyekhez rendeli.", - "facial_recognition_job_description": "A megtalÃĄlt arcokat szemÊlyekhez csoportosítja. Ez a lÊpÊs azutÃĄn kÃļvetkezik, amikor az ArckeresÊs lefutott. \"VisszaÃĄllítÃĄs\" (Ãējra)csoportosítja az Ãļsszes arcot. \"HiÃĄnyzÃŗk\" csak azokkal az arcokkal foglalkozik, amelyekhez mÊg nincsen ember rendelve.", + "face_detection_description": "GÊpi tanulÃĄs segítsÊgÊvel megkeresi, hogy hol talÃĄlhatÃŗak arcok az elemeken. VideÃŗk esetÊben csak a bÊlyegkÊpeken keres. \"FrissítÊs\" (Ãējra) feldolgozza az Ãļsszes elemet. \"VisszaÃĄllítÃĄs\" ezen felÃŧl tÃļrli az Ãļsszes aktuÃĄlis arcadatot. \"HiÃĄnyzÃŗk\" sorba ÃĄllítja azokat az elemeket, amelyek eddig mÊg nem lettek feldolgozva. A megtalÃĄlt arcok ezutÃĄn sorba lesznek ÃĄllítva az arcfelismerÊshez, ami ezutÃĄn az arcokat csoportosítja Ês meglevő vagy Ãēj szemÊlyekhez rendeli.", + "facial_recognition_job_description": "A megtalÃĄlt arcokat szemÊlyekhez csoportosítja. Ez a lÊpÊs azutÃĄn kÃļvetkezik, amikor az arckeresÊs lefutott. \"VisszaÃĄllítÃĄs\" (Ãējra)csoportosítja az Ãļsszes arcot. \"HiÃĄnyzÃŗk\" csak azokkal az arcokkal foglalkozik, amelyekhez mÊg nincsen szemÊly rendelve.", "failed_job_command": "A(z) {command} parancs nem sikerÃŧlt a kÃļvetkező feladathoz: {job}", - "force_delete_user_warning": "FIGYELEM: Ez azonnal eltÃĄvolítja a felhasznÃĄlÃŗt Ês az Ãļsszes hozzÃĄ tartozÃŗ elemet. A mÅąvelet nem visszavonhatÃŗ, Ês a fÃĄjlokat sem lehet kÊsőbb visszanyerni.", + "force_delete_user_warning": "FIGYELEM: Ez azonnal eltÃĄvolítja a felhasznÃĄlÃŗt Ês az Ãļsszes hozzÃĄ tartozÃŗ elemet. A mÅąvelet nem visszavonhatÃŗ Ês a fÃĄjlokat sem lehet kÊsőbb visszanyerni.", "image_format": "FormÃĄtum", "image_format_description": "WebP a JPEG-nÊl kisebb fÃĄjlokat kÊszít, de lassabban.", "image_fullsize_description": "Teljes mÊretÅą kÊp eltÃĄvolított metaadatokkal, nagyítÃĄskor hasznÃĄlva", @@ -96,7 +103,9 @@ "image_prefer_wide_gamut_setting_description": "A bÊlyegkÊpekhez DCI-P3 színtÊr hasznÃĄlata. Ez a szÊles színteret hasznÃĄlÃŗ kÊpek esetÊn (pl: Adobe RGB, P3) jobban megőrzi az ÊlÊnkebb színeket, de rÊgebbi eszkÃļzÃļkÃļn vagy bÃļngÊszőkben a kÊp színei mÃĄskÊppen jelenhetnek meg. Az sRGB kÊpek a színeltolÃŗdÃĄsok megelőzÊse ÊrdekÊben nem vÃĄltoznak.", "image_preview_description": "KÃļzepes mÊretÅą kÊp eltÃĄvolított metaadatokkal, egy kÊpes nÊzethez Ês a gÊpi tanulÃĄshoz", "image_preview_quality_description": "ElőnÊzet minősÊge 1-100 kÃļzÃļtt. A magasabb szÃĄm jobb minősÊget, de nagyobb fÃĄjlokat eredmÊnyez Ês belassíthatja az alkalmazÃĄst. TÃēl alacsony ÊrtÊk befolyÃĄsolhatja a gÊpi tanulÃĄs pontossÃĄgÃĄt.", - "image_preview_title": "ElőnÊzet BeÃĄllítÃĄsai", + "image_preview_title": "ElőnÊzet beÃĄllítÃĄsok", + "image_progressive": "Progresszív", + "image_progressive_description": "JPEG kÊpek progresszív kÃŗdolÃĄsa a fokozatos megjelenítÊshez betÃļltÊskor. Nincs hatÃĄssal a WebP kÊpekre.", "image_quality": "MinősÊg", "image_resolution": "FelbontÃĄs", "image_resolution_description": "A nagyobb felbontÃĄs tÃļbb rÊszletet őriz meg, de lassabb lÊtrehozni, nagyobb fÃĄjlt eredmÊnyez Ês belassíthatja az alkalmazÃĄst.", @@ -104,16 +113,16 @@ "image_settings_description": "A lÊtrehozott kÊpek minősÊgi Ês felbontÃĄsi beÃĄllítÃĄsainak kezelÊse", "image_thumbnail_description": "Kicsi bÊlyegkÊp eltÃĄvolított metaadatokkal, sok kis kÊp (pl idővonal) megjelenítÊsÊhez", "image_thumbnail_quality_description": "BÊlyegkÊp minősÊge 1-100 kÃļzÃļtt. A magasabb szÃĄm jobb minősÊget, de nagyobb fÃĄjlmÊretet eredmÊnyez Ês belassíthatja az alkalmazÃĄst.", - "image_thumbnail_title": "BÊlyegkÊp BeÃĄllítÃĄsok", + "image_thumbnail_title": "BÊlyegkÊp beÃĄllítÃĄsok", "import_config_from_json_description": "Rendszer konfigurÃĄciÃŗ importÃĄlÃĄsa JSON fÃĄjlbÃŗl", - "job_concurrency": "{job} pÃĄrhuzamossÃĄg", + "job_concurrency": "{job} pÃĄrhuzamosan", "job_created": "Feladat lÊtrehozva", "job_not_concurrency_safe": "Ez a feladat nem pÃĄrhuzamossÃĄg-biztos.", - "job_settings": "Feladat BeÃĄllítÃĄsok", - "job_settings_description": "Feladatok pÃĄrhuzamossÃĄgÃĄnak kezelÊse", + "job_settings": "Feladat beÃĄllítÃĄsok", + "job_settings_description": "PÃĄrhuzamosan futÃŗ feladatok kezelÊse", "jobs_delayed": "{jobCount, plural, other {# kÊsik}}", "jobs_failed": "{jobCount, plural, other {# sikertelen}}", - "jobs_over_time": "Feladatok idővel", + "jobs_over_time": "Feladat aktivitÃĄs", "library_created": "KÊptÃĄr lÊtrehozva: {library}", "library_deleted": "KÊptÃĄr tÃļrÃļlve", "library_details": "KÃļnyvtÃĄr rÊszletei", @@ -123,7 +132,7 @@ "library_scanning": "Időszakos ÁtfÊsÃŧlÊs", "library_scanning_description": "A kÊptÃĄr időszakos ÃĄtfÊsÃŧlÊsÊnek beÃĄllítÃĄsa", "library_scanning_enable_description": "KÊptÃĄr időszakos ÃĄtfÊsÃŧlÊsÊnek engedÊlyezÊse", - "library_settings": "KÃŧlső KÊptÃĄr", + "library_settings": "KÃŧlső kÊptÃĄr", "library_settings_description": "KÃŧlső kÊptÃĄr beÃĄllítÃĄsainak kezelÊse", "library_tasks_description": "KÃŧlső kÃļnyvtÃĄrak szkennelÊse Ãēj Ês/vagy mÃŗdosított elemek utÃĄn", "library_updated": "KÃļnyvtÃĄr frissítve", @@ -141,8 +150,8 @@ "machine_learning_availability_checks_timeout": "KÊrÊsek időkorlÃĄtja", "machine_learning_availability_checks_timeout_description": "ElÊrhetősÊg-ellenőrzÊsek időkorlÃĄtja milliszekundumban", "machine_learning_clip_model": "CLIP modell", - "machine_learning_clip_model_description": "Egy CLIP modell neve az itt felsoroltak kÃļzÃŧl. A modell megvÃĄltoztatÃĄsa utÃĄn Ãējra kell futtatni az 'Okos KeresÊs' feladatot minden kÊpre.", - "machine_learning_duplicate_detection": "DuplikÃĄciÃŗk KeresÊse", + "machine_learning_clip_model_description": "Egy CLIP modell neve az itt felsoroltak kÃļzÃŧl. A modell megvÃĄltoztatÃĄsa utÃĄn Ãējra kell futtatni az 'Okos keresÊs' feladatot minden kÊpre.", + "machine_learning_duplicate_detection": "DuplikÃĄciÃŗk keresÊse", "machine_learning_duplicate_detection_enabled": "DuplikÃĄciÃŗk keresÊsÊnek engedÊlyezÊse", "machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos elemek akkor sem lesznek duplikÃĄlva.", "machine_learning_duplicate_detection_setting_description": "CLIP beÃĄgyazÃĄsok hasznÃĄlata a valÃŗszínÅą mÃĄsolatok keresÊsÊhez", @@ -174,30 +183,41 @@ "machine_learning_ocr_min_score_recognition_description": "A szÃļvegfelismerÊs minimÃĄlis bizalmi szintje 0 Ês 1 kÃļzÃļtt. Az alacsonyabb ÊrtÊkek tÃļbb szÃļveget ismerhetnek fel, de nÃļvelhetik a tÊves talÃĄlatok szÃĄmÃĄt.", "machine_learning_ocr_model": "SzÃļvegfelismerő modell (OCR)", "machine_learning_ocr_model_description": "A szervermodellek pontosabbak, mint a mobilmodellek, de hosszabb feldolgozÃĄsi időt Ês tÃļbb memÃŗriÃĄt igÊnyelnek.", - "machine_learning_settings": "GÊpi TanulÃĄsi BeÃĄllítÃĄsok", + "machine_learning_settings": "GÊpi tanulÃĄs beÃĄllítÃĄsok", "machine_learning_settings_description": "GÊpi tanulÃĄsi funkciÃŗk Ês beÃĄllítÃĄsok kezelÊse", - "machine_learning_smart_search": "Okos KeresÊs", + "machine_learning_smart_search": "Okos keresÊs", "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_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 elÊrhető szervereket ÃĄtmenetileg figyelmen kívÃŧl lesznek hagyva, amíg Ãējra online nem lesznek.", + "maintenance_delete_backup": "BiztonsÃĄgi mentÊs tÃļrlÊse", + "maintenance_delete_backup_description": "A fÃĄjl tÃļrlÊse nem visszafordíthatÃŗ.", + "maintenance_delete_error": "A biztonsÃĄgi mentÊs tÃļrlÊse sikertelen volt.", + "maintenance_restore_backup": "BiztonsÃĄgi mentÊs visszaÃĄllítÃĄsa", + "maintenance_restore_backup_description": "Az Immich adatai tÃļrÃļlve lesznek Ês a kivÃĄlasztott biztonsÃĄgi mentÊs kerÃŧl visszaÃĄllítÃĄsra. Egy biztonsÃĄgi mentÊs kÊszÃŧl, mielőtt folytatnÃĄd.", + "maintenance_restore_backup_different_version": "Ez a biztonsÃĄgi mentÊs az Immich egy mÃĄsik verziÃŗjÃĄval kÊszÃŧlt!", + "maintenance_restore_backup_unknown_version": "A biztonsÃĄgi mentÊs verziÃŗjÃĄnak meghatÃĄrozÃĄsa sikertelen.", + "maintenance_restore_database_backup": "AdatbÃĄzis visszaÃĄllítÃĄsa biztonsÃĄgi mentÊsből", + "maintenance_restore_database_backup_description": "VisszaÃĄllítÃĄs egy korÃĄbbi adatbÃĄzis ÃĄllapotba egy biztonsÃĄgi mentÊs fÃĄjl segítsÊgÊvel", "maintenance_settings": "KarbantartÃĄs", "maintenance_settings_description": "Az Immich karbantartÃĄsi mÃŗdjÃĄnak beÃĄllítÃĄsa.", "maintenance_start": "KarbantartÃĄsi mÃŗd bekapcsolÃĄsa", "maintenance_start_error": "Hiba tÃļrtÊnt a karbantartÃĄsi mÃŗd bekapcsolÃĄs kÃļzben.", - "manage_concurrency": "PÃĄrhuzamos Feladatok KezelÊse", + "maintenance_upload_backup": "AdatbÃĄzis biztonsÃĄgi mentÊs fÃĄjl feltÃļltÊse", + "maintenance_upload_backup_error": "A biztonsÃĄgi mentÊs nem tÃļlthető fel. Biztos, hogy .sql/.sql.gz a fÃĄjlkiterjesztÊs?", + "manage_concurrency": "Feladatok pÃĄrhuzamossÃĄgÃĄnak kezelÊse", "manage_concurrency_description": "NavigÃĄlÃĄs a feladatok oldalra az egyidejÅą munkavÊgzÊs kezelÊsÊhez", - "manage_log_settings": "NaplÃŗzÃĄsi beÃĄllítÃĄsok kezelÊse", + "manage_log_settings": "NaplÃŗzÃĄs beÃĄllítÃĄsok kezelÊse", "map_dark_style": "SÃļtÊt stílus", "map_enable_description": "TÊrkÊp funkciÃŗk engedÊlyezÊse", - "map_gps_settings": "TÊrkÊp Ês GPS BeÃĄllítÃĄsok", - "map_gps_settings_description": "A TÊrkÊp Ês GPS (Fordított GeokÃŗdolÃĄs) BeÃĄllítÃĄsainak KezelÊse", + "map_gps_settings": "TÊrkÊp Ês GPS beÃĄllítÃĄsok", + "map_gps_settings_description": "A tÊrkÊp Ês GPS (fordított geokÃŗdolÃĄs) beÃĄllítÃĄsainak kezelÊse", "map_implications": "A tÊrkÊp szolgÃĄltatÃĄs egy kÃŧlső csempeszolgÃĄltatÃŗt hasznÃĄl (tiles.immich.cloud)", "map_light_style": "VilÃĄgos stílus", - "map_manage_reverse_geocoding_settings": "A Fordított GeokÃŗdolÃĄs beÃĄllítÃĄsainak kezelÊse", - "map_reverse_geocoding": "Fordított GeokÃŗdolÃĄs", + "map_manage_reverse_geocoding_settings": "A fordított geokÃŗdolÃĄs beÃĄllítÃĄsainak kezelÊse", + "map_reverse_geocoding": "Fordított geokÃŗdolÃĄs", "map_reverse_geocoding_enable_description": "Fordított geokÃŗdolÃĄs engedÊlyezÊse", - "map_reverse_geocoding_settings": "Fordított GeokÃŗdolÃĄsi BeÃĄllítÃĄsok", + "map_reverse_geocoding_settings": "Fordított geokÃŗdolÃĄs beÃĄllítÃĄsok", "map_settings": "TÊrkÊp", "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", @@ -207,7 +227,7 @@ "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", "metadata_faces_import_setting_description": "Arcok importÃĄlÃĄsa a kÊp EXIF adataibÃŗl Ês segÊdfÃĄjlokbÃŗl", - "metadata_settings": "Metaadat BeÃĄllítÃĄsok", + "metadata_settings": "Metaadat beÃĄllítÃĄsok", "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", @@ -219,7 +239,7 @@ "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", "nightly_tasks_missing_thumbnails_setting_description": "A bÊlyegkÊp nÊlkÃŧli elemek bÊlyegkÊpgenerÃĄlÃŗ vÃĄrÃŗlistÃĄra helyezÊse", - "nightly_tasks_settings": "Éjjeli Feladat BeÃĄllítÃĄsok", + "nightly_tasks_settings": "Éjjeli feladat beÃĄllítÃĄsok", "nightly_tasks_settings_description": "Éjjeli feladatok kezelÊse", "nightly_tasks_start_time_setting": "Kezdőidő", "nightly_tasks_start_time_setting_description": "Az az időpont, amikor a szerver elkezdi futtatni az Êjszakai feladatokat", @@ -227,7 +247,7 @@ "nightly_tasks_sync_quota_usage_setting_description": "A felhasznÃĄlÃŗ kvÃŗtÃĄjÃĄnak frissítÊse az aktuÃĄlis tÃĄrhelyhasznÃĄlat alapjÃĄn", "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_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 \". Figyelj hogy olyan címet adj meg ahonnan az email kÃŧldÊs engedÊlyezett.", @@ -245,14 +265,14 @@ "notification_email_test_email_sent": "Egy teszt emailt kÃŧldtÃŧnk a(z) {email} címre. Figyeld a beÊrkező Ãŧzeneteidet.", "notification_email_username_description": "Az email szerverrel valÃŗ hitelesítÊshez hasznÃĄlt felhasznÃĄlÃŗnÊv", "notification_enable_email_notifications": "Email ÊrtesítÊsek engedÊlyezÊse", - "notification_settings": "ÉrtesítÊs BeÃĄllítÃĄsok", + "notification_settings": "ÉrtesítÊs beÃĄllítÃĄsok", "notification_settings_description": "ÉrtesítÊsi Ês email beÃĄllítÃĄsok kezelÊse", "oauth_auto_launch": "Automatikus indítÃĄs", "oauth_auto_launch_description": "Az OAuth bejelentkezÊsi folyamat automatikus indítÃĄsa a bejelentkezÊsi oldal megnyitÃĄsakor", "oauth_auto_register": "Automatikus regisztrÃĄciÃŗ", "oauth_auto_register_description": "Új felhasznÃĄlÃŗk automatikus regisztrÃĄlÃĄsa az OAuth hasznÃĄlatÃĄval tÃļrtÊnő bejelentkezÊs utÃĄn", "oauth_button_text": "Gomb szÃļvege", - "oauth_client_secret_description": "KÃļtelező, ha az OAuth szolgÃĄltatÃŗ nem tÃĄmogatja a PKCE-t (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Bizalmas kliens esetÊn kÃļtelező, vagy ha az OAuth szolgÃĄltatÃŗ nem tÃĄmogatja a PKCE-t (Proof Key for Code Exchange) nyilvÃĄnos kliensnÊl.", "oauth_enable_description": "BejelentkezÊs OAuth hasznÃĄlatÃĄval", "oauth_mobile_redirect_uri": "Mobil ÃĄtirÃĄnyítÃĄsi URI", "oauth_mobile_redirect_uri_override": "Mobil ÃĄtirÃĄnyítÃĄsi URI felÃŧlírÃĄs", @@ -272,16 +292,16 @@ "oauth_timeout_description": "KÊrÊsek időkorlÃĄtja milliszekundumban", "ocr_job_description": "GÊpi tanulÃĄs hasznÃĄlata a kÊpeken lÊvő szÃļvegek felismerÊsÊre", "password_enable_description": "BejelentkezÊs emaillel Ês jelszÃŗval", - "password_settings": "Jelszavas BejelentkezÊs", + "password_settings": "Jelszavas bejelentkezÊs", "password_settings_description": "Jelszavas bejelentkezÊs beÃĄllítÃĄsok kezelÊse", "paths_validated_successfully": "Összes Ãētvonal sikeresen ÊrvÊnyesítve", "person_cleanup_job": "SzemÊlyek kipucolÃĄsa", "queue_details": "Sor rÊszletei", "queues": "Feladatsor", "queues_page_description": "Admin feladatsor oldala", - "quota_size_gib": "KvÃŗta MÊrete (GiB)", + "quota_size_gib": "KvÃŗta mÊrete (GiB)", "refreshing_all_libraries": "Összes kÊptÃĄr frissítÊse", - "registration": "Admin RegisztrÃĄciÃŗ", + "registration": "Admin regisztrÃĄciÃŗ", "registration_description": "Mivel ez az első felhasznÃĄlÃŗ a rendszerben, ezÊrt te leszel az Admin, aki az adminisztratív teendőkÊrt felelős Ês tovÃĄbbi felhasznÃĄlÃŗkat tud lÊtrehozni.", "remove_failed_jobs": "Sikertelen feladatok eltÃĄvolítÃĄsa", "require_password_change_on_login": "KÃļtelező jelszÃŗmÃŗdosítÃĄs az első bejelentkezÊskor", @@ -294,7 +314,7 @@ "server_external_domain_settings_description": "NyilvÃĄnosan megosztott linkek domainje (http(s)://-sel)", "server_public_users": "NyilvÃĄnos felhasznÃĄlÃŗk", "server_public_users_description": "Az Ãļsszes felhasznÃĄlÃŗ (nÊv Ês email) ki van írva, amikor egy felhasznÃĄlÃŗt adsz hozzÃĄ egy megosztott albumhoz. Amikor le van tiltva, a felhasznÃĄlÃŗlista csak adminok szÃĄmÃĄra lesz elÊrhető.", - "server_settings": "Szerver BeÃĄllítÃĄsok", + "server_settings": "Szerver beÃĄllítÃĄsok", "server_settings_description": "Szerver beÃĄllítÃĄsok kezelÊse", "server_stats_page_description": "Admin szerver statisztikai oldala", "server_welcome_message": "ÜdvÃļzlő Ãŧzenet", @@ -303,7 +323,7 @@ "sidecar_job": "SegÊdfÃĄjl metaadatok", "sidecar_job_description": "Metaadatok keresÊse vagy szinkronizÃĄlÃĄsa a fÃĄjlrendszeren lÊvő segÊdfÃĄjlokbÃŗl", "slideshow_duration_description": "Az egyes kÊpek megjelenítÊsÊnek időtartama mÃĄsodpercben", - "smart_search_job_description": "GÊpi tanulÃĄs futtatÃĄsa az elemeken, ami az Okos KeresÊshez szÃŧksÊges", + "smart_search_job_description": "GÊpi tanulÃĄs futtatÃĄsa az elemeken, ami az Okos keresÊshez szÃŧksÊges", "storage_template_date_time_description": "Az elem kÊszítÊsi időpontja lesz felhasznÃĄlva az időpont informÃĄciÃŗhoz", "storage_template_date_time_sample": "PÊlda időpont {date}", "storage_template_enable_description": "TÃĄrhely sablon motor engedÊlyezÊse", @@ -312,13 +332,13 @@ "storage_template_migration": "TÃĄrhely sablon migrÃĄlÃĄsa", "storage_template_migration_description": "A jelenlegi {template} alkalmazÃĄsa a mÃĄr feltÃļltÃļtt elemekre", "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_migration_job": "TÃĄrhely sablon migrÃĄlÃĄsa", + "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": "TÃĄrhely sablon", "storage_template_settings_description": "A feltÃļltÃļtt elemek mappaszerkezetÊnek Ês fÃĄjl elnevezÊsÊnek kezelÊse", - "storage_template_user_label": "A felhasznÃĄlÃŗ TÃĄrhely CímkÊje {label}", + "storage_template_user_label": "A felhasznÃĄlÃŗ tÃĄrhely címkÊje {label}", "system_settings": "RendszerbeÃĄllítÃĄsok", "tag_cleanup_job": "CímkÊk kipucolÃĄsa", "template_email_available_tags": "HasznÃĄlthatod a kÃļvetkező vÃĄltozÃŗkat a sablonodban: {tags}", @@ -328,18 +348,18 @@ "template_email_settings": "Email sablonok", "template_email_update_album": "Album frissítve sablon", "template_email_welcome": "ÜdvÃļzlő email sablon", - "template_settings": "ÉrtesítÊs sablon", + "template_settings": "ÉrtesítÊs sablonok", "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", - "theme_settings_description": "Az Immich webes felÃŧlet testreszabÃĄsÃĄnak kezelÊse", - "thumbnail_generation_job": "BÊlyegkÊpek GenerÃĄlÃĄsa", + "theme_custom_css_settings": "EgyÊni CSS", + "theme_custom_css_settings_description": "Cascading Style Sheet stíluslapokkal az Immich stílusa megvÃĄltoztathatÃŗ.", + "theme_settings": "TÊma beÃĄllítÃĄsok", + "theme_settings_description": "Az Immich webes felÃŧletÊnek testreszabÃĄsa", + "thumbnail_generation_job": "BÊlyegkÊpek generÃĄlÃĄsa", "thumbnail_generation_job_description": "Nagy, kicsi Ês elmosÃŗdott bÊlyegkÊpek lÊtrehozÃĄsa minden elemhez, valamint bÊlyegkÊpek generÃĄlÃĄsa minden szemÊlyhez", "transcoding_acceleration_api": "GyorsítÃŗ API", "transcoding_acceleration_api_description": "Az ÃĄtkÃŗdolÃĄs felgyorsítÃĄsÃĄhoz hasznÃĄlt eszkÃļzÃļdhÃļz tartozÃŗ API. Ez a beÃĄllítÃĄs „legtÃļbb, amit megtehetÃŧnk” alapon mÅąkÃļdik: problÊma esetÊn visszaÃĄll szoftveres ÃĄtkÃŗdolÃĄsra. A VP9 a hardvertől fÃŧggően vagy mÅąkÃļdik, vagy nem.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU-t igÊnyel)", - "transcoding_acceleration_qsv": "Gyors SzinkronizÃĄlÃĄs (7. generÃĄciÃŗs vagy Ãējabb Intel CPU-t igÊnyel)", + "transcoding_acceleration_qsv": "Quick Sync (7. generÃĄciÃŗs vagy Ãējabb Intel CPU-t igÊnyel)", "transcoding_acceleration_rkmpp": "RKMPP (csak Rockchip SOC-on)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Elfogadott audio kodekek", @@ -360,7 +380,7 @@ "transcoding_disabled_description": "Ne kÃŗdolja ÃĄt a videÃŗkat. NÊhÃĄny kliensnÊl nem lejÃĄtszhatÃŗ videÃŗkhoz vezethet", "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": "Hardveres gyorsítÃĄs", "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.", @@ -375,12 +395,12 @@ "transcoding_policy_description": "BeÃĄllíthatod, hogy egy videÃŗ mikor legyen ÃĄtkÃŗdolva", "transcoding_preferred_hardware_device": "ÁtkÃŗdolÃĄshoz preferÃĄlt hardver eszkÃļz", "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetÊn. BeÃĄllítja a hardveres ÃĄtkÃŗdolÃĄshoz hasznÃĄlt DRI node-ot.", - "transcoding_preset_preset": "Előre BeÃĄllított (-preset)", + "transcoding_preset_preset": "Előre beÃĄllított (-preset)", "transcoding_preset_preset_description": "TÃļmÃļrítÊsi sebessÊg. A lassabb beÃĄllítÃĄsok kisebb fÃĄjlokat hoznak lÊtre Ês nÃļvelik a minősÊget az adott bitrÃĄta mellett. A VP9 kÃŗdolÃĄs figyelmen kívÃŧl hagyja a 'gyorsabb (faster)'-nÊl nagyobb sebessÊgeket.", "transcoding_reference_frames": "Referencia kÊpkockÃĄk", "transcoding_reference_frames_description": "A hivatkozott kÊpkockÃĄk szÃĄma egy kÊpkocka tÃļmÃļrítÊsÊhez. Magasabb ÊrtÊkek nÃļvelik a tÃļmÃļrítÊsi hatÊkonysÃĄgot, de lelassítjÃĄk a kÃŗdolÃĄsi folyamatot. 0 esetÊn a szoftver magÃĄnak ÃĄllítja be az ÊrtÊket.", "transcoding_required_description": "Csak az el nem fogadott formÃĄtumÃē videÃŗkat", - "transcoding_settings": "VideÃŗ ÁtkÃŗdolÃĄsi BeÃĄllítÃĄsok", + "transcoding_settings": "VideÃŗ ÃĄtkÃŗdolÃĄs beÃĄllítÃĄsok", "transcoding_settings_description": "BeÃĄllíthatod, hogy mely videÃŗkat kell ÃĄtkÃŗdolni Ês hogyan kell feldolgozni őket", "transcoding_target_resolution": "CÊlfelbontÃĄs", "transcoding_target_resolution_description": "A magasabb felbontÃĄs jobb minősÊgben őrzi meg a rÊszleteket, de tovÃĄbb tart lÊtrehozni, nagyobb fÃĄjlmÊrethez vezet Ês belassíthatja az alkalmazÃĄst.", @@ -399,7 +419,7 @@ "trash_enabled_description": "LomtÃĄr engedÊlyezÊse", "trash_number_of_days": "Napok szÃĄma", "trash_number_of_days_description": "HÃĄny napig legyenek a lomtÃĄrban az elemek a vÊgleges tÃļrlÊs előtt", - "trash_settings": "LomtÃĄr BeÃĄllítÃĄsok", + "trash_settings": "LomtÃĄr beÃĄllítÃĄsok", "trash_settings_description": "LomtÃĄr beÃĄllítÃĄsok kezelÊse", "unlink_all_oauth_accounts": "Összes OAuth-fiÃŗk szÊtkapcsolÃĄsa", "unlink_all_oauth_accounts_description": "Ne felejtsd el, hogy az Ãēj szolgÃĄltatÃŗra valÃŗ ÃĄttÊrÊs előtt minden OAuth-fiÃŗk kapcsolatot meg kell szÃŧntetned.", @@ -411,39 +431,42 @@ "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_management": "FelhasznÃĄlÃŗk", "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.", "user_restore_description": "{user} felhasznÃĄlÃŗja vissza lesz ÃĄllítva.", "user_restore_scheduled_removal": "FelhasznÃĄlÃŗ visszaÃĄllítÃĄsa - tÃļrlÊsre jelÃļlve: {date, date, long}", - "user_settings": "FelhasznÃĄlÃŗ BeÃĄllítÃĄsok", + "user_settings": "FelhasznÃĄlÃŗ beÃĄllítÃĄsok", "user_settings_description": "FelhasznÃĄlÃŗ beÃĄllítÃĄsok kezelÊse", "user_successfully_removed": "{email} felhasznÃĄlÃŗ sikeresen eltÃĄvolítva.", "users_page_description": "Admin felhasznÃĄlÃŗk oldala", "version_check_enabled_description": "Új verziÃŗk elÊrhetősÊgÊnek ellenőrzÊse", "version_check_implications": "Az Ãēj verziÃŗk ellenőrzÊse időszakos kommunikÃĄciÃŗt igÊnyel a github.com oldallal", - "version_check_settings": "VerziÃŗ EllenőrzÊs", + "version_check_settings": "VerziÃŗ ellenőrzÊs", "version_check_settings_description": "Az Ãēj verziÃŗrÃŗl valÃŗ ÊrtesítÊs be- Ês kikapcsolÃĄsa", "video_conversion_job": "VideÃŗk ÁtkÃŗdolÃĄsa", "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Ãŗ", + "admin_password": "Admin jelszÃŗ", "administration": "AdminisztrÃĄciÃŗ", "advanced": "HaladÃŗ", + "advanced_settings_clear_image_cache": "FÊnykÊpek gyorsítÃŗtÃĄrÃĄnak kiÃŧrítÊse", + "advanced_settings_clear_image_cache_error": "FÊnykÊpek gyorsítÃŗtÃĄrÃĄnak kiÃŧrítÊse sikertelen", + "advanced_settings_clear_image_cache_success": "{size} sikeresen felszabadítva", "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ő 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": "Egyedi Proxy FejlÊcek [KÍSÉRLETI]", + "advanced_settings_proxy_headers_title": "Egyedi proxy fejlÊcek [KÍSÉRLETI]", "advanced_settings_readonly_mode_subtitle": "Bekapcsol egy írÃĄsvÊdett mÃŗdot ahol csak fotÃŗkat nÊzni lehetsÊges, egyebek, mint tÃļbb kÊp kivÃĄlasztÃĄsa, megosztÃĄs, kivetítÊs Ês tÃļrlÊs ki vannak kapcsolva. Ki/bekapcsolhatÃŗ a felhasznÃĄlÃŗ ikonjÃĄrÃŗl a fő kÊpernyőn", "advanced_settings_readonly_mode_title": "ÍrÃĄsvÊdett mÃŗd", "advanced_settings_self_signed_ssl_subtitle": "Nem ellenőrzi a szerver SSL tanÃēsítvÃĄnyÃĄt. ÖnalÃĄÃ­rt tanÃēsítvÃĄny esetÊn szÃŧksÊges beÃĄllítÃĄs.", "advanced_settings_self_signed_ssl_title": "ÖnalÃĄÃ­rt SSL tanÃēsítvÃĄnyok engedÊlyezÊse [KÍSÉRLETI]", "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_sync_remote_deletions_title": "TÃĄvoli tÃļrlÊsek szinkronizÃĄlÃĄsa [KÍSÉRLETI]", "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", @@ -462,15 +485,17 @@ "album_info_updated": "Album infÃŗ frissítve", "album_leave": "KilÊpsz az albumbÃŗl?", "album_leave_confirmation": "Biztos, hogy ki szeretnÊl lÊpni a(z) {album} albumbÃŗl?", - "album_name": "Album NÊv", + "album_name": "Album nÊv", "album_options": "Album beÃĄllítÃĄsok", "album_remove_user": "FelhasznÃĄlÃŗ tÃļrlÊse?", "album_remove_user_confirmation": "Biztos, hogy el szeretnÊd tÃĄvolítani {user} felhasznÃĄlÃŗt?", "album_search_not_found": "Nem talÃĄlhatÃŗ a keresÊsnek megfelelő album", + "album_selected": "Album kivÃĄlasztva", "album_share_no_users": "Úgy tÅąnik, hogy mÃĄr minden felhasznÃĄlÃŗval megosztottad ezt az albumot, vagy nincs senki, akivel meg tudnÃĄd osztani.", "album_summary": "Album ÃļsszefogalalÃŗ", "album_updated": "Album frissÃŧlt", "album_updated_setting_description": "KÃŧldjÃļn email Êrtesítőt, amikor egy megosztott albumhoz Ãēj elemeket adnak hozzÃĄ", + "album_upload_assets": "Elemek feltÃļltÊse Ês albumhoz adÃĄsa", "album_user_left": "KilÊptÊl a(z) {album} albumbÃŗl", "album_user_removed": "{user} eltÃĄvolítva", "album_viewer_appbar_delete_confirm": "Biztos, hogy tÃļrÃļlni szeretnÊd ezt az albumot?", @@ -479,7 +504,7 @@ "album_viewer_appbar_share_err_remove": "NÊhÃĄny elemet nem sikerÃŧlt tÃļrÃļlni az albumbÃŗl", "album_viewer_appbar_share_err_title": "Az album ÃĄtnevezÊse sikertelen", "album_viewer_appbar_share_leave": "KilÊpÊs az albumbÃŗl", - "album_viewer_appbar_share_to": "MegosztÃĄs Ide", + "album_viewer_appbar_share_to": "MegosztÃĄs ide", "album_viewer_page_share_add_users": "FelhasznÃĄlÃŗk hozzÃĄadÃĄsa", "album_with_link_access": "A link birtokÃĄban bÃĄrki lÃĄthatja a fotÃŗkat Ês a szemÊlyeket ebben az albumban.", "albums": "Albumok", @@ -488,9 +513,11 @@ "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.", "albums_on_device_count": "Albumok az eszkÃļzÃļn ({count})", + "albums_selected": "{count, plural, one {# album kivÃĄlasztva} other {# album kivÃĄlasztva}}", "all": "Mind", "all_albums": "Minden album", "all_people": "Minden szemÊly", + "all_photos": "Minden fÊnykÊp", "all_videos": "Minden videÃŗ", "allow_dark_mode": "SÃļtÊt tÊma engedÊlyezÊse", "allow_edits": "MÃŗdosítÃĄsok engedÊlyezÊse", @@ -498,17 +525,20 @@ "allow_public_user_to_upload": "EngedÊlyezi a feltÃļltÊst publikus felhasznÃĄlÃŗ szÃĄmÃĄra", "allowed": "EngedÊlyezett", "alt_text_qr_code": "QR kÃŗd kÊp", + "always_keep": "Tartsa meg mindig", + "always_keep_photos_hint": "A tÃĄrhely-felszabadítÃĄs nem tÃļrli az eszkÃļzÃļn talÃĄlhatÃŗ fÊnykÊpeket.", + "always_keep_videos_hint": "A tÃĄrhely-felszabadítÃĄs nem tÃļrli az eszkÃļzÃļn talÃĄlhatÃŗ videÃŗkat.", "anti_clockwise": "ÓramutatÃŗ jÃĄrÃĄsÃĄval ellentÊtes irÃĄny", - "api_key": "API Kulcs", + "api_key": "API kulcs", "api_key_description": "Ez csak most az egyszer jelenik meg. Az ablak bezÃĄrÃĄsa előtt feltÊtlenÃŧl mÃĄsold.", - "api_key_empty": "Az API Kulcs nÊv nem kÊne, hogy Ãŧres legyen", - "api_keys": "API Kulcsok", + "api_key_empty": "Az API kulcs nÊv nem lehet Ãŧres", + "api_keys": "API kulcsok", "app_architecture_variant": "Variant (ArchitektÃēra)", "app_bar_signout_dialog_content": "Biztos, hogy ki szeretnÊl jelentkezni?", "app_bar_signout_dialog_ok": "Igen", "app_bar_signout_dialog_title": "KijelentkezÊs", "app_download_links": "App letÃļltÊsi linkek", - "app_settings": "AlkalmazÃĄs BeÃĄllítÃĄsok", + "app_settings": "AlkalmazÃĄs beÃĄllítÃĄsok", "app_stores": "App Store-ok", "app_update_available": "Egy Ãēj frissítÊs Êrhető el", "appears_in": "Itt szerepel", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {ArchivÃĄlva #}}", "are_these_the_same_person": "Ugyanaz a szemÊly?", "are_you_sure_to_do_this": "Biztosan ezt szeretnÊd csinÃĄlni?", + "array_field_not_fully_supported": "Lista mezőkhÃļz a JSON manuÃĄlis szerkesztÊse szÃŧksÊges", "asset_action_delete_err_read_only": "Csak-olvashatÃŗ elem(ek)et nem lehet tÃļrÃļlni, így ezeket ÃĄtugorjuk", "asset_action_share_err_offline": "Nem lehet betÃļlteni a kapcsolat nÊlkÃŧli elem(ek)et, így ezeket kihagyjuk", "asset_added_to_album": "HozzÃĄadva az albumhoz", "asset_adding_to_album": "HozzÃĄadÃĄs az albumhozâ€Ļ", + "asset_created": "Elem lÊtrehozva", "asset_description_updated": "Az elem leírÃĄsa frissÃŧlt", "asset_filename_is_offline": "A(z) {filename} elem nem elÊrhető, mert offline", "asset_has_unassigned_faces": "Az elemnek hozzÃĄ nem rendelt arcai vannak", @@ -540,7 +572,10 @@ "asset_list_layout_sub_title": "ElrendezÊs", "asset_list_settings_subtitle": "FotÃŗrÃĄcs elrendezÊse", "asset_list_settings_title": "FotÃŗrÃĄcs", - "asset_offline": "Elem Offline", + "asset_not_found_on_device_android": "Az elem nem talÃĄlhatÃŗ az eszkÃļzÃļn", + "asset_not_found_on_device_ios": "Az elem nem talÃĄlhatÃŗ az eszkÃļzÃļn. Ha az iCloud-ot hasznÃĄlod, az elem lehet hogy azÊrt nem elÊrhető, mert rossz fÃĄjl van tÃĄrolva az iCloud-on", + "asset_not_found_on_icloud": "Az elem nem talÃĄlhatÃŗ az iCloud-on. Lehet, hogy azÊrt nem elÊrhető, mert rossz fÃĄjl van az iCloud-on tÃĄrolva", + "asset_offline": "Elem offline", "asset_offline_description": "Ez a kÃŧlső elem mÃĄr nem elÊrhető a lemezen. KÊrlek, lÊpj kapcsolatba az Immich adminisztrÃĄtorÃĄval.", "asset_restored_successfully": "Elem sikeresen helyreÃĄllítva", "asset_skipped": "Kihagyva", @@ -550,7 +585,7 @@ "asset_uploaded": "FeltÃļltve", "asset_uploading": "FeltÃļltÊsâ€Ļ", "asset_viewer_settings_subtitle": "A kÊpnÊzegető beÃĄllítÃĄsainak kezelÊse", - "asset_viewer_settings_title": "Elem Megjelenítő", + "asset_viewer_settings_title": "Elem megjelenítő", "assets": "Elemek", "assets_added_count": "{count, plural, other {# elem}} hozzÃĄadva", "assets_added_to_album_count": "{count, plural, other {# elem}} hozzÃĄadva az albumhoz", @@ -574,24 +609,24 @@ "assets_trashed_from_server": "{count} elem lomtÃĄrba helyezve az Immich szerveren", "assets_were_part_of_album_count": "{count, plural, other {# elem}} mÃĄr eleve szerepelt az albumban", "assets_were_part_of_albums_count": "Az {count, plural, one {elem} other {elemek}} mÃĄr az hozzÃĄ lettek adva az albumhoz", - "authorized_devices": "EngedÊlyezett EszkÃļzÃļk", - "automatic_endpoint_switching_subtitle": "A megadott WiFi-n keresztÃŧl helyi hÃĄlÃŗzaton keresztÃŧl kapcsolÃŗdolik, egyÊbkÊnt az alternatív címeket hasznÃĄlja", - "automatic_endpoint_switching_title": "Automatikus URL cím vÃĄltÃĄs", + "authorized_devices": "EngedÊlyezett eszkÃļzÃļk", + "automatic_endpoint_switching_subtitle": "A megadott Wi-Fi-n keresztÃŧl helyi hÃĄlÃŗzaton keresztÃŧl kapcsolÃŗdolik, egyÊbkÊnt az alternatív címeket hasznÃĄlja", + "automatic_endpoint_switching_title": "Automatikus URL vÃĄltÃĄs", "autoplay_slideshow": "Automatikus diavetítÊs", "back": "Vissza", "back_close_deselect": "Vissza, bezÃĄrÃĄs, vagy kijelÃļlÊs tÃļrlÊse", - "background_backup_running_error": "HÃĄttÊrben futÃŗ mentÊs folyamatban, kÊzi mentÊs nem indíthatÃŗ", + "background_backup_running_error": "HÃĄttÊrben futÃŗ mentÊs folyamatban, a kÊzi mentÊs nem indíthatÃŗ", "background_location_permission": "HÃĄttÊrben tÃļrtÊnő helymeghatÃĄrozÃĄsi engedÊly", "background_location_permission_content": "HÃĄlÃŗzatok automatikus vÃĄltÃĄsÃĄhoz az Immich-nek *mindenkÊppen* hozzÃĄ kell fÊrnie a pontos helyzethez, hogy az alkalmazÃĄs le tudja kÊrni a Wi-Fi hÃĄlÃŗzat nevÊt", "background_options": "HÃĄttÊrbeli futÃĄs beÃĄllítÃĄsai", - "backup": "MentÊs", + "backup": "BiztonsÃĄgi MentÊs", "backup_album_selection_page_albums_device": "Ezen az eszkÃļzÃļn lÊvő albumok ({count})", "backup_album_selection_page_albums_tap": "Koppints a hozzÃĄadÃĄshoz, duplÃĄn koppints az eltÃĄvolítÃĄshoz", "backup_album_selection_page_assets_scatter": "Egy elem tÃļbb albumban is lehet. EzÊrt a mentÊshez albumokat lehet hozzÃĄadni vagy azokat a mentÊsből kihagyni.", "backup_album_selection_page_select_albums": "VÃĄlassz albumokat", "backup_album_selection_page_selection_info": "ÖsszegzÊs", "backup_album_selection_page_total_assets": "Összes egyedi elem", - "backup_albums_sync": "Backup albumok szinkronizÃĄlÃĄsa", + "backup_albums_sync": "BiztonsÃĄgi mentÊs albumok szinkronizÃĄlÃĄsa", "backup_all": "Összes", "backup_background_service_backup_failed_message": "Az elemek mentÊse sikertelen. ÚjraprÃŗbÃĄlkozÃĄsâ€Ļ", "backup_background_service_complete_notification": "Az adatok mentÊse befejeződÃļtt", @@ -601,7 +636,7 @@ "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}", - "backup_controller_page_albums": "Albumok MentÊse", + "backup_controller_page_albums": "Albumok biztonsÃĄgi mentÊse", "backup_controller_page_background_app_refresh_disabled_content": "EngedÊlyezd a hÃĄttÊrben tÃļrtÊnő frissítÊst a BeÃĄllítÃĄsok > ÁltalÃĄnos > HÃĄttÊrben FrissítÊs menÃŧpontban.", "backup_controller_page_background_app_refresh_disabled_title": "HÃĄttÊrben frissítÊs kikapcsolva", "backup_controller_page_background_app_refresh_enable_button_text": "UgrÃĄs a beÃĄllítÃĄsokhoz", @@ -627,12 +662,12 @@ "backup_controller_page_failed": "Sikertelen ({count})", "backup_controller_page_filename": "FÃĄjlnÊv: {filename}[{size}]", "backup_controller_page_id": "AzonosítÃŗ: {id}", - "backup_controller_page_info": "MentÊsi InformÃĄciÃŗk", + "backup_controller_page_info": "MentÊsi informÃĄciÃŗk", "backup_controller_page_none_selected": "Egy sincs kivÃĄlasztva", "backup_controller_page_remainder": "HÃĄtralÊvő", "backup_controller_page_remainder_sub": "HÃĄtralÊvő fotÃŗk Ês videÃŗk a kijelÃļltek kÃļzÃŧl", - "backup_controller_page_server_storage": "Szerver TÃĄrhely", - "backup_controller_page_start_backup": "MentÊs IndítÃĄsa", + "backup_controller_page_server_storage": "Szerver tÃĄrhely", + "backup_controller_page_start_backup": "MentÊs indítÃĄsa", "backup_controller_page_status_off": "Automatikus mentÊs az előtÊrben ki van kapcsolva", "backup_controller_page_status_on": "Automatikus mentÊs az előtÊrben be van kapcsolva", "backup_controller_page_storage_format": "{used} / {total} felhasznÃĄlva", @@ -661,18 +696,18 @@ "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", - "bugs_and_feature_requests": "HibabejelentÊs Ês Új FunkciÃŗ KÊrÊse", + "bugs_and_feature_requests": "HibabejelentÊs Ês Ãēj funkciÃŗ kÊrÊse", "build": "FelÊpítÊs", - "build_image": "Build KÊp", + "build_image": "KÊp elkÊszítÊse", "bulk_delete_duplicates_confirmation": "Biztosan kitÃļrÃļlsz {count, plural, one {# duplikÃĄlt elemet} other {# duplikÃĄlt elemet}}? A mÅąvelet a legnagyobb mÊretÅą elemet tartja meg minden hasonlÃŗ csoportbÃŗl Ês minden mÃĄsik duplikÃĄlt elemet kitÃļrÃļl. Ez a mÅąvelet nem visszavonhatÃŗ!", "bulk_keep_duplicates_confirmation": "Biztosan meg szeretnÊl tartani {count, plural, other {# egyező elemet}}? Ez a mÅąvelet az elemek tÃļrlÊse nÊlkÃŧl megszÃŧnteti az Ãļsszes duplikÃĄlt csoportosítÃĄst.", "bulk_trash_duplicates_confirmation": "Biztosan kitÃļrÃļlsz {count, plural, one {# duplikÃĄlt fÃĄjlt} other {# duplikÃĄlt fÃĄjlt}}? Ez a mÅąvelet megtartja minden csoportbÃŗl a legnagyobb mÊretÅą elemet, Ês kitÃļrÃļl minden mÃĄsik duplikÃĄltat.", - "buy": "Immich MegvÃĄsÃĄrlÃĄsa", + "buy": "Immich megvÃĄsÃĄrlÃĄsa", "cache_settings_clear_cache_button": "GyorsítÃŗtÃĄr kiÃŧrítÊse", "cache_settings_clear_cache_button_title": "KiÃŧríti az alkalmazÃĄs gyorsítÃŗtÃĄrÃĄt. Ez jelentősen kihat az alkalmazÃĄs teljesítmÊnyÊre, amíg a gyorsítÃŗtÃĄr Ãējra nem ÊpÃŧl.", "cache_settings_duplicated_assets_clear_button": "KIÜRÍT", "cache_settings_duplicated_assets_subtitle": "FotÃŗk Ês videÃŗk, amiket az alkalmazÃĄs figyelmen kívÃŧl hagyott", - "cache_settings_duplicated_assets_title": "DuplikÃĄlt Elemek ({count})", + "cache_settings_duplicated_assets_title": "DuplikÃĄlt elemek ({count})", "cache_settings_statistics_album": "KÊptÃĄr bÊlyegkÊpei", "cache_settings_statistics_full": "Teljes mÊretÅą kÊpek", "cache_settings_statistics_shared": "Megosztott album bÊlyegkÊpei", @@ -680,8 +715,8 @@ "cache_settings_statistics_title": "GyorsítÃŗtÃĄr hasznÃĄlata", "cache_settings_subtitle": "Az Immich mobilalkalmazÃĄs gyorsítÃŗtÃĄr viselkedÊsÊnek beÃĄllítÃĄsa", "cache_settings_tile_subtitle": "Helyi tÃĄrhely viselkedÊsÊnek beÃĄllítÃĄsa", - "cache_settings_tile_title": "Helyi TÃĄrhely", - "cache_settings_title": "GyorsítÃŗtÃĄr BeÃĄllítÃĄsok", + "cache_settings_tile_title": "Helyi tÃĄrhely", + "cache_settings_title": "GyorsítÃŗtÃĄr beÃĄllítÃĄsok", "camera": "FÊnykÊpezőgÊp", "camera_brand": "FÊnykÊpezőgÊp mÃĄrka", "camera_model": "FÊnykÊpezőgÊp modell", @@ -703,14 +738,16 @@ "change_name_successfully": "A nÊv megvÃĄltoztatÃĄsa sikeres", "change_password": "JelszÃŗcsere", "change_password_description": "Most jelentkezel be a rendszerbe első alkalommal, vagy valaki jelszÃŗ-vÃĄltoztatÃĄst kezdemÊnyezett. KÊrjÃŧk, add meg az Ãēj jelszÃŗt.", - "change_password_form_confirm_password": "JelszÃŗ MegerősítÊse", - "change_password_form_description": "Szia {name}!\n\nMost jelentkezel be előszÃļr a rendszerbe vagy mÃĄs okbÃŗl szÃŧksÊges a jelszavad mevÃĄltoztatÃĄsa. KÊrjÃŧk, add meg Ãēj jelszavad.", + "change_password_form_confirm_password": "JelszÃŗ megerősítÊse", + "change_password_form_description": "Szia {name}!\n\nMost jelentkezel be előszÃļr a rendszerbe vagy mÃĄs okbÃŗl szÃŧksÊges a jelszavad megvÃĄltoztatÃĄsa. KÊrjÃŧk, add meg az Ãēj jelszavad.", "change_password_form_log_out": "KijelentkezÊs az Ãļsszes tÃļbbi eszkÃļzről", "change_password_form_log_out_description": "Javasolt kijelentkezni az Ãļsszes tÃļbbi eszkÃļzről", - "change_password_form_new_password": "Új JelszÃŗ", + "change_password_form_new_password": "Új jelszÃŗ", "change_password_form_password_mismatch": "A beírt jelszavak nem egyeznek", - "change_password_form_reenter_new_password": "JelszÃŗ (MÊg Egyszer)", + "change_password_form_reenter_new_password": "JelszÃŗ (mÊg egyszer)", "change_pin_code": "PIN kÃŗd megvÃĄltoztatÃĄsa", + "change_trigger": "FeltÊtel mÃŗdosítÃĄsa", + "change_trigger_prompt": "Biztosan mÃŗdosítani szeretnÊd az indítÃĄsi feltÊtelt? Ezzel tÃļrlÃļd az Ãļsszes mÅąveletet Ês szÅąrőt.", "change_your_password": "Jelszavad megvÃĄltoztatÃĄsa", "changed_visibility_successfully": "LÃĄthatÃŗsÃĄg sikeresen megvÃĄltoztatva", "charging": "TÃļltÊs", @@ -718,17 +755,30 @@ "check_corrupt_asset_backup": "SÊrÃŧlt elemek keresÊse a mentÊsben", "check_corrupt_asset_backup_button": "EllenőrzÊs", "check_corrupt_asset_backup_description": "Ezt az ellenőtzÊst csak Wi-Fi hÃĄlÃŗzaton futtasd Ês csak akkot, ha mÃĄr az Ãļsszes elem feltÃļltÊsre kerÃŧlt. A folyamat nÊhÃĄny percig is eltarthat.", - "check_logs": "HibanaplÃŗ MegnyitÃĄsa", + "check_logs": "HibanaplÃŗ megnyitÃĄsa", + "checksum": "Ellenőrző Ãļsszeg", "choose_matching_people_to_merge": "VÃĄlaszd ki a megegyező szemÊlyeket ÃļsszevonÃĄsra", "city": "VÃĄros", - "clear": "KitÃļrÃļl", + "cleanup_confirm_description": "Az Immich {count} elemet talÃĄlt ({date}-ig), amelyek biztonsÃĄgosan mentÊsre kerÃŧltek a szerveren. TÃļrlÊsre kerÃŧljenek a lokÃĄlis pÊldÃĄnyok erről az eszkÃļzről?", + "cleanup_confirm_prompt_title": "TÃļrlÊs erről az eszkÃļzről?", + "cleanup_deleted_assets": "{count} elem ÃĄthelyezve az eszkÃļz lomtÃĄrÃĄba", + "cleanup_deleting": "LomtÃĄrba helyezÊs...", + "cleanup_found_assets": "{count} feltÃļltÃļtt elem talÃĄlva", + "cleanup_found_assets_with_size": "{count} feltÃļltÃļtt elem talÃĄlva ({size})", + "cleanup_icloud_shared_albums_excluded": "Megosztott iCloud albumok nem kerÃŧlnek ÃĄtnÊzÊsre", + "cleanup_no_assets_found": "Nincs elem ezekkel a kritÊriumokkal. TÃĄrhely felszabadítÃĄsakor csak olyan elemeket tÃļrÃļlhet, amelyek mÃĄr fel lettek tÃļltve a szerverre", + "cleanup_preview_title": "TÃļrlendő elemek ({count})", + "cleanup_step3_description": "Szerverre feltÃļltÃļtt elemek keresÊse dÃĄtum Ês egyÊb megadott szÅąrÊsi kritÊriumok szerint.", + "cleanup_step4_summary": "{count} {date} előtti elem eltÃĄvolítÃĄsra fog kerÃŧlni erről az eszkÃļzről. Az elemek tovÃĄbbra is elÊrhetők lesznek az Immich alkalmazÃĄsban.", + "cleanup_trash_hint": "A tÃĄrhely visszanyerÊsÊhez nyisd meg a beÊpített galÊria alkalmazÃĄst Ês tÃļrÃļld a lomtÃĄrat", + "clear": "TÃļrlÊs", "clear_all": "Alaphelyzet", "clear_all_recent_searches": "LegutÃŗbbi keresÊsek tÃļrlÊse", "clear_file_cache": "GyorsítÃŗtÃĄr 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_enter_password": "JelszÃŗ megadÃĄsa", "client_cert_import": "ImportÃĄlÃĄs", "client_cert_import_success_msg": "Kliens tanÃēsítvÃĄny importÃĄlva", "client_cert_invalid_msg": "ÉrvÊnytelen tanÃēsítvÃĄny fÃĄjl vagy hibÃĄs jelszÃŗ", @@ -748,8 +798,8 @@ "comments_are_disabled": "A megjegyzÊsek le vannak tiltva", "common_create_new_album": "Új album lÊtrehozÃĄsa", "completed": "KÊsz", - "confirm": "JÃŗvÃĄhagy", - "confirm_admin_password": "Admin JelszÃŗ ÚjbÃŗl", + "confirm": "JÃŗvÃĄhagyÃĄs", + "confirm_admin_password": "Admin jelszÃŗ megerősítÊse", "confirm_delete_face": "Biztos, hogy tÃļrÃļlni szeretnÊd a(z) {name} arcÃĄt az elemről?", "confirm_delete_shared_link": "Biztosan tÃļrÃļlni szeretnÊd ezt a megosztott linket?", "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?", @@ -765,34 +815,35 @@ "control_bottom_app_bar_create_new_album": "Új album lÊtrehozÃĄsa", "control_bottom_app_bar_delete_from_immich": "TÃļrlÊs az Immich-ből", "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_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", + "control_bottom_app_bar_share_to": "MegosztÃĄs ide", + "control_bottom_app_bar_trash_from_immich": "LomtÃĄrba helyezÊs", "copied_image_to_clipboard": "KÊp a vÃĄgÃŗlapra mÃĄsolva.", "copied_to_clipboard": "VÃĄgÃŗlapra mÃĄsolva!", "copy_error": "MÃĄsolÃĄsi hiba", "copy_file_path": "FÃĄjlÃētvonal mÃĄsolÃĄsa", - "copy_image": "KÊp MÃĄsolÃĄsa", + "copy_image": "KÊp mÃĄsolÃĄsa", "copy_link": "Link mÃĄsolÃĄsa", "copy_link_to_clipboard": "Link mÃĄsolÃĄsa a vÃĄgÃŗlapra", "copy_password": "JelszÃŗ mÃĄsolÃĄsa", - "copy_to_clipboard": "MÃĄsolÃĄs a VÃĄgÃŗlapra", + "copy_to_clipboard": "MÃĄsolÃĄs a vÃĄgÃŗlapra", "country": "OrszÃĄg", "cover": "KitÃļltÊs", "covers": "BorítÃŗk", - "create": "LÊtrehoz", + "create": "LÊtrehozÃĄs", "create_album": "Album lÊtrehozÃĄsa", "create_album_page_untitled": "NÊvtelen", "create_api_key": "API kulcs lÊtrehozÃĄsa", - "create_library": "KÊptÃĄr LÊtrehozÃĄsa", + "create_first_workflow": "Az első folyamat lÊtrehozÃĄsa", + "create_library": "KÊptÃĄr lÊtrehozÃĄsa", "create_link": "Link lÊtrehozÃĄsa", "create_link_to_share": "MegosztÃĄsi link lÊtrehozÃĄsa", "create_link_to_share_description": "A kivÃĄlasztott fotÃŗkat mindenki lÃĄthassa, aki a linket hasznÃĄlja", "create_new": "ÚJ LÉTREHOZÁSA", "create_new_person": "Új szemÊly lÊtrehozÃĄsa", - "create_new_person_hint": "A kivÃĄlasztott elemeket Ãēj szemÊlyhez rendelÊse", + "create_new_person_hint": "KivÃĄlasztott elemek Ãēj szemÊlyhez rendelÊse", "create_new_user": "Új felhasznÃĄlÃŗ lÊtrehozÃĄsa", "create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA", "create_shared_album_page_share_select_photos": "FotÃŗk vÃĄlasztÃĄsa", @@ -800,39 +851,47 @@ "create_tag": "Címke lÊtrehozÃĄsa", "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", + "create_workflow": "Folyamat lÊtrehozÃĄsa", "created": "KÊszÃŧlt", "created_at": "LÊtrehozva", "creating_linked_albums": "Kapcsolt albumok lÊtrehozÃĄsa...", "crop": "KivÃĄgÃĄs", + "crop_aspect_ratio_fixed": "RÃļgzített", + "crop_aspect_ratio_free": "Tetszőleges", + "crop_aspect_ratio_original": "Eredeti", "curated_object_page_title": "Dolgok", "current_device": "Ez az eszkÃļz", "current_pin_code": "AktuÃĄlis PIN kÃŗd", "current_server_address": "Jelenlegi szerver cím", - "custom_locale": "EgyÊni TerÃŧleti BeÃĄllítÃĄs", + "custom_date": "EgyÊni dÃĄtum", + "custom_locale": "EgyÊni terÃŧleti beÃĄllítÃĄs", "custom_locale_description": "DÃĄtumok Ês szÃĄmok formÃĄzÃĄsa a nyelv Ês terÃŧlet szerint", - "custom_url": "Egyedi URL", + "custom_url": "EgyÊni URL", + "cutoff_date_description": "FotÃŗk megtartÃĄsa az elmÃēltâ€Ļ", + "cutoff_day": "{count, plural, one {nap} other {nap}}", + "cutoff_year": "{count, plural, one {Êv} other {Êv}}", "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "yyyy MMM dd (E)", "dark": "SÃļtÊt", "dark_theme": "SÃļtÊt tÊma kapcsolÃĄsa", "date": "DÃĄtum", "date_after": "DÃĄtumtÃŗl", - "date_and_time": "DÃĄtum Ês Idő", + "date_and_time": "DÃĄtum Ês idő", "date_before": "DÃĄtumig", "date_format": "y LLL d (E) â€ĸ HH:mm", "date_of_birth_saved": "SzÃŧletÊsnap sikeresen elmentve", "date_range": "DÃĄtum intervallum", "day": "Nap", "days": "Napok", - "deduplicate_all": "Az Összes DeduplikÃĄlÃĄsa", + "deduplicate_all": "Összes deduplikÃĄlÃĄsa", "deduplication_criteria_1": "KÊp mÊrete bÃĄjtokban", "deduplication_criteria_2": "EXIF adatok mennyisÊge", - "deduplication_info": "DeduplikÃĄciÃŗs InfÃŗ", + "deduplication_info": "DeduplikÃĄciÃŗs infÃŗ", "deduplication_info_description": "Az automatikus elővÃĄlogatÃĄshoz Ês a duplikÃĄtumok tÃļmeges eltÃĄvolítÃĄsÃĄhoz a kÃļvetkezőket vizsgÃĄljuk:", - "default_locale": "AlapÊrtelmezett TerÃŧleti BeÃĄllítÃĄs", + "default_locale": "AlapÊrtelmezett terÃŧleti beÃĄllítÃĄs", "default_locale_description": "DÃĄtumok Ês szÃĄmok formÃĄzÃĄsa a bÃļngÊsződ terÃŧleti beÃĄllítÃĄsa alapjÃĄn", "delete": "TÃļrlÊs", - "delete_action_confirmation_message": "Biztosan tÃļrÃļlni szeretnÊd ezt az elemet? Így az elem a szerver lomtÃĄrÃĄba kerÃŧl, Ês a megkÊrdezi, hogy tÃļrÃļlni szeretnÊd-e a helyi mÃĄsolatot is", + "delete_action_confirmation_message": "Biztosan tÃļrÃļlni szeretnÊd ezt az elemet? Így az elem a szerver lomtÃĄrÃĄba kerÃŧl, Ês megkÊrdezi, hogy tÃļrÃļlni szeretnÊd-e a az eszkÃļzÃļn is", "delete_action_prompt": "{count} tÃļrÃļlve", "delete_album": "Album tÃļrlÊse", "delete_api_key_prompt": "Biztosan tÃļrÃļlni szeretnÊd ezt az API kulcsot?", @@ -840,21 +899,21 @@ "delete_dialog_alert_local": "Ezek az elemek vÊglegesen tÃļrÃļlve lesznek az eszkÃļzÃļdről, de tovÃĄbbra is elÊrhetőek maradnak az Immich szerveren", "delete_dialog_alert_local_non_backed_up": "NÊhÃĄny elem nem lett elmentve az Immich szerverre Ês most vÊglegesen tÃļrÃļlve lesznek az eszkÃļzÃļdről is", "delete_dialog_alert_remote": "Ezek az elemek vÊglegesen tÃļrlÊsre kerÃŧlnek az Immich szerverről", - "delete_dialog_ok_force": "TÃļrlÊs MindenkÊpp", - "delete_dialog_title": "VÊgleges TÃļrlÊs", + "delete_dialog_ok_force": "TÃļrlÊs mindenkÊpp", + "delete_dialog_title": "VÊgleges tÃļrlÊs", "delete_duplicates_confirmation": "Biztosan vÊglegesen tÃļrÃļlni szeretnÊd ezeket a duplikÃĄtumokat?", "delete_face": "Arc tÃļrlÊse", "delete_key": "Kulcs tÃļrlÊse", - "delete_library": "KÊptÃĄr TÃļrlÊse", + "delete_library": "KÊptÃĄr tÃļrlÊse", "delete_link": "Link tÃļrlÊse", "delete_local_action_prompt": "{count} tÃļrÃļlve az eszkÃļzről", - "delete_local_dialog_ok_backed_up_only": "Csak a BiztonsÃĄgi MentÊs TÃļrlÊse", - "delete_local_dialog_ok_force": "TÃļrlÊs MindenkÊpp", + "delete_local_dialog_ok_backed_up_only": "Csak a biztonsÃĄgi mentÊs tÃļrlÊse", + "delete_local_dialog_ok_force": "TÃļrlÊs mindenkÊpp", "delete_others": "TÃļbbi tÃļrlÊse", "delete_permanently": "TÃļrlÊs vÊglegesen", "delete_permanently_action_prompt": "{count} tÃļrÃļlve vÊglegesen", "delete_shared_link": "Megosztott link tÃļrlÊse", - "delete_shared_link_dialog_title": "Megosztott Link TÃļrlÊse", + "delete_shared_link_dialog_title": "Megosztott link tÃļrlÊse", "delete_tag": "Címke tÃļrlÊse", "delete_tag_confirmation_prompt": "Biztosan tÃļrÃļlni szeretnÊd a(z) {tagName} címkÊt?", "delete_user": "FelhasznÃĄlÃŗ tÃļrlÊse", @@ -866,6 +925,7 @@ "deselect_all": "KijelÃļlÊs megszÃŧntetÊs", "details": "RÊszletek", "direction": "IrÃĄny", + "disable": "LetiltÃĄs", "disabled": "Letiltott", "disallow_edits": "MÃŗdosítÃĄsok letiltÃĄsa", "discord": "Discord", @@ -885,12 +945,13 @@ "download_canceled": "LetÃļltÊs megszakítva", "download_complete": "LetÃļltÊs kÊsz", "download_enqueue": "LetÃļltÊs sorba ÃĄllítva", - "download_error": "LetÃļltÊsi Hiba", + "download_error": "LetÃļltÊsi hiba", "download_failed": "Sikertelen letÃļltÊs", "download_finished": "LetÃļltÊs kÊsz", "download_include_embedded_motion_videos": "BeÃĄgyazott videÃŗk", - "download_include_embedded_motion_videos_description": "MozgÃŗ kÊpekbe beÃĄgyazott videÃŗk mutatÃĄsa kÃŧlÃļn fÃĄjlkÊnt", + "download_include_embedded_motion_videos_description": "MozgÃŗ kÊpekbe ÃĄgyazott videÃŗk megjelenítÊse kÃŧlÃļn fÃĄjlkÊnt", "download_notfound": "LetÃļltÊs nem talÃĄlhatÃŗ", + "download_original": "Eredeti letÃļltÊse", "download_paused": "LetÃļltÊs szÃŧneteltetve", "download_settings": "LetÃļltÊs", "download_settings_description": "Elemek letÃļltÊsÊvel kapcsolatos beÃĄllítÃĄsok kezelÊse", @@ -900,6 +961,7 @@ "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_from_icloud": "LetÃļltÊs az iCloudrÃŗl", "downloading_media": "MÊdia letÃļltÊse", "drop_files_to_upload": "A feltÃļltÊshez hÃēzd bÃĄrhova a fÃĄjlokat", "duplicates": "DuplikÃĄtumok", @@ -926,13 +988,19 @@ "edit_name": "NÊv mÃŗdosítÃĄsa", "edit_people": "SzemÊlyek mÃŗdosítÃĄsa", "edit_tag": "Címke mÃŗdosítÃĄsa", - "edit_title": "Cím MÃŗdosítÃĄsa", + "edit_title": "Cím mÃŗdosítÃĄsa", "edit_user": "FelhasznÃĄlÃŗ mÃŗdosítÃĄsa", + "edit_workflow": "Folyamat mÃŗdosítÃĄsa", "editor": "Szerkesztő", "editor_close_without_save_prompt": "A vÃĄltoztatÃĄsok nem lesznek elmentve", "editor_close_without_save_title": "Szerkesztő bezÃĄrÃĄsa?", - "editor_crop_tool_h2_aspect_ratios": "OldalarÃĄnyok", - "editor_crop_tool_h2_rotation": "ForgatÃĄs", + "editor_confirm_reset_all_changes": "Biztosan vissza szeretnÊd ÃĄllítani az Ãļsszes mÃŗdosítÃĄst?", + "editor_flip_horizontal": "Vízszintes tÃŧkrÃļzÊs", + "editor_flip_vertical": "FÃŧggőleges tÃŧkrÃļzÊs", + "editor_orientation": "OrientÃĄciÃŗ", + "editor_reset_all_changes": "MÃŗdosítÃĄsok visszaÃĄllítÃĄsa", + "editor_rotate_left": "ForgatÃĄs balra 90°-kal", + "editor_rotate_right": "ForgatÃĄs jobbra 90°-kal", "email": "E-mail", "email_notifications": "E-mail ÊrtesítÊsek", "empty_folder": "Ez a mappa Ãŧres", @@ -951,11 +1019,14 @@ "error_change_sort_album": "Album sorbarendezÊsÊnek megvÃĄltoztatÃĄsa sikertelen", "error_delete_face": "Hiba az arc tÃļrlÊse sorÃĄn", "error_getting_places": "Hiba a helyek betÃļltÊsekor", + "error_loading_albums": "Hiba az albumok betÃļltÊsekor", "error_loading_image": "Hiba a kÊp betÃļltÊse kÃļzben", "error_loading_partners": "Hiba a partnerek betÃļltÊsÊnÊl: {error}", + "error_retrieving_asset_information": "Hiba az elem adatainak lekÊrÊ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", + "error_while_navigating": "Hiba az elemhez navigÃĄlÃĄs kÃļzben", "errors": { "cannot_navigate_next_asset": "Nem lehet a kÃļvetkező elemhez navigÃĄlni", "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigÃĄlni", @@ -1013,6 +1084,7 @@ "unable_to_complete_oauth_login": "OAuth bejelentkezÊs befejezÊse sikertelen", "unable_to_connect": "CsatlakozÃĄs sikertelen", "unable_to_copy_to_clipboard": "Nem lehet a vÃĄgÃŗlapra mÃĄsolni. Ellenőrizd, hogy az oldalt https-en keresztÃŧl hasznÃĄlod-e", + "unable_to_create": "Folyamat lÊtrehozÃĄsa sikertelen", "unable_to_create_admin_account": "Admin felhasznÃĄlÃŗ lÊtrehozÃĄsa sikertelen", "unable_to_create_api_key": "Új API kulcs lÊtrehozÃĄsa sikertelen", "unable_to_create_library": "KÊptÃĄr lÊtrehozÃĄsa sikertelen", @@ -1023,6 +1095,7 @@ "unable_to_delete_exclusion_pattern": "KizÃĄrÃĄsi minta (pattern) tÃļrlÊse sikertelen", "unable_to_delete_shared_link": "Megosztott link tÃļrlÊse sikertelen", "unable_to_delete_user": "FelhasznÃĄlÃŗ tÃļrlÊse sikertelen", + "unable_to_delete_workflow": "Folyamat tÃļrlÊse sikertelen", "unable_to_download_files": "FÃĄjlok letÃļltÊse sikertelen", "unable_to_edit_exclusion_pattern": "KizÃĄrÃĄsi minta (pattern) mÃŗdosítÃĄsa sikertelen", "unable_to_empty_trash": "LomtÃĄr ÃŧrítÊse sikertelen", @@ -1058,10 +1131,11 @@ "unable_to_save_name": "NÊv mentÊse sikertelen", "unable_to_save_profile": "Profil mentÊse sikertelen", "unable_to_save_settings": "BeÃĄllítÃĄsok mentÊse sikertelen", - "unable_to_scan_libraries": "A KÊptÃĄrak ÃĄtfÊsÃŧlÊse sikertelen", - "unable_to_scan_library": "A KÊptÃĄr ÃĄtfÊsÃŧlÊse sikertelen", + "unable_to_scan_libraries": "A kÊptÃĄrak ÃĄtfÊsÃŧlÊse sikertelen", + "unable_to_scan_library": "A kÊptÃĄr ÃĄtfÊsÃŧlÊse sikertelen", "unable_to_set_feature_photo": "KijelÃļlt fÊnykÊp beÃĄllítÃĄsa sikertelen", "unable_to_set_profile_picture": "ProfilkÊp beÃĄllítÃĄsa sikertelen", + "unable_to_set_rating": "Nem sikerÃŧlt mÃŗdosítani az ÊrtÊkelÊst", "unable_to_submit_job": "A feladat elindítÃĄsa sikertelen", "unable_to_trash_asset": "Elem lomtÃĄrba helyezÊse sikertelen", "unable_to_unlink_account": "A fiÃŗk szÊtkapcsolÃĄsa sikertelen", @@ -1073,8 +1147,10 @@ "unable_to_update_settings": "BeÃĄllítÃĄsok mÃŗdosítÃĄsa sikertelen", "unable_to_update_timeline_display_status": "Az idővonal megjelenítÊsi stÃĄtuszÃĄnak frissítÊse sikertelen", "unable_to_update_user": "FelhasznÃĄlÃŗ mÃŗdosítÃĄsa sikertelen", + "unable_to_update_workflow": "Folyamat mÃŗdosítÃĄsa sikertelen", "unable_to_upload_file": "FÃĄjlfeltÃļltÊs sikertelen" }, + "errors_text": "HibÃĄk", "exclusion_pattern": "KizÃĄrÃĄsi minta", "exif": "Exif", "exif_bottom_sheet_description": "LeírÃĄs HozzÃĄadÃĄsa...", @@ -1084,7 +1160,7 @@ "exif_bottom_sheet_no_description": "Nincs leírÃĄs", "exif_bottom_sheet_people": "EMBEREK", "exif_bottom_sheet_person_add_person": "Elnevez", - "exit_slideshow": "KilÊpÊs a DiavetítÊsből", + "exit_slideshow": "KilÊpÊs a diavetítÊsből", "expand_all": "Összes kinyitÃĄsa", "experimental_settings_new_asset_list_subtitle": "FejlesztÊs alatt", "experimental_settings_new_asset_list_title": "KisÊrleti kÊprÃĄcs engedÊlyezÊse", @@ -1096,16 +1172,17 @@ "explore": "BÃļngÊszÊs", "explorer": "BÃļngÊsző", "export": "ExportÃĄlÃĄs", - "export_as_json": "ExportÃĄlÃĄs JSON formÃĄtumban", - "export_database": "AdatbÃĄzis ExportÃĄlÃĄsa", + "export_as_json": "ExportÃĄlÃĄs JSON-kÊnt", + "export_database": "AdatbÃĄzis exportÃĄlÃĄsa", "export_database_description": "Az SQLite adatbÃĄzis exportÃĄlÃĄsa", "extension": "KiterjesztÊs", - "external": "KÃŧlső KÊptÃĄr", - "external_libraries": "KÃŧlső KÊptÃĄrak", + "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 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_count": "Sikertelen: {count}", "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", @@ -1115,17 +1192,18 @@ "favorites": "Kedvencek", "favorites_page_no_favorites": "Nem talÃĄlhatÃŗ kedvencnek jelÃļlt elem", "feature_photo_updated": "CímlapkÊp frissítve", - "features": "Jellemzők", + "features": "BeÃĄllítÃĄsok", "features_in_development": "Folyamatban lÊvő fejlesztÊsek", "features_setting_description": "Az alkalmazÃĄs jellemzőinek kezelÊse", - "file_name": "FÃĄjlnÊv", "file_name_or_extension": "FÃĄjlnÊv vagy kiterjesztÊs", "file_size": "FÃĄjlmÊret", "filename": "FÃĄjlnÊv", "filetype": "FÃĄjltípus", "filter": "SzÅąrő", + "filter_description": "Az elemek szÅąrÊsi feltÊtelei", "filter_people": "SzemÊlyek szÅąrÊse", "filter_places": "Helyszínek szÅąrÊse", + "filters": "SzÅąrők", "find_them_fast": "NÊv alapjÃĄn keresÊssel gyorsan megtalÃĄlhatÃŗak", "first": "Első", "fix_incorrect_match": "HibÃĄs talÃĄlat javítÃĄsa", @@ -1135,14 +1213,18 @@ "folders_feature_description": "A fÃĄjlrendszerben lÊvő fÊnykÊpek Ês videÃŗk mappanÊzetben valÃŗ bÃļngÊszÊse", "forgot_pin_code_question": "Elfelejtetted a PIN kÃŗdod?", "forward": "Előre", + "free_up_space": "TÃĄrhely felszabadítÃĄsa", + "free_up_space_description": "Hely felszabadítÃĄsa ÊrdekÊben helyezze ÃĄt a mentett fotÃŗkat Ês videÃŗkat az eszkÃļz kukÃĄjÃĄba. A szerveren lÊvő mÃĄsolatok biztonsÃĄgban maradnak.", + "free_up_space_settings_subtitle": "EszkÃļz tÃĄrhely felszabadítÃĄsa", "full_path": "Teljes elÊrÊi Ãētvonal: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ez a funkciÃŗ a Google-től tÃļlti be a mÅąkÃļdÊsÊhez szÃŧksÊges kÃŧlső adatokat.", "general": "ÁltalÃĄnos", "geolocation_instruction_location": "Kattints egy elemre, amelynek ismert a helyszíne a pozíciÃŗ kivÃĄlasztÃĄsÃĄhoz, vagy vÃĄlassz a tÊrkÊpen", "get_help": "SegítsÊgkÊrÊs", + "get_people_error": "Hiba a szemÊlyek beszerzÊse kÃļzben", "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", + "getting_started": "Kezdő lÊpÊsek", "go_back": "VisszalÊpÊs", "go_to_folder": "UgrÃĄs a mappÃĄhoz", "go_to_search": "UgrÃĄs a keresÊshez", @@ -1156,7 +1238,7 @@ "group_places_by": "Helyszínek csoportosítÃĄsa...", "group_year": "CsoportosítÃĄs Êv szerint", "haptic_feedback_switch": "RezgÊses visszajelzÊs engedÊlyezÊse", - "haptic_feedback_title": "RezgÊses VisszajelzÊs", + "haptic_feedback_title": "RezgÊses visszajelzÊs", "has_quota": "KvÃŗta", "hash_asset": "Elem hash-elÊse", "hashed_assets": "Hash-elt elemek", @@ -1166,12 +1248,14 @@ "header_settings_header_name_input": "FejlÊc neve", "header_settings_header_value_input": "FejlÊc ÊrtÊke", "headers_settings_tile_title": "EgyÊni proxy fejlÊcek", + "height": "MagassÃĄg", "hi_user": "Szia {name} ({email})", "hide_all_people": "Minden szemÊly elrejtÊse", "hide_gallery": "GalÊria elrejtÊse", "hide_named_person": "{name} elrejtÊse", "hide_password": "JelszÃŗ elrejtÊse", "hide_person": "SzemÊly elrejtÊse", + "hide_schema": "SÊma elrejtÊse", "hide_text_recognition": "SzÃļvegfelismerÊs 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.", @@ -1186,7 +1270,7 @@ "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, 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_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", @@ -1209,12 +1293,12 @@ "image_alt_text_date_place_3_people": "{isVideo, select, true {VideÃŗ} other {KÊp}} itt: {country}, {city}, velÃŧk: {person1}, {person2} Ês {person3} (kÊszÃŧlt: {date})", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {VideÃŗ} other {KÊp}} itt: {country}, {city}, velÃŧk: {person1}, {person2} Ês tovÃĄbbi {additionalCount, number} szemÊly (kÊszÃŧlt: {date})", "image_saved_successfully": "KÊp elmentve", - "image_viewer_page_state_provider_download_started": "LetÃļltÊs MegkezdődÃļtt", - "image_viewer_page_state_provider_download_success": "LetÃļltÊs Sikeres", - "image_viewer_page_state_provider_share_error": "MegosztÃĄs Hiba", - "immich_logo": "Immich LogÃŗ", - "immich_web_interface": "Immich Webes FelÃŧlet", - "import_from_json": "ImportÃĄlÃĄs JSON formÃĄtumbÃŗl", + "image_viewer_page_state_provider_download_started": "A letÃļltÊs elkezdődÃļtt", + "image_viewer_page_state_provider_download_success": "LetÃļltÊs sikeres", + "image_viewer_page_state_provider_share_error": "MegosztÃĄsi hiba", + "immich_logo": "Immich logÃŗ", + "immich_web_interface": "Immich webes felÃŧlet", + "import_from_json": "ImportÃĄlÃĄs JSON-ből", "import_path": "ImportÃĄlÃĄsi Ãētvonal", "in_albums": "{count, plural, one {# albumban} other {# albumban}}", "in_archive": "Archívumban", @@ -1234,7 +1318,7 @@ }, "invalid_date": "ÉrvÊnytelen dÃĄtum", "invalid_date_format": "ÉrvÊnytelen dÃĄtumformÃĄtum", - "invite_people": "SzemÊlyek MeghívÃĄsa", + "invite_people": "SzemÊlyek meghívÃĄsa", "invite_to_album": "MeghívÃĄs az albumba", "ios_debug_info_fetch_ran_at": "LetÃļltÊs futtatva {dateTime}", "ios_debug_info_last_sync_at": "UtoljÃĄra szinkronizÃĄlva {dateTime}", @@ -1244,9 +1328,18 @@ "ios_debug_info_processing_ran_at": "A feldolgozÃĄs ekkor futott: {dateTime}", "items_count": "{count, plural, other {# elem}}", "jobs": "Feladatok", + "json_editor": "JSON szerkesztő", + "json_error": "JSON hiba", "keep": "Megtart", - "keep_all": "Összeset Megtart", + "keep_albums": "Albumok megtartÃĄsa", + "keep_albums_count": "{count} album megtartÃĄsa", + "keep_all": "Összes megtartÃĄsa", + "keep_description": "VÃĄlaszd ki, mi maradjon az eszkÃļzÃļdÃļn tÃĄrhely felszabadítÃĄsakor.", + "keep_favorites": "Kedvencek megtartÃĄsa", + "keep_on_device": "Maradjon az eszkÃļzÃļn", + "keep_on_device_hint": "VÃĄlaszd ki az eszkÃļzÃļn tartandÃŗ elemeket", "keep_this_delete_others": "Ennek a meghagyÃĄsa, a tÃļbbi tÃļrlÊse", + "keeping": "Meg lesz tartva: {items}", "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", @@ -1254,11 +1347,11 @@ "language_no_results_title": "Nem talÃĄlhatÃŗ nyelv", "language_search_hint": "Nyelvek keresÊse...", "language_setting_description": "VÃĄlaszd ki preferÃĄlt nyelvet", - "large_files": "Nagy FÃĄjlok", + "large_files": "Nagy fÃĄjlok", "last": "UtolsÃŗ", "last_months": "{count, plural, one {UtolsÃŗ hÃŗnap} other {UtolsÃŗ # hÃŗnap}}", "last_seen": "UtoljÃĄra bejelentkezve", - "latest_version": "Legfrissebb VerziÃŗ", + "latest_version": "Legfrissebb verziÃŗ", "latitude": "SzÊlessÊg", "leave": "ElhagyÃĄs", "leave_album": "Album elhagyÃĄsa", @@ -1269,7 +1362,7 @@ "library_add_folder": "KÃļnyvtÃĄr hozzÃĄadÃĄsa", "library_edit_folder": "KÃļnyvtÃĄr szerkesztÊse", "library_options": "KÊptÃĄr beÃĄllítÃĄsok", - "library_page_device_albums": "Albumok az EszkÃļzÃļn", + "library_page_device_albums": "Albumok az eszkÃļzÃļn", "library_page_new_album": "Új album", "library_page_sort_asset_count": "Elemek szÃĄma", "library_page_sort_created": "LÊtrehozÃĄs ideje", @@ -1281,19 +1374,20 @@ "like_deleted": "ReakciÃŗ tÃļrÃļlve", "link_motion_video": "Motion videÃŗ hozzÃĄrendelÊse", "link_to_oauth": "CsatlakoztatÃĄs OAuth-hoz", - "linked_oauth_account": "Csatlakoztatott OAuth felhasznÃĄlÃŗ", + "linked_oauth_account": "Csatlakoztatott OAuth fiÃŗk", "list": "Lista", "loading": "BetÃļltÊs", "loading_search_results_failed": "KeresÊsi eredmÊnyek betÃļltÊse sikertelen", "local": "Helyi", "local_asset_cast_failed": "Nem lehet olyan elemet vetíteni, ami nincs a szerverre feltÃļltve", - "local_assets": "Helyi Elemek", + "local_assets": "Helyi elemek", + "local_id": "Helyi azonosítÃŗ", "local_media_summary": "Helyi mÊdia ÃļsszegzÊs", "local_network": "Helyi hÃĄlÃŗzat", - "local_network_sheet_info": "Az alkalmazÊs ezen az URL címen fogja elÊrni a szervert, ha a megadott WiFi hÃĄlÃŗzathoz van csatlankozva", + "local_network_sheet_info": "Az alkalmazÊs ezen az URL címen fogja elÊrni a szervert, ha a megadott Wi-Fi hÃĄlÃŗzathoz van csatlankozva", "location": "LokÃĄciÃŗ", "location_permission": "HelymeghatÃĄrozÃĄsi engedÊly", - "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_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", @@ -1303,7 +1397,7 @@ "locked_folder": "ZÃĄrolt mappa", "log_detail_title": "NaplÃŗk rÊszletei", "log_out": "KijelentkezÊs", - "log_out_all_devices": "KijelentkezÊs Minden EszkÃļzÃļn", + "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", @@ -1322,7 +1416,7 @@ "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.", + "login_form_handshake_exception": "Handshake hiba tÃļrtÊnt a szerverrel. EngedÊlyezd a sajÃĄt alÃĄÃ­rÃĄsÃē tanÃēsítvÃĄnyok hasznÃĄlatÃĄt a beÃĄllítÃĄsokban, ha ilyen tanÃēsítvÃĄnyt hasznÃĄlsz.", "login_form_password_hint": "jelszÃŗ", "login_form_save_login": "Maradjon bejelentkezve", "login_form_server_empty": "Add meg a szerver címÊt.", @@ -1339,10 +1433,28 @@ "loop_videos_description": "EngedÊlyezi a videÃŗk folyamatosan ismÊtelt lejÃĄtszÃĄsÃĄt.", "main_branch_warning": "Fejlesztői verziÃŗt hasznÃĄlsz. Javasoljuk a stabil verziÃŗ hasznÃĄlatÃĄt!", "main_menu": "FőmenÃŧ", + "maintenance_action_restore": "AdatbÃĄzis helyreÃĄllítÃĄsa", "maintenance_description": "Az Immich maintenance mode-ba lett ÃĄllítva.", "maintenance_end": "KarbantartÃĄsi mÃŗd kikapcsolÃĄsa", "maintenance_end_error": "KarbantartÃĄsi mÃŗd kikapcsolÃĄsa sikertelen.", "maintenance_logged_in_as": "Bejelentkezve mint: {user}", + "maintenance_restore_from_backup": "HelyreÃĄllítÃĄs biztonsÃĄgi mentÊsből", + "maintenance_restore_library": "KÃļnyvtÃĄr helyreÃĄllítÃĄsa", + "maintenance_restore_library_confirm": "Ha ez jÃŗnak tÅąnik, tovÃĄbb a biztonsÃĄgi mentÊs visszaÃĄllítÃĄsÃĄra!", + "maintenance_restore_library_description": "AdatbÃĄzis helyreÃĄllítÃĄsa", + "maintenance_restore_library_folder_has_files": "{folder} {count} mappÃĄval rendelkezik", + "maintenance_restore_library_folder_no_files": "{folder}-bÃŗl/-ből fÃĄjlok hiÃĄnyoznak!", + "maintenance_restore_library_folder_pass": "olvashatÃŗ Ês írhatÃŗ", + "maintenance_restore_library_folder_read_fail": "nem olvashatÃŗ", + "maintenance_restore_library_folder_write_fail": "nem írhatÃŗ", + "maintenance_restore_library_hint_missing_files": "Fontos fÃĄjlok hiÃĄnyozhatnak", + "maintenance_restore_library_hint_regenerate_later": "RegenerÃĄlhatja ezeket kÊsőbb a beÃĄllítÃĄsokban", + "maintenance_restore_library_hint_storage_template_missing_files": "A tÃĄrolÃĄsi sablon-t hasznÃĄlaja? Lehet, hogy hiÃĄnyoznak fÃĄjlok", + "maintenance_restore_library_loading": "IntegritÃĄsellenőrzÊs Ês heurisztikÃĄk betÃļltÊseâ€Ļ", + "maintenance_task_backup": "AdatbÃĄzis biztonsÃĄgi mentÊse folyamatbanâ€Ļ", + "maintenance_task_migrations": "AdatbÃĄzis migrÃĄlÃĄsa folyamatbanâ€Ļ", + "maintenance_task_restore": "A vÃĄlasztott biztonsÃĄgi mentÊs visszaÃĄllítÃĄsaâ€Ļ", + "maintenance_task_rollback": "A visszaÃĄllítÃĄs sikertelen, kezdeti ÃĄllapot visszatÃļltÊse folyamatbanâ€Ļ", "maintenance_title": "Átmenetileg nem elÊrhető", "make": "GyÃĄrtÃŗ", "manage_geolocation": "Helyadatok kezelÊse", @@ -1363,7 +1475,7 @@ "map_location_dialog_yes": "Igen", "map_location_picker_page_use_location": "KivÃĄlasztott hely hasznÃĄlata", "map_location_service_disabled_content": "A helymeghatÃĄrozÃĄs szolgÃĄltatÃĄst engedÊlyezni kell a jelenlegi helyednÊl lÊvő elemek megjelenítÊsÊhez. SzeretnÊd most engedÊlyezni?", - "map_location_service_disabled_title": "HelymeghatÃĄrozÃĄs SzolgÃĄltatÃĄs letiltva", + "map_location_service_disabled_title": "HelymeghatÃĄrozÃĄs szolgÃĄltatÃĄs letiltva", "map_marker_for_images": "{country}, {city} helyen kÊszÃŧlt kÊpek tÊrkÊpjelÃļlője", "map_marker_with_image": "TÊrkÊpjelÃļlő kÊppel", "map_no_location_permission_content": "A helymeghatÃĄrozÃĄst engedÊlyezni kell a jelenlegi helyednÊl lÊvő elemek megjelenítÊsÊhez. SzeretnÊd most engedÊlyezni?", @@ -1374,11 +1486,11 @@ "map_settings_date_range_option_days": "ElmÃēlt {days} nap", "map_settings_date_range_option_year": "ElmÃēlt Êv", "map_settings_date_range_option_years": "ElmÃēlt {years} Êv", - "map_settings_dialog_title": "TÊrkÊp BeÃĄllítÃĄsok", - "map_settings_include_show_archived": "Archívokkal EgyÃŧtt", - "map_settings_include_show_partners": "PartnerÊvel EgyÃŧtt", - "map_settings_only_show_favorites": "Csak Kedvencek MutatÃĄsa", - "map_settings_theme_settings": "TÊrkÊp TÊmÃĄja", + "map_settings_dialog_title": "TÊrkÊp beÃĄllítÃĄsok", + "map_settings_include_show_archived": "ArchivÃĄltakkal egyÃŧtt", + "map_settings_include_show_partners": "Partnerekkel egyÃŧtt", + "map_settings_only_show_favorites": "Csak kedvencek megjelenítÊse", + "map_settings_theme_settings": "TÊrkÊp tÊma", "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", @@ -1404,6 +1516,8 @@ "minimize": "KicsinyítÊs", "minute": "Perc", "minutes": "Percek", + "mirror_horizontal": "Vízszintesen", + "mirror_vertical": "FÃŧggőlegesen", "missing": "HiÃĄnyzÃŗk", "mobile_app": "MobilapplikÃĄciÃŗ", "mobile_app_download_onboarding_note": "TÃļltse le a kiegÊszítő mobilalkalmazÃĄst az alÃĄbbi opciÃŗk segítsÊgÊvel", @@ -1412,13 +1526,16 @@ "monthly_title_text_date_format": "y MMMM", "more": "TovÃĄbbiak", "move": "ÁthelyezÊs", + "move_down": "Lejjebb", "move_off_locked_folder": "ÁtmozgatÃĄs a zÃĄrolt mappÃĄbÃŗl", "move_to": "MozgatÃĄs", + "move_to_device_trash": "ÁthelyezÊs az eszkÃļz szemetesÊbe", "move_to_lock_folder_action_prompt": "{count} hozzÃĄadva a zÃĄrolt mappÃĄhoz", "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", + "move_up": "Feljebb", + "moved_to_archive": "{count, plural, one {# elem} other {# elem}} archivÃĄlva", + "moved_to_library": "{count, plural, one {# elem} other {# elem}} mÃĄsik kÃļnyvtÃĄrba helyezve", "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", @@ -1426,6 +1543,7 @@ "my_albums": "SajÃĄt albumaim", "name": "NÊv", "name_or_nickname": "NÊv vagy becenÊv", + "name_required": "KÃļtelező megadni egy nevet", "navigate": "NavigÃĄciÃŗ", "navigate_to_time": "NavigÃĄlÃĄs adott időponthoz", "network_requirement_photos_upload": "Mobil adatforgalmat hasznÃĄljon a fÊnykÊpek biztonsÃĄgi mentÊsÊhez", @@ -1435,8 +1553,8 @@ "networking_settings": "HÃĄlÃŗzat", "networking_subtitle": "Szerver vÊgpont beÃĄllítÃĄsok kezelÊse", "never": "Soha", - "new_album": "Új Album", - "new_api_key": "Új API Kulcs", + "new_album": "Új album", + "new_api_key": "Új API kulcs", "new_date_range": "Új dÃĄtumtartomÃĄny", "new_password": "Új jelszÃŗ", "new_person": "Új szemÊly", @@ -1450,25 +1568,29 @@ "next": "KÃļvetkező", "next_memory": "KÃļvetkező emlÊk", "no": "Nem", + "no_actions_added": "MÊg nincsenek mÅąveletek", + "no_albums_found": "Nem talÃĄlhatÃŗk albumok", "no_albums_message": "FotÃŗid Ês videÃŗid rendszerezÊsÊhez hozz lÊtre egy Ãēj albumot", "no_albums_with_name_yet": "Úgy tÅąnik, hogy ilyen nÊvvel mÊg nincs albumod.", "no_albums_yet": "Úgy tÅąnik, hogy mÊg egy albumod sincs.", "no_archived_assets_message": "ArchivÃĄld a fÊnykÊpeket Ês videÃŗkat, hogy elrejtsd azokat a KÊpek nÊzetből", - "no_assets_message": "KATTINTS AZ ELSŐ FÉNYKÉP FELTÖLTÉSÉHEZ", + "no_assets_message": "Kattints ide az első fotÃŗd feltÃļltÊsÊhez", "no_assets_to_show": "Nincs megjeleníthető elem", "no_cast_devices_found": "Nem talÃĄlhatÃŗ eszkÃļz vetítÊshez", - "no_checksum_local": "Nincs elÊrhető ellenőrzőÃļsszeg - a helyi eszkÃļzÃļk nem kÊrhetők le", - "no_checksum_remote": "Nincs elÊrhető ellenőrzőÃļsszeg - a tÃĄvoli eszkÃļz nem kÊrhető le", + "no_checksum_local": "Nincs elÊrhető ellenőrző Ãļsszeg - a helyi elemek nem kÊrhetők le", + "no_checksum_remote": "Nincs elÊrhető ellenőrző Ãļsszeg - a tÃĄvoli elem nem kÊrhető le", + "no_configuration_needed": "Nincs szÃŧksÊg konfigurÃĄciÃŗra", "no_devices": "Nincs engedÊlyezett eszkÃļz", "no_duplicates_found": "Nem talÃĄlhatÃŗk duplikÃĄtumok.", "no_exif_info_available": "Nincs elÊrhető Exif informÃĄciÃŗ", "no_explore_results_message": "TÃļlts fel tÃļbb kÊpet, hogy bÃļngÊszhesd a gyÅąjtemÊnyed.", "no_favorites_message": "Add hozzÃĄ a kedvencekhez, hogy gyorsan megtalÃĄld a legjobb kÊpeidet Ês videÃŗidat", + "no_filters_added": "MÊg nincsenek szÅąrők", "no_libraries_message": "Hozz lÊtre kÃŧlső kÊptÃĄrat a fÊnykÊpeid Ês videÃŗid megtekintÊsÊhez", "no_local_assets_found": "Nem talÃĄlhatÃŗk helyi eszkÃļzÃļk ezzel az ellenőrzőÃļsszeggel", "no_location_set": "Nincs hely megadva", "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_name": "Nincs nÊv", "no_notifications": "Nincsenek ÊrtesítÊsek", "no_people_found": "Nem talÃĄlhatÃŗ szemÊly", "no_places": "Nincsenek helyek", @@ -1477,25 +1599,25 @@ "no_results_description": "PrÃŗbÃĄlkozz szinonimÃĄkkal vagy ÃĄltalÃĄnosabb kulcsszavakkal", "no_shared_albums_message": "Hozz lÊtre egy Ãēj albumot, hogy megoszthasd fÊnykÊpeid Ês videÃŗid mÃĄsokkal", "no_uploads_in_progress": "Nincs folyamatban lÊvő feltÃļltÊs", + "none": "Semelyik", "not_allowed": "Nem engedÊlyezett", "not_available": "N/A", "not_in_any_album": "Nincs albumban", "not_selected": "Nincs kivÃĄlasztva", - "note_apply_storage_label_to_previously_uploaded assets": "MegjegyzÊs: a korÃĄbban feltÃļltÃļtt elemek TÃĄrhely CímkÊzÊsÊhez futtasd a(z)", "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", + "notification_permission_list_tile_enable_button": "ÉrtesítÊsek engedÊlyezÊse", "notification_permission_list_tile_title": "EngedÊly az ÉrtesítÊsekhez", "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", - "obtainium_configurator": "Obtainium KonfigurÃĄtor", + "obtainium_configurator": "Obtainium konfigurÃĄtor", "obtainium_configurator_instructions": "Az Obtainium segítsÊgÊvel kÃļzvetlenÃŧl az Immich GitHub-os kiadÃĄsÃĄbÃŗl telepítheted Ês frissítheted az Android-alkalmazÃĄst. Hozz lÊtre egy API-kulcsot Ês vÃĄlassz egy vÃĄltozatot az Obtainium konfigurÃĄciÃŗs hivatkozÃĄs elkÊszítÊsÊhez", "ocr": "OCR", - "official_immich_resources": "Hivatalos Immich ForrÃĄsok", + "official_immich_resources": "Hivatalos Immich forrÃĄsok", "offline": "Nem elÊrhető (offline)", "offset": "EltolÃĄs", "ok": "Rendben", @@ -1529,21 +1651,21 @@ "page": "Oldal", "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_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", "partner_list_user_photos": "{user} fÊnykÊpei", - "partner_list_view_all": "Összes mutatÃĄsa", + "partner_list_view_all": "Összes megjelenítÊse", "partner_page_empty_message": "MÊg senkivel nem osztottad meg a fÊnykÊpeidet.", "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_stop_sharing_content": "{partner} nem fog tÃļbbÊ hozzÃĄfÊrni a fotÃŗidhoz.", - "partner_sharing": "Partner MegosztÃĄs", + "partner_sharing": "Partnerrel megosztÃĄs", "partners": "Partnerek", "password": "JelszÃŗ", "password_does_not_match": "A jelszavak nem egyeznek", - "password_required": "JelszÃŗ SzÃŧksÊges", + "password_required": "JelszÃŗ szÃŧksÊges", "password_reset_success": "A jelszÃŗ visszaÃĄllítÃĄsa sikeres", "past_durations": { "days": "{days, plural, one {Tegnap} other {ElmÃēlt # nap}}", @@ -1559,6 +1681,7 @@ "people": "SzemÊlyek", "people_edits_count": "{count, plural, other {# szemÊly}} mÃŗdosítva", "people_feature_description": "SzemÊlyek szerint csoportosított fÊnykÊpek Ês videÃŗk bÃļngÊszÊse", + "people_selected": "{count, plural, other {# szemÊly}} kivÃĄlasztva", "people_sidebar_description": "SzemÊlyek link megjelenítÊse az oldalsÃĄvban", "permanent_deletion_warning": "FigyelmeztetÊs vÊgleges tÃļrlÊsről", "permanent_deletion_warning_setting_description": "Figyelmeztessen elemek vÊgleges tÃļrlÊse előtt", @@ -1583,11 +1706,14 @@ "person_age_years": "{years, plural, other {# Êve}}", "person_birthdate": "SzÃŧletett: {date}", "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}", + "person_recognized": "SzemÊly felismerve", + "person_selected": "SzemÊly kivÃĄlasztva", "photo_shared_all_users": "Úgy tÅąnik, hogy mÃĄr mindenkivel megosztottad a fÊnykÊpeidet, vagy nincs senki, akivel meg tudnÃĄd osztani.", "photos": "FÊnykÊpek", - "photos_and_videos": "FÊnykÊpek Ês VideÃŗk", - "photos_count": "{count, plural, one {{count, number} FotÃŗ} other {{count, number} FotÃŗ}}", + "photos_and_videos": "FÊnykÊpek Ês videÃŗk", + "photos_count": "{count, plural, one {{count, number} fotÃŗ} other {{count, number} fotÃŗ}}", "photos_from_previous_years": "FÊnykÊpek az előző Êvekből", + "photos_only": "Csak kÊpek", "pick_a_location": "Hely vÃĄlasztÃĄsa", "pick_custom_range": "Egyedi tartomÃĄny", "pick_date_range": "VÃĄlasszon egy dÃĄtumtartomÃĄnyt", @@ -1597,7 +1723,7 @@ "pin_verification": "PIN kÃŗd megerősítÊse", "place": "Hely", "places": "Helyek", - "places_count": "{count, plural, one {{count, number} Helyszín} other {{count, number} Helyszín}}", + "places_count": "{count, plural, one {{count, number} helyszín} other {{count, number} helyszín}}", "play": "LejÃĄtszÃĄs", "play_memories": "EmlÊkek lejÃĄtszÃĄsa", "play_motion_photo": "MozgÃŗkÊp lejÃĄtszÃĄsa", @@ -1610,7 +1736,7 @@ "preferences_settings_subtitle": "AlkalmazÃĄsbeÃĄllítÃĄsok kezelÊse", "preferences_settings_title": "BeÃĄllítÃĄsok", "preparing": "ElőkÊszítÊs", - "preset": "Sablon", + "preset": "Előre definiÃĄlt", "preview": "ElőnÊzet", "previous": "Előző", "previous_memory": "Előző emlÊk", @@ -1622,13 +1748,13 @@ "privacy": "MagÃĄnszfÊra", "profile": "Profil", "profile_drawer_app_logs": "NaplÃŗk", - "profile_drawer_client_server_up_to_date": "A Kliens Ês a Szerver is naprakÊsz", + "profile_drawer_client_server_up_to_date": "A kliens Ês a szerver is naprakÊsz", "profile_drawer_github": "GitHub", "profile_drawer_readonly_mode": "Csak olvashatÃŗ mÃŗd engedÊlyezve. A kilÊpÊshez hosszan nyomja meg a felhasznÃĄlÃŗi avatar ikont.", "profile_image_of_user": "{user} profilkÊpe", "profile_picture_set": "ProfilkÊp beÃĄllítva.", "public_album": "NyilvÃĄnos album", - "public_share": "NyilvÃĄnos MegosztÃĄs", + "public_share": "NyilvÃĄnos megosztÃĄs", "purchase_account_info": "TÃĄmogatÃŗ", "purchase_activated_subtitle": "KÃļszÃļnjÃŧk, hogy tÃĄmogattad az Immich-et Ês a nyílt forrÃĄskÃŗdÃē szoftvereket", "purchase_activated_time": "AktivÃĄlva ekkor: {date}", @@ -1653,7 +1779,7 @@ "purchase_panel_title": "TÃĄmogasd a projektet", "purchase_per_server": "SzerverenkÊnt", "purchase_per_user": "FelhasznÃĄlÃŗnkÊnt", - "purchase_remove_product_key": "TermÊkkulcs EltÃĄvolítÃĄsa", + "purchase_remove_product_key": "TermÊkkulcs eltÃĄvolítÃĄsa", "purchase_remove_product_key_prompt": "Biztosan el szeretnÊd tÃĄvolítani a termÊkkulcsot?", "purchase_remove_server_product_key": "Szerver termÊkkulcs eltÃĄvolítÃĄsa", "purchase_remove_server_product_key_prompt": "Biztosan el szeretnÊd tÃĄvolítani a szerver termÊkkulcsot?", @@ -1663,12 +1789,14 @@ "purchase_settings_server_activated": "A szerver termÊkkulcsot az admin kezeli", "query_asset_id": "LekÊrdezÊsi eszkÃļz azonosítÃŗja", "queue_status": "Feldolgozva {count}/{total}", + "rate_asset": "Elem ÊrtÊkelÊse", "rating": "ÉrtÊkelÊs csillagokkal", "rating_clear": "ÉrtÊkelÊs tÃļrlÊse", "rating_count": "{count, plural, one {# csillag} other {# csillag}}", "rating_description": "Exif ÊrtÊkelÊs megjelenítÊse az infÃŗpanelen", + "rating_set": "ÉrtÊkelÊs beÃĄllítva: {rating, plural, one {# csillag} other {# csillag}}", "reaction_options": "ReakciÃŗ lehetősÊgek", - "read_changelog": "VÃĄltozÃĄsnaplÃŗ ElolvasÃĄsa", + "read_changelog": "VÃĄltozÃĄsnaplÃŗ elolvasÃĄsa", "readonly_mode_disabled": "Csak olvashatÃŗ mÃŗd kikapcsolva", "readonly_mode_enabled": "Csak olvashatÃŗ mÃŗd bekapcsolva", "ready_for_upload": "KÊszen ÃĄll a feltÃļltÊsre", @@ -1677,10 +1805,10 @@ "reassigned_assets_to_new_person": "{count, plural, other {# elem}} hozzÃĄrendelve egy Ãēj szemÊlyhez", "reassing_hint": "KijelÃļlt elemek lÊtező szemÊlyhez rendelÊse", "recent": "Friss", - "recent-albums": "LegutÃŗbbi albumok", + "recent_albums": "LegutÃŗbbi albumok", "recent_searches": "LegutÃŗbbi keresÊsek", "recently_added": "NemrÊg hozzÃĄadott", - "recently_added_page_title": "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", @@ -1695,14 +1823,14 @@ "refreshing_metadata": "Metaadatok frissítÊse folyamatban", "regenerating_thumbnails": "BÊlyegkÊpek ÃējragenerÃĄlÃĄsa folyamatban", "remote": "TÃĄvoli", - "remote_assets": "TÃĄvoli Elemek", + "remote_assets": "TÃĄvoli elemek", "remote_media_summary": "TÃĄvoli mÊdiaÃļsszefoglalÃŗ", "remove": "EltÃĄvolítÃĄs", "remove_assets_album_confirmation": "Biztosan el szeretnÊl tÃĄvolítani {count, plural, one {# elemet} other {# elemet}} az albumbÃŗl?", "remove_assets_shared_link_confirmation": "Biztosan el szeretnÊl tÃĄvolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", "remove_assets_title": "Elemek eltÃĄvolítÃĄsa?", "remove_custom_date_range": "EgyÊni időintervallum eltÃĄvolítÃĄsa", - "remove_deleted_assets": "TÃļrÃļlt Elemek EltÃĄvolítÃĄsa", + "remove_deleted_assets": "TÃļrÃļlt elemek eltÃĄvolítÃĄsa", "remove_from_album": "EltÃĄvolítÃĄs az albumbÃŗl", "remove_from_album_action_prompt": "{count} eltÃĄvolítva az albumbÃŗl", "remove_from_favorites": "EltÃĄvolítÃĄs a kedvencekből", @@ -1715,7 +1843,7 @@ "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}", + "removed_api_key": "API kulcs eltÃĄvolítva: {name}", "removed_from_archive": "ArchívumbÃŗl eltÃĄvolítva", "removed_from_favorites": "Kedvencekből eltÃĄvolítva", "removed_from_favorites_count": "A kedvencekből {count, plural, other {# elem}} eltÃĄvolítva", @@ -1737,7 +1865,7 @@ "reset_pin_code_description": "Ha elfelejtetted a PIN-kÃŗdod, vedd fel a kapcsolatot a szerver rendszergazdÃĄjÃĄval, hogy visszaÃĄllíthassa azt", "reset_pin_code_success": "PIN kÃŗd sikeresen visszaÃĄllítva", "reset_pin_code_with_password": "A PIN kÃŗdod mindig visszaÃĄllíthatod a jelszavaddal", - "reset_sqlite": "SQLite AdatbÃĄzis VisszaÃĄllítÃĄsa", + "reset_sqlite": "SQLite adatbÃĄzis visszaÃĄllítÃĄsa", "reset_sqlite_confirmation": "Biztosan vissza szeretnÊd ÃĄllítani az SQLite adatbÃĄzist? Az adatok ÃējraszinkronizÃĄlÃĄsÃĄhoz ki kell jelentkezed, majd Ãējra be kell lÊpned", "reset_sqlite_success": "SQLite adatbÃĄzis sikeresen visszaÃĄllítva", "reset_to_default": "VisszaÃĄllítÃĄs alapÃĄllapotba", @@ -1761,14 +1889,16 @@ "save": "MentÊs", "save_to_gallery": "MentÊs a galÊriÃĄba", "saved": "Mentve", - "saved_api_key": "API Kulcs Elmentve", + "saved_api_key": "API kulcs elmentve", "saved_profile": "Profil elmentve", "saved_settings": "Elmentett beÃĄllítÃĄsok", "say_something": "SzÃŗlj hozzÃĄ", "scaffold_body_error_occurred": "Hiba tÃļrtÊnt", - "scan_all_libraries": "Minden KÊptÃĄr ÁtfÊsÃŧlÊse", - "scan_library": "ÁtfÊsÃŧlÊs", - "scan_settings": "ÁtfÊsÃŧlÊsi BeÃĄllítÃĄsok", + "scan": "ÁtfÊsÃŧl", + "scan_all_libraries": "Minden kÊptÃĄr ÃĄtfÊsÃŧlÊse", + "scan_library": "BeolvasÃĄs", + "scan_settings": "ÁtfÊsÃŧlÊsi beÃĄllítÃĄsok", + "scanning": "ÁtfÊsÃŧlÊs folyamatban", "scanning_for_album": "Albumok ÃĄtfÊsÃŧlÊse...", "search": "KeresÊs", "search_albums": "Albumok keresÊse", @@ -1790,14 +1920,15 @@ "search_filter_date_interval": "{start} - {end}", "search_filter_date_title": "VÃĄlassz dÃĄtum intervallumot", "search_filter_display_option_not_in_album": "Nincs albumban", - "search_filter_display_options": "MegjelenítÊsi BeÃĄllítÃĄsok", + "search_filter_display_options": "MegjelenítÊsi beÃĄllítÃĄsok", "search_filter_filename": "KeresÊs fÃĄjlnÊv alapjÃĄn", "search_filter_location": "Hely", "search_filter_location_title": "VÃĄlassz helyet", - "search_filter_media_type": "MÊdia Típus", + "search_filter_media_type": "MÊdia típus", "search_filter_media_type_title": "VÃĄlassz mÊdia típust", "search_filter_ocr": "KeresÊs szÃļvegfelismerÊssel (OCR)", "search_filter_people_title": "VÃĄlassz embereket", + "search_filter_star_rating": "ÉrtÊkelÊs", "search_for": "KeresÊs", "search_for_existing_person": "MÃĄr meglÊvő szemÊly keresÊse", "search_no_more_result": "Nincs tÃļbb talÃĄlat", @@ -1807,19 +1938,19 @@ "search_options": "KeresÊsi lehetősÊgek", "search_page_categories": "KategÃŗriÃĄk", "search_page_motion_photos": "MozgÃŗkÊpek", - "search_page_no_objects": "Nincs InformÃĄciÃŗ a TÃĄrgyakrÃŗl", - "search_page_no_places": "Nincs InformÃĄciÃŗ a Helyekről", + "search_page_no_objects": "Nincs informÃĄciÃŗ a tÃĄrgyakrÃŗl", + "search_page_no_places": "Nincs informÃĄciÃŗ a helyekről", "search_page_screenshots": "KÊpernyőkÊpek", "search_page_search_photos_videos": "KeresÊs a fotÃŗid Ês videÃŗid kÃļzt", "search_page_selfies": "Szelfik", "search_page_things": "Dolgok", - "search_page_view_all_button": "Összes mutatÃĄsa", + "search_page_view_all_button": "Összes megjelenítÊse", "search_page_your_activity": "TevÊkenysÊgeid", "search_page_your_map": "TÊrkÊped", "search_people": "SzemÊlyek keresÊse", "search_places": "Helyek keresÊse", "search_rating": "KeresÊs ÊrtÊkelÊs szerint...", - "search_result_page_new_search_hint": "Új KeresÊs", + "search_result_page_new_search_hint": "Új keresÊs", "search_settings": "BeÃĄllítÃĄsok keresÊse", "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 ", @@ -1827,42 +1958,48 @@ "search_tags": "CímkÊk keresÊse...", "search_timezone": "IdőzÃŗna keresÊse...", "search_type": "Típus keresÊse", - "search_your_photos": "FotÃŗid keresÊse", + "search_your_photos": "KeresÊs", "searching_locales": "Helyszín keresÊse...", "second": "MÃĄsodperc", "see_all_people": "Minden szemÊly megtekintÊse", "select": "KivÃĄlasztÃĄs", + "select_album": "Album kivÃĄlasztÃĄsa", "select_album_cover": "AlbumborítÃŗ kivÃĄlasztÃĄsa", + "select_albums": "Albumok kivÃĄlasztÃĄsa", "select_all": "Összes kijelÃļlÊse", "select_all_duplicates": "Minden duplikÃĄtum kijelÃļlÊse", "select_all_in": "Összes kijelÃļlÊse itt: {group}", "select_avatar_color": "AvatÃĄr színÊnek vÃĄlasztÃĄsa", + "select_count": "{count, plural, one {# kivÃĄlasztÃĄsa} other {# kivÃĄlasztÃĄsa}}", + "select_cutoff_date": "HatÃĄrdÃĄtum vÃĄlasztÃĄsa", "select_face": "Arc kivÃĄlasztÃĄsa", "select_featured_photo": "AlapÊrtelmezett fÊnykÊp kivÃĄlasztÃĄsa", "select_from_computer": "KivÃĄlasztÃĄs a szÃĄmítÃŗgÊpről", "select_keep_all": "'Megtart' kijelÃļlÊse", "select_library_owner": "VÃĄlaszd ki a kÊptÃĄr tulajdonosÃĄt", "select_new_face": "Új arc vÃĄlasztÃĄsa", + "select_people": "SzemÊlyek kivÃĄlasztÃĄsa", + "select_person": "SzemÊly kivÃĄlasztÃĄsa", "select_person_to_tag": "VÃĄlassz ki egy szemÊlyt a megjelÃļlÊshez", "select_photos": "FotÃŗk vÃĄlasztÃĄsa", "select_trash_all": "'LomtÃĄr' kijelÃļlÊse", "select_user_for_sharing_page_err_album": "Az album lÊtrehozÃĄsa sikertelen", "selected": "KivÃĄlasztott", "selected_count": "{count, plural, other {# kivÃĄlasztva}}", - "selected_gps_coordinates": "KivÃĄlasztott GPS KordinÃĄtÃĄk", + "selected_gps_coordinates": "KivÃĄlasztott GPS kordinÃĄtÃĄk", "send_message": "Üzenet kÃŧldÊse", "send_welcome_email": "ÜdvÃļzlő email kÃŧldÊse", - "server_endpoint": "Szerver VÊgpont", - "server_info_box_app_version": "AlkalmazÃĄs VerziÃŗ", - "server_info_box_server_url": "Szerver Címe", - "server_offline": "Szerver Nem ElÊrhető", - "server_online": "Szerver ElÊrhető", + "server_endpoint": "Szerver vÊgpont", + "server_info_box_app_version": "AlkalmazÃĄs verziÃŗ", + "server_info_box_server_url": "Szerver URL", + "server_offline": "A szerver nem elÊrhető", + "server_online": "A szerver elÊrhető", "server_privacy": "Szerver biztonsÃĄg", "server_restarting_description": "Az oldal pillanatokon belÃŧl frissÃŧl.", "server_restarting_title": "A szerver Ãējraindul", - "server_stats": "Szerver StatisztikÃĄk", + "server_stats": "Szerver statisztikÃĄk", "server_update_available": "SzerverfrissítÊs Êrhető el", - "server_version": "Szerver VerziÃŗ", + "server_version": "Szerver verziÃŗ", "set": "BeÃĄllít", "set_as_album_cover": "BeÃĄllítÃĄs albumborítÃŗkÊnt", "set_as_featured_photo": "BeÃĄllítÃĄs kiemelt fotÃŗnak", @@ -1908,7 +2045,7 @@ "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?", - "shared_album_activity_remove_title": "TevÊkenysÊg TÃļrlÊse", + "shared_album_activity_remove_title": "TevÊkenysÊg tÃļrlÊse", "shared_album_section_people_action_error": "Hiba az albummal kapcsolatos kilÊpÊs/eltÃĄvolítÃĄs kÃļzben", "shared_album_section_people_action_leave": "FelhasznÃĄlÃŗ eltÃĄvolítÃĄsa az albumbÃŗl", "shared_album_section_people_action_remove_user": "FelhasznÃĄlÃŗ eltÃĄvolítÃĄsa az albumbÃŗl", @@ -1917,8 +2054,8 @@ "shared_by_user": "{user} osztotta meg", "shared_by_you": "Te osztottad meg", "shared_from_partner": "{partner} fÊnykÊpei", - "shared_intent_upload_button_progress_text": "{current} / {total} FeltÃļltve", - "shared_link_app_bar_title": "Megosztott Linkek", + "shared_intent_upload_button_progress_text": "{current} / {total} feltÃļltve", + "shared_link_app_bar_title": "Megosztott linkek", "shared_link_clipboard_copied_massage": "VÃĄgÃŗlapra mÃĄsolva", "shared_link_clipboard_text": "Link: {link}\nJelszÃŗ: {password}", "shared_link_create_error": "Hiba a megosztott link lÊtrehozÃĄsakor", @@ -1963,26 +2100,27 @@ "sharing_silver_appbar_create_shared_album": "Új megosztott album", "sharing_silver_appbar_share_partner": "MegosztÃĄs partnerrel", "shift_to_permanent_delete": "nyomd meg a ⇧ nyilat az elem vÊgleges tÃļrlÊsÊhez", - "show_album_options": "Album beÃĄllítÃĄsok mutatÃĄsa", - "show_albums": "Albumok mutatÃĄsa", - "show_all_people": "Minden szemÊly mutatÃĄsa", - "show_and_hide_people": "SzemÊlyek mutatÃĄsa Ês elrejtÊse", - "show_file_location": "FÃĄjl helyÊnek mutatÃĄsa", - "show_gallery": "GalÊria mutatÃĄsa", - "show_hidden_people": "Rejtett szemÊlyek mutatÃĄsa", + "show_album_options": "Album beÃĄllítÃĄsok megjelenítÊse", + "show_albums": "Albumok megjelenítÊse", + "show_all_people": "Minden szemÊly megjelenítÊse", + "show_and_hide_people": "SzemÊlyek megjelenítÊse Ês elrejtÊse", + "show_file_location": "FÃĄjl helyÊnek megjelenítÊse", + "show_gallery": "GalÊria megjelenítÊse", + "show_hidden_people": "Rejtett szemÊlyek megjelenítÊse", "show_in_timeline": "MutatÃĄs az idővonalon", "show_in_timeline_setting_description": "Ennek a felhasznÃĄlÃŗnak a kÊpei Ês videÃŗi jelenjenek meg az idővonaladon", - "show_keyboard_shortcuts": "BillentyÅąparancsok mutatÃĄsa", - "show_metadata": "Metaadatok mutatÃĄsa", - "show_or_hide_info": "Info mutatÃĄsa vagy elrejtÊse", - "show_password": "JelszÃŗ mutatÃĄsa", - "show_person_options": "SzemÊly beÃĄllítÃĄsok mutatÃĄsa", - "show_progress_bar": "Folyamatjelző MutatÃĄsa", - "show_search_options": "KeresÊsi lehetősÊgek mutatÃĄsa", + "show_keyboard_shortcuts": "BillentyÅąparancsok megjelenítÊse", + "show_metadata": "Metaadatok megjelenítÊse", + "show_or_hide_info": "InformÃĄciÃŗk megjelenítÊse vagy elrejtÊse", + "show_password": "JelszÃŗ megjelenítÊse", + "show_person_options": "SzemÊly beÃĄllítÃĄsok megjelenítÊse", + "show_progress_bar": "Folyamatjelző megjelenítÊse", + "show_schema": "SÊma megjelenítÊse", + "show_search_options": "KeresÊsi beÃĄllítÃĄsok megjelenítÊse", "show_shared_links": "Megosztott linkek megjelenítÊse", - "show_slideshow_transition": "VetítÊs ÃĄttÅąnÊsi effekt mutatÃĄsa", + "show_slideshow_transition": "VetítÊs ÃĄttÅąnÊsi effektus megjelenítÊse", "show_supporter_badge": "TÃĄmogatÃŗ jelvÊny", - "show_supporter_badge_description": "TÃĄmogatÃŗ jelvÊny mutatÃĄsa", + "show_supporter_badge_description": "TÃĄmogatÃŗ jelvÊny megjelenítÊse", "show_text_recognition": "Mutasd a szÃļvegfelismerÊst", "show_text_search_menu": "Mutasd a szÃļvegkeresÊsi menÃŧt", "shuffle": "VÊletlenszerÅą", @@ -1995,6 +2133,8 @@ "skip_to_folders": "UgrÃĄs a mappÃĄkhoz", "skip_to_tags": "UgrÃĄs a címkÊkhez", "slideshow": "DiavetítÊs", + "slideshow_repeat": "DiavetítÊs ismÊtlÊse", + "slideshow_repeat_description": "Ha a diavetítÊs vÊget Êr, Ãējraindul az elejÊtől", "slideshow_settings": "DiavetítÊs beÃĄllítÃĄsai", "sort_albums_by": "Albumok rendezÊse...", "sort_created": "LÊtrehozÃĄs dÃĄtuma", @@ -2006,7 +2146,7 @@ "sort_recent": "LegÃējabb fÊnykÊp", "sort_title": "Cím", "source": "ForrÃĄs", - "stack": "FotÃŗk csoportosítÃĄsa", + "stack": "KollÃĄzs", "stack_action_prompt": "{count} egymÃĄsra helyezve", "stack_duplicates": "DuplikÃĄtumok csoportosítÃĄsa", "stack_select_one_photo": "VÃĄlassz egy fő kÊpet a csoportbÃŗl", @@ -2019,8 +2159,8 @@ "state": "Megye/Állam", "status": "Állapot", "stop_casting": "VetítÊs megszÃŧntetÊse", - "stop_motion_photo": "MozgÃŗkÊp MegÃĄllítÃĄsa", - "stop_photo_sharing": "FotÃŗid megosztÃĄsÃĄnak megszÃŧntetÊse?", + "stop_motion_photo": "Stop motion kÊp", + "stop_photo_sharing": "MegszÃŧnteted fotÃŗid megosztÃĄsÃĄt?", "stop_photo_sharing_description": "{partner} mostantÃŗl nem fog tudni hozzÃĄfÊrni a fÊnykÊpeidhez.", "stop_sharing_photos_with_user": "FÊnykÊpeid megosztÃĄsÃĄnak megszÃŧntetÊse ezzel a felhasznÃĄlÃŗval", "storage": "TÃĄrhely", @@ -2032,17 +2172,17 @@ "suggestions": "Javaslatok", "sunrise_on_the_beach": "Napkelte a tengerparton", "support": "TÃĄmogatÃĄs", - "support_and_feedback": "TÃĄmogatÃĄs Ês VisszajelzÊs", + "support_and_feedback": "TÃĄmogatÃĄs Ês visszajelzÊs", "support_third_party_description": "Az Immich telepítÊsedet egy harmadik fÊl csomagolta. Mivel elkÊpzelhető, hogy az esetlegesen felmerÃŧlő problÊmÃĄkat ez a csomag okozza, ezÊrt kÊrjÃŧk, előszÃļr velÃŧk kÃļzÃļld a problÊmÃĄkat az alÃĄbbi linkek segítsÊgÊvel.", "swap_merge_direction": "EgyesítÊs irÃĄnyÃĄnak megfordítÃĄsa", "sync": "SzinkronizÃĄlÃĄs", "sync_albums": "Albumok szinkronizÃĄlÃĄsa", - "sync_albums_manual_subtitle": "Összes fotÃŗ Ês videÃŗ lÊtrehozÃĄsa Ês szinkronizÃĄlÃĄsa a kivÃĄlasztott Immich albumokba", - "sync_local": "Helyi SzinkronizÃĄlÃĄsa", - "sync_remote": "TÃĄvoli SzinkronizÃĄlÃĄsa", + "sync_albums_manual_subtitle": "Összes feltÃļltÃļtt fotÃŗ Ês videÃŗ szinkronizÃĄlÃĄsa a kivÃĄlasztott albumokba", + "sync_local": "Helyi szinkronizÃĄlÃĄsa", + "sync_remote": "TÃĄvoli szinkronizÃĄlÃĄsa", "sync_status": "SzinkronizÃĄlÃĄs ÃĄllapota", "sync_status_subtitle": "SzinkronizÃĄlÃĄs megtekintÊse Ês kezelÊse", - "sync_upload_album_setting_subtitle": "FotÃŗk Ês videÃŗk lÊtrehozÃĄsa Ês szinkronizÃĄlÃĄsa a kivÃĄlasztott Immich albumba", + "sync_upload_album_setting_subtitle": "FotÃŗk Ês videÃŗk lÊtrehozÃĄsa Ês szinkronizÃĄlÃĄsa a kivÃĄlasztott Immich albumokba", "tag": "Címke", "tag_assets": "Elemek címkÊzÊse", "tag_created": "LÊtrehozott címke: {tag}", @@ -2058,7 +2198,7 @@ "theme": "TÊma", "theme_selection": "TÊmavÃĄlasztÃĄs", "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_storage_indicator_title": "TÃĄrhely ikon megjelenítÊse 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_title": "Színes felhasznÃĄlÃŗi felÃŧlet", @@ -2071,8 +2211,9 @@ "theme_setting_theme_subtitle": "AlkalmazÃĄs tÊmÃĄjÃĄnak vÃĄlasztÃĄsa", "theme_setting_three_stage_loading_subtitle": "A hÃĄromlÊpcsős betÃļltÊs javíthatja a betÃļltÊsi teljesítmÊnyt, de jelentősen nÃļveli a hÃĄlÃŗzati forgalmat", "theme_setting_three_stage_loading_title": "HÃĄromlÊpcsős betÃļltÊs engedÊlyezÊse", + "then": "Akkor", "they_will_be_merged_together": "Egyesítve lesznek", - "third_party_resources": "Harmadik FÊltől SzÃĄrmazÃŗ ForrÃĄsok", + "third_party_resources": "Harmadik fÊltől szÃĄrmazÃŗ forrÃĄsok", "time": "Idő", "time_based_memories": "EmlÊkek idő alapjÃĄn", "time_based_memories_duration": "MÃĄsodpercek szÃĄma, egyes kÊpek mutatÃĄsÃĄra.", @@ -2094,17 +2235,24 @@ "trash_action_prompt": "{count} lomtÃĄrba helyezve", "trash_all": "Mindet lomtÃĄrba", "trash_count": "{count, number} elem lomtÃĄrba helyezÊse", - "trash_delete_asset": "Elem TÃļrlÊse / LomtÃĄrba HelyezÊse", + "trash_delete_asset": "Elem tÃļrlÊse / lomtÃĄrba helyezÊse", "trash_emptied": "LomtÃĄr kiÃŧrítve", "trash_no_results_message": "Itt lesznek lÃĄthatÃŗak a lomtÃĄrba tett kÊpek Ês videÃŗk.", - "trash_page_delete_all": "Mindet TÃļrÃļl", + "trash_page_delete_all": "Összes tÃļrlÊse", "trash_page_empty_trash_dialog_content": "Ki szeretnÊd Ãŧríteni a lomtÃĄrban lÊvő elemeket? Ezeket vÊglegesen eltÃĄvolítjuk az Immich-ből", "trash_page_info": "A LomÃĄtrba helyezett elemek {days} nap utÃĄn vÊglegesen tÃļrlődnek", "trash_page_no_assets": "A LomtÃĄr Ãŧres", - "trash_page_restore_all": "Mindet VisszaÃĄllít", + "trash_page_restore_all": "Összes visszaÃĄllítÃĄsa", "trash_page_select_assets_btn": "Elemek kivÃĄlasztÃĄsa", "trash_page_title": "LomtÃĄr ({count})", "trashed_items_will_be_permanently_deleted_after": "A lomtÃĄrban lÊvő elemek vÊglegesen tÃļrlÊsre kerÃŧlnek {days, plural, other {# nap}} mÃēlva.", + "trigger": "FeltÊtel", + "trigger_asset_uploaded": "Elem feltÃļltve", + "trigger_asset_uploaded_description": "Új elem feltÃļltÊsekor indul el", + "trigger_description": "Egy esemÊny, ami elindítja a folyamatot", + "trigger_person_recognized": "SzemÊly felismerve", + "trigger_person_recognized_description": "SzemÊly felismerÊsekor indul el", + "trigger_type": "FeltÊtel típusa", "troubleshoot": "HibaelhÃĄrítÃĄs", "type": "Típus", "unable_to_change_pin_code": "Sikertelen PIN kÃŗd vÃĄltoztatÃĄs", @@ -2119,36 +2267,39 @@ "unhide_person": "Nem rejtett szemÊly", "unknown": "Ismeretlen", "unknown_country": "Ismeretlen orszÃĄg", + "unknown_date": "Ismeretlen dÃĄtum", "unknown_year": "Ismeretlen Év", "unlimited": "KorlÃĄtlan", "unlink_motion_video": "MozgÃŗkÊp levÃĄlasztÃĄsa", "unlink_oauth": "OAuth levÃĄlasztÃĄsa", "unlinked_oauth_account": "LevÃĄlasztott OAuth fiÃŗk", - "unmute_memories": "EmlÊkek mutatÃĄsa", - "unnamed_album": "NÊvtelen Album", + "unmute_memories": "EmlÊkek nÊmítÃĄsÃĄnak feloldÃĄsa", + "unnamed_album": "NÊvtelen album", "unnamed_album_delete_confirmation": "Biztosan tÃļrÃļlni szeretnÊd ezt az albumot?", - "unnamed_share": "NÊvtelen MegosztÃĄs", + "unnamed_share": "NÊvtelen megosztÃĄs", "unsaved_change": "Nem mentett vÃĄltoztatÃĄs", "unselect_all": "KijelÃļlÊsek megszÃŧntetÊse", "unselect_all_duplicates": "DuplikÃĄtumok kijelÃļlÊsÊnek megszÃŧntetÊse", "unselect_all_in": "KijelÃļlÊs megszÃŧntetÊse itt: {group}", - "unstack": "Csoport SzÊtszedÊse", + "unstack": "Csoport szÊtbontÃĄsa", "unstack_action_prompt": "{count} egymÃĄsra helyezÊs megszÃŧntetÊse", "unstacked_assets_count": "{count, plural, other {# elemből}} ÃĄllÃŗ csoport szÊtszedve", + "unsupported_field_type": "Nem tÃĄmogatott mezőtípus", "untagged": "Címke eltÃĄvolítva", + "untitled_workflow": "NÊvtelen folyamat", "up_next": "KÃļvetkezik", "update_location_action_prompt": "{count} elem pozíciÃŗjÃĄnak frissítÊse a kÃļvetkezővel:", - "updated_at": "Frissített", + "updated_at": "Frissítve", "updated_password": "JelszÃŗ megvÃĄltoztatva", "upload": "FeltÃļltÊs", - "upload_action_prompt": "{count} sorba rakva a feltÃļltÊshez", "upload_concurrency": "PÃĄrhuzamos feltÃļltÊs", - "upload_details": "FeltÃļltÊsi RÊszletek", + "upload_details": "FeltÃļltÊs ÃĄllapota", "upload_dialog_info": "SzeretnÊl mentÊst kÊszíteni a kivÃĄlasztott elem(ek)ről a szerverre?", - "upload_dialog_title": "Elem FeltÃļltÊse", + "upload_dialog_title": "Elem feltÃļltÊse", + "upload_error_with_count": "FeltÃļltÊsi hiba {count} elemnÊl", "upload_errors": "FeltÃļltÊs befejezve {count, plural, other {# hibÃĄval}}, frissítsd az oldalt az Ãējonnan feltÃļltÃļtt elemek megtekintÊsÊhez.", "upload_finished": "FeltÃļltÊs befejezve", - "upload_progress": "HÃĄtra van {remaining, number} - Feldolgozva {processed, number}/{total, number}", + "upload_progress": "{remaining, number} hÃĄtra van - {processed, number}/{total, number} feldolgozva", "upload_skipped_duplicates": "{count, plural, other {# duplikÃĄtum}} kihagyva", "upload_status_duplicates": "DuplikÃĄtumok", "upload_status_errors": "HibÃĄk", @@ -2180,7 +2331,8 @@ "users_added_to_album_count": "{count, plural, one {# felhasznÃĄlÃŗ} other {# felhasznÃĄlÃŗ}} hozzÃĄadva az albumhoz", "utilities": "SegÊdeszkÃļzÃļk", "validate": "EllenőrzÊs", - "validate_endpoint_error": "KÊrlek, ÊrvÊnyes URL címet adj meg", + "validate_endpoint_error": "KÊrlek, ÊrvÊnyes URL-t adj meg", + "validation_error": "ValidÃĄciÃŗs hiba", "variables": "VÃĄltozÃŗk", "version": "VerziÃŗ", "version_announcement_closing": "BarÃĄtsÃĄggal, Alex", @@ -2191,13 +2343,14 @@ "video_hover_setting": "KismÊretÅą videÃŗ elindítÃĄsa, ha az egÊr az elem felÊ megy", "video_hover_setting_description": "Ha az egÊr az elem felÊ megy, akkor induljon el a kismÊretÅą videÃŗ lejÃĄtszÃĄsa. MÊg ha ez az opciÃŗ ki is van kapcsolva, a lejÃĄtszÃĄs akkor is elindíthatÃŗ a lejÃĄtszÃĄs gombbal.", "videos": "VideÃŗk", - "videos_count": "{count, plural, one {# VideÃŗ} other {# VideÃŗ}}", - "view": "NÊzet", - "view_album": "Album MegtekintÊse", - "view_all": "Összes MegtekintÊse", - "view_all_users": "Minden FelhasznÃĄlÃŗ MegtekintÊse", + "videos_count": "{count, plural, one {# videÃŗ} other {# videÃŗ}}", + "videos_only": "Csak videÃŗk", + "view": "MegtekintÊs", + "view_album": "Album megtekintÊse", + "view_all": "Összes megtekintÊse", + "view_all_users": "Minden felhasznÃĄlÃŗ megtekintÊse", "view_asset_owners": "Elemtulajdonosok megtekintÊse", - "view_details": "RÊszletek MegtekintÊse", + "view_details": "RÊszletek megtekintÊse", "view_in_timeline": "MegtekintÊs az idővonalon", "view_link": "Link megtekintÊse", "view_links": "Linkek megtekintÊse", @@ -2206,25 +2359,42 @@ "view_previous_asset": "Előző elem megtekintÊse", "view_qr_code": "QR kÃŗd megtekintÊse", "view_similar_photos": "HasonlÃŗ kÊpek keresÊse", - "view_stack": "Csoport MegtekintÊse", - "view_user": "FelhasznÃĄlÃŗ MegtekintÊse", - "viewer_remove_from_stack": "EltÃĄvolít a CsoportbÃŗl", - "viewer_stack_use_as_main_asset": "Fő Elemnek BeÃĄllít", - "viewer_unstack": "Csoport MegszÃŧntetÊse", + "view_stack": "Csoport megtekintÊse", + "view_user": "FelhasznÃĄlÃŗ megtekintÊse", + "viewer_remove_from_stack": "EltÃĄvolítÃĄs a csoportbÃŗl", + "viewer_stack_use_as_main_asset": "Fő elemnek beÃĄllítÃĄs", + "viewer_unstack": "Csoport megszÃŧntetÊse", "visibility_changed": "{count, plural, other {# szemÊly}} lÃĄthatÃŗsÃĄga megvÃĄltozott", - "waiting": "VÃĄrakozÃĄs", + "visual": "VizuÃĄlis", + "visual_builder": "VizuÃĄlis ÃļsszerakÃŗ", + "waiting": "VÃĄrakozik", + "waiting_count": "VÃĄrakozik: {count}", "warning": "FigyelmeztetÊs", "week": "HÊt", "welcome": "ÜdvÃļzlÃŧnk", "welcome_to_immich": "ÜdvÃļzÃļl az Immich", - "wifi_name": "Wi-Fi Neve", - "workflow": "Munkafolyamat", + "width": "SzÊlessÊg", + "wifi_name": "Wi-Fi neve", + "workflow_delete_prompt": "Biztosan tÃļrÃļlni szeretnÊd ezt a folyamatot?", + "workflow_deleted": "Folyamat tÃļrÃļlve", + "workflow_description": "Folyamat leírÃĄsa", + "workflow_info": "Folyamat rÊszletei", + "workflow_json": "Folyamat JSON", + "workflow_json_help": "Itt mÃŗdosíthatod a folyamatot JSON formÃĄtumban. A vÃĄltozÃĄsokat szinkronban tartjuk a grafikus felÃŧlettel.", + "workflow_name": "Folyamat neve", + "workflow_navigation_prompt": "Biztosan tovÃĄbb szeretnÊl lÊpni a vÃĄltozÃĄsok mentÊse nÊlkÃŧl?", + "workflow_summary": "Folyamat ÃļsszefoglalÃŗ", + "workflow_update_success": "Folyamat sikeresen frissítve", + "workflow_updated": "Folyamat frissítve", + "workflows": "Folyamatok", + "workflows_help_text": "A folyamatok automatizÃĄlt mÅąveleteket hajtanak vÊgre elemeken, indítÃĄsi feltÊtelek Ês szÅąrők alapjÃĄn", "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 Wi-Fi hÃĄlÃŗzatod neve", - "zoom_image": "KÊp NagyítÃĄsa", + "zero_to_clear_rating": "0: ÊrtÊkelÊs eltÃĄvolítÃĄsa", + "zoom_image": "KÊp nagyítÃĄsa", "zoom_to_bounds": "NagyítÃĄs a hatÃĄrokhoz" } diff --git a/i18n/id.json b/i18n/id.json index 6f0f950a4c..acde13c7d8 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -5,18 +5,25 @@ "acknowledge": "Mengerti", "action": "Tindakan", "action_common_update": "Perbarui", + "action_description": "Tindakan yang perlu dijalankan pada aset yang terfilter", "actions": "Tindakan", "active": "Aktif", + "active_count": "Aktif: {count}", "activity": "Aktivitas", - "activity_changed": "Aktivitas {enabled, select, true {diaktifkan} other {dinonaktifkan}}", + "activity_changed": "Aktivitas {enabled, select, true {aktif} other {nonaktif}}", "add": "Tambahkan", - "add_a_description": "Tambahkan sebuah deskripsi", + "add_a_description": "Tambah keterangan", "add_a_location": "Tambahkan lokasi", "add_a_name": "Tambahkan nama", "add_a_title": "Tambahkan judul", - "add_birthday": "Tambahkan Tanggal Lahir", + "add_action": "Tambah tindakan", + "add_action_description": "Klik untuk menambahkan tindakan yang perlu dijalankan", + "add_assets": "Tambahkan aset", + "add_birthday": "Tambahkan tanggal lahir", "add_endpoint": "Tambahkan titik akhir", "add_exclusion_pattern": "Tambahkan pola pengecualian", + "add_filter": "Tambahkan filter", + "add_filter_description": "Klik untuk menambahkan kondisi filter", "add_location": "Tambahkan lokasi", "add_more_users": "Tambahkan lebih banyak pengguna", "add_partner": "Tambahkan partner", @@ -35,6 +42,7 @@ "add_to_shared_album": "Tambahkan ke album terbagi", "add_upload_to_stack": "Tambahkan unggahan ke tumpukan", "add_url": "Tambahkan URL", + "add_workflow_step": "Tambahkan langkah alur kerja", "added_to_archive": "Ditambahkan ke arsip", "added_to_favorites": "Ditambahkan ke favorit", "added_to_favorites_count": "Ditambahkan {count, number} ke favorit", @@ -96,9 +104,11 @@ "image_preview_description": "Gambar berukuran sedang tanpa metadata, digunakan ketika melihat aset satuan dan untuk pembelajaran mesin", "image_preview_quality_description": "Kualitas pratinjau dari 1-100. Lebih tinggi lebih baik, tetapi menghasilkan berkas lebih besar dan respons aplikasi. Menetapkan nilai rendah dapat memengaruhi kualitas pembelajaran mesin.", "image_preview_title": "Pengaturan Pratinjau", + "image_progressive": "Progresif", + "image_progressive_description": "Enkode gambar-gambar JPEG secara progresif untuk memuat tampilan secara bertahap. Ini tidak berpengaruh pada gambar-gambar WebP.", "image_quality": "Kualitas", "image_resolution": "Resolusi", - "image_resolution_description": "Resolusi lebih tinggi dapat menjaga lebih banyak detail tetapi dapat memerlukan waktu lebih lama untuk dienkode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.", + "image_resolution_description": "Resolusi yang lebih tinggi dapat menyimpan lebih banyak detail tetapi memerlukan waktu yang lebih lama untuk di-enkode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.", "image_settings": "Pengaturan Gambar", "image_settings_description": "Kelola kualitas dan resolusi gambar yang dibuat", "image_thumbnail_description": "Gambar kecil tanpa metadata, digunakan ketika melihat kelompok foto seperti lini masa utama", @@ -112,6 +122,7 @@ "job_settings_description": "Kelola konkurensi tugas", "jobs_delayed": "{jobCount, plural, other {# tertunda}}", "jobs_failed": "{jobCount, plural, other {# gagal}}", + "jobs_over_time": "Tugas dari waktu ke waktu", "library_created": "Pustaka dibuat: {library}", "library_deleted": "Pustaka dihapus", "library_details": "Detail pustaka", @@ -179,11 +190,23 @@ "machine_learning_smart_search_enabled": "Aktifkan pencarian pintar", "machine_learning_smart_search_enabled_description": "Jika dinonaktifkan, gambar tidak akan dienkode untuk pencarian pintar.", "machine_learning_url_description": "URL server pembelajaran mesin. Jika lebih dari satu URL disediakan, setiap server akan dicoba satu per satu sampai salah satu berhasil merespons, dari urutan pertama sampai terakhir. Server yang tidak merespons akan diabaikan sementara sampai kembali daring.", + "maintenance_delete_backup": "Hapus Cadangan", + "maintenance_delete_backup_description": "File ini akan dihapus secara permanen.", + "maintenance_delete_error": "Gagal menghapus cadangan.", + "maintenance_restore_backup": "Mengembalikan Cadangan", + "maintenance_restore_backup_description": "Immich akan dihapus dan dikembalikan dari candangan yang dipilih. Sebuah candangan akan dibuat sebelum dilanjutkan.", + "maintenance_restore_backup_different_version": "Cadangan ini dibuat dengan versi Immich yang berbeda!", + "maintenance_restore_backup_unknown_version": "Tidak dapat menentukan versi candangan.", + "maintenance_restore_database_backup": "Mengembalikan cadangan database", + "maintenance_restore_database_backup_description": "Kembalikan ke status basis data yang lebih awal menggunakan berkas cadangan", "maintenance_settings": "Pemeliharaan", - "maintenance_settings_description": "Setel mode pemeliharaan Immich", - "maintenance_start": "Mulai mode pemeliharaan", + "maintenance_settings_description": "Setel mode pemeliharaan Immich.", + "maintenance_start": "Pindah ke mode pemeliharaan", "maintenance_start_error": "Gagal memulai mode pemeliharaan.", + "maintenance_upload_backup": "Unggah berkas cadangan basis data", + "maintenance_upload_backup_error": "Tidak dapat mengunggah cadangan, apakah ini sebuah file .sql/.sql.gz?", "manage_concurrency": "Kelola Konkurensi", + "manage_concurrency_description": "Pindah ke halaman tugas untuk mengelola konkurensi tugas", "manage_log_settings": "Kelola pengaturan log", "map_dark_style": "Gaya gelap", "map_enable_description": "Aktifkan fitur peta", @@ -249,7 +272,7 @@ "oauth_auto_register": "Pendaftaran otomatis", "oauth_auto_register_description": "Daftar pengguna baru secara otomatis setelah log masuk dengan OAuth", "oauth_button_text": "Teks tombol", - "oauth_client_secret_description": "Diperlukan jika PKCE (Proof Key for Code Exchange) tidak didukung oleh penyedia OAuth", + "oauth_client_secret_description": "Diperlukan untuk klien yang konfidensial, atau jika PKCE (Proof Key for Code Exchange) tidak didukung untuk klien umum.", "oauth_enable_description": "Log masuk dengan OAuth", "oauth_mobile_redirect_uri": "URI pengalihan ponsel", "oauth_mobile_redirect_uri_override": "Penimpaan URI penerusan ponsel", @@ -273,10 +296,14 @@ "password_settings_description": "Kelola pengaturan log masuk kata sandi", "paths_validated_successfully": "Semua jalur berhasil divalidasi", "person_cleanup_job": "Pembersihan data pribadi", + "queue_details": "Detail Antrian", + "queues": "Antrian Tugas", + "queues_page_description": "Halaman antrian tugas Admin", "quota_size_gib": "Ukuran Kuota (GiB)", "refreshing_all_libraries": "Menyegarkan semua pustaka", "registration": "Pendaftaran Admin", "registration_description": "Karena Anda merupakan pengguna pertama dalam sistem, Anda akan ditetapkan sebagai Admin dan bertanggung jawab atas tugas administratif dan pengguna tambahan akan dibuat oleh Anda.", + "remove_failed_jobs": "Hapus tugas-tugas gagal", "require_password_change_on_login": "Memerlukan pengguna untuk mengubah kata sandi pada log masuk pertama", "reset_settings_to_default": "Atur ulang pengaturan ke bawaan", "reset_settings_to_recent_saved": "Atur ulang pengaturan ke pengaturan tersimpan terkini", @@ -289,8 +316,10 @@ "server_public_users_description": "Semua pengguna (nama dan email) didaftarkan ketika menambahkan pengguna ke album terbagi. Ketika dinonaktifkan, daftar pengguna hanya akan tersedia kepada pengguna admin.", "server_settings": "Pengaturan Server", "server_settings_description": "Kelola pengaturan server", + "server_stats_page_description": "Halaman statistik server Admin", "server_welcome_message": "Pesan selamat datang", "server_welcome_message_description": "Pesan yang ditampilkan di laman log masuk.", + "settings_page_description": "Laman pengaturan admin", "sidecar_job": "Metadata sespan", "sidecar_job_description": "Jelajahi atau sinkronisasikan metadata sespan dari sistem berkas", "slideshow_duration_description": "Jumlah detik untuk menampilkan setiap gambar", @@ -358,7 +387,7 @@ "transcoding_max_b_frames": "Bingkai B maksimum", "transcoding_max_b_frames_description": "Nilai yang lebih tinggi meningkatkan efisiensi kompresi, tetapi membuat pengodean lebih lambat. Mungkin tidak kompatibel dengan akselerasi perangkat keras pada perangkat lawas. 0 menonaktifkan bingkai B, sedangkan -1 mengatur nilai ini secara otomatis.", "transcoding_max_bitrate": "Kecepatan bit maksimum", - "transcoding_max_bitrate_description": "Menetapkan kecepatan bit maksimum dapat membuat ukuran berkas lebih dapat diprediksi dengan kekurangan minor pada kualitas. Pada 720p, nilai umum adalah 2600 kbit/s untuk VP9 atau HEVC, atau 4500 kbit/s untuk H.264. Dinonaktifkan jika ditetapkan ke 0.", + "transcoding_max_bitrate_description": "Menetapkan kecepatan bit maksimum dapat membuat ukuran berkas lebih dapat diprediksi dengan kekurangan minor pada kualitas. Pada 720p, nilai umum adalah 2600 kbit/s untuk VP9 atau HEVC, atau 4500 kbit/s untuk H.264. Dinonaktifkan jika ditetapkan ke 0. Ketika tidak ada unit yang dipilih, k (untuk kbit/s) akan diasumsikan; oleh karena itu 5000, 5000k, dan 5M (untuk Mbit/s) terhitung setara.", "transcoding_max_keyframe_interval": "Interval bingkai kunci maksimum", "transcoding_max_keyframe_interval_description": "Menetapkan jarak bingkai maksimum antara bingkai kunci. Nilai yang lebih rendah membuat efisiensi kompresi lebih buruk, tetapi meningkatkan waktu pencarian dan dapat meningkatkan kualitas dalam adegan dengan gerakan cepat. 0 menetapkan nilai ini secara otomatis.", "transcoding_optimal_description": "Video lebih tinggi dari resolusi sasaran atau tidak dalam format yang diterima", @@ -376,7 +405,7 @@ "transcoding_target_resolution": "Resolusi sasaran", "transcoding_target_resolution_description": "Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi memerlukan waktu lebih lama untuk dienkode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.", "transcoding_temporal_aq": "AQ Temporal", - "transcoding_temporal_aq_description": "Hanya diterapkan pada NVENC. Meningkatkan kualitas adegan berdetail tinggi dan rendah gerakan. Mungkin tidak kompatibel dengan perangkat yang lawas.", + "transcoding_temporal_aq_description": "Hanya diterapkan pada NVENC. Kuantisasi Adaptif Temporal meningkatkan kualitas adegan berdetail tinggi dan rendah gerakan. Mungkin tidak kompatibel dengan perangkat lawas.", "transcoding_threads": "Utas", "transcoding_threads_description": "Nilai yang lebih tinggi dapat mengode dengan cepat, tetapi mengurangi ruang bagi server untuk memproses tugas lain selagi aktif. Nilai ini seharusnya tidak lebih dari jumlah inti CPU. Memaksimalkan pemakaian jika ditetapkan ke 0.", "transcoding_tone_mapping": "Pemetaan nada", @@ -409,6 +438,8 @@ "user_restore_scheduled_removal": "Pulihkan pengguna - jadwalkan pelepasan pada {date, date, long}", "user_settings": "Pengaturan Pengguna", "user_settings_description": "Kelola pengaturan pengguna", + "user_successfully_removed": "Pengguna {email} berhasil dihapus.", + "users_page_description": "Laman pengguna admin", "version_check_enabled_description": "Aktifkan pemeriksaan versi", "version_check_implications": "Fitur pemeriksaan versi tergantung komunikasi berkala dengan github.com", "version_check_settings": "Pemeriksaan Versi", @@ -420,17 +451,20 @@ "admin_password": "Kata Sandi Admin", "administration": "Administrasi", "advanced": "Tingkat lanjut", + "advanced_settings_clear_image_cache": "Bersihkan Cache Gambar", + "advanced_settings_clear_image_cache_error": "Gagal untuk membersihkan cache gambar", + "advanced_settings_clear_image_cache_success": "Sukses menghapus {size}", "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 akan lambat memuat gambar kecil dari lokal. Menyalakan ini akan memuat gambar kecil dari peladen.", "advanced_settings_prefer_remote_title": "Prioritaskan gambar dari server", "advanced_settings_proxy_headers_subtitle": "Tentukan header proxy yang harus dikirim Immich dengan setiap permintaan jaringan", - "advanced_settings_proxy_headers_title": "Tajuk Proksi", + "advanced_settings_proxy_headers_title": "Header proxy kustom [EKSPERIMENTAL]", "advanced_settings_readonly_mode_subtitle": "Mengaktifkan mode baca-saja, di mana foto hanya bisa dilihat. Fitur seperti memilih banyak foto, berbagi, cast, dan hapus akan dinonaktifkan. Mode baca-saja bisa diaktifkan/nonaktifkan lewat avatar pengguna di layar utama", - "advanced_settings_readonly_mode_title": "Mode Baca-Saja", + "advanced_settings_readonly_mode_title": "Mode Hanya-Baca", "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_self_signed_ssl_title": "Izinkan sertifikat SSL yang ditandatangani sendiri [EKSPERIMENTAL]", "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", @@ -439,6 +473,7 @@ "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 #}}", + "album": "Album", "album_added": "Album ditambahkan", "album_added_notification_setting_description": "Terima notifikasi surel ketika Anda ditambahkan ke album terbagi", "album_cover_updated": "Kover album diperbarui", @@ -455,10 +490,12 @@ "album_remove_user": "Keluarkan pengguna?", "album_remove_user_confirmation": "Apakah Anda yakin ingin mengeluarkan {user}?", "album_search_not_found": "Tidak ada album yang ditemukan sesuai pencarian Anda", + "album_selected": "Album yang dipilih", "album_share_no_users": "Sepertinya Anda telah membagikan album ini dengan semua pengguna atau tidak memiliki pengguna siapa pun untuk dibagikan.", "album_summary": "Ringkasan album", "album_updated": "Album diperbarui", "album_updated_setting_description": "Terima notifikasi surel ketika album terbagi memiliki aset baru", + "album_upload_assets": "Unggah aset dari komputer mu dan tambahkan ke album", "album_user_left": "Keluar dari {album}", "album_user_removed": "{user} dikeluarkan", "album_viewer_appbar_delete_confirm": "Hapus album ini dari akun anda?", @@ -476,9 +513,11 @@ "albums_default_sort_order_description": "Urutan awal aset saat membuat album baru.", "albums_feature_description": "Koleksi foto atau video yang dapat dibagikan kepada pengguna lain.", "albums_on_device_count": "Album di perangkat ({count})", + "albums_selected": "{count, plural, one {# album yang dipilih} other {# album yang dipilih}}", "all": "Semua", "all_albums": "Semua album", "all_people": "Semua orang", + "all_photos": "Semua foto", "all_videos": "Semua video", "allow_dark_mode": "Perbolehkan mode gelap", "allow_edits": "Perbolehkan penyuntingan", @@ -486,6 +525,9 @@ "allow_public_user_to_upload": "Perbolehkan pengguna publik untuk mengunggah", "allowed": "Diijinkan", "alt_text_qr_code": "Gambar kode QR", + "always_keep": "Selalu simpan", + "always_keep_photos_hint": "Fitur Bebaskan Ruang ruang akan menyimpan semua foto di perangkat ini.", + "always_keep_videos_hint": "Fitur Bebaskan Ruang ruang akan menyimpan semua video di perangkat ini.", "anti_clockwise": "Berlawanan arah jarum jam", "api_key": "Kunci API", "api_key_description": "Nilai ini hanya akan ditampilkan sekali. Pastikan untuk menyalin sebelum menutup jendela ini.", @@ -512,10 +554,12 @@ "archived_count": "{count, plural, other {# terarsip}}", "are_these_the_same_person": "Apakah ini adalah orang yang sama?", "are_you_sure_to_do_this": "Apakah Anda yakin ingin melakukan ini?", + "array_field_not_fully_supported": "Bidang-bidang pada array membutuhkan suntingan JSON secara manual", "asset_action_delete_err_read_only": "Tidak dapat menghapus aset yang bersifat hanya-baca, proses dilewati", "asset_action_share_err_offline": "Tidak dapat mengambil aset luring, dilewati", "asset_added_to_album": "Telah ditambahkan ke album", "asset_adding_to_album": "Menambahkan ke albumâ€Ļ", + "asset_created": "Aset berhasil dibuat", "asset_description_updated": "Deskripsi aset telah diperbarui", "asset_filename_is_offline": "Aset {filename} sedang luring", "asset_has_unassigned_faces": "Aset memiliki wajah yang belum ditetapkan", @@ -528,6 +572,9 @@ "asset_list_layout_sub_title": "Penataan", "asset_list_settings_subtitle": "Setelan grid foto", "asset_list_settings_title": "Grid Foto", + "asset_not_found_on_device_android": "Aset tidak ditemukan di perangkat", + "asset_not_found_on_device_ios": "Aset tidak ditemukan di perangkat. Jika kamu menggunakan iCloud, aset mungkin tidak dapat diakses karena berkas rusak di iCloud", + "asset_not_found_on_icloud": "Aset tidak ditemukan di iCloud. Aset mungkin tidak dapat diakses karena berkas rusak di iCloud", "asset_offline": "Aset Luring", "asset_offline_description": "Aset eksternal ini tidak ada lagi di diska. Silakan hubungi administrator Immich Anda untuk bantuan.", "asset_restored_successfully": "Aset telah berhasil dipulihkan", @@ -579,7 +626,7 @@ "backup_album_selection_page_select_albums": "Pilih album", "backup_album_selection_page_selection_info": "Info Pilihan", "backup_album_selection_page_total_assets": "Total aset unik", - "backup_albums_sync": "Sinkronisasi cadangan album", + "backup_albums_sync": "Sinkronisasi Cadangan Album", "backup_all": "Semua", "backup_background_service_backup_failed_message": "Gagal mencadangkan aset. Mencoba lagiâ€Ļ", "backup_background_service_complete_notification": "Pencadangan aset selesai", @@ -640,6 +687,7 @@ "backup_options_page_title": "Setelan cadangan", "backup_setting_subtitle": "Kelola pengaturan unggahan latar belakang dan latar depan", "backup_settings_subtitle": "Kelola pengaturan unggahan", + "backup_upload_details_page_more_details": "Ketuk untuk detail lebih", "backward": "Maju", "biometric_auth_enabled": "Autentikasi biometrik diaktifkan", "biometric_locked_out": "Anda terkunci oleh autentikasi biometrik", @@ -698,6 +746,8 @@ "change_password_form_password_mismatch": "Sandi tidak cocok", "change_password_form_reenter_new_password": "Masukkan Ulang Sandi Baru", "change_pin_code": "Ubah kode PIN", + "change_trigger": "Ubah pemicu", + "change_trigger_prompt": "Apakah anda yakin ingin mengubah pemicunya? Tindakan ini akan menghapus seluruh aksi dan filter yang sudah ada.", "change_your_password": "Ubah kata sandi Anda", "changed_visibility_successfully": "Keterlihatan berhasil diubah", "charging": "Mengisi daya", @@ -706,8 +756,21 @@ "check_corrupt_asset_backup_button": "Lakukan pemeriksaan", "check_corrupt_asset_backup_description": "Jalankan pemeriksaan ini hanya melalui Wi-Fi dan setelah semua aset dicadangkan. Prosedur ini mungkin memerlukan waktu beberapa menit.", "check_logs": "Periksa Log", + "checksum": "Jumlah kontrol", "choose_matching_people_to_merge": "Pilih orang yang cocok untuk digabungkan", "city": "Kota", + "cleanup_confirm_description": "Immich menemukan {count} aset (dibuat sebelum {date}) telah aman dicadangkan di server. Hapus salinan lokal dari perangkat ini?", + "cleanup_confirm_prompt_title": "Hapus dari perangkat ini?", + "cleanup_deleted_assets": "Pindahkan {count} aset ke tempat sampah di perangkat", + "cleanup_deleting": "Memindahkan ke tempat sampah...", + "cleanup_found_assets": "Menemukan {count} aset cadangan", + "cleanup_found_assets_with_size": "Menemukan {count} aset cadangan ({size})", + "cleanup_icloud_shared_albums_excluded": "Album Bersama iCloud dikecualikan dari pemindaian", + "cleanup_no_assets_found": "Tidak ada aset yang ditemukan dengan kriteria diatas. Fitur membebaskan ruang hanya dapat menghapus aset yang dicadangkan ke server", + "cleanup_preview_title": "Aset yang akan dihapus ({count})", + "cleanup_step3_description": "Pindah untuk cadangan aset yang sesuai dengan tanggal mu dan simpan pengaturan.", + "cleanup_step4_summary": "{count} aset (dibuat sebelum {date}) untuk dihapus dari perangkat lokal. Foto akan tetap dapat diakses dari aplikasi Immich.", + "cleanup_trash_hint": "Untuk dapat mengambil semua ruang penyimpanan, buka aplikasi galeri pada sistem dan kosongkan tempat sampah", "clear": "Hapus", "clear_all": "Hapus semua", "clear_all_recent_searches": "Hapus semua pencarian terakhir", @@ -719,15 +782,18 @@ "client_cert_import": "Impor", "client_cert_import_success_msg": "Sertifikat klien telah diimpor", "client_cert_invalid_msg": "File sertifikat tidak valid atau kata sandi salah", + "client_cert_password_message": "Masukkan kata sandi untuk sertifikat ini", + "client_cert_password_title": "Kata Sandi Sertifikat", "client_cert_remove_msg": "Sertifikat klien dihapus", - "client_cert_subtitle": "Hanya mendukung format PKCS12 (.p12, .pfx). Impor/Hapus Sertifikat hanya tersedia sebelum login", - "client_cert_title": "Sertifikat SSL klien", + "client_cert_subtitle": "Hanya mendukung format PKCS12 (.p12, .pfx). Impor/hapus sertifikat hanya tersedia sebelum login", + "client_cert_title": "Sertifikat SSL klien [EKSPERIMENTAL]", "clockwise": "Searah jarum jam", "close": "Tutup", "collapse": "Tutup", "collapse_all": "Tutup Semua", "color": "Warna", "color_theme": "Tema warna", + "command": "Perintah", "comment_deleted": "Komentar dihapus", "comment_options": "Opsi komentar", "comments_and_likes": "Komentar & suka", @@ -772,6 +838,7 @@ "create_album": "Buat album", "create_album_page_untitled": "Tak berjudul", "create_api_key": "Buat kunci API", + "create_first_workflow": "Buat alur kerja pertama", "create_library": "Buat Pustaka", "create_link": "Buat tautan", "create_link_to_share": "Buat tautan untuk dibagikan", @@ -786,17 +853,25 @@ "create_tag": "Buat tag", "create_tag_description": "Buat tag baru. Untuk tag bersarang, harap input jalur tag secara lengkap termasuk tanda garis miring ke depan.", "create_user": "Buat pengguna", + "create_workflow": "Buat alur kerja", "created": "Dibuat", "created_at": "Dibuat", "creating_linked_albums": "Membuat album tertaut...", "crop": "Pangkas", + "crop_aspect_ratio_fixed": "Diperbaiki", + "crop_aspect_ratio_free": "Bebas", + "crop_aspect_ratio_original": "Asli", "curated_object_page_title": "Benda", "current_device": "Perangkat saat ini", "current_pin_code": "Kode PIN saat ini", "current_server_address": "Alamat server saat ini", + "custom_date": "Tanggal kustom", "custom_locale": "Lokal Khusus", "custom_locale_description": "Format tanggal dan angka berdasarkan bahasa dan wilayah", "custom_url": "URL Kustom", + "cutoff_date_description": "Simpan foto dari â€Ļ terakhir", + "cutoff_day": "{count, plural, one {hari} other {hari}}", + "cutoff_year": "{count, plural, one {tahun} other {tahun}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM yyyy", "dark": "Gelap", @@ -852,6 +927,7 @@ "deselect_all": "Batalkan semua pilihan", "details": "Detail", "direction": "Arah", + "disable": "Nonaktifkan", "disabled": "Dinonaktifkan", "disallow_edits": "Jangan izinkan penyuntingan", "discord": "Discord", @@ -877,6 +953,7 @@ "download_include_embedded_motion_videos": "Video tertanam", "download_include_embedded_motion_videos_description": "Sertakan video yang di sematkan dalam foto bergerak sebagai file terpisah", "download_notfound": "Unduhan tidak ditemukan", + "download_original": "Unduh berkas asli", "download_paused": "Unduhan dijeda", "download_settings": "Unduhan", "download_settings_description": "Kelola pengaturan berkaitan dengan pengunduhan aset", @@ -886,6 +963,7 @@ "download_waiting_to_retry": "Menunggu untuk mencoba lagi", "downloading": "Mengunduh", "downloading_asset_filename": "Mengunduh aset {filename}", + "downloading_from_icloud": "Mengunduh dari iCloud", "downloading_media": "Mengunduh media", "drop_files_to_upload": "Lepaskan berkas di mana saja untuk mengunggah", "duplicates": "Duplikat", @@ -914,11 +992,22 @@ "edit_tag": "Ubah tag", "edit_title": "Sunting Judul", "edit_user": "Sunting pengguna", + "edit_workflow": "Sunting alur kerja", "editor": "Penyunting", "editor_close_without_save_prompt": "Perubahan tidak akan di simpan", "editor_close_without_save_title": "Tutup editor?", - "editor_crop_tool_h2_aspect_ratios": "Perbandingan aspek", - "editor_crop_tool_h2_rotation": "Rotasi", + "editor_confirm_reset_all_changes": "Apakah anda yakin mau mengatur ulang semua perubahan?", + "editor_discard_edits_confirm": "Buang suntingan", + "editor_discard_edits_prompt": "Anda memiliki suntingan yang belum disimpan. Apakah Anda yakin ingin membuangnya?", + "editor_discard_edits_title": "Buang suntingan?", + "editor_edits_applied_error": "Gagal menerapkan suntingan", + "editor_edits_applied_success": "Suntingan berhasil diterapkan", + "editor_flip_horizontal": "Balik horizontal", + "editor_flip_vertical": "Balik vertikal", + "editor_orientation": "Orientasi", + "editor_reset_all_changes": "Mengatur ulang perubahan", + "editor_rotate_left": "Putar 90° berlawanan arah jarum jam", + "editor_rotate_right": "Putar 90° searah jarum jam", "email": "Surel", "email_notifications": "Notifikasi surel", "empty_folder": "Folder ini kosong", @@ -937,11 +1026,14 @@ "error_change_sort_album": "Gagal mengubah urutan album", "error_delete_face": "Terjadi kesalahan menghapus wajah dari aset", "error_getting_places": "Kesalahan saat mengambil lokasi", + "error_loading_albums": "Gagal memuat album", "error_loading_image": "Terjadi eror memuat gambar", "error_loading_partners": "Kesalahan saat memuat partner: {error}", + "error_retrieving_asset_information": "Gagal mendapatkan informasi aset", "error_saving_image": "Kesalahan: {error}", "error_tag_face_bounding_box": "Galat saat memberi tag wajah – tidak dapat memperoleh koordinat kotak pembatas", "error_title": "Eror - Ada yang salah", + "error_while_navigating": "Gagal saat berpindah ke aset", "errors": { "cannot_navigate_next_asset": "Tidak dapat menuju ke aset berikutnya", "cannot_navigate_previous_asset": "Tidak dapat menuju ke aset sebelumnya", @@ -976,6 +1068,7 @@ "failed_to_unstack_assets": "Gagal membatalkan penumpukan aset", "failed_to_update_notification_status": "Gagal membarui status notifikasi", "incorrect_email_or_password": "Surel atau kata sandi tidak benar", + "library_folder_already_exists": "Jalur impor ini sudah ada.", "paths_validation_failed": "{paths, plural, one {# jalur} other {# jalur}} gagal validasi", "profile_picture_transparent_pixels": "Foto profil tidak dapat memiliki piksel transparan. Silakan perbesar dan/atau pindah posisi gambar.", "quota_higher_than_disk_size": "Anda menetapkan kuota lebih tinggi dari ukuran disk", @@ -998,6 +1091,7 @@ "unable_to_complete_oauth_login": "Tidak dapat menyelesaikan log masuk OAuth", "unable_to_connect": "Tidak dapat menghubungkan", "unable_to_copy_to_clipboard": "Tidak dapat menyalin ke papan klip, pastikan Anda mengakses laman ini melalui HTTPS", + "unable_to_create": "Tidak dapat membuat alur kerja", "unable_to_create_admin_account": "Tidak dapat membuat akun admin", "unable_to_create_api_key": "Tidak dapat membuat Kunci API baru", "unable_to_create_library": "Tidak dapat membuat pustaka", @@ -1008,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Tidak dapat menghapus pola pengecualian", "unable_to_delete_shared_link": "Tidak dapat menghapus tautan terbagi", "unable_to_delete_user": "Tidak dapat menghapus pengguna", + "unable_to_delete_workflow": "Tidak dapat menghapus alur kerja", "unable_to_download_files": "Tidak dapat mengunduh berkas", "unable_to_edit_exclusion_pattern": "Tidak dapat menyunting pola pengecualian", "unable_to_empty_trash": "Tidak dapat menghapus sampah", @@ -1047,6 +1142,7 @@ "unable_to_scan_library": "Tidak dapat memindai pustaka", "unable_to_set_feature_photo": "Tidak dapat menyeting foto unggulan", "unable_to_set_profile_picture": "Tidak dapat mengatur foto profil", + "unable_to_set_rating": "Tidak dapat mengatur penilaian", "unable_to_submit_job": "Tidak dapat mengirim tugas", "unable_to_trash_asset": "Tidak dapat membuang aset", "unable_to_unlink_account": "Tidak dapat memutuskan akun", @@ -1058,8 +1154,11 @@ "unable_to_update_settings": "Tidak dapat memperbarui pengaturan", "unable_to_update_timeline_display_status": "Tidak dapat memperbarui status penampilan lini masa", "unable_to_update_user": "Tidak dapat memperbarui pengguna", + "unable_to_update_workflow": "Tidak dapat memperbarui alur kerja", "unable_to_upload_file": "Tidak dapat mengunggah berkas" }, + "errors_text": "Gagal", + "exclusion_pattern": "Pola pengecualian", "exif": "EXIF", "exif_bottom_sheet_description": "Tambahkan Deskripsi...", "exif_bottom_sheet_description_error": "Galat saat memperbaharui deskripsi", @@ -1090,6 +1189,7 @@ "external_network_sheet_info": "Ketika tidak berada di jaringan Wi-Fi yang disukai, aplikasi akan terhubung ke server melalui URL pertama di bawah ini yang dapat dijangkaunya, mulai dari atas ke bawah", "face_unassigned": "Tidak ada nama", "failed": "Gagal", + "failed_count": "Gagal: {count}", "failed_to_authenticate": "Autentikasi gagal", "failed_to_load_assets": "Gagal memuat aset", "failed_to_load_folder": "Gagal memuat berkas", @@ -1102,14 +1202,17 @@ "features": "Fitur", "features_in_development": "Fitur dalam Pengembangan", "features_setting_description": "Kelola fitur aplikasi", - "file_name": "Nama berkas", "file_name_or_extension": "Nama berkas atau ekstensi", + "file_name_text": "Nama berkas", + "file_name_with_value": "Nama berkas: {file_name}", "file_size": "Ukuran berkas", "filename": "Nama berkas", "filetype": "Jenis berkas", "filter": "Filter", + "filter_description": "Kondisi untuk memfilter aset-aset target", "filter_people": "Saring orang", "filter_places": "Saring tempat", + "filters": "Filter-filter", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", "first": "Pertama", "fix_incorrect_match": "Perbaiki pencocokan salah", @@ -1119,11 +1222,16 @@ "folders_feature_description": "Menjelajahi tampilan folder untuk foto dan video pada sistem file", "forgot_pin_code_question": "Lupa PIN?", "forward": "Maju", + "free_up_space": "Bebaskan ruang", + "free_up_space_description": "Pindahkan foto dan video yang dicadangkan ke tempat sampah perangkat Anda untuk mengosongkan ruang. Salinan Anda di server tetap aman.", + "free_up_space_settings_subtitle": "Kosongkan penyimpanan perangkat", + "full_path": "Jalur lengkap: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Fitur ini memuat sumber daya eksternal dari Google agar dapat berfungsi.", "general": "Umum", "geolocation_instruction_location": "Klik aset yang memiliki koordinat GPS untuk menggunakan lokasinya, atau pilih lokasi langsung dari peta", "get_help": "Dapatkan Bantuan", + "get_people_error": "Kesalahan dalam mendapatkan orang-orang", "get_wifiname_error": "Tidak dapat mendapatkan nama Wi-Fi. Pastikan Anda telah memberikan izin yang diperlukan dan terhubung ke jaringan Wi-Fi", "getting_started": "Memulai", "go_back": "Kembali", @@ -1149,12 +1257,15 @@ "header_settings_header_name_input": "Nama header", "header_settings_header_value_input": "Nilai header", "headers_settings_tile_title": "Header proksi kustom", + "height": "Tinggi", "hi_user": "Hai {name} ({email})", "hide_all_people": "Sembunyikan semua orang", "hide_gallery": "Sembunyikan galeri", "hide_named_person": "Sembunyikan orang {name}", "hide_password": "Sembunyikan kata sandi", "hide_person": "Sembunyikan orang", + "hide_schema": "Sembunyikan skema", + "hide_text_recognition": "Sembunyikan teks rekognisi", "hide_unnamed_people": "Sembunyikan orang tanpa nama", "home_page_add_to_album_conflicts": "Aset {added} telah ditambahkan ke album {album}. Aset {failed} sudah ada dalam album.", "home_page_add_to_album_err_local": "Belum dapat menambahkan aset lokal ke album, dilewati", @@ -1200,6 +1311,8 @@ "import_path": "Jalur pengimporan", "in_albums": "Dalam {count, plural, one {# album} other {# album}}", "in_archive": "Dalam arsip", + "in_year": "Dalam {year}", + "in_year_selector": "Dalam", "include_archived": "Termasuk terarsip", "include_shared_albums": "Termasuk album terbagi", "include_shared_partner_assets": "Termasuk aset terbagi dengan partner", @@ -1224,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Pemrosesan dijalankan {dateTime}", "items_count": "{count, plural, one {# item} other {# item}}", "jobs": "Tugas", + "json_editor": "Editor JSON", + "json_error": "Kesalahan JSON", "keep": "Simpan", + "keep_albums": "Simpan album", + "keep_albums_count": "Menyimpan {count} {count, plural, one {album} other {album}}", "keep_all": "Simpan Semua", + "keep_description": "Pilih apa yang tetap berada di perangkat Anda saat mengosongkan ruang.", + "keep_favorites": "Simpan favorit", + "keep_on_device": "Simpan di perangkat", + "keep_on_device_hint": "Pilih item yang akan disimpan di perangkat ini", "keep_this_delete_others": "Pertahankan ini, hapus lainnya", + "keeping": "Menyimpan: {items}", "kept_this_deleted_others": "Aset ini dipertahankan dan {count, plural, one {# asset} other {# assets}} dihapus", "keyboard_shortcuts": "Pintasan papan ketik", "language": "Bahasa", @@ -1236,6 +1358,7 @@ "language_setting_description": "Pilih bahasa Anda yang disukai", "large_files": "File Besar", "last": "Terakhir", + "last_months": "{count, plural, one {Bulan lalu} other {# Bulan lalu}}", "last_seen": "Terakhir dilihat", "latest_version": "Versi Terkini", "latitude": "Lintang", @@ -1245,6 +1368,8 @@ "let_others_respond": "Biarkan orang lain merespons", "level": "Tingkat", "library": "Pustaka", + "library_add_folder": "Tambahkan folder", + "library_edit_folder": "Sunting folder", "library_options": "Opsi pustaka", "library_page_device_albums": "Album pada Perangkat", "library_page_new_album": "Album baru", @@ -1265,6 +1390,7 @@ "local": "Lokal", "local_asset_cast_failed": "Tidak dapat melakukan cast aset yang belum diunggah ke server", "local_assets": "Aset Lokal", + "local_id": "ID Lokal", "local_media_summary": "Ringkasan Media Lokal", "local_network": "Jaringan Lokal", "local_network_sheet_info": "Aplikasi akan terhubung ke server melalui URL ini saat menggunakan jaringan Wi-Fi yang ditentukan", @@ -1316,8 +1442,35 @@ "loop_videos_description": "Aktifkan untuk mengulangi video secara otomatis dalam penampil detail.", "main_branch_warning": "Anda menggunakan versi pengembangan; kami sangat menyarankan menggunakan versi rilis!", "main_menu": "Menu utama", + "maintenance_action_restore": "Memulihkan Basis Data", + "maintenance_description": "Immich telah ditempatkan di mode pemeliharaan.", + "maintenance_end": "Akhiri mode pemeliharaan", + "maintenance_end_error": "Gagal mengakhiri mode pemeliharaan.", + "maintenance_logged_in_as": "Saat ini masuk sebagai {user}", + "maintenance_restore_from_backup": "Pulihkan Dari Cadangan", + "maintenance_restore_library": "Pulihkan Pustaka Anda", + "maintenance_restore_library_confirm": "Jika ini terlihat benar, lanjutkan untuk memulihkan cadangan!", + "maintenance_restore_library_description": "Memulihkan Basis Data", + "maintenance_restore_library_folder_has_files": "{folder} memiliki {count} folder", + "maintenance_restore_library_folder_no_files": "{folder} kehilangan berkas!", + "maintenance_restore_library_folder_pass": "dapat dibaca dan dapat ditulis", + "maintenance_restore_library_folder_read_fail": "tidak dapat dibaca", + "maintenance_restore_library_folder_write_fail": "tidak dapat ditulis", + "maintenance_restore_library_hint_missing_files": "Anda mungkin kehilangan berkas penting", + "maintenance_restore_library_hint_regenerate_later": "Anda dapat membuat ulang ini nanti di pengaturan", + "maintenance_restore_library_hint_storage_template_missing_files": "Menggunakan templat penyimpanan? Anda mungkin kehilangan berkas", + "maintenance_restore_library_loading": "Memuat pemeriksaan integritas dan heuristikâ€Ļ", + "maintenance_task_backup": "Membuat cadangan dari basis data yang adaâ€Ļ", + "maintenance_task_migrations": "Menjalankan migrasi basis dataâ€Ļ", + "maintenance_task_restore": "Memulihkan cadangan yang dipilihâ€Ļ", + "maintenance_task_rollback": "Pemulihan gagal, mengembalikan ke titik pemulihanâ€Ļ", + "maintenance_title": "Tidak Tersedia untuk Sementara", "make": "Merek", "manage_geolocation": "Atur lokasi", + "manage_media_access_rationale": "Izin ini diperlukan untuk menangani perpindahan aset-aset secara tepat ke tempat sampah dan mengembalikannya dari sana.", + "manage_media_access_settings": "Buka pengaturan", + "manage_media_access_subtitle": "Izinkan aplikasi Immich untuk mengelola dan memindahkan berkas media.", + "manage_media_access_title": "Akses Manajemen Media", "manage_shared_links": "Kelola tautan terbagi", "manage_sharing_with_partners": "Kelola pembagian dengan partner", "manage_the_app_settings": "Kelola pengaturan aplikasi", @@ -1372,6 +1525,8 @@ "minimize": "Kecilkan", "minute": "Menit", "minutes": "Menit", + "mirror_horizontal": "Horisontal", + "mirror_vertical": "Vertikal", "missing": "Hilang", "mobile_app": "Aplikasi Seluler", "mobile_app_download_onboarding_note": "Unduh aplikasi seluler pendamping dengan menggunakan opsi berikut", @@ -1380,10 +1535,14 @@ "monthly_title_text_date_format": "BBBB t", "more": "Lainnya", "move": "Pindah", + "move_down": "Pindah ke bawah", "move_off_locked_folder": "Pindahkan dari folder terkunci", + "move_to": "Pindah ke", + "move_to_device_trash": "Pindahkan ke tempat sampah perangkat", "move_to_lock_folder_action_prompt": "{count} ditambahkan ke folder terkunci", "move_to_locked_folder": "Pindahkan dari folder terkunci", "move_to_locked_folder_confirmation": "Foto dan video ini akan dihapus dari semua album, dan hanya dapat dilihat dari folder terkunci", + "move_up": "Pindah ke atas", "moved_to_archive": "Dipindahkan {count, plural, one {# asset} other {# assets}} ke arsip", "moved_to_library": "Dipindahkan {count, plural, one {# asset} other {# assets}} ke pustaka", "moved_to_trash": "Dipindahkan ke sampah", @@ -1393,6 +1552,7 @@ "my_albums": "Album saya", "name": "Nama", "name_or_nickname": "Nama atau nama panggilan", + "name_required": "Nama diperlukan", "navigate": "Navigasi", "navigate_to_time": "Navigasi ke Waktu", "network_requirement_photos_upload": "Gunakan data seluler untuk cadangkan foto", @@ -1410,27 +1570,34 @@ "new_pin_code": "Kode PIN baru", "new_pin_code_subtitle": "Ini adalah akses pertama Anda ke folder terkunci. Buat kode PIN untuk mengamankan akses ke halaman ini", "new_timeline": "Linimasa Baru", + "new_update": "Pembaruan baru", "new_user_created": "Pengguna baru dibuat", "new_version_available": "VERSI BARU TERSEDIA", "newest_first": "Terkini dahulu", "next": "Berikutnya", "next_memory": "Kenangan berikutnya", "no": "Tidak", + "no_actions_added": "Belum ada aksi yang ditambahkan", + "no_albums_found": "Tidak ada album yang ditemukan", "no_albums_message": "Buat album untuk mengelola foto dan video Anda", "no_albums_with_name_yet": "Sepertinya Anda belum memiliki album apa pun dengan nama ini.", "no_albums_yet": "Sepertinya Anda belum memiliki album apa pun.", "no_archived_assets_message": "Arsipkan foto dan video untuk menyembunyikannya dari tampilan Foto", - "no_assets_message": "KLIK UNTUK MENGUNGGAH FOTO PERTAMA ANDA", + "no_assets_message": "Klik untuk mengunggah foto pertama Anda", "no_assets_to_show": "Tidak ada aset", "no_cast_devices_found": "Tidak ada perangkat cast yang ditemukan", "no_checksum_local": "Tidak ada checksum yang tersedia - tidak dapat mengambil aset lokal", "no_checksum_remote": "Tidak ada checksum yang tersedia - tidak dapat mengambil aset jarak jauh", + "no_configuration_needed": "Tidak ada konfigurasi yang diperlukan", + "no_devices": "Tidak ada perangkat terotorisasi", "no_duplicates_found": "Tidak ada duplikat yang ditemukan.", "no_exif_info_available": "Tidak ada info EXIF yang tersedia", "no_explore_results_message": "Unggah lebih banyak foto untuk menjelajahi koleksi Anda.", "no_favorites_message": "Tambahkan favorit untuk mencari foto dan video terbaik Anda dengan cepat", + "no_filters_added": "Belum ada filter yang ditambahkan", "no_libraries_message": "Buat pustaka eksternal untuk menampilkan foto dan video Anda", "no_local_assets_found": "Tidak ada aset lokal yang ditemukan dengan checksum ini", + "no_location_set": "Tidak ada lokasi yang ditetapkan", "no_locked_photos_message": "Foto dan video di folder terkunci disembunyikan dan tidak akan muncul saat Anda menelusuri atau mencari di pustaka.", "no_name": "Tidak Ada Nama", "no_notifications": "Tidak ada notifikasi", @@ -1441,10 +1608,11 @@ "no_results_description": "Coba sinonim atau kata kunci yang lebih umum", "no_shared_albums_message": "Buat sebuah album untuk membagikan foto dan video dengan orang-orang dalam jaringan Anda", "no_uploads_in_progress": "Tidak ada unggahan yang sedang berlangsung", + "none": "Tidak ada", + "not_allowed": "Tidak diperbolehkan", "not_available": "T/T", "not_in_any_album": "Tidak ada dalam album apa pun", "not_selected": "Belum dipilih", - "note_apply_storage_label_to_previously_uploaded assets": "Catatan: Untuk menerapkan Label Penyimpanan pada aset yang sebelumnya telah diunggah, jalankan", "notes": "Catatan", "nothing_here_yet": "Masih kosong", "notification_permission_dialog_content": "Untuk mengaktifkan notifikasi, buka Pengaturan lalu berikan izin.", @@ -1489,6 +1657,7 @@ "other_variables": "Variabel lain", "owned": "Dimiliki", "owner": "Pemilik", + "page": "Laman", "partner": "Rekan", "partner_can_access": "{partner} dapat mengakses", "partner_can_access_assets": "Semua foto dan video Anda kecuali yang ada di Arsip dan Terhapus", @@ -1521,6 +1690,7 @@ "people": "Orang", "people_edits_count": "{count, plural, one {# orang} other {# orang}} disunting", "people_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan orang", + "people_selected": "{count, plural, one {# orang dipilih} other {# orang dipilih}}", "people_sidebar_description": "Tampilkan tautan ke Orang dalam bilah samping", "permanent_deletion_warning": "Peringatan penghapusan permanen", "permanent_deletion_warning_setting_description": "Tampilkan peringatan ketika menghapus aset secara permanen", @@ -1545,12 +1715,17 @@ "person_age_years": "{years, plural, other {# tahun}} old", "person_birthdate": "Lahir pada {date}", "person_hidden": "{name}{hidden, select, true { (tersembunyi)} other {}}", + "person_recognized": "Orang yang dikenali", + "person_selected": "Orang yang dipilih", "photo_shared_all_users": "Sepertinya Anda membagikan foto Anda dengan semua pengguna atau Anda tidak memiliki pengguna siapa pun untuk dibagikan.", "photos": "Foto", "photos_and_videos": "Foto & Video", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto dari tahun lalu", + "photos_only": "Hanya foto", "pick_a_location": "Pilih lokasi", + "pick_custom_range": "Rentang kustom", + "pick_date_range": "Pilih rentang tanggal", "pin_code_changed_successfully": "Berhasil mengubah kode PIN", "pin_code_reset_successfully": "Berhasil mereset kode PIN", "pin_code_setup_successfully": "Berhasil memasang kode PIN", @@ -1623,10 +1798,12 @@ "purchase_settings_server_activated": "Kunci produk server dikelola oleh admin", "query_asset_id": "ID Aset Kueri", "queue_status": "Antrian {count}/{total}", + "rate_asset": "Menilai Aset", "rating": "Peringkat bintang", "rating_clear": "Hapus peringkat", "rating_count": "{count, plural, one {# peringkat} other {# peringkat}}", "rating_description": "Tampilkan peringkat EXIF pada panel info", + "rating_set": "Mengatur nilai menjadi {rating, plural, one {# bintang} other {# bintang}}", "reaction_options": "Opsi reaksi", "read_changelog": "Baca Log Perubahan", "readonly_mode_disabled": "Mode baca-saja dimatikan", @@ -1637,7 +1814,7 @@ "reassigned_assets_to_new_person": "Menetapkan ulang {count, plural, one {# aset} other {# aset}} kepada orang baru", "reassing_hint": "Tetapkan aset yang dipilih ke orang yang sudah ada", "recent": "Terkini", - "recent-albums": "Album terkini", + "recent_albums": "Album terkini", "recent_searches": "Pencarian terkini", "recently_added": "Barusaja ditambahkan", "recently_added_page_title": "Baru Ditambahkan", @@ -1726,9 +1903,11 @@ "saved_settings": "Pengaturan disimpan", "say_something": "Ucapkan sesuatu", "scaffold_body_error_occurred": "Terjadi kesalahan", + "scan": "Pindai", "scan_all_libraries": "Pindai Semua Pustaka", "scan_library": "Pindai", "scan_settings": "Pengaturan Pemindaian", + "scanning": "Memindai", "scanning_for_album": "Memindai album...", "search": "Cari", "search_albums": "Cari album", @@ -1758,6 +1937,7 @@ "search_filter_media_type_title": "Pilih jenis media", "search_filter_ocr": "Cari dengan OCR", "search_filter_people_title": "Pilih orang", + "search_filter_star_rating": "Peringkat Bintang", "search_for": "Cari", "search_for_existing_person": "Cari orang yang sudah ada", "search_no_more_result": "Tidak ada hasil lagi", @@ -1792,17 +1972,23 @@ "second": "Detik", "see_all_people": "Lihat semua orang", "select": "Pilih", + "select_album": "Pilih album", "select_album_cover": "Pilih kover album", + "select_albums": "Pilih album-album", "select_all": "Pilih semua", "select_all_duplicates": "Pilih semua duplikat", "select_all_in": "Pilih semua di {group}", "select_avatar_color": "Pilih warna avatar", + "select_count": "{count, plural, one {Pilih #} other {Pilih #}}", + "select_cutoff_date": "Pilih tanggal batas", "select_face": "Pilih wajah", "select_featured_photo": "Pilih foto terfitur", "select_from_computer": "Pilih dari komputer", "select_keep_all": "Pilih simpan semua", "select_library_owner": "Pilih pemilik pustaka", "select_new_face": "Pilih wajah baru", + "select_people": "Pilih orang", + "select_person": "Pilih orang", "select_person_to_tag": "Pilih orang untuk ditandai", "select_photos": "Pilih foto", "select_trash_all": "Pilih buang semua", @@ -1818,6 +2004,8 @@ "server_offline": "Server Luring", "server_online": "Server Daring", "server_privacy": "Privasi server", + "server_restarting_description": "Laman ini akan dimuat ulang sesaat lagi.", + "server_restarting_title": "Server sedang dimulai ulang", "server_stats": "Statistik Server", "server_update_available": "Pembaruan server tersedia", "server_version": "Versi Server", @@ -1936,11 +2124,13 @@ "show_password": "Tampilkan kata sandi", "show_person_options": "Tampilkan opsi orang", "show_progress_bar": "Tampilkan Bilah Progres", + "show_schema": "Tampilkan skema", "show_search_options": "Tampilkan opsi pencarian", "show_shared_links": "Tampilkan tautan terbagi", "show_slideshow_transition": "Tampilkan transisi salindia", "show_supporter_badge": "Lencana suporter", "show_supporter_badge_description": "Tampilkan lencana suporter", + "show_text_recognition": "Tampilkan teks rekognisi", "show_text_search_menu": "Tampilkan menu pencarian teks", "shuffle": "Acak", "sidebar": "Bilah sisi", @@ -1952,6 +2142,8 @@ "skip_to_folders": "Lewati ke berkas", "skip_to_tags": "Lewati ke tag", "slideshow": "Salindia", + "slideshow_repeat": "Ulangi slideshow", + "slideshow_repeat_description": "Ulangi dari awal saat slideshow berakhir", "slideshow_settings": "Pengaturan salindia", "sort_albums_by": "Urutkan album berdasarkan...", "sort_created": "Tanggal dibuat", @@ -2011,6 +2203,7 @@ "tags": "Tag", "tap_to_run_job": "Ketuk untuk menjalankan pekerjaan", "template": "Templat", + "text_recognition": "Teks rekognisi", "theme": "Tema", "theme_selection": "Pemilihan tema", "theme_selection_description": "Tetapkan tema ke terang atau gelap secara otomatis berdasarkan preferensi sistem peramban Anda", @@ -2027,10 +2220,12 @@ "theme_setting_theme_subtitle": "Pilih setelan tema aplikasi", "theme_setting_three_stage_loading_subtitle": "Pemuatan tiga tahap dapat meningkatkan performa pemuatan, namun akan menyebabkan beban jaringan meningkat secara signifikan", "theme_setting_three_stage_loading_title": "Aktifkan pemuatan tiga tahap", + "then": "Lalu", "they_will_be_merged_together": "Mereka akan digabungkan bersama", "third_party_resources": "Sumber Daya Pihak Ketiga", "time": "Waktu", "time_based_memories": "Kenangan berbasis waktu", + "time_based_memories_duration": "Jumlah detik untuk menampilkan tiap gambar.", "timeline": "Lini masa", "timezone": "Zona waktu", "to_archive": "Arsipkan", @@ -2042,6 +2237,7 @@ "to_select": "untuk memilih", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", + "toggle_theme_description": "Sakelar tema", "total": "Jumlah", "total_usage": "Jumlah penggunaan", "trash": "Sampah", @@ -2059,6 +2255,13 @@ "trash_page_select_assets_btn": "Pilih aset", "trash_page_title": "Sampah ({count})", "trashed_items_will_be_permanently_deleted_after": "Item yang dibuang akan dihapus secara permanen setelah {days, plural, one {# hari} other {# hari}}.", + "trigger": "Pemicu", + "trigger_asset_uploaded": "Asset telah terunggah", + "trigger_asset_uploaded_description": "Terpicu saat aset baru telah terunggah", + "trigger_description": "Sebuah peristiwa yang memicu alur kerja", + "trigger_person_recognized": "Orang telah dikenali", + "trigger_person_recognized_description": "Terpicu saat seseorang terdeteksi", + "trigger_type": "Tipe pemicu", "troubleshoot": "Pemecahan Masalah", "type": "Jenis", "unable_to_change_pin_code": "Tidak dapat mengubah kode PIN", @@ -2073,6 +2276,7 @@ "unhide_person": "Munculkan orang", "unknown": "Tidak diketahui", "unknown_country": "Negara Tidak Diketahui", + "unknown_date": "Tanggal tidak diketahui", "unknown_year": "Tahun Tidak Diketahui", "unlimited": "Tidak terbatas", "unlink_motion_video": "Membatalkan tautan video gerak", @@ -2089,17 +2293,19 @@ "unstack": "Batalkan penumpukan", "unstack_action_prompt": "{count} Tidak dalam tumpukan", "unstacked_assets_count": "Penumpukan {count, plural, one {# aset} other {# aset}} dibatalkan", + "unsupported_field_type": "Tipe bidang tidak didukung", "untagged": "Tidak ditandai", + "untitled_workflow": "Alur kerja tak berjudul", "up_next": "Berikutnya", "update_location_action_prompt": "Perbarui lokasi {count} aset yang dipilih dengan:", "updated_at": "Diperbarui", "updated_password": "Kata sandi diperbarui", "upload": "Unggah", - "upload_action_prompt": "{count} antrian untuk diunggah", "upload_concurrency": "Konkurensi pengunggahan", "upload_details": "Detil unggahan", "upload_dialog_info": "Apakah akan mencadangkan aset terpilih ke server?", "upload_dialog_title": "Unggah Aset", + "upload_error_with_count": "Kesalahan unggah untuk {count, plural, one {# aset} other {# aset}}", "upload_errors": "Unggahan selesai dengan {count, plural, one {# eror} other {# eror}}, muat ulang laman untuk melihat aset terunggah baru.", "upload_finished": "Unggahan berhasil", "upload_progress": "Tersisa {remaining, number} - Di proses {processed, number}/{total, number}", @@ -2135,6 +2341,7 @@ "utilities": "Peralatan", "validate": "Validasi", "validate_endpoint_error": "Masukkan URL yang valid", + "validation_error": "Kesalahan validasi", "variables": "Variabel", "version": "Versi", "version_announcement_closing": "Temanmu, Alex", @@ -2146,10 +2353,12 @@ "video_hover_setting_description": "Putar gambar kecil video ketika tetikus berada di atas item. Bahkan saat dinonaktifkan, pemutaran dapat dimulai dengan mengambang di atas ikon putar.", "videos": "Video", "videos_count": "{count, plural, one {# Video} other {# Video}}", + "videos_only": "Hanya video", "view": "Tampilkan", "view_album": "Tampilkan Album", "view_all": "Tampilkan Semua", "view_all_users": "Tampilkan semua pengguna", + "view_asset_owners": "Lihat pemilik asset", "view_details": "Tampilkan detil", "view_in_timeline": "Lihat di timeline", "view_link": "Tampilkan tautan", @@ -2165,18 +2374,36 @@ "viewer_stack_use_as_main_asset": "Gunakan sebagai aset utama", "viewer_unstack": "Lepas tumpukan", "visibility_changed": "Keterlihatan diubah untuk {count, plural, one {# orang} other {# orang}}", + "visual": "Visual", + "visual_builder": "Pembuat visual", "waiting": "Menunggu", + "waiting_count": "Menunggu: {count}", "warning": "Peringatan", "week": "Pekan", "welcome": "Selamat datang", "welcome_to_immich": "Selamat datang di Immich", + "width": "Lebar", "wifi_name": "Nama Wi-Fi", + "workflow_delete_prompt": "Apakah anda yakin ingin menghapus alur kerja ini?", + "workflow_deleted": "Alur kerja telah dihapus", + "workflow_description": "Deskripsi alur kerja", + "workflow_info": "Informasi alur kerja", + "workflow_json": "JSON alur kerja", + "workflow_json_help": "Ubah konfigurasi alur kerja dengan format JSON. Perubahan akan disinkronisasikan ke pembuat visual.", + "workflow_name": "Nama alur kerja", + "workflow_navigation_prompt": "Apakah anda yakin ingin keluar tanpa menyimpan perubahan anda?", + "workflow_summary": "Ringkasan alur kerja", + "workflow_update_success": "Alur kerja berhasil diubah", + "workflow_updated": "Alur kerja diubah", + "workflows": "Alur kerja", + "workflows_help_text": "Alur kerja untuk otomasi kegiatan pada aset anda sesuai dengan pemicu dan filter", "wrong_pin_code": "Kode PIN salah", "year": "Tahun", "years_ago": "{years, plural, one {# tahun} other {# tahun}} yang lalu", "yes": "Ya", "you_dont_have_any_shared_links": "Anda tidak memiliki tautan terbagi", "your_wifi_name": "Nama Wi-Fi Anda", + "zero_to_clear_rating": "tekan 0 untuk menghapus penilaian pada aset", "zoom_image": "Perbesar Gambar", "zoom_to_bounds": "Perbesar ke batas" } diff --git a/i18n/is.json b/i18n/is.json index d534e62cd4..a355b71661 100644 --- a/i18n/is.json +++ b/i18n/is.json @@ -836,8 +836,6 @@ "editor": "Myndvinnsla", "editor_close_without_save_prompt": "Breytingarnar verða ekki vistaðar", "editor_close_without_save_title": "Loka myndvinnslu?", - "editor_crop_tool_h2_aspect_ratios": "HlutfÃļll", - "editor_crop_tool_h2_rotation": "SnÃēningur", "email": "Netfang", "email_notifications": "Meldingar í tÃļlvupÃŗsti", "empty_folder": "Þessi mappa er tÃŗm", @@ -1024,7 +1022,6 @@ "features": "Eiginleikar", "features_in_development": "Eiginleikar í ÞrÃŗun", "features_setting_description": "SÃŊsla með eiginleika smÃĄforrits", - "file_name": "SkrÃĄarheiti", "file_name_or_extension": "SkrÃĄarheiti eða nafnauki", "file_size": "SkrÃĄarstÃĻrð", "filename": "SkrÃĄarheiti", diff --git a/i18n/it.json b/i18n/it.json index fbc1b32e36..e9cc787096 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -5,6 +5,7 @@ "acknowledge": "Ho capito", "action": "Azione", "action_common_update": "Aggiorna", + "action_description": "Un insieme di azioni da eseguire sulle risorse filtrate", "actions": "Azioni", "active": "Attivo", "active_count": "Attivi: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Aggiungi una posizione", "add_a_name": "Aggiungi un nome", "add_a_title": "Aggiungi un titolo", + "add_action": "Aggiungi azione", + "add_action_description": "Fare clic per aggiungere un'azione da eseguire", + "add_assets": "Aggiungi risorse", "add_birthday": "Aggiungi compleanno", "add_endpoint": "Aggiungi un endpoint", "add_exclusion_pattern": "Aggiungi un pattern di esclusione", + "add_filter": "Aggiungi filtro", + "add_filter_description": "Fare clic per aggiungere una condizione di filtro", "add_location": "Aggiungi posizione", "add_more_users": "Aggiungi altri utenti", "add_partner": "Aggiungi partner", @@ -36,6 +42,7 @@ "add_to_shared_album": "Aggiungi ad album condiviso", "add_upload_to_stack": "Aggiungi caricamento allo stack", "add_url": "Aggiungi URL", + "add_workflow_step": "Aggiungi passaggio del flusso di lavoro", "added_to_archive": "Aggiunto all'archivio", "added_to_favorites": "Aggiunto ai preferiti", "added_to_favorites_count": "Aggiunto {count, number} ai preferiti", @@ -63,7 +70,7 @@ "cleared_jobs": "Cancellati i processi per: {job}", "config_set_by_file": "La configurazione è attualmente impostata da un file di configurazione", "confirm_delete_library": "Sei sicuro di voler cancellare la libreria {library}?", - "confirm_delete_library_assets": "Sei sicuro di voler cancellare questa libreria? Questo cancellerà {count, plural, one {# asset} other {tutti e # gli assets}} da Immich senza possibilità di tornare indietro. I file non verranno cancellati.", + "confirm_delete_library_assets": "Sei sicuro di voler cancellare questa libreria? CiÃ˛ rimuoverà {count, plural, one {# risorsa} other {tutte le # risorse}} da Immich senza possibilità di tornare indietro. I file rimarranno comunque sul disco.", "confirm_email_below": "Per confermare, scrivi \"{email}\" qui sotto", "confirm_reprocess_all_faces": "Sei sicuro di voler riprocessare tutti i volti? Questo cancellerà anche tutte le persone associate.", "confirm_user_password_reset": "Sei sicuro di voler resettare la password di {user}?", @@ -74,15 +81,15 @@ "cron_expression_description": "Imposta il tempo di scansione utilizzando il formato Cron. Per ulteriori informazioni fare riferimento a Crontab Guru", "cron_expression_presets": "Espressione Cron preimpostata", "disable_login": "Disabilita login", - "duplicate_detection_job_description": "Esegui il machine learning sugli assets per rilevare immagini simili. Basato su Ricerca Intelligente", + "duplicate_detection_job_description": "Esegui il machine learning sulle risorse per rilevare immagini simili. Basato su Ricerca Intelligente", "exclusion_pattern_description": "I modelli di esclusione ti permettono di ignorare file e cartelle durante la scansione della tua libreria. Questo è utile se hai cartelle che contengono file che non vuoi importare, come ad esempio, i file RAW.", "export_config_as_json_description": "Scarica la configurazione attuale del sistema come file JSON", "external_libraries_page_description": "Pagina librerie esterne (admin)", "face_detection": "Rilevamento Volti", - "face_detection_description": "Rileva i volti presenti negli asset utilizzando il machine-learning. Per i video, viene presa in considerazione solo la miniatura. Utilizzare \"Ripristina\" per cancellare tutti i volti presenti, \"Ricarica\" per processare di nuovo tutti gli asset, \"Mancanti\" processa solo gli asset che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", + "face_detection_description": "Rileva i volti presenti nelle risorse utilizzando il machine-learning. Per i video, viene presa in considerazione solo la miniatura. Utilizzare \"Ripristina\" per cancellare tutti i volti presenti, \"Ricarica\" per processare di nuovo tutti le risorse, \"Mancanti\" processa solo le risorse che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", "facial_recognition_job_description": "Raggruppa i volti rilevati in persone. Questo processo viene eseguito dopo che il rilevamento volti è stato completato. \"Reset\" (ri-)unisce tutti i volti. \"Mancanti\" processa i volti che non hanno una persona assegnata.", "failed_job_command": "Il comando {command} è fallito per il processo: {job}", - "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti i suoi assets. Non è possibile tornare indietro e i file non potranno essere recuperati.", + "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti le sue risorse. Non è possibile tornare indietro e i file non potranno essere recuperati.", "image_format": "Formato", "image_format_description": "WebP produce file piÚ piccoli rispetto a JPEG, ma è piÚ lento da codificare.", "image_fullsize_description": "Immagini a dimensioni reali senza metadati, sono utilizzate durante lo zoom", @@ -97,6 +104,8 @@ "image_preview_description": "Immagine a media dimensione senza metadati, utilizzata durante la visualizzazione di una singola risorsa e per il machine learning", "image_preview_quality_description": "Qualità dell'anteprima da 1 a 100. PiÚ alto è meglio ma produce file piÚ pesanti e puÃ˛ ridurre la reattività dell'app. Impostare un valore basso puÃ˛ influenzare negativamente la qualità del machine learning.", "image_preview_title": "Impostazioni dell'anteprima", + "image_progressive": "Progressiva", + "image_progressive_description": "Codifica progressivamente le immagini JPEG per mostrarle con un caricamento graduale. Questo non ha effetto sulle immagini WebP.", "image_quality": "Qualità", "image_resolution": "Risoluzione", "image_resolution_description": "Risoluzioni piÚ elevate possono preservare piÚ dettagli ma richiedere piÚ tempo per la codifica, avere dimensioni di file piÚ grandi e ridurre la reattività dell'app.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Attiva ricerca intelligente", "machine_learning_smart_search_enabled_description": "Se disabilitato le immagini non saranno codificate per la ricerca intelligente.", "machine_learning_url_description": "URL del server machine learning. Se sono stati forniti piÚ di un URL, verrà testato un server alla volta finchÊ uno non risponderà, in ordine dal primo all'ultimo. I server che non rispondono saranno temporaneamente ignorati finchÊ non torneranno online.", + "maintenance_delete_backup": "Elimina Backup", + "maintenance_delete_backup_description": "Questo file verrà eliminato irreversibilmente.", + "maintenance_delete_error": "Eliminazione del backup fallita.", + "maintenance_restore_backup": "Ripristina Backup", + "maintenance_restore_backup_description": "Immich verrà cancellato e ripristinato dal backup scelto. Prima di procedere, verrà creato un backup.", + "maintenance_restore_backup_different_version": "Questo backup è stato creato con un'altra versione di Immich!", + "maintenance_restore_backup_unknown_version": "Impossibile determinare la versione del backup.", + "maintenance_restore_database_backup": "Ripristina il backup del database", + "maintenance_restore_database_backup_description": "Torna a uno stato precedente del database usando un file di backup", "maintenance_settings": "Manutenzione", "maintenance_settings_description": "Metti Immich in modalità manutenzione.", - "maintenance_start": "Avvia modalità manutenzione", + "maintenance_start": "Passa a modalità manutenzione", "maintenance_start_error": "Errore nell'avvio della modalità manutenzione.", + "maintenance_upload_backup": "Carica file di backup del database", + "maintenance_upload_backup_error": "Impossibile caricare il backup, è un file .sql/.sql.gz?", "manage_concurrency": "Gestisci Concorrenza", "manage_concurrency_description": "Vai alla pagina dei processi per gestire la concorrenza dei job", "manage_log_settings": "Gestisci le impostazioni dei log", @@ -210,7 +230,7 @@ "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", + "migration_job_description": "Migra le anteprime per le risorse e i 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", @@ -227,7 +247,7 @@ "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", + "note_apply_storage_label_previous_assets": "Nota: Per assegnare l'etichetta storage a risorse precedentemente caricate, esegui", "note_cannot_be_changed_later": "NOTA: Non potrà essere modificato in futuro!", "notification_email_from_address": "Indirizzo mittente", "notification_email_from_address_description": "Indirizzo email del mittente, ad esempio: \"Immich Photo Server \". Assicurati di utilizzare un indirizzo da cui sei autorizzato a inviare email.", @@ -252,7 +272,7 @@ "oauth_auto_register": "Registrazione automatica", "oauth_auto_register_description": "Automaticamente registra nuovi utenti dopo il login OAuth", "oauth_button_text": "Testo pulsante", - "oauth_client_secret_description": "Richiesto se PKCE (Proof Key for Code Exchange) non è supportato dal provider OAuth", + "oauth_client_secret_description": "Richiesto per client confidenziali o se PKCE (Proof Key for Code Exchange) non è supportato dal client pubblico.", "oauth_enable_description": "Login con OAuth", "oauth_mobile_redirect_uri": "URI di reindirizzamento per app mobile", "oauth_mobile_redirect_uri_override": "Sovrascrivi URI di reindirizzamento per app mobile", @@ -291,7 +311,7 @@ "search_jobs": "Cerca Attivitàâ€Ļ", "send_welcome_email": "Invia email di benvenuto", "server_external_domain_settings": "Dominio esterno", - "server_external_domain_settings_description": "Dominio per link condivisi pubblicamente, incluso http(s)://", + "server_external_domain_settings_description": "Dominio utilizzato per i link esterni", "server_public_users": "Utenti Pubblici", "server_public_users_description": "Tutti gli utenti (nome ed e-mail) sono elencati quando si aggiunge un utente agli album condivisi. Quando disabilitato, l'elenco degli utenti sarà disponibile solo per gli utenti amministratori.", "server_settings": "Impostazioni Server", @@ -303,15 +323,15 @@ "sidecar_job": "Metadati sidecar", "sidecar_job_description": "Scopri o sincronizza metadati sidecar dal filesystem", "slideshow_duration_description": "Numero di secondi per cui mostrare ciascuna immagine", - "smart_search_job_description": "Esegui il machine learning sugli asset per permettere la ricerca intelligente", + "smart_search_job_description": "Esegui il machine learning sulle risorse per permettere la ricerca intelligente", "storage_template_date_time_description": "Data e ora di creazione del media vengono usate come data e ora dello stesso", "storage_template_date_time_sample": "Esempio di data {date}", "storage_template_enable_description": "Attiva il motore del modello di archiviazione", "storage_template_hash_verification_enabled": "Verifica hash abilitata", "storage_template_hash_verification_enabled_description": "Attiva verifica hash, non disabilitare questo se non sei certo delle implicazioni", "storage_template_migration": "Migrazione modello archiviazione", - "storage_template_migration_description": "Applica il {template} attuale agli asset caricati in precedenza", - "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui {job}.", + "storage_template_migration_description": "Applica il {template} attuale alle risorse caricate in precedenza", + "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo alle nuove risorse. Per applicare le modifiche retroattivamente esegui {job}.", "storage_template_migration_job": "Processo di migrazione del Modello di Archiviazione", "storage_template_more_details": "Per maggiori informazioni riguardo a questa funzionalità, consulta il Modello di Archiviazione e le sue conseguenze", "storage_template_onboarding_description_v2": "Se attiva, questa funzionalità organizzerà automaticamente i file utilizzando un modello definito dall'utente. Per maggiori informazioni, consultare la documentazione.", @@ -431,6 +451,9 @@ "admin_password": "Password Amministratore", "administration": "Amministrazione", "advanced": "Avanzate", + "advanced_settings_clear_image_cache": "Cancella la cache dell' immagine", + "advanced_settings_clear_image_cache_error": "Impossibile cancellare la cache dell'immagine", + "advanced_settings_clear_image_cache_success": "Cancellato/i con successo {size}", "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}", @@ -467,16 +490,18 @@ "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_selected": "Album selezionato", "album_share_no_users": "Sembra che tu abbia condiviso questo album con tutti gli utenti oppure non hai nessun utente con cui condividere.", "album_summary": "Sommario Album", "album_updated": "Album aggiornato", "album_updated_setting_description": "Ricevi una notifica email quando un album condiviso ha nuovi media", + "album_upload_assets": "Carica risorse dal tuo computer e aggiungile all'album", "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": "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_remove": "Ci sono problemi nella rimozione di risorse 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", @@ -485,12 +510,14 @@ "albums": "Album", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", "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_default_sort_order_description": "Ordine iniziale delle risorse nei nuovi album.", + "albums_feature_description": "Raggruppamento delle risorse che possono essere condivise con altri utenti.", "albums_on_device_count": "Album sul dispositivo ({count})", + "albums_selected": "{count,plural, one{# album selezionato} other {# album selezionati}}", "all": "Tutti", "all_albums": "Tutti gli album", "all_people": "Tutte le persone", + "all_photos": "Tutte le foto", "all_videos": "Tutti i video", "allow_dark_mode": "Permetti Tema Scuro", "allow_edits": "Permetti modifiche", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permetti agli utenti pubblici di caricare", "allowed": "Consentito", "alt_text_qr_code": "Immagine QR", + "always_keep": "Mantieni sempre", + "always_keep_photos_hint": "Libera Spazio mantiene tutte le foto su questo dispositivo.", + "always_keep_videos_hint": "Libera Spazio mantiene tutti i video su questo dispositivo.", "anti_clockwise": "Senso anti-orario", "api_key": "Chiave API", "api_key_description": "Questo valore verrà mostrato una sola volta. Assicurati di copiarlo prima di chiudere la finestra.", @@ -516,7 +546,7 @@ "archive": "Archivio", "archive_action_prompt": "Aggiunti {count} elementi all'Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", - "archive_page_no_archived_assets": "Non è stato trovato nessun elemento archiviato", + "archive_page_no_archived_assets": "Non è stato trovato nessuna risorsa archiviata", "archive_page_title": "Archivio ({count})", "archive_size": "Dimensioni Archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", @@ -524,56 +554,61 @@ "archived_count": "{count, plural, other {Archiviati #}}", "are_these_the_same_person": "Sono la stessa persona?", "are_you_sure_to_do_this": "Sei sicuro di voler procedere?", + "array_field_not_fully_supported": "Insieme di campi richiedono una modifica manuale del JSON", "asset_action_delete_err_read_only": "Non puoi eliminare risorse in sola lettura, azione ignorata", "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 dell'elemento è stata aggiornata", - "asset_filename_is_offline": "Il media {filename} è offline", - "asset_has_unassigned_faces": "Il media ha dei volti non categorizzati", + "asset_adding_to_album": "Inserimento nell'albumâ€Ļ", + "asset_created": "Risorsa creata", + "asset_description_updated": "La descrizione della risorsa è stata aggiornata", + "asset_filename_is_offline": "La risorsa {filename} è offline", + "asset_has_unassigned_faces": "La risoesa 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 gli elementi per", + "asset_list_layout_settings_group_by": "Raggruppa le risorse per", "asset_list_layout_settings_group_by_month_day": "Mese + giorno", "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Impostazioni del layout della griglia delle foto", "asset_list_settings_title": "Griglia foto", + "asset_not_found_on_device_android": "Risorsa non trovata sul dispositivo", + "asset_not_found_on_device_ios": "Risorsa non trovata sul dispositivo. Se stai usando iCloud, la risorsa potrebbe essere inaccessibile a causa di un file errato salvato su iCloud", + "asset_not_found_on_icloud": "Risorsa non trovata su iCloud. La risorsa potrebbe essere inaccessibile a causa di un file errato salvato su iCloud", "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_offline_description": "Questa risorsa esterna non esiste piÚ sul disco. Contatta il tuo amministratore di Immich per assistenza.", + "asset_restored_successfully": "Risorsa ripristinata con successo", "asset_skipped": "Saltato", "asset_skipped_in_trash": "Nel cestino", - "asset_trashed": "Asset cestinato", - "asset_troubleshoot": "Risoluzione dei problemi dell'asset", - "asset_uploaded": "Caricato", + "asset_trashed": "Risorsa cestinata", + "asset_troubleshoot": "Risoluzione dei problemi della risorsa", + "asset_uploaded": "Caricata", "asset_uploading": "Caricamentoâ€Ļ", "asset_viewer_settings_subtitle": "Gestisci le impostazioni del visualizzatore della galleria", - "asset_viewer_settings_title": "Visualizzazione risorse", + "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_albums_count": "Aggiunto {assetTotal, plural, one {# elemento} other {# elementi}} a {albumTotal, plural, one {# album} other {# album}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {L'elemento} other {Gli elementi}} non possono essere aggiunti all'album", - "assets_cannot_be_added_to_albums": "Non Ê stato possibile aggiungere {count, plural, one {l'elemento} other {gli elementi}} a nessun album", - "assets_count": "{count, plural, one {# elemento} other {# elementi}}", - "assets_deleted_permanently": "{count} elementi cancellati definitivamente", - "assets_deleted_permanently_from_server": "{count} elementi cancellati definitivamente dal server Immich", + "assets_added_count": "{count, plural, one {# risorsa aggiunta} other {# risorse aggiunte}}", + "assets_added_to_album_count": "{count, plural, one {# risorsa aggiunta} other {# risorse aggiunte}} all'album", + "assets_added_to_albums_count": "Aggiunto {assetTotal, plural, one {# risorsa} other {# risorse}} a {albumTotal, plural, one {# album} other {# album}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {La risorsa} other {Le risorse}} non possono essere aggiunte all'album", + "assets_cannot_be_added_to_albums": "Non Ê stato possibile aggiungere {count, plural, one {la risorsa} other {le risorse}} a nessun album", + "assets_count": "{count, plural, one {# risorsa} other {# risorse}}", + "assets_deleted_permanently": "{count} risorsa/e cancellate definitivamente", + "assets_deleted_permanently_from_server": "{count} risorsa/e cancellate definitivamente sul server Immich", "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 {# 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 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", - "assets_trashed_count": "{count, plural, one {Spostato # asset} other {Spostati # assets}} nel cestino", - "assets_trashed_from_server": "{count} elementi cestinati dal server Immich", - "assets_were_part_of_album_count": "{count, plural, one {L'asset era} other {Gli asset erano}} già parte dell'album", - "assets_were_part_of_albums_count": "{count, plural, one {L'elemento fa} other {Gli elementi fanno}} già parte degli album", + "assets_moved_to_trash_count": "{count, plural, one {# risorsa spostata} other {# risorse spostate}} nel cestino", + "assets_permanently_deleted_count": "{count, plural, one {# risorsa cancellata} other {# risorse cancellate}} definitivamente", + "assets_removed_count": "{count, plural, one {# risorsa rimossa} other {# risorse rimosse}}", + "assets_removed_permanently_from_device": "{count} risorsa/e cancellate definitivamente sul tuo dispositivo", + "assets_restore_confirmation": "Sei sicuro di voler ripristinare tutti le risorse cancellate? Non puoi annullare questa azione! Tieni presente che eventuali risorse offline non potranno essere ripristinate in questo modo.", + "assets_restored_count": "{count, plural, one {# risorsa ripristinata} other {# risorse ripristinate}}", + "assets_restored_successfully": "{count} risorsa/e ripristinati", + "assets_trashed": "{count} risorsa/e cestinati", + "assets_trashed_count": "{count, plural, one {Spostato # risorsa} other {Spostate # risorse}} nel cestino", + "assets_trashed_from_server": "{count} risorsa/e cestinate sul server Immich", + "assets_were_part_of_album_count": "{count, plural, one {La risorsa fa} other {Le risorse facevano}} già parte dell'album", + "assets_were_part_of_albums_count": "{count, plural, one {La risorsa fa} other {Le risorse facevano}} già parte degli album", "authorized_devices": "Dispositivi autorizzati", "automatic_endpoint_switching_subtitle": "Connetti localmente alla rete Wi-Fi specificata, se disponibile; altrimenti utilizza connessioni alternative", "automatic_endpoint_switching_title": "Cambio automatico di URL", @@ -591,15 +626,15 @@ "backup_album_selection_page_select_albums": "Seleziona gli album", "backup_album_selection_page_selection_info": "Informazioni sulla selezione", "backup_album_selection_page_total_assets": "Numero totale delle risorse", - "backup_albums_sync": "Sincronizzazione album di backup", + "backup_albums_sync": "Sincronizzazione Album di Backup", "backup_all": "Tutti", - "backup_background_service_backup_failed_message": "È stato impossibile fare il backup dei contenuti. Riprovoâ€Ļ", + "backup_background_service_backup_failed_message": "Impossibile effettuare il backup delle risorse. Riprovoâ€Ļ", "backup_background_service_complete_notification": "Backup completato", "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â€Ļ", + "backup_background_service_default_notification": "Ricerca di nuove risorseâ€Ļ", "backup_background_service_error_title": "Errore di backup", - "backup_background_service_in_progress_notification": "Backup dei tuoi contenutiâ€Ļ", + "backup_background_service_in_progress_notification": "Backup delle tue risorseâ€Ļ", "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.", @@ -611,8 +646,8 @@ "backup_controller_page_background_battery_info_title": "Ottimizzazioni batteria", "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 nuovi contenuti senza la necessità di aprire l'app", + "backup_controller_page_background_delay": "Ritarda il backup delle nuove risorse: {duration}", + "backup_controller_page_background_description": "Abilita il servizio in background per effettuare il backup delle nuove risorse 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", @@ -664,15 +699,15 @@ "bugs_and_feature_requests": "Bug & Richieste di nuove funzionalità", "build": "Compilazione", "build_image": "Immagine Compilata", - "bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset piÚ pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!", - "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", - "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# asset duplicato} other {# assets duplicati}}? Questa operazione manterrà l'asset piÚ pesante di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", + "bulk_delete_duplicates_confirmation": "Sei sicuro di voler cancellare {count, plural, one {# risorsa duplicata} other {# risorse duplicate}}? Questa operazione manterrà la risorsa piÚ grande di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati. Non puoi annullare questa operazione!", + "bulk_keep_duplicates_confirmation": "Sei sicuro di voler tenere {count, plural, one {# risorsa duplicata} other {# risorse duplicate}}? Questa operazione risolverà tutti i gruppi duplicati senza cancellare nulla.", + "bulk_trash_duplicates_confirmation": "Sei davvero sicuro di voler cancellare {count, plural, one {# risorsa duplicata} other {# risorse duplicate}}? Questa operazione manterrà la risorsa piÚ grande di ogni gruppo e cancellerà permanentemente tutti gli altri duplicati.", "buy": "Acquista Immich", "cache_settings_clear_cache_button": "Pulisci cache", "cache_settings_clear_cache_button_title": "Pulisce la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.", "cache_settings_duplicated_assets_clear_button": "PULISCI", "cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione", - "cache_settings_duplicated_assets_title": "Elementi duplicati ({count})", + "cache_settings_duplicated_assets_title": "Risorse duplicate ({count})", "cache_settings_statistics_album": "Anteprime librerie", "cache_settings_statistics_full": "Immagini complete", "cache_settings_statistics_shared": "Anteprime album condivisi", @@ -711,17 +746,31 @@ "change_password_form_password_mismatch": "Le password non coincidono", "change_password_form_reenter_new_password": "Inserisci ancora la nuova password", "change_pin_code": "Cambia il codice PIN", + "change_trigger": "Cambia il trigger", + "change_trigger_prompt": "Sei sicuro di voler cambiare il trigger? Questo rimuoverà tutte le esistenti azioni e filtri.", "change_your_password": "Modifica la tua password", "changed_visibility_successfully": "Visibilità modificata con successo", "charging": "In carica", "charging_requirement_mobile_backup": "Il backup in background richiede che il dispositivo sia in carica", - "check_corrupt_asset_backup": "Verifica la presenza di backup di asset corrotti", + "check_corrupt_asset_backup": "Verifica la presenza di backup di risorse corrotte", "check_corrupt_asset_backup_button": "Effettua controllo", - "check_corrupt_asset_backup_description": "Effettua questo controllo solo sotto rete Wi-Fi e quando tutti gli asset sono stati sottoposti a backup. La procedura potrebbe impiegare qualche minuto.", + "check_corrupt_asset_backup_description": "Effettua questo controllo solo su rete Wi-Fi e solo quando tutte le risorse saranno state sottoposte a backup. La procedura potrebbe impiegare qualche minuto.", "check_logs": "Controlla i log", "checksum": "Checksum", "choose_matching_people_to_merge": "Scegli persone combacianti da unire", "city": "Città", + "cleanup_confirm_description": "Immich ha trovato {count} risorse (create prima del {date}) e già salvate sul server. Rimuovo le copie locali da questo dispositivo?", + "cleanup_confirm_prompt_title": "Rimuovo da questo dispositivo?", + "cleanup_deleted_assets": "Spostate {count} risorse nel cestino", + "cleanup_deleting": "Spostamento nel cestino...", + "cleanup_found_assets": "Trovate {count} risorse già salvate", + "cleanup_found_assets_with_size": "Trovate {count} risorse salvate ({size})", + "cleanup_icloud_shared_albums_excluded": "Gli Album Condivisi di iCloud sono esclusi dalla ricerca", + "cleanup_no_assets_found": "Nessuna risorsa trovata con i criteri specificati. Libera Spazio puÃ˛ solo rimuovere le risorse che sono state salvate sul server", + "cleanup_preview_title": "Risorse da rimuovere ({count})", + "cleanup_step3_description": "Ricerca risorse già salvate sul server corrispondenti alle opzioni di ricerca.", + "cleanup_step4_summary": "{count} risorse (create prima del {date}) da rimuovere sul tuo dispositivo. Rimarrano comunque accessibili dall'app Immich.", + "cleanup_trash_hint": "Per recuperare completamente lo spazio devi aprire l'app della galleria e svuotarne il cestino", "clear": "Pulisci", "clear_all": "Pulisci tutto", "clear_all_recent_searches": "Rimuovi tutte le ricerche recenti", @@ -733,6 +782,8 @@ "client_cert_import": "Importa", "client_cert_import_success_msg": "Certificato client importato", "client_cert_invalid_msg": "File certificato invalido o password errata", + "client_cert_password_message": "Inserisci la password per questo certificato", + "client_cert_password_title": "Password del certificato", "client_cert_remove_msg": "Certificato client rimosso", "client_cert_subtitle": "Supporta solo il formato PKCS12 (.p12, .pfx). L'importazione/rimozione del certificato è disponibile solo prima del login", "client_cert_title": "Certificato Client SSL [SPERIMENTALE]", @@ -743,6 +794,11 @@ "color": "Colore", "color_theme": "Colore Tema", "command": "Comando", + "command_palette_prompt": "Trova rapidamente pagine, azioni o comandi", + "command_palette_to_close": "per chiudere", + "command_palette_to_navigate": "per entrare", + "command_palette_to_select": "per selezionare", + "command_palette_to_show_all": "per mostrare tutto", "comment_deleted": "Commento eliminato", "comment_options": "Opzioni per i commenti", "comments_and_likes": "Commenti & mi piace", @@ -751,9 +807,9 @@ "completed": "Completato", "confirm": "Conferma", "confirm_admin_password": "Conferma password dell'amministratore", - "confirm_delete_face": "Sei sicuro di voler cancellare il volto di {name} dall'asset?", + "confirm_delete_face": "Sei sicuro di voler cancellare il volto di {name} dalla risorsa?", "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?", - "confirm_keep_this_delete_others": "Tutti gli altri asset nello stack saranno eliminati, eccetto questo asset. Sei sicuro di voler continuare?", + "confirm_keep_this_delete_others": "Tutti le altre risorse nello stack saranno eliminate, eccetto questa. Sei sicuro di voler continuare?", "confirm_new_pin_code": "Conferma il nuovo codice PIN", "confirm_password": "Conferma password", "confirm_tag_face": "Vuoi taggare questo volto come {name}?", @@ -787,31 +843,40 @@ "create_album": "Crea album", "create_album_page_untitled": "Senza titolo", "create_api_key": "Crea chiave API", + "create_first_workflow": "Crea il primo workflow", "create_library": "Crea libreria", "create_link": "Crea link", "create_link_to_share": "Crea link da condividere", "create_link_to_share_description": "Permetti a chiunque con il link di vedere le foto selezionate", "create_new": "CREA NUOVO", "create_new_person": "Crea nuova persona", - "create_new_person_hint": "Assegna gli asset selezionati a una nuova persona", + "create_new_person_hint": "Assegna le risorse selezionate a una nuova persona", "create_new_user": "Crea nuovo utente", - "create_shared_album_page_share_add_assets": "AGGIUNGI OGGETTI", + "create_shared_album_page_share_add_assets": "AGGIUNGI RISORSE", "create_shared_album_page_share_select_photos": "Seleziona foto", "create_shared_link": "Crea link condiviso", "create_tag": "Crea tag", "create_tag_description": "Crea un nuovo tag. Per i tag nidificati, inserisci il percorso completo del tag includendo le barre oblique (/).", "create_user": "Crea utente", + "create_workflow": "Crea il workflow", "created": "Creato", "created_at": "Creato il", "creating_linked_albums": "Creazione di album collegati...", "crop": "Ritaglia", + "crop_aspect_ratio_fixed": "Fisso", + "crop_aspect_ratio_free": "Libero", + "crop_aspect_ratio_original": "Originale", "curated_object_page_title": "Oggetti", "current_device": "Dispositivo attuale", "current_pin_code": "Attuale codice PIN", "current_server_address": "Indirizzo del server in uso", + "custom_date": "Data specifica", "custom_locale": "Localizzazione personalizzata", "custom_locale_description": "Formatta data e numeri in base alla lingua e al paese", "custom_url": "URL personalizzato", + "cutoff_date_description": "Mantieni le foto fino alâ€Ļ", + "cutoff_day": "{count, plural, one {giorno} other {giorni}}", + "cutoff_year": "{count, plural, one {anno} other {anni}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Scuro", @@ -829,11 +894,11 @@ "deduplication_criteria_1": "Dimensione immagine in bytes", "deduplication_criteria_2": "Numero di dati EXIF", "deduplication_info": "Informazioni di deduplicazione", - "deduplication_info_description": "Per preselezionare automaticamente gli asset e rimuovere i duplicati in massa, verifichiamo:", + "deduplication_info_description": "Per preselezionare automaticamente le risorse e rimuovere i duplicati in massa, verifichiamo:", "default_locale": "Localizzazione preimpostata", "default_locale_description": "Formatta la data e i numeri in base alle impostazioni del tuo browser", "delete": "Elimina", - "delete_action_confirmation_message": "Vuoi davvero eliminare questo asset? Questa azione sposterà l'asset nel cestino del server e ti chiederà se desideri eliminarla localmente", + "delete_action_confirmation_message": "Vuoi davvero eliminare questa risorsa? Questa azione sposterà la risorsa nel cestino del server e ti chiederà se desideri eliminarla dal dispositivo", "delete_action_prompt": "{count} elementi eliminati", "delete_album": "Elimina album", "delete_api_key_prompt": "Sei sicuro di voler eliminare questa chiave API?", @@ -860,13 +925,14 @@ "delete_tag_confirmation_prompt": "Sei sicuro di voler cancellare il tag {tagName}?", "delete_user": "Elimina utente", "deleted_shared_link": "Elimina link condiviso", - "deletes_missing_assets": "Cancella gli asset mancanti dal disco", + "deletes_missing_assets": "Cancella le risorse mancanti dal disco", "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", + "disable": "Disabilita", "disabled": "Disabilitato", "disallow_edits": "Blocca modifiche", "discord": "Discord", @@ -877,12 +943,12 @@ "display_options": "Impostazioni visualizzazione", "display_order": "Ordine di visualizzazione", "display_original_photos": "Visualizza foto originali", - "display_original_photos_setting_description": "Visualizza la foto originale anzichÊ le miniature quando l'asset originale è compatibile con il web. Questo potrebbe causare un ritardo nella visualizzazione delle foto.", + "display_original_photos_setting_description": "Visualizza la foto originale anzichÊ le miniature quando la risorsa originale è compatibile con il web. Questo potrebbe causare un ritardo nella visualizzazione delle foto.", "do_not_show_again": "Non mostrare piÚ questo messaggio", "documentation": "Documentazione", "done": "Fatto", "download": "Scarica", - "download_action_prompt": "Scaricando {count} elementi", + "download_action_prompt": "Sto scaricando {count} risorse", "download_canceled": "Download annullato", "download_complete": "Download completato", "download_enqueue": "Download in coda", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Video incorporati", "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato", "download_notfound": "Download non trovato", + "download_original": "Scarica l'originale", "download_paused": "Download in pausa", "download_settings": "Scarica", "download_settings_description": "Gestisci le impostazioni relative al download delle risorse", @@ -899,8 +966,9 @@ "download_sucess": "Download completato", "download_sucess_android": "I contenuti multimediali sono stati scaricati in DCIM/Immich", "download_waiting_to_retry": "In attesa di riprovare", - "downloading": "Scaricando", - "downloading_asset_filename": "Scaricando la risorsa {filename}", + "downloading": "Scaricamento", + "downloading_asset_filename": "Sto scaricando la risorsa {filename}", + "downloading_from_icloud": "Scaricamento da iCloud", "downloading_media": "Scaricamento file multimediali", "drop_files_to_upload": "Rilascia i file ovunque per caricarli", "duplicates": "Duplicati", @@ -929,11 +997,22 @@ "edit_tag": "Modifica tag", "edit_title": "Modifica Titolo", "edit_user": "Modifica utente", + "edit_workflow": "Edita il workflow", "editor": "Editor", "editor_close_without_save_prompt": "Le modifiche non verranno salvate", "editor_close_without_save_title": "Vuoi chiudere l'editor?", - "editor_crop_tool_h2_aspect_ratios": "Proporzioni", - "editor_crop_tool_h2_rotation": "Rotazione", + "editor_confirm_reset_all_changes": "Sicuro di voler resettare tutte le modifiche?", + "editor_discard_edits_confirm": "Ignora modifiche", + "editor_discard_edits_prompt": "Hai delle modifiche non salvate. Vuoi davvero eliminarle?", + "editor_discard_edits_title": "Ignorare le modifiche?", + "editor_edits_applied_error": "Impossibile applicare le modifiche", + "editor_edits_applied_success": "Modifiche applicate con successo", + "editor_flip_horizontal": "Capovolgi in orizzontale", + "editor_flip_vertical": "Capovolgi in verticale", + "editor_orientation": "Orientamento", + "editor_reset_all_changes": "Annulla modifiche", + "editor_rotate_left": "Ruota di 90° antiorario", + "editor_rotate_right": "Ruota di 90° orario", "email": "Email", "email_notifications": "Notifiche email", "empty_folder": "La cartella è vuota", @@ -950,45 +1029,48 @@ "enter_your_pin_code_subtitle": "Inserire il codice PIN per accedere alla cartella protetta", "error": "Errore", "error_change_sort_album": "Errore nel cambiare l'ordine di degli album", - "error_delete_face": "Errore nel cancellare la faccia dalla foto", + "error_delete_face": "Errore nella rimozione del volto dalla risorsa", "error_getting_places": "Errore durante il recupero dei luoghi", + "error_loading_albums": "Errore nel caricamento degli album", "error_loading_image": "Errore nel caricamento dell'immagine", "error_loading_partners": "Errore durante il caricamento dei partner: {error}", + "error_retrieving_asset_information": "Errore nel recuperare informazioni sull'elemento", "error_saving_image": "Errore: {error}", "error_tag_face_bounding_box": "Errore durante il tag del volto - impossibile ricavare le coordinate del riquadro", "error_title": "Errore - Qualcosa è andato storto", + "error_while_navigating": "Errore durante la navigazione verso l'elemento", "errors": { "cannot_navigate_next_asset": "Impossibile passare alla risorsa successiva", "cannot_navigate_previous_asset": "Impossibile passare alla risorsa precedente", "cant_apply_changes": "Impossibile applicare le modifiche", "cant_change_activity": "Impossibile {enabled, select, true {disabilitare} other {abilitare}} l'attività", - "cant_change_asset_favorite": "Impossibile cambiare il preferito per l'asset", - "cant_change_metadata_assets_count": "Impossibile cambiare i metadati di {count, plural, one {# asset} other {# assets}}", + "cant_change_asset_favorite": "Impossibile cambiare il preferito per la risorsa", + "cant_change_metadata_assets_count": "Impossibile cambiare i metadati di {count, plural, one {# risorsa} other {# risorse}}", "cant_get_faces": "Impossibile ottenere i volti", "cant_get_number_of_comments": "Impossibile ottenere il numero di commenti", "cant_search_people": "Impossibile cercare persone", "cant_search_places": "Impossibile cercare luoghi", - "error_adding_assets_to_album": "Errore aggiungendo le risorse all'album", + "error_adding_assets_to_album": "Errore nell'aggiunta di risorse all'album", "error_adding_users_to_album": "Errore aggiungendo gli utenti all'album", "error_deleting_shared_user": "Errore durante la cancellazione dell'utente condiviso", "error_downloading": "Errore scaricando {filename}", "error_hiding_buy_button": "Errore nel nascondere il pulsante di acquisto", - "error_removing_assets_from_album": "Errore rimuovendo le risorse dall'album, controlla la console per ulteriori dettagli", - "error_selecting_all_assets": "Errore selezionando tutte le risorse", + "error_removing_assets_from_album": "Errore nella rimozione di risorse dall'album, controlla la console per ulteriori dettagli", + "error_selecting_all_assets": "Errore nella selezione di tutte le risorse", "exclusion_pattern_already_exists": "Questo pattern di esclusione è già presente.", "failed_to_create_album": "Creazione dell'album non riuscita", "failed_to_create_shared_link": "Creazione del link condivisibile non riuscita", "failed_to_edit_shared_link": "Errore durante la modifica del link condivisibile", "failed_to_get_people": "Impossibile ottenere le persone", - "failed_to_keep_this_delete_others": "Impossibile conservare questa risorsa ed eliminare le altre risorse", + "failed_to_keep_this_delete_others": "Impossibile conservare questa risorsa ed eliminare le altre", "failed_to_load_asset": "Errore durante il caricamento della risorsa", "failed_to_load_assets": "Errore durante il caricamento delle risorse", "failed_to_load_notifications": "Errore nel caricamento delle notifiche", "failed_to_load_people": "Caricamento delle persone non riuscito", "failed_to_remove_product_key": "Rimozione del codice del prodotto fallita", "failed_to_reset_pin_code": "Impossibile reimpostare il codice PIN", - "failed_to_stack_assets": "Errore durante il raggruppamento degli assets", - "failed_to_unstack_assets": "Errore durante la separazione degli assets", + "failed_to_stack_assets": "Errore durante il raggruppamento delle risorse", + "failed_to_unstack_assets": "Errore durante la separazione delle risorse", "failed_to_update_notification_status": "Aggiornamento stato notifiche fallito", "incorrect_email_or_password": "Email o password non corretta", "library_folder_already_exists": "Questo path di importazione esiste già.", @@ -997,33 +1079,35 @@ "quota_higher_than_disk_size": "Hai impostato un limite piÚ alto della dimensione del disco", "something_went_wrong": "Qualcosa è andato storto", "unable_to_add_album_users": "Impossibile aggiungere utenti all'album", - "unable_to_add_assets_to_shared_link": "Impossibile aggiungere gli assets al link condiviso", + "unable_to_add_assets_to_shared_link": "Impossibile aggiungere le risorse al link condiviso", "unable_to_add_comment": "Impossibile aggiungere commento", "unable_to_add_exclusion_pattern": "Impossibile aggiungere pattern di esclusione", "unable_to_add_partners": "Impossibile aggiungere compagni", - "unable_to_add_remove_archive": "Impossibile {archived, select, true {rimuovere l'asset dall'archivio} other {aggiungere l'asset all'archivio}}", - "unable_to_add_remove_favorites": "Impossibile {favorite, select, true {rimuovere l'asset dai} other {aggiungere l'asset ai}} preferiti", + "unable_to_add_remove_archive": "Impossibile {archived, select, true {rimuovere la risorsa dall'archivio} other {aggiungere la risorsa all'archivio}}", + "unable_to_add_remove_favorites": "Impossibile {favorite, select, true {aggiungere la risorsa ai} other {rimuovere la risorsa dai}} preferiti", "unable_to_archive_unarchive": "Impossible {archived, select, true {archiviare} other {rimuovere dall'archivio}}", "unable_to_change_album_user_role": "Impossibile modificare il ruolo dell'utente nell'album", "unable_to_change_date": "Impossibile modificare la data", "unable_to_change_description": "Impossibile modificare la descrizione", - "unable_to_change_favorite": "Errore durante il cambio dello stato preferito dell'asset", + "unable_to_change_favorite": "Errore durante il cambio di stato preferito della risorsa", "unable_to_change_location": "Impossibile modificare posizione", "unable_to_change_password": "Impossibile modificare password", "unable_to_change_visibility": "Errore durante la modifica della visibilità per {count, plural, one {# persona} other {# persone}}", "unable_to_complete_oauth_login": "Errore durante l'accesso tramite OAuth", "unable_to_connect": "Impossibile connettersi", "unable_to_copy_to_clipboard": "Impossibile copiare negli appunti, assicurati di aver aperto la pagina in https", + "unable_to_create": "Impossibile create il workflow", "unable_to_create_admin_account": "Impossibile creare un account admin", "unable_to_create_api_key": "Impossibile creare una nuova chiave API", "unable_to_create_library": "Impossibile creare la libreria", "unable_to_create_user": "Impossibile creare utente", "unable_to_delete_album": "Impossibile cancellare album", - "unable_to_delete_asset": "Impossibile cancellare asset", - "unable_to_delete_assets": "Errore durante l'eliminazione degli asset", + "unable_to_delete_asset": "Impossibile cancellare la risorsa", + "unable_to_delete_assets": "Errore durante l'eliminazione delle risorse", "unable_to_delete_exclusion_pattern": "Impossibile cancellare pattern di esclusione", "unable_to_delete_shared_link": "Impossibile cancellare link condiviso", "unable_to_delete_user": "Impossibile cancellare utente", + "unable_to_delete_workflow": "Impossibile eleminare il workflow", "unable_to_download_files": "Impossibile scaricare i file", "unable_to_edit_exclusion_pattern": "Impossibile modificare pattern di esclusione", "unable_to_empty_trash": "Impossibile svuotare il cestino", @@ -1038,19 +1122,19 @@ "unable_to_log_out_device": "Impossibile eseguire il logout dal dispositivo", "unable_to_login_with_oauth": "Impossibile effettuare l'accesso tramite OAuth", "unable_to_play_video": "Impossibile riprodurre il video", - "unable_to_reassign_assets_existing_person": "Errore durante la riassegnazione degli assets a {name, select, null {una persona esistente} other {{name}}}", - "unable_to_reassign_assets_new_person": "Errore durante la riassegnazione degli assets ad una nuova persona", + "unable_to_reassign_assets_existing_person": "Errore durante la riassegnazione delle risorse a {name, select, null {una persona esistente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Errore durante la riassegnazione delle risorse ad una nuova persona", "unable_to_refresh_user": "Impossibile aggiornare l'utente", "unable_to_remove_album_users": "Impossibile rimuovere gli utenti dall'album", "unable_to_remove_api_key": "Impossibile rimuovere la chiave API", - "unable_to_remove_assets_from_shared_link": "Errore durante la rimozione degli assets da un link condiviso", + "unable_to_remove_assets_from_shared_link": "Errore durante la rimozione delle risorse dal link condiviso", "unable_to_remove_library": "Impossibile rimuovere libreria", "unable_to_remove_partner": "Impossibile rimuovere compagno", "unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_reset_password": "Impossibile reimpostare la password", "unable_to_reset_pin_code": "Impossibile resettare il codice PIN", "unable_to_resolve_duplicate": "Impossibile risolvere duplicato", - "unable_to_restore_assets": "Impossibile ripristinare gli asset", + "unable_to_restore_assets": "Impossibile ripristinare le risorse", "unable_to_restore_trash": "Impossibile ripristinare cestino", "unable_to_restore_user": "Impossibile ripristinare utente", "unable_to_save_album": "Impossibile salvare album", @@ -1063,8 +1147,9 @@ "unable_to_scan_library": "Impossibile analizzare la libreria", "unable_to_set_feature_photo": "Impossibile impostare la foto in evidenza", "unable_to_set_profile_picture": "Impossibile impostare la foto profilo", + "unable_to_set_rating": "Impossibile impostare il rating", "unable_to_submit_job": "Impossibile eseguire l'attività", - "unable_to_trash_asset": "Impossibile cestinare l'asset", + "unable_to_trash_asset": "Impossibile cestinare la risorsa", "unable_to_unlink_account": "Impossibile scollegare l'account", "unable_to_unlink_motion_video": "Impossibile scollegare video in movimento", "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Impossibile aggiornare le impostazioni", "unable_to_update_timeline_display_status": "Impossibile aggiornare lo stato di visualizzazione della sequenza temporale", "unable_to_update_user": "Impossibile aggiornare l'utente", + "unable_to_update_workflow": "Impossibile aggiornare il workflow", "unable_to_upload_file": "Impossibile caricare il file" }, + "errors_text": "Errori", "exclusion_pattern": "Pattern di esclusione", "exif": "Exif", "exif_bottom_sheet_description": "Aggiungi una descrizione...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "PERSONE", "exif_bottom_sheet_person_add_person": "Aggiungi nome", "exit_slideshow": "Esci dalla presentazione", + "expand": "Espandi", "expand_all": "Espandi tutto", "experimental_settings_new_asset_list_subtitle": "Lavori in corso", "experimental_settings_new_asset_list_title": "Attiva griglia foto sperimentale", @@ -1109,7 +1197,7 @@ "failed": "Fallito", "failed_count": "Falliti: {count}", "failed_to_authenticate": "Autenticazione non riuscita", - "failed_to_load_assets": "Impossibile caricare gli asset", + "failed_to_load_assets": "Impossibile caricare le risorse", "failed_to_load_folder": "Impossibile caricare la cartella", "favorite": "Preferito", "favorite_action_prompt": "{count} elementi aggiunti ai preferiti", @@ -1120,14 +1208,17 @@ "features": "Funzionalità", "features_in_development": "Funzionalità in fase di sviluppo", "features_setting_description": "Gestisci le funzionalità dell'app", - "file_name": "Nome file", "file_name_or_extension": "Nome file o estensione", + "file_name_text": "Nome del file", + "file_name_with_value": "Nome del file: {file_name}", "file_size": "Dimensione del file", "filename": "Nome file", "filetype": "Tipo file", "filter": "Filtro", + "filter_description": "Condizioni per filtrare le risorse obiettivo", "filter_people": "Filtra persone", "filter_places": "Filtra luoghi", + "filters": "Filtri", "find_them_fast": "Trovale velocemente con la ricerca", "first": "Primo", "fix_incorrect_match": "Correggi corrispondenza errata", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Navigare la visualizzazione a cartelle per le foto e i video sul file system", "forgot_pin_code_question": "Hai dimenticato il tuo PIN?", "forward": "Avanti", + "free_up_space": "Libera Spazio", + "free_up_space_description": "Sposta le foto e i video del tuo dispositivo nel cestino per liberare spazio. Le copie sul server rimarranno al sicuro.", + "free_up_space_settings_subtitle": "Libera spazio sul dispositivo", "full_path": "Percorso completo: {path}", "gcast_enabled": "Google Cast Abilitato", "gcast_enabled_description": "Questa funzione carica risorse esterne da Google per poter funzionare.", "general": "Generale", "geolocation_instruction_location": "Fai clic su una risorsa con coordinate GPS per utilizzare la sua posizione oppure seleziona una posizione direttamente dalla mappa", "get_help": "Chiedi Aiuto", + "get_people_error": "Errore nel ritrovare le persone", "get_wifiname_error": "Non sono riuscito a recuperare il nome della rete Wi-Fi. Accertati di aver concesso i permessi necessari e di essere connesso ad una rete Wi-Fi", "getting_started": "Iniziamo", "go_back": "Torna indietro", @@ -1160,8 +1255,8 @@ "haptic_feedback_switch": "Abilita feedback aptico", "haptic_feedback_title": "Feedback aptico", "has_quota": "Ha limite", - "hash_asset": "Risorsa hash", - "hashed_assets": "Risorse hash", + "hash_asset": "Hash risorsa", + "hashed_assets": "Hash risorse", "hashing": "Hashing", "header_settings_add_header_tip": "Aggiungi header", "header_settings_field_validator_msg": "Il valore non puÃ˛ essere vuoto", @@ -1175,24 +1270,25 @@ "hide_named_person": "Nascondi {name}", "hide_password": "Nascondi password", "hide_person": "Nascondi persona", + "hide_schema": "Nascondi schema", "hide_text_recognition": "Nascondi riconoscimento del testo", "hide_unnamed_people": "Nascondi persone senza nome", - "home_page_add_to_album_conflicts": "Aggiunti {added} elementi all'album {album}. {failed} elementi erano già presenti nell'album.", - "home_page_add_to_album_err_local": "Non puoi aggiungere in album risorse non ancora caricate, azione ignorata", - "home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}.", - "home_page_album_err_partner": "Non puoi aggiungere risorse del partner a un album, azione ignorata", - "home_page_archive_err_local": "Non puoi archiviare immagini non ancora caricate, azione ignorata", + "home_page_add_to_album_conflicts": "Aggiunte {added} risorse all'album {album}. {failed} risorse erano già presenti nell'album.", + "home_page_add_to_album_err_local": "Non puoi aggiungere all'album risorse non ancora caricate, azione ignorata", + "home_page_add_to_album_success": "Aggiunte {added} risorse all'album {album}.", + "home_page_album_err_partner": "Non puoi ancora aggiungere risorse del partner a un album, azione ignorata", + "home_page_archive_err_local": "Non puoi archiviare risorse non ancora caricate, azione ignorata", "home_page_archive_err_partner": "Non puoi archiviare risorse del partner, azione ignorata", "home_page_building_timeline": "Caricamento della timeline", "home_page_delete_err_partner": "Non puoi eliminare risorse del partner, azione ignorata", "home_page_delete_remote_err_local": "Risorse locali presenti nella selezione della eliminazione remota, azione ignorata", - "home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti delle risorse non ancora caricate, azione ignorata", - "home_page_favorite_err_partner": "Non puoi mettere le risorse del partner nei preferiti, azione ignorata", + "home_page_favorite_err_local": "Non puoi aggiungere ai preferiti le risorse non ancora caricate, azione ignorata", + "home_page_favorite_err_partner": "Non puoi aggiungere le risorse del partner ai preferiti, azione ignorata", "home_page_first_time_notice": "Se è la prima volta che utilizzi l'app, assicurati di scegliere uno o piÚ album di backup, in modo che la timeline possa popolare le foto e i video presenti negli album", - "home_page_locked_error_local": "Non puoi spostare la risorsa locale nella cartella privata, azione ignorata", + "home_page_locked_error_local": "Non puoi spostare le risorse locali nella cartella privata, azione ignorata", "home_page_locked_error_partner": "Non puoi spostare le risorse del partner nella cartella privata, azione ignorata", "home_page_share_err_local": "Non puoi condividere una risorsa locale tramite link, azione ignorata", - "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso", + "home_page_upload_err_limit": "Puoi caricare al massimo 30 risorse per volta, azione ignorata", "host": "Host", "hour": "Ora", "hours": "Ore", @@ -1225,7 +1321,7 @@ "in_year_selector": "Nel", "include_archived": "Includi Archiviati", "include_shared_albums": "Includi album condivisi", - "include_shared_partner_assets": "Includi elementi condivisi dai compagni", + "include_shared_partner_assets": "Includi risorse condivise dai compagni", "individual_share": "Condivisione individuale", "individual_shares": "Condivisioni individuali", "info": "Info", @@ -1247,10 +1343,19 @@ "ios_debug_info_processing_ran_at": "Processo eseguito {dateTime}", "items_count": "{count, plural, one {# elemento} other {# elementi}}", "jobs": "Processi", + "json_editor": "Modificatore JSON", + "json_error": "JSON errore", "keep": "Mantieni", + "keep_albums": "Mantieni gli album", + "keep_albums_count": "{count} {count, plural, one {Album} other {Album}} mantenuti", "keep_all": "Tieni tutto", + "keep_description": "Scegli cosa rimane sul tuo dispositivo quando liberi spazio.", + "keep_favorites": "Mantieni i favoriti", + "keep_on_device": "Mantieni sul dispositivo", + "keep_on_device_hint": "Seleziona le risorse da mantenere sul dispositivo", "keep_this_delete_others": "Tieni questo, elimina gli altri", - "kept_this_deleted_others": "Mantenuto questo asset ed eliminati {count, plural, one {# asset} other {# assets}}", + "keeping": "Mantieni: {items}", + "kept_this_deleted_others": "Mantenuto questa risorsa ed {count, plural, one {eliminata # risorsa} other {eliminate # risorse}}", "keyboard_shortcuts": "Scorciatoie da tastiera", "language": "Lingua", "language_no_results_subtitle": "Prova a cambiare i tuoi termini di ricerca", @@ -1274,7 +1379,7 @@ "library_options": "Impostazioni Libreria", "library_page_device_albums": "Album sul dispositivo", "library_page_new_album": "Nuovo Album", - "library_page_sort_asset_count": "Numero di elementi", + "library_page_sort_asset_count": "Numero di risorse", "library_page_sort_created": "Data di creazione", "library_page_sort_last_modified": "Ultima modifica", "library_page_sort_title": "Titolo album", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Abilita per riprodurre automaticamente un video in loop nel visualizzatore dei dettagli.", "main_branch_warning": "Stai utilizzando una versione di sviluppo. Ti consigliamo vivamente di utilizzare una versione di rilascio!", "main_menu": "Menu Principale", + "maintenance_action_restore": "Ripristinando Database", "maintenance_description": "Immich è stato posto in modalità manutenzione.", "maintenance_end": "Termina modalità manutenzione", "maintenance_end_error": "Errore nel terminare la modalità manutenzione.", "maintenance_logged_in_as": "Accesso effettuato come {user}", + "maintenance_restore_from_backup": "Ripristina da Backup", + "maintenance_restore_library": "Ripristina la tua Libreria", + "maintenance_restore_library_confirm": "Se questo sembra corretto, procedi al ripristino del backup!", + "maintenance_restore_library_description": "Ripristinando Database", + "maintenance_restore_library_folder_has_files": "{folder} contiene {count} cartelle", + "maintenance_restore_library_folder_no_files": "File mancanti in {folder}!", + "maintenance_restore_library_folder_pass": "leggibile e scrivibile", + "maintenance_restore_library_folder_read_fail": "illeggibile", + "maintenance_restore_library_folder_write_fail": "non scrivibile", + "maintenance_restore_library_hint_missing_files": "Potrebbero mancarti file importanti", + "maintenance_restore_library_hint_regenerate_later": "Puoi rigenerarli piÚ tardi dalle impostazioni", + "maintenance_restore_library_hint_storage_template_missing_files": "Stai usando un modello di archiviazione? Potrebbero mancarti dei file", + "maintenance_restore_library_loading": "Caricamento controlli di integrità ed euristicheâ€Ļ", + "maintenance_task_backup": "Creando un backup del database esistenteâ€Ļ", + "maintenance_task_migrations": "Esecuzione delle migrazioni del databaseâ€Ļ", + "maintenance_task_restore": "Ripristinando il backup sceltoâ€Ļ", + "maintenance_task_rollback": "Ripristino fallito, tornando al punto di ripristinoâ€Ļ", "maintenance_title": "Temporaneamente non disponibile", "make": "Produttore", "manage_geolocation": "Gestisci posizione", @@ -1362,15 +1485,15 @@ "manage_your_devices": "Gestisci i tuoi dispositivi collegati", "manage_your_oauth_connection": "Gestisci la tua connessione OAuth", "map": "Mappa", - "map_assets_in_bounds": "{count, plural, =0 {Nessuna foto in quest’area} one {# foto} other {# foto}}", + "map_assets_in_bounds": "{count, plural, =0 {Nessuna risorsa in quest’area} one {# risorsa} other {# risorse}}", "map_cannot_get_user_location": "Non è possibile ottenere la posizione dell'utente", "map_location_dialog_yes": "Si", "map_location_picker_page_use_location": "Usa questa posizione", - "map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per visualizzare gli elementi per la tua posizione attuale. Vuoi attivarli adesso?", + "map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per poter visualizzare le risorse dalla tua posizione attuale. Vuoi attivarli adesso?", "map_location_service_disabled_title": "Servizio Localizzazione disattivato", "map_marker_for_images": "Indicatore mappa per le immagini scattate in {city}, {country}", "map_marker_with_image": "Segnaposto con immagine", - "map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare gli elementi per la tua posizione attuale. Vuoi consentirlo adesso?", + "map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare le risorse dalla tua posizione attuale. Vuoi consentirlo adesso?", "map_no_location_permission_title": "Autorizzazione Posizione negata", "map_settings": "Impostazioni Mappa", "map_settings_dark_mode": "Modalità scura", @@ -1388,7 +1511,7 @@ "mark_as_read": "Segna come letto", "marked_all_as_read": "Segnato tutto come letto", "matches": "Corrispondenze", - "matching_assets": "Assets Corrispondenti", + "matching_assets": "Risorse Corrispondenti", "media_type": "Tipo Media", "memories": "Ricordi", "memories_all_caught_up": "Tutto a posto", @@ -1408,6 +1531,8 @@ "minimize": "Minimizza", "minute": "Minuto", "minutes": "Minuti", + "mirror_horizontal": "Orizzontale", + "mirror_vertical": "Verticale", "missing": "Mancanti", "mobile_app": "App Cellulare", "mobile_app_download_onboarding_note": "Scarica l’app mobile dedicata utilizzando una delle seguenti opzioni", @@ -1416,13 +1541,16 @@ "monthly_title_text_date_format": "MMMM y", "more": "Di piÚ", "move": "Sposta", + "move_down": "Muovi in basso", "move_off_locked_folder": "Sposta al di fuori della cartella privata", "move_to": "Sposta in", + "move_to_device_trash": "Sposta nel cestino del dispositivo", "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", - "moved_to_library": "Spostati {count, plural, one {# asset} other {# assets}} nella libreria", + "move_up": "Muovi in alto", + "moved_to_archive": "{count, plural, one {Spostata # risorsa} other {Spostate # risorse}} nell'archivio", + "moved_to_library": "{count, plural, one {Spostata # risorsa} other {Spostate # risorse}} nella libreria", "moved_to_trash": "Spostato nel cestino", "multiselect_grid_edit_date_time_err_read_only": "Non puoi modificare la data di risorse in sola lettura, azione ignorata", "multiselect_grid_edit_gps_err_read_only": "Non puoi modificare la posizione di risorse in sola lettura, azione ignorata", @@ -1430,6 +1558,7 @@ "my_albums": "I miei album", "name": "Nome", "name_or_nickname": "Nome o soprannome", + "name_required": "Nome è richiesto", "navigate": "Naviga", "navigate_to_time": "Navigazione alla data", "network_requirement_photos_upload": "Utilizza la connessione dati per il backup delle foto", @@ -1454,38 +1583,42 @@ "next": "Prossimo", "next_memory": "Prossima memoria", "no": "No", + "no_actions_added": "Nessuna azione è stata ancora aggiunta", + "no_albums_found": "Nessun album trovato", "no_albums_message": "Crea un album per organizzare le tue foto ed i tuoi video", "no_albums_with_name_yet": "Sembra che tu non abbia ancora nessun album con questo nome.", "no_albums_yet": "Sembra che tu non abbia ancora nessun album.", - "no_archived_assets_message": "Archivia foto e video per nasconderli dalla galleria di foto", - "no_assets_message": "CLICCA PER CARICARE LA TUA PRIMA FOTO", + "no_archived_assets_message": "Archivia foto e video per nasconderli dalla visualizzazione galleria", + "no_assets_message": "Clicca per caricare la tua prima foto", "no_assets_to_show": "Nessuna risorsa da mostrare", "no_cast_devices_found": "Nessun dispositivo di trasmissione trovato", - "no_checksum_local": "Nessun checksum disponibile: impossibile recuperare gli assets locali", - "no_checksum_remote": "Nessun checksum disponibile: impossibile recuperare l'asset remoto", + "no_checksum_local": "Nessun checksum disponibile: impossibile recuperare le risorse locali", + "no_checksum_remote": "Nessun checksum disponibile: impossibile recuperare la risorsa remota", + "no_configuration_needed": "Nessuna configurazione è necessaria", "no_devices": "Nessun device autorizzato", "no_duplicates_found": "Nessun duplicato trovato.", "no_exif_info_available": "Nessuna informazione exif disponibile", "no_explore_results_message": "Carica piÚ foto per esplorare la tua collezione.", "no_favorites_message": "Aggiungi preferiti per trovare facilmente le tue migliori foto e video", + "no_filters_added": "Nessun filtro ancora aggiunto", "no_libraries_message": "Crea una libreria esterna per vedere le tue foto e i tuoi video", - "no_local_assets_found": "Nessun asset locale trovato con questo checksum", + "no_local_assets_found": "Nessuna risorsa locale trovata con questo checksum", "no_location_set": "Nessuna posizione impostata", "no_locked_photos_message": "Le foto e i video nella cartella privata sono nascosti e non vengono visualizzati mentre navighi o cerchi nella tua libreria.", "no_name": "Nessun nome", "no_notifications": "Nessuna notifica", "no_people_found": "Nessuna persona trovata", "no_places": "Nessun posto", - "no_remote_assets_found": "Nessun asset remoto trovato con questo checksum", + "no_remote_assets_found": "Nessuna risorsa remota trovata con questo checksum", "no_results": "Nessun risultato", "no_results_description": "Prova ad usare un sinonimo oppure una parola chiave piÚ generica", "no_shared_albums_message": "Crea un album per condividere foto e video con le persone nella tua rete", "no_uploads_in_progress": "Nessun upload in corso", + "none": "Nulla", "not_allowed": "Non permesso", "not_available": "N/A", "not_in_any_album": "In nessun album", "not_selected": "Non selezionato", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Per aggiungere l'etichetta dell'archiviazione agli asset caricati in precedenza, esegui", "notes": "Note", "nothing_here_yet": "Ancora nulla qui", "notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi.", @@ -1515,6 +1648,7 @@ "online": "Online", "only_favorites": "Solo preferiti", "open": "Apri", + "open_calendar": "Apri il calendario", "open_in_map_view": "Apri nella visualizzazione mappa", "open_in_openstreetmap": "Apri su OpenStreetMap", "open_the_search_filters": "Apri filtri di ricerca", @@ -1563,14 +1697,15 @@ "people": "Persone", "people_edits_count": "{count, plural, one {Modificata # persona} other {Modificate # persone}}", "people_feature_description": "Navigare foto e video raggruppati da persone", + "people_selected": "{count, plural, one {# persona selezionata} other {# persone selezionate}}", "people_sidebar_description": "Mostra un link alle persone nella barra laterale", "permanent_deletion_warning": "Avviso eliminazione permanente", - "permanent_deletion_warning_setting_description": "Mostra un avviso all'eliminazione definitiva di un asset", + "permanent_deletion_warning_setting_description": "Mostra un avviso all'eliminazione definitiva di una risorsa", "permanently_delete": "Elimina definitivamente", - "permanently_delete_assets_count": "Cancella definitivamente {count, plural, one {l'asset} other {gli assets}}", - "permanently_delete_assets_prompt": "Sei sicuro di voler cancellare definitivamente {count, plural, one {questo asset?} other {# assets?}} Questa operazione {count, plural, one {lo cancellerà dal suo} other {li cancellerà dai loro}} album.", - "permanently_deleted_asset": "Asset eliminato definitivamente", - "permanently_deleted_assets_count": "Cancellati {count, plural, one {# asset} other {# assets}} definitivamente", + "permanently_delete_assets_count": "Cancella definitivamente {count, plural, one {la risorsa} other {le risorse}}", + "permanently_delete_assets_prompt": "Sei sicuro di voler cancellare definitivamente {count, plural, one {questa risorsa?} other {# risorse?}} Questa operazione {count, plural, one {la cancellerà dal suo} other {le cancellerà dai loro}} album.", + "permanently_deleted_asset": "Risorsa eliminata definitivamente", + "permanently_deleted_assets_count": "{count, plural, one {Cancellata # risorsa} other {Cancellate # risorse}} definitivamente", "permission": "Autorizzazione", "permission_empty": "La tua autorizzazione non puÃ˛ essere vuota", "permission_onboarding_back": "Indietro", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, one {# anno} other {# anni}}", "person_birthdate": "Nato il {date}", "person_hidden": "{name}{hidden, select, true { (nascosto)} other {}}", + "person_recognized": "Persona riconosciuta", + "person_selected": "Persona selezionata", "photo_shared_all_users": "Sembra che tu abbia condiviso le tue foto con tutti gli utenti, oppure che tu non abbia alcun utente con cui condividerle.", "photos": "Foto", "photos_and_videos": "Foto & Video", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foto}}", "photos_from_previous_years": "Foto dagli anni scorsi", + "photos_only": "Solo foto", "pick_a_location": "Scegli una posizione", "pick_custom_range": "Intervallo personalizzato", "pick_date_range": "Seleziona un periodo temporale", @@ -1665,23 +1803,25 @@ "purchase_server_description_2": "Stato di Contributore", "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", - "query_asset_id": "Esegui una query sull'ID dell'asset", + "query_asset_id": "Esegui una query sull'ID della risorsa", "queue_status": "Messi in coda {count}/{total}", + "rate_asset": "Valuta la risorsa", "rating": "Valutazione a stelle", - "rating_clear": "Crea valutazione", + "rating_clear": "Azzera valutazione", "rating_count": "{count, plural, one {# stella} other {# stelle}}", "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni", + "rating_set": "Valutazione impostata a {rating, plural, one {# stella} other {# stelle}}", "reaction_options": "Impostazioni Reazioni", "read_changelog": "Leggi Riepilogo Modifiche", "readonly_mode_disabled": "Modalità di sola lettura disabilitata", "readonly_mode_enabled": "Modalità di sola lettura abilitata", "ready_for_upload": "Pronto per il caricamento", "reassign": "Riassegna", - "reassigned_assets_to_existing_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} {name, select, null {ad una persona esistente} other {a {name}}}", - "reassigned_assets_to_new_person": "{count, plural, one {Riassegnato # asset} other {Riassegnati # assets}} ad una nuova persona", - "reassing_hint": "Assegna gli assets selezionati ad una persona esistente", + "reassigned_assets_to_existing_person": "{count, plural, one {Riassegnata # risorsa} other {Riassegnate # risorse}} {name, select, null {ad una persona esistente} other {a {name}}}", + "reassigned_assets_to_new_person": "{count, plural, one {Riassegnata # risorsa} other {Riassegnate # risorse}} ad una nuova persona", + "reassing_hint": "Assegna le risorse selezionate ad una persona esistente", "recent": "Recenti", - "recent-albums": "Album recenti", + "recent_albums": "Album recenti", "recent_searches": "Ricerche recenti", "recently_added": "Aggiunti recentemente", "recently_added_page_title": "Aggiunti di recente", @@ -1702,11 +1842,11 @@ "remote_assets": "Risorse remote", "remote_media_summary": "Riepilogo dei Media Remoti", "remove": "Rimuovi", - "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} dall'album?", - "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", - "remove_assets_title": "Rimuovere asset?", + "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# risorsa} other {# risorse}} dall'album?", + "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# risorsa} other {# risorse}} da questo link condiviso?", + "remove_assets_title": "Rimuovo le risorse?", "remove_custom_date_range": "Rimuovi intervallo data personalizzato", - "remove_deleted_assets": "Rimuovi file offline", + "remove_deleted_assets": "Rimuovi le Risorse cancellate", "remove_from_album": "Rimuovere dall'album", "remove_from_album_action_prompt": "{count} elementi rimossi dall'album", "remove_from_favorites": "Rimuovi dai preferiti", @@ -1725,7 +1865,7 @@ "removed_from_favorites_count": "{count, plural, one {Rimosso } other {Rimossi #}} dai preferiti", "removed_memory": "Memoria rimossa", "removed_photo_from_memory": "Foto rimossa dalla memoria", - "removed_tagged_assets": "Rimossa etichetta {count, plural, one {# dall'asset} other {# dagli asset}}", + "removed_tagged_assets": "Rimossa etichetta {count, plural, one {# dalla risorsa} other {# dalle risorse}}", "rename": "Rinomina", "repair": "Ripara", "repair_no_results_message": "I file mancanti e non tracciati saranno mostrati qui", @@ -1752,7 +1892,7 @@ "restore_all": "Ripristina tutto", "restore_trash_action_prompt": "{count} ripristinati dal cestino", "restore_user": "Ripristina utente", - "restored_asset": "Asset ripristinato", + "restored_asset": "Risorsa ripristinata", "resume": "Riprendi", "resume_paused_jobs": "Riprendi {count, plural, one {# processo in pausa} other {# i processi in pausa}}", "retry_upload": "Riprova caricamento", @@ -1770,9 +1910,11 @@ "saved_settings": "Impostazioni salvate", "say_something": "Dici qualcosa", "scaffold_body_error_occurred": "Si è verificato un errore", + "scan": "Scansione", "scan_all_libraries": "Analizza tutte le librerie", "scan_library": "Scansione", "scan_settings": "Impostazioni Analisi", + "scanning": "Scansione in corso", "scanning_for_album": "Sto cercando l'album...", "search": "Cerca", "search_albums": "Cerca album", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Seleziona il tipo di media", "search_filter_ocr": "Cerca tramite OCR", "search_filter_people_title": "Seleziona persone", + "search_filter_star_rating": "Voto in Stelle", "search_for": "Cerca per", "search_for_existing_person": "Cerca per persona esistente", "search_no_more_result": "Non ci sono altri risultati", @@ -1836,17 +1979,23 @@ "second": "Secondo", "see_all_people": "Vedi tutte le persone", "select": "Seleziona", + "select_album": "Seleziona album", "select_album_cover": "Seleziona copertina album", + "select_albums": "Seleziona gli album", "select_all": "Seleziona tutto", "select_all_duplicates": "Seleziona tutti i duplicati", "select_all_in": "Seleziona tutto in {group}", "select_avatar_color": "Seleziona colore avatar", + "select_count": "{count, plural, one {Seleziona #} other {Seleziona #}}", + "select_cutoff_date": "Seleziona la data limite", "select_face": "Seleziona volto", "select_featured_photo": "Seleziona foto in evidenza", "select_from_computer": "Seleziona dal computer", "select_keep_all": "Seleziona mantieni tutto", "select_library_owner": "Seleziona proprietario libreria", "select_new_face": "Seleziona nuovo volto", + "select_people": "Seleziona persone", + "select_person": "Seleziona una persona", "select_person_to_tag": "Seleziona una persona da taggare", "select_photos": "Seleziona foto", "select_trash_all": "Seleziona cestina tutto", @@ -1903,8 +2052,8 @@ "settings_require_restart": "Si prega di riavviare Immich perchÊ vengano applicate le impostazioni", "settings_saved": "Impostazioni salvate", "setup_pin_code": "Configura un codice PIN", - "share": "Condivisione", - "share_action_prompt": "Condivisi {count} elementi", + "share": "Condividi", + "share_action_prompt": "Condivisi {count} risorse", "share_add_photos": "Aggiungi foto", "share_assets_selected": "{count} selezionati", "share_dialog_preparing": "Preparoâ€Ļ", @@ -1955,7 +2104,7 @@ "shared_link_password_description": "Imposta una password per questo link condiviso", "shared_links": "Link condivisi", "shared_links_description": "Condividi foto e video con un link", - "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video condivisi.}}", + "shared_photos_and_videos_count": "{assetCount, plural, other {# foto e video condivisi.}}", "shared_with_me": "Condivisi con me", "shared_with_partner": "Condiviso con {partner}", "sharing": "Condivisione", @@ -1966,7 +2115,7 @@ "sharing_sidebar_description": "Mostra un link a Condivisione nella barra laterale", "sharing_silver_appbar_create_shared_album": "Crea album condiviso", "sharing_silver_appbar_share_partner": "Condividi con partner", - "shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset", + "shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente la risorsa", "show_album_options": "Mostra opzioni album", "show_albums": "Mostra gli album", "show_all_people": "Mostra tutte le persone", @@ -1982,6 +2131,7 @@ "show_password": "Mostra password", "show_person_options": "Mostra opzioni persona", "show_progress_bar": "Mostra Barra Avanzamento", + "show_schema": "Mostra lo schema", "show_search_options": "Mostra impostazioni di ricerca", "show_shared_links": "Mostra link condivisi", "show_slideshow_transition": "Mostra la transizione della presentazione", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Salta alle cartelle", "skip_to_tags": "Salta alle etichette", "slideshow": "Presentazione", + "slideshow_repeat": "Ripeti presentazione", + "slideshow_repeat_description": "Ricomincia da capo quando la presentazione termina", "slideshow_settings": "Impostazioni presentazione", "sort_albums_by": "Ordina album per...", "sort_created": "Data creazione", @@ -2015,7 +2167,7 @@ "stack_duplicates": "Raggruppa i duplicati", "stack_select_one_photo": "Seleziona una foto principale per il gruppo", "stack_selected_photos": "Raggruppa foto selezionate", - "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # asset}}", + "stacked_assets_count": "{count, plural, one {Raggruppata # risorsa} other {Raggruppate # risorse}}", "stacktrace": "Traccia dell'errore", "start": "Avvia", "start_date": "Data di inizio", @@ -2038,6 +2190,7 @@ "support": "Supporto", "support_and_feedback": "Supporto & Feedback", "support_third_party_description": "La tua installazione di Immich è stata costruita da terze parti. I problemi che riscontri potrebbero essere causati da altri pacchetti, quindi ti preghiamo di sollevare il problema in prima istanza utilizzando i link sottostanti.", + "supporter": "Sostenitore", "swap_merge_direction": "Scambia direzione di unione", "sync": "Sincronizza", "sync_albums": "Sincronizza album", @@ -2054,7 +2207,7 @@ "tag_not_found_question": "Non riesci a trovare un tag? Creane uno nuovo.", "tag_people": "Tagga persone", "tag_updated": "Tag {tag} aggiornata", - "tagged_assets": "{count, plural, one {# asset etichettato} other {# asset etichettati}}", + "tagged_assets": "{count, plural, one {# risorsa etichettata} other {# risorse etichettate}}", "tags": "Tag", "tap_to_run_job": "Tocca per eseguire l'attività", "template": "Modello", @@ -2062,8 +2215,8 @@ "theme": "Tema", "theme_selection": "Selezione tema", "theme_selection_description": "Imposta automaticamente il tema chiaro o scuro in base all'impostazione del tuo browser", - "theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti", - "theme_setting_asset_list_tiles_per_row_title": "Numero di elementi per riga ({count})", + "theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli delle risorse", + "theme_setting_asset_list_tiles_per_row_title": "Numero di risorse per riga ({count})", "theme_setting_colorful_interface_subtitle": "Applica il colore primario alle superfici di sfondo.", "theme_setting_colorful_interface_title": "Interfaccia colorata", "theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Scegli un'impostazione per il tema dell'app", "theme_setting_three_stage_loading_subtitle": "Il caricamento a tre stage aumenterà le performance di caricamento ma anche il consumo di banda", "theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage", + "then": "Allora", "they_will_be_merged_together": "Verranno uniti insieme", "third_party_resources": "Risorse di Terze Parti", "time": "Orario", @@ -2098,17 +2252,24 @@ "trash_action_prompt": "{count} elementi spostati nel cestino", "trash_all": "Cestina Tutto", "trash_count": "Cancella {count, number}", - "trash_delete_asset": "Cestina/Cancella Asset", + "trash_delete_asset": "Cestina/Cancella Risorsa", "trash_emptied": "Cestino svuotato", "trash_no_results_message": "Le foto cestinate saranno mostrate qui.", "trash_page_delete_all": "Elimina tutti", - "trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich", + "trash_page_empty_trash_dialog_content": "Vuoi eliminare le risorse dal cestino? Saranno eliminate definitivamente da Immich", "trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {days} giorni", - "trash_page_no_assets": "Nessun elemento cestinato", + "trash_page_no_assets": "Nessuna risorsa cestinata", "trash_page_restore_all": "Ripristina tutto", - "trash_page_select_assets_btn": "Seleziona elemento", + "trash_page_select_assets_btn": "Seleziona risorse", "trash_page_title": "Cestino ({count})", "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.", + "trigger": "Evento di attivazione", + "trigger_asset_uploaded": "Risorsa Caricata", + "trigger_asset_uploaded_description": "Attivato quando una nuova risorsa viene caricata", + "trigger_description": "Un evento che attiva il flusso di lavoro", + "trigger_person_recognized": "Persona Riconosciuta", + "trigger_person_recognized_description": "Attivato quando è rilevata una persona", + "trigger_type": "Tipo di trigger", "troubleshoot": "Risoluzione dei problemi", "type": "Tipo", "unable_to_change_pin_code": "Impossibile cambiare il codice PIN", @@ -2123,6 +2284,7 @@ "unhide_person": "Mostra persona", "unknown": "Sconosciuto", "unknown_country": "Paese sconosciuto", + "unknown_date": "Data sconosciuta", "unknown_year": "Anno sconosciuto", "unlimited": "Illimitato", "unlink_motion_video": "Scollega video in movimento", @@ -2138,38 +2300,40 @@ "unselect_all_in": "Deseleziona tutto in {group}", "unstack": "Separa dal gruppo", "unstack_action_prompt": "{count} separati", - "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}", + "unstacked_assets_count": "{count, plural, one {Separata # risorsa} other {Separate # risorse}}", + "unsupported_field_type": "Tipo di campo non supportato", "untagged": "Senza tag", + "untitled_workflow": "Flusso di lavoro senza titolo", "up_next": "Prossimo", "update_location_action_prompt": "Aggiorna la posizione di {count} risorse selezionate con:", "updated_at": "Aggiornato il", "updated_password": "Password aggiornata", "upload": "Carica", - "upload_action_prompt": "{count} accodati per l'upload", "upload_concurrency": "Caricamenti contemporanei", "upload_details": "Dettagli di caricamento", "upload_dialog_info": "Vuoi fare il backup sul server delle risorse selezionate?", - "upload_dialog_title": "Carica file", - "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere gli asset caricati.", + "upload_dialog_title": "Carica Risorsa", + "upload_error_with_count": "Invio in errore per {count, plural, one {# risorsa} other {# risorse}}", + "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere le risorse caricate.", "upload_finished": "Upload terminato", "upload_progress": "Rimanenti {remaining, number} - Processati {processed, number}/{total, number}", - "upload_skipped_duplicates": "{count, plural, one {Ignorato # asset duplicato} other {Ignorati # asset duplicati}}", + "upload_skipped_duplicates": "{count, plural, one {Ignorata # risorsa duplicata} other {Ignorate # risorse duplicate}}", "upload_status_duplicates": "Duplicati", "upload_status_errors": "Errori", "upload_status_uploaded": "Caricato", - "upload_success": "Caricamento completato con successo, aggiorna la pagina per vedere i nuovi asset caricati.", + "upload_success": "Caricamento completato, aggiorna la pagina per vedere le nuove risorse caricate.", "upload_to_immich": "Carica su Immich ({count})", "uploading": "Caricamento", "uploading_media": "Caricando i media", "url": "URL", "usage": "Utilizzo", "use_biometric": "Usa biometrica", - "use_current_connection": "usa la connessione attuale", + "use_current_connection": "Usa la connessione attuale", "use_custom_date_range": "Altrimenti utilizza un intervallo date personalizzato", "user": "Utente", "user_has_been_deleted": "L'utente è stato rimosso.", "user_id": "ID utente", - "user_liked": "A {user} piace {type, select, photo {questa foto} video {questo video} asset {questo asset} other {questo elemento}}", + "user_liked": "A {user} piace {type, select, photo {questa foto} video {questo video} asset {questa risorsa} other {questo elemento}}", "user_pin_code_settings": "Codice PIN", "user_pin_code_settings_description": "Gestisci il tuo codice PIN", "user_privacy": "Privacy dell'utente", @@ -2185,6 +2349,7 @@ "utilities": "Utilità", "validate": "Validazione", "validate_endpoint_error": "Inserisci un URL valido", + "validation_error": "Erroe di validazione", "variables": "Variabili", "version": "Versione", "version_announcement_closing": "Il tuo amico, Alex", @@ -2196,26 +2361,29 @@ "video_hover_setting_description": "Riproduci miniatura video quando il mouse passa sopra l'elemento. Anche se disabilitato, la riproduzione puÃ˛ essere avviata passando con il mouse sopra l'icona riproduci.", "videos": "Video", "videos_count": "{count, plural, one {# Video} other {# Video}}", - "view": "Vista", + "videos_only": "Solo video", + "view": "Visualizza", "view_album": "Visualizza Album", "view_all": "Vedi tutto", "view_all_users": "Visualizza tutti gli utenti", - "view_asset_owners": "Visualizza proprietari dell'asset", + "view_asset_owners": "Visualizza proprietari della risorsa", "view_details": "Visualizza Dettagli", "view_in_timeline": "Visualizza in timeline", "view_link": "Visualizza link", "view_links": "Visualizza i link", - "view_name": "Visualizza", + "view_name": "Vista", "view_next_asset": "Visualizza risorsa successiva", "view_previous_asset": "Visualizza risorsa precedente", "view_qr_code": "Visualizza Codice QR", - "view_similar_photos": "Visualizza le foto simili", + "view_similar_photos": "Visualizza foto simili", "view_stack": "Visualizza Raggruppamento", "view_user": "Visualizza Utente", "viewer_remove_from_stack": "Rimuovi dal gruppo", "viewer_stack_use_as_main_asset": "Usa come risorsa principale", "viewer_unstack": "Separa dal gruppo", "visibility_changed": "Visibilità modificata per {count, plural, one {# persona} other {# persone}}", + "visual": "Visuale", + "visual_builder": "Costruttore di visuale", "waiting": "In Attesa", "waiting_count": "In attesa: {count}", "warning": "Attenzione", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Benvenuto in Immich", "width": "Larghezza", "wifi_name": "Nome rete Wi-Fi", - "workflow": "Flusso di lavoro", + "workflow_delete_prompt": "Sei sicuro di voler cancellare questo flusso di lavoro?", + "workflow_deleted": "Flusso di lavoro cancellato", + "workflow_description": "Descrizione del flusso di lavoro", + "workflow_info": "Informazioni sul flusso di lavoro", + "workflow_json": "Flusso di lavoro JSON", + "workflow_json_help": "Edita la configurazione del flusso di lavoro in formato JSON. I cambiamenti verranno sincronizzati con il costruttore visuale.", + "workflow_name": "Nome del flusso di lavoro", + "workflow_navigation_prompt": "Sei sicuro di voler uscire senza salvare i cambiamenti?", + "workflow_summary": "Sommario del flusso di lavoro", + "workflow_update_success": "Flusso di lavoro aggiornato con successo", + "workflow_updated": "Flusso di lavoro aggiornato", + "workflows": "Flussi di lavoro", + "workflows_help_text": "I flussi di lavoro automatizzano azioni sulle tue risorse a seconda di eventi e filtri", "wrong_pin_code": "Codice PIN errato", "year": "Anno", "years_ago": "{years, plural, one {# anno} other {# anni}} fa", "yes": "SÃŦ", "you_dont_have_any_shared_links": "Non hai nessun link condiviso", "your_wifi_name": "Nome della tua rete Wi-Fi", + "zero_to_clear_rating": "Premi 0 per eliminare la valutazione", "zoom_image": "Ingrandisci immagine", "zoom_to_bounds": "Ingrandisci fino ai bordi" } diff --git a/i18n/ja.json b/i18n/ja.json index 1ca31fd9e1..218e615ac1 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -5,8 +5,10 @@ "acknowledge": "äē†č§Ŗ", "action": "ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗ", "action_common_update": "更新", + "action_description": "æŠŊå‡ēã•ã‚ŒãŸå†™įœŸ/動į”ģãĢ寞しãĻčĄŒã†æ‰‹é †", "actions": "ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗ", "active": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ–", + "active_count": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ–: {count}", "activity": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗ", "activity_changed": "ã‚ĸã‚¯ãƒ†ã‚Ŗãƒ“ãƒ†ã‚Ŗã¯{enabled, select, true {有劚} other {į„ĄåŠš}}ãĢãĒりぞした", "add": "čŋŊ加", @@ -14,9 +16,14 @@ "add_a_location": "場所をčŋŊ加", "add_a_name": "名前をčŋŊ加", "add_a_title": "ã‚ŋイトãƒĢをčŋŊ加", + "add_action": "ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗã‚’čŋŊ加", + "add_action_description": "クãƒĒックしã‚ĸã‚¯ã‚ˇãƒ§ãƒŗã‚’čŋŊ加", + "add_assets": "é …į›Žã‚’čŋŊ加", "add_birthday": "čĒ•į”Ÿæ—Ĩã‚’č¨­åŽš", "add_endpoint": "ã‚¨ãƒŗãƒ‰ãƒã‚¤ãƒŗãƒˆã‚’čŋŊ加", "add_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’čŋŊ加", + "add_filter": "ãƒ•ã‚ŖãƒĢã‚ŋãƒŧをčŋŊ加", + "add_filter_description": "ãƒ•ã‚ŖãƒĢã‚ŋãƒŧã™ã‚‹æĄäģļをčŋŊ加", "add_location": "場所をčŋŊ加", "add_more_users": "ãƒĻãƒŧã‚ļãƒŧをčŋŊ加", "add_partner": "パãƒŧトナãƒŧをčŋŊ加", @@ -31,10 +38,11 @@ "add_to_album_toggle": "{album}ぎ選択を切りæ›ŋえ", "add_to_albums": "ã‚ĸãƒĢバムãĢčŋŊ加", "add_to_albums_count": "{count}つぎã‚ĸãƒĢバムへčŋŊ加", - "add_to_bottom_bar": "čŋŊ加先", + "add_to_bottom_bar": "čŋŊ加する", "add_to_shared_album": "å…ąæœ‰ã‚ĸãƒĢバムãĢčŋŊ加", "add_upload_to_stack": "゚ã‚ŋックãĢã‚ĸップロãƒŧドをčŋŊ加", "add_url": "URLをčŋŊ加", + "add_workflow_step": "ワãƒŧクフロãƒŧぎ゚テップをčŋŊ加", "added_to_archive": "ã‚ĸãƒŧã‚ĢイブãĢしぞした", "added_to_favorites": "お気ãĢå…ĨりãĢčŋŊ加済", "added_to_favorites_count": "{count, number} 枚ぎį”ģ像をお気ãĢå…ĨりãĢčŋŊ加しぞした", @@ -67,6 +75,7 @@ "confirm_reprocess_all_faces": "æœŦåŊ“ãĢすずãĻãŽéĄ”ã‚’å†å‡Ļį†ã—ãžã™ã‹? これãĢより名前がäģ˜ã‘られたäēēį‰Šã‚‚æļˆåŽģされぞす。", "confirm_user_password_reset": "æœŦåŊ“ãĢ {user} ぎパ゚ワãƒŧドをãƒĒã‚ģットしぞすかīŧŸ", "confirm_user_pin_code_reset": "{user}ぎPINã‚ŗãƒŧドをãƒĒã‚ģットしãĻよいですかīŧŸ", + "copy_config_to_clipboard_description": "JSONã‚ĒブジェクトとしãĻįžåœ¨ãŽã‚ˇã‚šãƒ†ãƒ ã‚ŗãƒŗãƒ•ã‚Ŗã‚°ã‚’ã‚¯ãƒĒップボãƒŧドãĢã‚ŗãƒ”ãƒŧする", "create_job": "ジョブぎäŊœæˆ", "cron_expression": "Cronåŧ", "cron_expression_description": "cronぎフりãƒŧマットをäŊŋãŖãĻã‚šã‚­ãƒŖãƒŗé–“éš”ã‚’č¨­åŽšã—ãžã™ã€‚čŠŗã—ãã¯Crontab GuruãĒãŠã‚’å‚į…§ã—ãĻください", @@ -74,6 +83,8 @@ "disable_login": "ãƒ­ã‚°ã‚¤ãƒŗã‚’į„ĄåŠšãĢする", "duplicate_detection_job_description": "抟æĸ°å­Ļįŋ’ã‚’į”¨ã„ãĻ類äŧŧį”ģ像ぎ検å‡ēã‚’čĄŒã„ãžã™ã€‚īŧˆã‚šãƒžãƒŧトã‚ĩãƒŧチãĢ䞝存īŧ‰", "exclusion_pattern_description": "除外パã‚ŋãƒŧãƒŗã‚’äŊŋį”¨ã™ã‚‹ã¨ã€ãƒŠã‚¤ãƒ–ãƒŠãƒĒã‚’ã‚šã‚­ãƒŖãƒŗã™ã‚‹éš›ãĢãƒ•ã‚Ąã‚¤ãƒĢやフりãƒĢãƒ€ã‚’į„ĄčĻ–ã™ã‚‹ã“ã¨ãŒã§ããžã™ã€‚RAWãƒ•ã‚Ąã‚¤ãƒĢãĒãŠã€ã‚¤ãƒŗãƒãƒŧトしたくãĒã„ãƒ•ã‚Ąã‚¤ãƒĢをåĢむフりãƒĢダがある場合ãĢäžŋ刊です。", + "export_config_as_json_description": "įžåœ¨ãŽã‚ˇã‚šãƒ†ãƒ ã‚ŗãƒŗãƒ•ã‚Ŗã‚°ã‚’JSONãƒ•ã‚Ąã‚¤ãƒĢとしãĻダã‚Ļãƒŗãƒ­ãƒŧド", + "external_libraries_page_description": "įŽĄį†č€…į”¨ 外部ナイブナãƒĒ ペãƒŧジ", "face_detection": "éĄ”æ¤œå‡ē", "face_detection_description": "抟æĸ°å­Ļįŋ’ã‚’äŊŋį”¨ã—ãĻã‚ĸã‚ģãƒƒãƒˆå†…ãŽéĄ”ã‚’æ¤œå‡ēしぞす。動į”ģぎ場合は、ã‚ĩムネイãƒĢぎãŋãŒå¯žčąĄã¨ãĒりぞす。\"すずãĻ\" はすずãĻぎã‚ĸã‚ģットをīŧˆå†īŧ‰å‡Ļį†ã—ãžã™ã€‚ \"æŦ čŊ\" はぞだå‡Ļį†ã•ã‚ŒãĻいãĒいã‚ĸã‚ģットをキãƒĨãƒŧãĢå…Ĩã‚Œãžã™ã€‚éĄ”æ¤œå‡ēぎ厌äē†åžŒã€æ¤œå‡ēã•ã‚ŒãŸéĄ”ã¯éĄ”čĒč­˜ãŽã‚­ãƒĨãƒŧへå…Ĩれられ、æ—ĸ存ぞたは新čĻãŽäēēį‰ŠãĢグãƒĢãƒŧプ化されぞす。", "facial_recognition_job_description": "検å‡ēã•ã‚ŒãŸéĄ”ã‚’äēēį‰ŠãĢグãƒĢãƒŧãƒ—åŒ–ã—ãžã™ã€‚ã“ãŽã‚šãƒ†ãƒƒãƒ—ã¯éĄ”æ¤œå‡ēが厌äē†ã—た垌ãĢåŽŸčĄŒã•ã‚Œãžã™ã€‚ \"すずãĻ\" はすずãĻãŽéĄ”ã‚’īŧˆå†īŧ‰ã‚¯ãƒŠã‚šã‚ŋãƒĒãƒŗã‚°ã—ã€ \"æŦ čŊ\" はäēēį‰ŠãŒå‰˛ã‚ŠåŊ“ãĻられãĻいãĒã„éĄ”ã‚’ã‚­ãƒĨãƒŧãĢå…Ĩれぞす。", @@ -93,6 +104,8 @@ "image_preview_description": "単一ぎã‚ĸã‚ģãƒƒãƒˆã‚’čĄ¨į¤ēする時や抟æĸ°å­Ļįŋ’ãĢäŊŋã‚ã‚Œã‚‹ãƒĄã‚ŋデãƒŧã‚ŋを取り除いた中ã‚ĩイã‚ēぎį”ģ像", "image_preview_quality_description": "プãƒŦビãƒĨãƒŧぎį”ģčŗĒは1〜100ã§č¨­åŽšã§ããžã™ã€‚å€¤ãŒéĢ˜ã„ãģお品čŗĒã¯č‰¯ããĒã‚Šãžã™ãŒãƒ•ã‚Ąã‚¤ãƒĢã‚ĩイã‚ēが大きくãĒãŖãĻã‚ĸプãƒĒぎåŋœį­”性がäŊŽä¸‹ã™ã‚‹ãŠãã‚ŒãŒã‚りぞす。äŊŽã„å€¤ã‚’č¨­åŽšã™ã‚‹ã¨æŠŸæĸ°å­Ļįŋ’ぎ品čŗĒãĢåŊąéŸŋを与えるおそれがありぞす。", "image_preview_title": "プãƒŦビãƒĨãƒŧč¨­åŽš", + "image_progressive": "æŧ¸é€˛įš„čĒ­ãŋčžŧãŋ", + "image_progressive_description": "JPEGį”ģ像をæŽĩéšŽįš„ãĢã‚¨ãƒŗã‚ŗãƒŧドし、į”ģ像を垐々ãĢ襨į¤ēã—ãžã™ã€‚ã“ãŽč¨­åŽšã¯WebPį”ģ像ãĢåŊąéŸŋを及ãŧしぞせん。", "image_quality": "品čŗĒ", "image_resolution": "č§ŖåƒåēĻ", "image_resolution_description": "č§ŖåƒåēĻã‚’ä¸Šã’ã‚‹ã¨ã‚ˆã‚Šį˛žį´°ãĢäŋå­˜ã§ããžã™ãŒã€ã‚¨ãƒŗã‚ŗãƒŧドãĢæ™‚é–“ãŒã‹ã‹ã‚Šãƒ•ã‚Ąã‚¤ãƒĢã‚ĩイã‚ēが大きくãĒãŖãĻã‚ĸプãƒĒぎåŋœį­”性がäŊŽä¸‹ã™ã‚‹ãŠãã‚ŒãŒã‚りぞす。", @@ -101,6 +114,7 @@ "image_thumbnail_description": "ãƒĄã‚¤ãƒŗãŽã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗãŽã‚ˆã†ãĒå†™įœŸã‚°ãƒĢãƒŧãƒ—ã§čĄ¨į¤ēする際ãĢäŊŋã‚ã‚Œã‚‹ãƒĄã‚ŋデãƒŧã‚ŋを取り除いた小さãĒã‚ĩムネイãƒĢ", "image_thumbnail_quality_description": "ã‚ĩムネイãƒĢぎį”ģčŗĒを1〜100ãŽé–“ã§č¨­åŽšã§ããžã™ã€‚å€¤ãŒå¤§ãã„ãģãŠč‰¯ã„å“čŗĒã§ã™ãŒãƒ•ã‚Ąã‚¤ãƒĢã‚ĩイã‚ēが大きくãĒりã‚ĸプãƒĒぎåŋœį­”性がäŊŽä¸‹ã—ぞす。", "image_thumbnail_title": "ã‚ĩムネイãƒĢč¨­åŽš", + "import_config_from_json_description": "ã‚ˇã‚šãƒ†ãƒ ã‚ŗãƒŗãƒ•ã‚Ŗã‚°ãŽJSONãƒ•ã‚Ąã‚¤ãƒĢをã‚ĸップロãƒŧãƒ‰ã—ã‚¤ãƒŗãƒãƒŧト", "job_concurrency": "{job} ãŽåŒæ™‚åŽŸčĄŒæ•°", "job_created": "ジョブをäŊœæˆã—ぞした", "job_not_concurrency_safe": "こぎジョブは厉全ãĢåŒæ™‚åŽŸčĄŒã§ããžã›ã‚“ã€‚", @@ -108,6 +122,7 @@ "job_settings_description": "ã‚¸ãƒ§ãƒ–ãŽåŒæ™‚åŽŸčĄŒã‚’įŽĄį†ã—ãžã™", "jobs_delayed": "{jobCount, plural, other {#äģļ}}ぎ遅åģļ", "jobs_failed": "{jobCount, plural, other {#äģļ}}ãŽå¤ąæ•—", + "jobs_over_time": "įĩ‚わらãĒã‹ãŖãŸã‚¸ãƒ§ãƒ–", "library_created": "äŊœæˆã•れたナイブナãƒĒīŧš{library}", "library_deleted": "ナイブナãƒĒは削除されぞした", "library_details": "ナイブナãƒĒãŽčŠŗį´°", @@ -175,11 +190,23 @@ "machine_learning_smart_search_enabled": "゚マãƒŧトã‚ĩãƒŧチを有劚ãĢしぞす", "machine_learning_smart_search_enabled_description": "į„ĄåŠšãĢすると、į”ģ像ぱマãƒŧトã‚ĩãƒŧãƒį”¨ãĢã‚¨ãƒŗã‚ŗãƒŧドされぞせん。", "machine_learning_url_description": "抟æĸ°å­Ļįŋ’ã‚ĩãƒŧバãƒŧぎURLã€‚č¤‡æ•°ãŽURLãŒč¨­åŽšã•ã‚ŒãŸå ´åˆã¯1つずつã‚ĩãƒŧバãƒŧãŒæ­Ŗå¸¸ãĢåŋœį­”するぞでæŽĨįļšã‚’čŠĻãŋぞす。åŋœį­”ぎãĒいã‚ĩãƒŧバãƒŧはã‚ĒãƒŗãƒŠã‚¤ãƒŗãĢãĒã‚‹ãžã§ä¸€æ™‚įš„ãĢį„ĄčĻ–ã•ã‚Œãžã™ã€‚", + "maintenance_delete_backup": "バックã‚ĸップを削除", + "maintenance_delete_backup_description": "ã“ãŽãƒ•ã‚Ąã‚¤ãƒĢã¯ä¸å¯é€†įš„ãĢ削除されぞす。", + "maintenance_delete_error": "バックã‚ĸップぎ削除ãĢå¤ąæ•—ã—ãžã—ãŸã€‚", + "maintenance_restore_backup": "バックã‚ĸップを垊元", + "maintenance_restore_backup_description": "įžåœ¨ãŽImmichは削除され、選択したバックã‚ĸップから垊元されぞす。įļščĄŒå‰ãĢバックã‚ĸップがäŊœæˆã•れぞす。", + "maintenance_restore_backup_different_version": "こぎバックã‚ĸãƒƒãƒ—ã¯į•°ãĒるバãƒŧã‚¸ãƒ§ãƒŗãŽImmichãĢよりäŊœæˆã•れたもぎですīŧ", + "maintenance_restore_backup_unknown_version": "バックã‚ĸップぎバãƒŧã‚¸ãƒ§ãƒŗã‚’į‰šåŽšã§ããžã›ã‚“ã€‚", + "maintenance_restore_database_backup": "デãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸップを垊元", + "maintenance_restore_database_backup_description": "バックã‚ĸãƒƒãƒ—ãƒ•ã‚Ąã‚¤ãƒĢã‚’į”¨ã„ãĻ、äģĨ前ぎデãƒŧトペãƒŧ゚ぎįŠļ態ãĢロãƒŧãƒĢバックしぞす", "maintenance_settings": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚š", "maintenance_settings_description": "Immichã‚’ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドãĢする。", - "maintenance_start": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドを開始する", + "maintenance_start": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドへ切りæ›ŋえる", "maintenance_start_error": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドぎ開始ãĢå¤ąæ•—ã—ãžã—ãŸã€‚", + "maintenance_upload_backup": "デãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸãƒƒãƒ—ãƒ•ã‚Ąã‚¤ãƒĢをã‚ĸップロãƒŧド", + "maintenance_upload_backup_error": "バックã‚ĸップをã‚ĸップロãƒŧãƒ‰ã§ããžã›ã‚“ã€‚ããŽãƒ•ã‚Ąã‚¤ãƒĢは.sql/.sql.gzãƒ•ã‚Ąã‚¤ãƒĢですかīŧŸ", "manage_concurrency": "åŒæ™‚åŽŸčĄŒæ•°ãŽįŽĄį†", + "manage_concurrency_description": "ジョブ ペãƒŧジで、同時ä¸ĻčĄŒã§į¨ŧåƒã™ã‚‹ã‚¸ãƒ§ãƒ–æ•°ã‚’įŽĄį†ã§ããžã™", "manage_log_settings": "ãƒ­ã‚°č¨­åŽšã‚’įŽĄį†ã—ãžã™", "map_dark_style": "ダãƒŧクãƒĸãƒŧド", "map_enable_description": "åœ°å›ŗčĄ¨į¤ē抟čƒŊを有劚ãĢしぞす", @@ -269,10 +296,14 @@ "password_settings_description": "パ゚ワãƒŧド ãƒ­ã‚°ã‚¤ãƒŗč¨­åŽšã‚’įŽĄį†ã—ãžã™", "paths_validated_successfully": "すずãĻãŽãƒ‘ã‚šãŒæ­Ŗå¸¸ãĢ検č¨ŧされぞした", "person_cleanup_job": "äēēį‰ŠãŽã‚¯ãƒĒãƒŧãƒŗã‚ĸップ", + "queue_details": "垅抟中ã‚ŋã‚šã‚¯ãŽčŠŗį´°", + "queues": "垅抟中ぎジョブ", + "queues_page_description": "įŽĄį†č€…į”¨ ã‚¸ãƒ§ãƒ–åž…ãĄåˆ— ペãƒŧジ", "quota_size_gib": "å‰˛ã‚ŠåŊ“ãĻ厚量 (GiB)", "refreshing_all_libraries": "すずãĻぎナイブナãƒĒを更新", "registration": "įŽĄį†č€…į™ģ錞", "registration_description": "あãĒãŸã¯ã‚ˇã‚šãƒ†ãƒ ãŽæœ€åˆãŽãƒĻãƒŧã‚ļãƒŧã§ã‚ã‚‹ãŸã‚ã€įŽĄį†č€…ã¨ã—ãĻå‰˛ã‚ŠåŊ“ãĻã‚‰ã‚Œã€įŽĄį†ã‚ŋ゚クを担åŊ“し、čŋŊ加ぎãƒĻãƒŧã‚ļãƒŧはあãĒたãĢã‚ˆãŖãĻäŊœæˆã•れぞす。", + "remove_failed_jobs": "å¤ąæ•—ã—ãŸã‚¸ãƒ§ãƒ–ã‚’å‰Šé™¤", "require_password_change_on_login": "åˆå›žãƒ­ã‚°ã‚¤ãƒŗæ™‚ãĢパ゚ワãƒŧド変更をčĻæą‚ã™ã‚‹", "reset_settings_to_default": "č¨­åŽšã‚’ãƒ‡ãƒ•ã‚ŠãƒĢトãĢãƒĒã‚ģットしぞす", "reset_settings_to_recent_saved": "å‰å›žãŽč¨­åŽšå€¤ãĢæˆģす", @@ -285,8 +316,10 @@ "server_public_users_description": "å…ąæœ‰ã‚ĸãƒĢバムãĢãƒĻãƒŧã‚ļãƒŧをčŋŊ加するとすずãĻぎãƒĻãƒŧã‚ļãƒŧ (åå‰ã¨ãƒĄãƒŧãƒĢã‚ĸドãƒŦ゚) がãƒĒã‚šãƒˆåŒ–ã•ã‚Œãžã™ã€‚į„ĄåŠšãĢするとãƒĻãƒŧã‚ļãƒŧãƒĒã‚šãƒˆã¯įŽĄį†č€…ãŽãŋåˆŠį”¨å¯čƒŊãĢãĒりぞす。", "server_settings": "ã‚ĩãƒŧバãƒŧč¨­åŽš", "server_settings_description": "ã‚ĩãƒŧバãƒŧč¨­åŽšã‚’įŽĄį†ã—ãžã™", + "server_stats_page_description": "įŽĄį†č€…į”¨ ã‚ĩãƒŧバãƒŧįĩąč¨ˆæƒ…å ą ペãƒŧジ", "server_welcome_message": "ã‚ĻェãƒĢã‚Ģム ãƒĄãƒƒã‚ģãƒŧジ", "server_welcome_message_description": "ãƒ­ã‚°ã‚¤ãƒŗãƒšãƒŧジãĢãƒĄãƒƒã‚ģãƒŧã‚¸ã‚’čĄ¨į¤ēしぞす。", + "settings_page_description": "įŽĄį†č€…į”¨ č¨­åŽš ペãƒŧジ", "sidecar_job": "XMPãƒĄã‚ŋデãƒŧã‚ŋ", "sidecar_job_description": "ãƒ•ã‚Ąã‚¤ãƒĢã‚ˇã‚šãƒ†ãƒ ã‹ã‚‰XMPãƒĄã‚ŋデãƒŧã‚ŋを検å‡ēぞたは同期する", "slideshow_duration_description": "各į”ģåƒã‚’čĄ¨į¤ēã™ã‚‹į§’æ•°", @@ -405,6 +438,8 @@ "user_restore_scheduled_removal": "ãƒĻãƒŧã‚ļãƒŧを垊元 - {date, date, long}ãĢ削除äēˆåޚ", "user_settings": "ãƒĻãƒŧã‚ļãƒŧč¨­åŽš", "user_settings_description": "ãƒĻãƒŧã‚ļãƒŧč¨­åŽšã‚’įŽĄį†ã—ãžã™", + "user_successfully_removed": "ãƒĻãƒŧã‚ļãƒŧ {email} ã¯æ­Ŗå¸¸ãĢ削除されぞした。", + "users_page_description": "įŽĄį†č€…į”¨ ãƒĻãƒŧã‚ļãƒŧ ペãƒŧジ", "version_check_enabled_description": "バãƒŧã‚¸ãƒ§ãƒŗãŽįĸēčĒã‚’æœ‰åŠšãĢする", "version_check_implications": "こぎバãƒŧã‚¸ãƒ§ãƒŗįĸēčĒæŠŸčƒŊã¯åŽšæœŸįš„ãĒgithub.comとぎ通äŋĄãĢよりぞす", "version_check_settings": "バãƒŧã‚¸ãƒ§ãƒŗãƒã‚§ãƒƒã‚¯", @@ -416,6 +451,9 @@ "admin_password": "įŽĄį†č€…ãƒ‘ã‚šãƒ¯ãƒŧド", "administration": "įŽĄį†", "advanced": "čŠŗį´°č¨­åŽš", + "advanced_settings_clear_image_cache": "į”ģåƒãŽã‚­ãƒŖãƒƒã‚ˇãƒĨを削除", + "advanced_settings_clear_image_cache_error": "į”ģåƒãŽã‚­ãƒŖãƒƒã‚ˇãƒĨぎ削除ãĢå¤ąæ•—ã—ãžã—ãŸ", + "advanced_settings_clear_image_cache_success": "{size}ぎ削除ãĢ成功しぞした", "advanced_settings_enable_alternate_media_filter_subtitle": "åˆĨぎåŸēæē–ãĢåž“ãŖãĻãƒĄãƒ‡ã‚Ŗã‚ĸãƒ•ã‚Ąã‚¤ãƒĢãĢãƒ•ã‚ŖãƒĢã‚ŋãƒŧをかけãĻã€åŒæœŸã‚’čĄŒã„ãžã™ã€‚ã‚ĸプãƒĒがすずãĻぎã‚ĸãƒĢバムをčĒ­ãŋčžŧんでくれãĒい場合ãĢぎãŋ、こぎ抟čƒŊをčŠĻしãĻください。", "advanced_settings_enable_alternate_media_filter_title": "[čŠĻ鍓運ᔍ] åˆĨぎデバイ゚ぎã‚ĸãƒĢãƒãƒ åŒæœŸãƒ•ã‚ŖãƒĢã‚ŋãƒŧをäŊŋį”¨ã™ã‚‹", "advanced_settings_log_level_title": "ログãƒŦベãƒĢ: {level}", @@ -452,10 +490,12 @@ "album_remove_user": "ãƒĻãƒŧã‚ļãƒŧを削除しぞすか?", "album_remove_user_confirmation": "æœŦåŊ“ãĢ{user}を削除しぞすか?", "album_search_not_found": "検į´ĸãĢä¸€č‡´ã™ã‚‹ã‚ĸãƒĢバムがありぞせん", + "album_selected": "ã‚ĸãƒĢバム選択中", "album_share_no_users": "こぎã‚ĸãƒĢバムを全ãĻぎãƒĻãƒŧã‚ļãƒŧã¨å…ąæœ‰ã—ãŸã‹ã€å…ąæœ‰ã™ã‚‹ãƒĻãƒŧã‚ļãƒŧがいãĒいようです。", "album_summary": "ã‚ĸãƒĢバムぎぞとめ", "album_updated": "ã‚ĸãƒĢバム更新", "album_updated_setting_description": "å…ąæœ‰ã‚ĸãƒĢバムãĢæ–°ã—ã„é …į›ŽãŒčŋŊ加されたとき通įŸĨを受け取る", + "album_upload_assets": "ã‚ŗãƒŗãƒ”ãƒĨãƒŧã‚ŋã‹ã‚‰é …į›Žã‚’ã‚ĸップロãƒŧドし、ã‚ĸãƒĢバムãĢčŋŊ加する", "album_user_left": "{album} をåŽģりぞした", "album_user_removed": "{user} を削除しぞした", "album_viewer_appbar_delete_confirm": "æœŦåŊ“ãĢこぎã‚ĸãƒĢバムを削除しぞすかīŧŸ", @@ -473,9 +513,11 @@ "albums_default_sort_order_description": "新čĻã‚ĸãƒĢバムäŊœæˆæ™‚ãŽåˆæœŸčĄ¨į¤ē順.", "albums_feature_description": "äģ–ぎãƒĻãƒŧã‚ļãƒŧã¨å…ąæœ‰ã§ãã‚‹ã‚ĸã‚ģãƒƒãƒˆãŽã‚ŗãƒŦã‚¯ã‚ˇãƒ§ãƒŗ.", "albums_on_device_count": "デバイ゚上ぎã‚ĸãƒĢバム ({count})", + "albums_selected": "{count, plural, one {# ã‚ĸãƒĢバム選択中} other {# ã‚ĸãƒĢバム選択中}}", "all": "すずãĻ", "all_albums": "全ãĻぎã‚ĸãƒĢバム", "all_people": "全ãĻぎäēēį‰Š", + "all_photos": "全ãĻãŽå†™įœŸ", "all_videos": "全ãĻぎ動į”ģ", "allow_dark_mode": "ダãƒŧクãƒĸãƒŧãƒ‰ã‚’č¨ąå¯", "allow_edits": "įˇ¨é›†ã‚’č¨ąå¯", @@ -483,6 +525,9 @@ "allow_public_user_to_upload": "一čˆŦãƒĻãƒŧã‚ļãƒŧãĢよるã‚ĸップロãƒŧãƒ‰ã‚’č¨ąå¯", "allowed": "č¨ąå¯ã•ã‚ŒãĻいる", "alt_text_qr_code": "QRã‚ŗãƒŧドį”ģ像", + "always_keep": "常ãĢäŋæŒ", + "always_keep_photos_hint": "「゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã€ã§ã€å…¨ãĻãŽå†™įœŸãŒã“ãŽãƒ‡ãƒã‚¤ã‚šãĢäŋæŒã•れぞす。", + "always_keep_videos_hint": "「゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã€ã§ã€å…¨ãĻぎ動į”ģがこぎデバイ゚ãĢäŋæŒã•れぞす。", "anti_clockwise": "åæ™‚č¨ˆå›žã‚Š", "api_key": "APIキãƒŧ", "api_key_description": "こぎ値は一回ぎãŋ襨į¤ēされぞす。 ã‚Ļã‚Ŗãƒŗãƒ‰ã‚Ļを閉じる前ãĢåŋ…ãšã‚ŗãƒ”ãƒŧしãĻください。", @@ -509,10 +554,12 @@ "archived_count": "ã‚ĸãƒŧã‚Ģイブされた{count, plural, other {#å€‹ãŽé …į›Ž}}", "are_these_the_same_person": "これらは同じäēēį‰Šã§ã™ã‹?", "are_you_sure_to_do_this": "æœŦåŊ“ãĢã“ã‚Œã‚’čĄŒã„ãžã™ã‹?", + "array_field_not_fully_supported": "é…åˆ—ãƒ•ã‚ŖãƒŧãƒĢドは手動でJSONįˇ¨é›†ã™ã‚‹åŋ…čĻãŒã‚ã‚Šãžã™", "asset_action_delete_err_read_only": "čĒ­ãŋå–ã‚Šå°‚į”¨ãŽé …į›Žã¯å‰Šé™¤ã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™", "asset_action_share_err_offline": "ã‚Ēãƒ•ãƒŠã‚¤ãƒŗãŽé …į›Žã‚’ã‚˛ãƒƒãƒˆã§ããžã›ã‚“ã€‚ã‚šã‚­ãƒƒãƒ—ã—ãžã™", "asset_added_to_album": "ã‚ĸãƒĢバムãĢčŋŊ加", "asset_adding_to_album": "ã‚ĸãƒĢバムãĢčŋŊ加しãĻいぞすâ€Ļ", + "asset_created": "é …į›ŽãŒäŊœæˆã•れぞした", "asset_description_updated": "é …į›ŽãŽčĒŦ明文が更新されぞした", "asset_filename_is_offline": "é …į›Ž {filename} がã‚Ēãƒ•ãƒŠã‚¤ãƒŗã§ã™", "asset_has_unassigned_faces": "é …į›ŽãĢ名前ぎついãĻいãĒいäēēį‰ŠãŽéĄ”ãŒã‚ã‚Šãžã™", @@ -525,6 +572,9 @@ "asset_list_layout_sub_title": "ãƒŦイã‚ĸã‚Ļト", "asset_list_settings_subtitle": "グãƒĒッドãĢé–ĸã™ã‚‹č¨­åŽš", "asset_list_settings_title": "グãƒĒッド", + "asset_not_found_on_device_android": "デバイ゚上ãĢå†™įœŸ/動į”ģがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", + "asset_not_found_on_device_ios": "デバイ゚上ãĢå†™įœŸ/動į”ģがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã§ã—ãŸã€‚iCloudをäŊĩせãĻã”åˆŠį”¨ãŽå ´åˆã¯ã€iCloudãŽãƒ•ã‚Ąã‚¤ãƒĢäŋįŽĄæ–šæŗ•ãĢå•éĄŒãŒã‚ã‚Šã€ã‚ĸクã‚ģ゚できãĒい可čƒŊ性がありぞす。", + "asset_not_found_on_icloud": "iCloudä¸ŠãŽå†™įœŸ/動į”ģがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã§ã—ãŸã€‚", "asset_offline": "é …į›ŽãŒã‚Ēãƒ•ãƒŠã‚¤ãƒŗã§ã™", "asset_offline_description": "ã“ãŽå¤–éƒ¨é …į›Žã¯ãƒ‡ã‚Ŗã‚šã‚¯ä¸ŠãĢもうありぞせん。Immichã‚ĩãƒŧバãƒŧãŽįŽĄį†č€…ãĢ逪įĩĄã‚’しãĻください。", "asset_restored_successfully": "垊元できぞした", @@ -637,6 +687,7 @@ "backup_options_page_title": "バックã‚ĸップã‚Ēãƒ—ã‚ˇãƒ§ãƒŗ", "backup_setting_subtitle": "ã‚ĸップロãƒŧドãĢé–ĸã™ã‚‹č¨­åŽš", "backup_settings_subtitle": "ã‚ĸップロãƒŧãƒ‰č¨­åŽšã‚’įŽĄį†", + "backup_upload_details_page_more_details": "ã‚ŋãƒƒãƒ—ã§čŠŗį´°é–˛čϧ", "backward": "新しい斚へ", "biometric_auth_enabled": "į”ŸäŊ“čĒč¨ŧを有劚化しぞした", "biometric_locked_out": "į”ŸäŊ“čĒč¨ŧãĢより、ã‚ĸクã‚ģ゚できぞせん", @@ -695,16 +746,31 @@ "change_password_form_password_mismatch": "パ゚ワãƒŧãƒ‰ãŒä¸€č‡´ã—ãžã›ã‚“", "change_password_form_reenter_new_password": "再åēĻパ゚ワãƒŧドをå…Ĩ力しãĻください", "change_pin_code": "PINã‚ŗãƒŧドを変更", + "change_trigger": "トãƒĒã‚Ŧãƒŧを変更", + "change_trigger_prompt": "トãƒĒã‚Ŧãƒŧを変えãĻもよいですかīŧŸã‚ĸã‚¯ã‚ˇãƒ§ãƒŗãƒģãƒ•ã‚ŖãƒĢã‚ŋãƒŧが全ãĻ削除されぞす", "change_your_password": "パ゚ワãƒŧドを変更しぞす", "changed_visibility_successfully": "非表į¤ēč¨­åŽšã‚’æ­Ŗå¸¸ãĢ変更しぞした", "charging": "充é›ģ中", "charging_requirement_mobile_backup": "バックグナã‚Ļãƒŗãƒ‰ã§ãŽãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—ã‚’čĄŒã†ãŸã‚ãĢは、デバイ゚が充é›ģ中であるåŋ…čĻãŒã‚ã‚Šãžã™", "check_corrupt_asset_backup": "į ´æã•ã‚ŒãĻã„ã‚‹é …į›Žã‚’æŽĸす", "check_corrupt_asset_backup_button": "ãƒã‚§ãƒƒã‚¯ã‚’čĄŒã†", - "check_corrupt_asset_backup_description": "å†™įœŸã‚„å‹•į”ģãĒおが全ãĻã‚ĸップロãƒŧドしįĩ‚えãĻからWi-FiãĢæŽĨį™‚ぎãŋãƒã‚§ãƒƒã‚¯ã‚’čĄŒãĒãŖãĻください。äŊœæĨ­ãŒåތäē†ã™ã‚‹ãĢは数分かかる場合がありぞす", + "check_corrupt_asset_backup_description": "å†™įœŸã‚„å‹•į”ģãĒおが全ãĻã‚ĸップロãƒŧドしįĩ‚えãĻからWi-FiãĢæŽĨį™‚ぎãŋãƒã‚§ãƒƒã‚¯ã‚’čĄŒãĒãŖãĻください。äŊœæĨ­ãŒåތäē†ã™ã‚‹ãĢは数分かかる場合がありぞす。", "check_logs": "ログをįĸēčĒ", + "checksum": "チェックã‚ĩム", "choose_matching_people_to_merge": "įĩąåˆå…ˆãŽäēēį‰Šã‚’é¸ã‚“ã§ãã ã•ã„", "city": "市į”ē村", + "cleanup_confirm_description": "ã‚ĩãƒŧバãƒŧãĢバックã‚ĸップ済ãŋãŽå†™įœŸ/動į”ģīŧˆ{date}äģĨ前ãĢäŊœæˆīŧ‰ã‚’{count}äģļį™ēčĻ‹ã—ãžã—ãŸã€‚ã“ãŽãƒ‡ãƒã‚¤ã‚šã‹ã‚‰ãƒ­ãƒŧã‚ĢãƒĢã‚ŗãƒ”ãƒŧを削除しぞすかīŧŸ", + "cleanup_confirm_prompt_title": "こぎデバイ゚から削除しぞすかīŧŸ", + "cleanup_deleted_assets": "{count}äģļãŽå†™įœŸ/動į”ģã‚’ãƒ‡ãƒã‚¤ã‚šãŽã‚´ãƒŸįŽąãĢį§ģ動しぞした", + "cleanup_deleting": "ã‚´ãƒŸįŽąãĢį§ģ動中â€Ļ", + "cleanup_found_assets": "{count}äģļぎバックã‚ĸップ済ãŋå†™įœŸ/動į”ģを検å‡ē", + "cleanup_found_assets_with_size": "{count}å€‹ãŽå†™įœŸ/動į”ģぎバックã‚ĸップがčĻ‹ã¤ã‹ã‚Šãžã—ãŸ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloudãŽå…ąæœ‰ã‚ĸãƒĢãƒãƒ ã¯ã‚šã‚­ãƒŖãƒŗãŽå¯žčąĄå¤–ãĢãĒりぞす", + "cleanup_no_assets_found": "ä¸Šč¨˜ãŽæĄäģļãĢåŊ“ãĻã¯ãžã‚‹å†™įœŸ/動į”ģがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã§ã—ãŸã€‚ã€Œã‚šãƒˆãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã€ã¯ã‚ĩãƒŧバãĢバックã‚ĸップされãĻã„ã‚‹å†™įœŸ/動į”ģぎãŋ削除できぞす", + "cleanup_preview_title": "å‰Šé™¤ã•ã‚Œã‚‹å†™įœŸ/動į”ģ ({count})", + "cleanup_step3_description": "あãĒãŸãŽč¨­åŽšã—ãŸæœŸé–“ãĢåˆč‡´ã™ã‚‹ãƒãƒƒã‚¯ã‚ĸップ済ãŋå†™įœŸ/動į”ģをæŽĸしå‡ēã—ã€č¨­åŽšã‚’įļ­æŒã—ぞす。", + "cleanup_step4_summary": "あãĒたぎロãƒŧã‚ĢãƒĢデバイ゚から{count}æžšãŽå†™įœŸ/動į”ģ({date}äģĨ前ãĢäŊœæˆã•れたもぎ)が削除されぞす。操äŊœåžŒã‚‚å†™įœŸã¯Immichã‚ĸプãƒĒからã‚ĸクã‚ģ゚できぞす。", + "cleanup_trash_hint": "゚トãƒŦãƒŧジぎ厚量を取りæˆģすãĢã¯ã€ã‚ˇã‚šãƒ†ãƒ ãŽã‚ŽãƒŖãƒŠãƒĒãƒŧã‚ĸプãƒĒã‚’é–‹ãã€ã‚´ãƒŸįŽąã‚’įŠēãĢしãĻください", "clear": "クãƒĒã‚ĸ", "clear_all": "全ãĻクãƒĒã‚ĸ", "clear_all_recent_searches": "全ãĻぎ最čŋ‘ぎ検į´ĸをクãƒĒã‚ĸ", @@ -716,6 +782,8 @@ "client_cert_import": "ã‚¤ãƒŗãƒãƒŧト", "client_cert_import_success_msg": "クナイã‚ĸãƒŗãƒˆč¨ŧ明書が導å…Ĩされぞした", "client_cert_invalid_msg": "パ゚ワãƒŧãƒ‰ãŒé–“é•ãŖãĻいるかč¨ŧæ˜Žæ›¸ãŒį„ĄåŠšã§ã™", + "client_cert_password_message": "こぎč¨ŧ明書ぎパ゚ワãƒŧドをå…Ĩ力しãĻください", + "client_cert_password_title": "č¨ŧ明書パ゚ワãƒŧド", "client_cert_remove_msg": "クナイã‚ĸãƒŗãƒˆč¨ŧ明書が削除されぞした", "client_cert_subtitle": "PKCS12 (.p12 .pfx) フりãƒŧマットぎãŋ寞åŋœã—ãĻいぞす。č¨ŧ明書ぎ導å…Ĩã‚„å‰Šé™¤ã¯ãƒ­ã‚°ã‚¤ãƒŗå‰ãŽãŋčĄŒãˆãžã™", "client_cert_title": "SSLクナイã‚ĸãƒŗãƒˆč¨ŧ明書 [åŽŸé¨“įš„]", @@ -725,6 +793,7 @@ "collapse_all": "全ãĻåą•é–‹", "color": "ã‚Ģナãƒŧ", "color_theme": "ã‚Ģナãƒŧテãƒŧマ", + "command": "ã‚ŗãƒžãƒŗãƒ‰", "comment_deleted": "ã‚ŗãƒĄãƒŗãƒˆãŒå‰Šé™¤ã•ã‚Œãžã—ãŸ", "comment_options": "ã‚ŗãƒĄãƒŗãƒˆč¨­åŽš", "comments_and_likes": "ã‚ŗãƒĄãƒŗãƒˆã¨ã„ã„ã­", @@ -769,6 +838,7 @@ "create_album": "ã‚ĸãƒĢバムをäŊœæˆ", "create_album_page_untitled": "į„ĄéĄŒãŽã‚ŋイトãƒĢ", "create_api_key": "APIキãƒŧをäŊœæˆ", + "create_first_workflow": "初めãĻぎワãƒŧクフロãƒŧをäŊœæˆ", "create_library": "ナイブナãƒĒをäŊœæˆ", "create_link": "ãƒĒãƒŗã‚¯ã‚’äŊœã‚‹", "create_link_to_share": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’äŊœã‚‹", @@ -783,17 +853,25 @@ "create_tag": "ã‚ŋグをäŊœæˆã™ã‚‹", "create_tag_description": "ã‚ŋグをäŊœæˆã—ぞす。å…Ĩれ子構造ぎã‚ŋã‚°ã¯ã€ã¯ã˜ã‚ãŽã‚šãƒŠãƒƒã‚ˇãƒĨをåĢめた、ã‚ŋグぎ厌全ãĒパ゚をå…Ĩ力しãĻください。", "create_user": "ãƒĻãƒŧã‚ļãƒŧをäŊœæˆ", + "create_workflow": "ワãƒŧクフロãƒŧをäŊœæˆ", "created": "äŊœæˆ", "created_at": "äŊœæˆ:", "creating_linked_albums": "ãƒĒãƒŗã‚¯ã•ã‚ŒãŸã‚ĸãƒĢバムをäŊœæˆä¸­ãƒģãƒģãƒģ", "crop": "クロップ", + "crop_aspect_ratio_fixed": "å›ē厚", + "crop_aspect_ratio_free": "č‡Ēį”ą", + "crop_aspect_ratio_original": "ã‚ĒãƒĒジナãƒĢ", "curated_object_page_title": "čĸĢ写äŊ“", "current_device": "įžåœ¨ãŽãƒ‡ãƒã‚¤ã‚š", "current_pin_code": "įžåœ¨ãŽPINã‚ŗãƒŧド", "current_server_address": "įžåœ¨ãŽã‚ĩãƒŧバãƒŧURL", + "custom_date": "ã‚Ģ゚ã‚ŋムæ—Ĩäģ˜", "custom_locale": "ã‚Ģ゚ã‚ŋãƒ ãƒ­ã‚ąãƒŧãƒĢ", "custom_locale_description": "言čĒžã¨åœ°åŸŸãĢåŸēãĨいãĻæ—Ĩäģ˜ã¨æ•°å€¤ã‚’フりãƒŧマットしぞす", "custom_url": "ã‚Ģ゚ã‚ŋムURL", + "cutoff_date_description": "å†™įœŸã‚’äŋæŒã™ã‚‹æœŸé–“:", + "cutoff_day": "{count, plural, one {(æ—Ĩ)} other {(æ—Ĩ)}}", + "cutoff_year": "{count, plural, one {åš´} other {åš´}}", "daily_title_text_date": "MM DD, EE", "daily_title_text_date_year": "yyyy MM DD, EE", "dark": "ダãƒŧクãƒĸãƒŧド", @@ -819,9 +897,9 @@ "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": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯ã‚ĩãƒŧバãƒŧã¨ãƒ‡ãƒã‚¤ã‚šãŽä¸Ąæ–šã‹ã‚‰åŽŒå…¨ãĢ削除されぞす", + "delete_dialog_alert_local": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯ãƒ‡ãƒã‚¤ã‚šã‹ã‚‰åŽŒå…¨ãĢ削除されぞすが、ã‚ĩãƒŧバãƒŧãĢは掋りぞす", + "delete_dialog_alert_local_non_backed_up": "é¸æŠžã•ã‚ŒãŸé …į›ŽãŽä¸€éƒ¨ã¯ã‚ĩãƒŧバãƒŧãĢバックã‚ĸップされãĻおらず、デバイ゚から厌全ãĢ削除されぞす", "delete_dialog_alert_remote": "é¸æŠžã•ã‚ŒãŸé …į›Žã¯ã‚ĩãƒŧバãƒŧから厌全ãĢ削除されぞす", "delete_dialog_ok_force": "削除しぞす", "delete_dialog_title": "厌全ãĢ削除", @@ -849,6 +927,7 @@ "deselect_all": "すずãĻãŽé¸æŠžã‚’č§Ŗé™¤", "details": "čŠŗį´°", "direction": "斚向", + "disable": "į„ĄåŠšåŒ–", "disabled": "į„ĄåŠš", "disallow_edits": "įˇ¨é›†ã‚’č¨ąå¯ã—ãĒい", "discord": "Discord", @@ -874,6 +953,7 @@ "download_include_embedded_motion_videos": "埋めčžŧぞれた動į”ģ", "download_include_embedded_motion_videos_description": "åˆĨãƒ•ã‚Ąã‚¤ãƒĢとしãĻ、ãƒĸãƒŧã‚ˇãƒ§ãƒŗãƒ•ã‚ŠãƒˆãĢ埋めčžŧぞれた動į”ģをåĢめる", "download_notfound": "ダã‚Ļãƒŗãƒ­ãƒŧドがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", + "download_original": "ã‚ĒãƒĒジナãƒĢをダã‚Ļãƒŗãƒ­ãƒŧド", "download_paused": "ダã‚Ļãƒŗãƒ­ãƒŧド一時停æ­ĸ中", "download_settings": "ダã‚Ļãƒŗãƒ­ãƒŧド", "download_settings_description": "å†™įœŸ/動į”ģぎダã‚Ļãƒŗãƒ­ãƒŧドãĢé–ĸé€Ŗã™ã‚‹č¨­åŽšã‚’įŽĄį†ã—ãžã™", @@ -883,6 +963,7 @@ "download_waiting_to_retry": "ãƒĒトナイ中", "downloading": "ダã‚Ļãƒŗãƒ­ãƒŧド中", "downloading_asset_filename": "å†™įœŸ/動į”ģ {filename} をダã‚Ļãƒŗãƒ­ãƒŧド中", + "downloading_from_icloud": "iCloudからダã‚Ļãƒŗãƒ­ãƒŧド", "downloading_media": "ダã‚Ļãƒŗãƒ­ãƒŧド中", "drop_files_to_upload": "ãƒ•ã‚Ąã‚¤ãƒĢをドロップしãĻã‚ĸップロãƒŧド", "duplicates": "重複", @@ -911,16 +992,27 @@ "edit_tag": "ã‚ŋã‚°ã‚’įˇ¨é›†ã™ã‚‹", "edit_title": "ã‚ŋイトãƒĢã‚’įˇ¨é›†", "edit_user": "ãƒĻãƒŧã‚ļãƒŧã‚’įˇ¨é›†", + "edit_workflow": "ワãƒŧクフロãƒŧã‚’įˇ¨é›†", "editor": "ᎍ集į”ģéĸ", "editor_close_without_save_prompt": "å¤‰æ›´ã¯į ´æŖ„ã•ã‚Œãžã™", "editor_close_without_save_title": "ᎍ集į”ģéĸを閉じぞすか?", - "editor_crop_tool_h2_aspect_ratios": "ã‚ĸ゚ペクト比", - "editor_crop_tool_h2_rotation": "回čģĸ", + "editor_confirm_reset_all_changes": "æœŦåŊ“ãĢ全ãĻぎ変更をãƒĒã‚ģットしぞすかīŧŸ", + "editor_discard_edits_confirm": "įˇ¨é›†ã‚’į ´æŖ„ã™ã‚‹", + "editor_discard_edits_prompt": "įˇ¨é›†å†…åŽšãŒäŋå­˜ã•れãĻã„ãžã›ã‚“ã€‚į ´æŖ„ã—ãžã™ã‹īŧŸ", + "editor_discard_edits_title": "įˇ¨é›†ã‚’į ´æŖ„ã—ãžã™ã‹īŧŸ", + "editor_edits_applied_error": "įˇ¨é›†ãŽéŠį”¨ãĢå¤ąæ•—ã—ãžã—ãŸ", + "editor_edits_applied_success": "įˇ¨é›†ãŒæ­Ŗå¸¸ãĢ反映されぞした", + "editor_flip_horizontal": "æ°´åšŗæ–šå‘ãĢ反čģĸ", + "editor_flip_vertical": "åž‚į›´ãĢ反čģĸ", + "editor_orientation": "向き", + "editor_reset_all_changes": "変更をãƒĒã‚ģット", + "editor_rotate_left": "åæ™‚č¨ˆå›žã‚ŠãĢ90°回čģĸ", + "editor_rotate_right": "æ™‚č¨ˆå›žã‚ŠãĢ90°回čģĸ", "email": "ãƒĄãƒŧãƒĢã‚ĸドãƒŦ゚", "email_notifications": "EãƒĄãƒŧãƒĢ通įŸĨ", "empty_folder": "こぎフりãƒĢダãƒŧはįŠēです", "empty_trash": "ã‚´ãƒŸįŽąã‚’įŠēãĢする", - "empty_trash_confirmation": "æœŦåŊ“ãĢã‚´ãƒŸįŽąã‚’įŠēãĢしぞすか? ã‚´ãƒŸįŽąå†…ãŽã™ãšãĻãŽå†™įœŸ/動į”ģが Immich から永䚅ãĢ削除されぞす。\nこぎ操äŊœã‚’å…ƒãĢæˆģすことはできぞせん!", + "empty_trash_confirmation": "æœŦåŊ“ãĢã‚´ãƒŸįŽąã‚’įŠēãĢしぞすか? ã‚´ãƒŸįŽąå†…ãŽã™ãšãĻãŽå†™įœŸ/動į”ģがImmichから永įļšįš„ãĢ削除されぞす。\nこぎ操äŊœã‚’å…ƒãĢæˆģすことはできぞせん!", "enable": "有劚化", "enable_backup": "バックã‚ĸップを有劚化", "enable_biometric_auth_description": "į”ŸäŊ“čĒč¨ŧを有劚化するためãĢ、PINã‚ŗãƒŧドをå…Ĩ力しãĻください", @@ -934,11 +1026,14 @@ "error_change_sort_album": "ã‚ĸãƒĢãƒãƒ ãŽčĄ¨į¤ē順ぎ変更ãĢå¤ąæ•—ã—ãžã—ãŸ", "error_delete_face": "å†™įœŸ/動į”ģã‹ã‚‰éĄ”ãŽå‰Šé™¤ãŒã§ããžã›ã‚“ã§ã—ãŸ", "error_getting_places": "場所ぎ取垗ãĢå¤ąæ•—ã—ãžã—ãŸ", + "error_loading_albums": "ã‚ĸãƒĢバムぎčĒ­ãŋčžŧãŋエナãƒŧ", "error_loading_image": "į”ģ像ぎčĒ­ãŋčžŧãŋエナãƒŧ", "error_loading_partners": "パãƒŧトナãƒŧぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—ã—ãžã—ãŸ: {error}", + "error_retrieving_asset_information": "é …į›Žæƒ…å ąãŽå–åž—ã‚¨ãƒŠãƒŧ", "error_saving_image": "エナãƒŧ: {error}", "error_tag_face_bounding_box": "éĄ”ãŽį™ģ錞ãĢå¤ąæ•—ã—ãžã—ãŸ - éĄ”ã‚’å›˛ã‚€å››č§’åŊĸぎåē§æ¨™å–åž—ãĢå¤ąæ•—", "error_title": "エナãƒŧ - å•éĄŒãŒį™ēį”Ÿã—ãžã—ãŸ", + "error_while_navigating": "é …į›ŽãŽãƒŠãƒ“ã‚˛ãƒŧã‚ˇãƒ§ãƒŗä¸­ãŽã‚¨ãƒŠãƒŧ", "errors": { "cannot_navigate_next_asset": "æŦĄãŽå†™įœŸ/動į”ģãĢį§ģ動できぞせん", "cannot_navigate_previous_asset": "å‰ãŽå†™įœŸ/動į”ģãĢį§ģ動できぞせん", @@ -996,6 +1091,7 @@ "unable_to_complete_oauth_login": "OAuth ãƒ­ã‚°ã‚¤ãƒŗã‚’åŽŒäē†ã§ããžã›ã‚“", "unable_to_connect": "æŽĨįļšã§ããžã›ã‚“", "unable_to_copy_to_clipboard": "クãƒĒップボãƒŧドãĢã‚ŗãƒ”ãƒŧできぞせん。https įĩŒį”ąã§ãƒšãƒŧジãĢã‚ĸクã‚ģ゚しãĻいることをįĸēčĒã—ãĻください", + "unable_to_create": "ワãƒŧクフロãƒŧをäŊœæˆã§ããžã›ã‚“", "unable_to_create_admin_account": "įŽĄį†č€…ã‚ĸã‚Ģã‚Ļãƒŗãƒˆã‚’äŊœæˆã§ããžã›ã‚“", "unable_to_create_api_key": "新しいAPI キãƒŧをäŊœæˆã§ããžã›ã‚“", "unable_to_create_library": "ナイブナãƒĒをäŊœæˆã§ããžã›ã‚“", @@ -1006,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’å‰Šé™¤ã§ããžã›ã‚“", "unable_to_delete_shared_link": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’å‰Šé™¤ã§ããžã›ã‚“", "unable_to_delete_user": "ãƒĻãƒŧã‚ļãƒŧを削除できぞせん", + "unable_to_delete_workflow": "ワãƒŧクフロãƒŧを削除できぞせん", "unable_to_download_files": "ãƒ•ã‚Ąã‚¤ãƒĢをダã‚Ļãƒŗãƒ­ãƒŧドできぞせん", "unable_to_edit_exclusion_pattern": "除外パã‚ŋãƒŧãƒŗã‚’įˇ¨é›†ã§ããžã›ã‚“", "unable_to_empty_trash": "ã‚´ãƒŸįŽąã‚’įŠēãĢできぞせん", @@ -1045,6 +1142,7 @@ "unable_to_scan_library": "ナイブナãƒĒã‚’ã‚šã‚­ãƒŖãƒŗã§ããžã›ã‚“", "unable_to_set_feature_photo": "ã‚ĸã‚¤ã‚­ãƒŖãƒƒãƒå†™įœŸã‚’č¨­åŽšã§ããžã›ã‚“", "unable_to_set_profile_picture": "ãƒ—ãƒ­ãƒ•ã‚ŖãƒŧãƒĢį”ģåƒã‚’č¨­åŽšã§ããžã›ã‚“", + "unable_to_set_rating": "čŠ•äžĄã‚’č¨­åŽšã§ããžã›ã‚“", "unable_to_submit_job": "ジョブを送äŋĄã§ããžã›ã‚“", "unable_to_trash_asset": "å†™įœŸ/動į”ģã‚’ã‚´ãƒŸįŽąãĢį§ģ動できぞせん", "unable_to_unlink_account": "ã‚ĸã‚Ģã‚ĻãƒŗãƒˆãŽãƒĒãƒŗã‚¯ã‚’č§Ŗé™¤ã§ããžã›ã‚“", @@ -1056,8 +1154,10 @@ "unable_to_update_settings": "č¨­åŽšã‚’æ›´æ–°ã§ããžã›ã‚“", "unable_to_update_timeline_display_status": "ã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗã§ãŽčĄ¨į¤ēãŽč¨­åŽšįŠļ態を更新できぞせん", "unable_to_update_user": "ãƒĻãƒŧã‚ļãƒŧを更新できぞせん", + "unable_to_update_workflow": "ワãƒŧクフロãƒŧを更新できぞせん", "unable_to_upload_file": "ãƒ•ã‚Ąã‚¤ãƒĢをã‚ĸップロãƒŧドできぞせん" }, + "errors_text": "エナãƒŧ", "exclusion_pattern": "除外パã‚ŋãƒŧãƒŗ", "exif": "Exif", "exif_bottom_sheet_description": "čĒŦ明をčŋŊ加", @@ -1089,6 +1189,7 @@ "external_network_sheet_info": "指厚されたWi-FiãĢįš‹ãŒãŖãĻいãĒい時ã‚ĸプãƒĒはã‚ĩãƒŧバãƒŧへぎæŽĨįļšã‚’指厚されたURLã§čĄŒã„ãžã™ã€‚å„Ē先順äŊã¯ä¸Šã‹ã‚‰ä¸‹ã§ã™", "face_unassigned": "æœĒå‰˛ã‚ŠåŊ“ãĻ", "failed": "å¤ąæ•—", + "failed_count": "å¤ąæ•—: {count}", "failed_to_authenticate": "čĒč¨ŧãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_assets": "å†™įœŸ/動į”ģぎロãƒŧドãĢå¤ąæ•—ã—ãžã—ãŸ", "failed_to_load_folder": "フりãƒĢダãƒŧぎčĒ­ãŋčžŧãŋãĢå¤ąæ•—", @@ -1101,14 +1202,17 @@ "features": "抟čƒŊ", "features_in_development": "開į™ē中ぎ抟čƒŊ", "features_setting_description": "ã‚ĸプãƒĒぎ抟čƒŊã‚’įŽĄį†ã™ã‚‹", - "file_name": "ãƒ•ã‚Ąã‚¤ãƒĢ名", "file_name_or_extension": "ãƒ•ã‚Ąã‚¤ãƒĢåãžãŸã¯æ‹Ąåŧĩ子", + "file_name_text": "ãƒ•ã‚Ąã‚¤ãƒĢ名", + "file_name_with_value": "ãƒ•ã‚Ąã‚¤ãƒĢ名: {file_name}", "file_size": "ãƒ•ã‚Ąã‚¤ãƒĢã‚ĩイã‚ē", "filename": "ãƒ•ã‚Ąã‚¤ãƒĢ名", "filetype": "ãƒ•ã‚Ąã‚¤ãƒĢã‚ŋイプ", "filter": "ãƒ•ã‚ŖãƒĢã‚ŋãƒŧ", + "filter_description": "å¯žčąĄã¨ã™ã‚‹ã‚ĸã‚ģットぎæŠŊå‡ēæĄäģļ", "filter_people": "äēēį‰Šã‚’įĩžã‚Ščžŧãŋ", "filter_places": "å ´æ‰€ã‚’ãƒ•ã‚ŖãƒĢã‚ŋãƒŧ", + "filters": "ãƒ•ã‚ŖãƒĢã‚ŋãƒŧ", "find_them_fast": "名前で検į´ĸしãĻį´ æ—Šãį™ēčĻ‹", "first": "はじめ", "fix_incorrect_match": "é–“é•ãŖãŸä¸€č‡´ã‚’äŋŽæ­Ŗ", @@ -1118,12 +1222,16 @@ "folders_feature_description": "ãƒ•ã‚Ąã‚¤ãƒĢã‚ˇã‚šãƒ†ãƒ ä¸ŠãŽå†™įœŸã¨å‹•į”ģぎフりãƒĢダビãƒĨãƒŧã‚’é–˛čĻ§ã™ã‚‹", "forgot_pin_code_question": "PINをåŋ˜ã‚Œãžã—たか?", "forward": "前へ", + "free_up_space": "゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”ž", + "free_up_space_description": "バックã‚ĸãƒƒãƒ—ã•ã‚ŒãŸå†™įœŸã¨å‹•į”ģをあãĒãŸãŽãƒ‡ãƒã‚¤ã‚šãŽã‚´ãƒŸįŽąã¸į§ģ動し、゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã—ãžã™ã€‚ã‚ŗãƒ”ãƒŧはã‚ĩãƒŧバ上ãĢ厉全ãĢäŋįŽĄã•ã‚ŒãĻいぞす。", + "free_up_space_settings_subtitle": "デバイ゚ぎ゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã™ã‚‹", "full_path": "フãƒĢパ゚: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "こぎ抟čƒŊは動äŊœãŽãŸã‚ãĢGoogleぎãƒĒã‚Ŋãƒŧ゚をčĒ­ãŋčžŧãŋぞす。", "general": "一čˆŦ", "geolocation_instruction_location": "äŊįŊŽæƒ…å ąäģ˜ããŽé …į›Žã‚’ã‚¯ãƒĒックしãĻ、そぎäŊįŊŽæƒ…å ąã‚’åˆŠį”¨ã—ãžã™ã€‚ã‚ã‚‹ã„ã¯ã€åœ°å›ŗä¸ŠãŽåœ°į‚šã‚’į›´æŽĨ選ãļことも可čƒŊです", "get_help": "åŠŠã‘ã‚’æą‚ã‚ã‚‹", + "get_people_error": "äēēį‰ŠãŽå–åž—æ™‚ãĢエナãƒŧ", "get_wifiname_error": "Wi-Fiぎ名前(SSID)がå…Ĩ手できぞせんでした。Wi-FiãĢįš‹ãŒãŖãĻるぎとåŋ…čρãĒæ¨Šé™ã‚’č¨ąå¯ã—ãŸã‹įĸēčĒã—ãĻください", "getting_started": "はじめる", "go_back": "æˆģる", @@ -1149,12 +1257,14 @@ "header_settings_header_name_input": "ヘッダぎ名前", "header_settings_header_value_input": "ヘッダぎバãƒĒãƒĨãƒŧ", "headers_settings_tile_title": "ã‚Ģ゚ã‚ŋãƒ ãƒ—ãƒ­ã‚­ã‚ˇãƒ˜ãƒƒãƒ€", + "height": "éĢ˜ã•", "hi_user": "こんãĢãĄã¯ã€{name}( {email})さん", "hide_all_people": "全ãĻぎäēēį‰Šã‚’éžčĄ¨į¤ē", "hide_gallery": "ã‚ŽãƒŖãƒŠãƒĒãƒŧã‚’éžčĄ¨į¤ē", "hide_named_person": "äēēį‰Š {name} ã‚’éžčĄ¨į¤ē", "hide_password": "パ゚ワãƒŧドを隠す", "hide_person": "äēēį‰Šã‚’éžčĄ¨į¤ē", + "hide_schema": "゚キãƒŧãƒžã‚’éžčĄ¨į¤ē", "hide_text_recognition": "文字čĒč­˜ã‚’éžčĄ¨į¤ē", "hide_unnamed_people": "名前がãĒいäēēį‰Šã‚’éžčĄ¨į¤ē", "home_page_add_to_album_conflicts": "{album}ãĢ{added}å€‹ãŽå†™įœŸ/動į”ģをčŋŊ加しぞした。čŋŊ加済ãŋぎ{failed}個ぱキップしぞした。", @@ -1179,7 +1289,7 @@ "id": "ID", "idle": "ã‚ĸイドãƒĒãƒŗã‚°", "ignore_icloud_photos": "iCloudä¸ŠãŽå†™įœŸã‚’ã‚šã‚­ãƒƒãƒ—", - "ignore_icloud_photos_description": "iCloudãĢäŋå­˜æ¸ˆãŋãŽé …į›Žã‚’Immichã‚ĩãƒŧバãƒŧ上ãĢã‚ĸップロãƒŧドしぞせん", + "ignore_icloud_photos_description": "iCloudãĢäŋå­˜ã•れãĻã„ã‚‹å†™įœŸ/動į”ģはImmichã‚ĩãƒŧバãƒŧãĢã‚ĸップロãƒŧドされぞせん", "image": "å†™įœŸ", "image_alt_text_date": "{isVideo, select, true {動į”ģ} other {å†™įœŸ}}は{date} ãĢæ’ŽåŊą", "image_alt_text_date_1_person": "{date}ぎ、{person1}とぎ{isVideo, select, true {動į”ģ} other {į”ģ像}}", @@ -1195,8 +1305,8 @@ "image_viewer_page_state_provider_download_started": "ダã‚Ļãƒŗãƒ­ãƒŧドが始ぞりぞす", "image_viewer_page_state_provider_download_success": "ダã‚Ļãƒŗãƒ­ãƒŧド成功", "image_viewer_page_state_provider_share_error": "å…ąæœ‰ã‚¨ãƒŠãƒŧ", - "immich_logo": "Immich ロゴ", - "immich_web_interface": "Immich Webã‚¤ãƒŗã‚ŋãƒŧフェãƒŧ゚", + "immich_logo": "Immichぎロゴ", + "immich_web_interface": "ImmichぎWebã‚¤ãƒŗã‚ŋãƒŧフェãƒŧ゚", "import_from_json": "JSONã‹ã‚‰ã‚¤ãƒŗãƒãƒŧト", "import_path": "ã‚¤ãƒŗãƒãƒŧトパ゚", "in_albums": "{count, plural, one {#äģļぎã‚ĸãƒĢバム} other {#äģļぎã‚ĸãƒĢバム}}ぎ中", @@ -1227,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "å‡Ļį†åŽŸčĄŒæ¸ˆãŋ: {dateTime}", "items_count": "{count, plural, one {#個} other {#個}}ãŽé …į›Ž", "jobs": "ジョブ", + "json_editor": "JSONã‚¨ãƒ‡ã‚Ŗã‚ŋãƒŧ", + "json_error": "JSONエナãƒŧ", "keep": "äŋæŒ", + "keep_albums": "ã‚ĸãƒĢバムをäŋæŒ", + "keep_albums_count": "{count}個ぎã‚ĸãƒĢバムを掋す", "keep_all": "全ãĻäŋæŒ", + "keep_description": "゚トãƒŦãƒŧã‚¸ã‚’č§Ŗæ”žã™ã‚‹éš›ãĢ、デバイ゚ãĢ掋すもぎを選択できぞす。", + "keep_favorites": "お気ãĢå…ĨりをäŋæŒ", + "keep_on_device": "デバイ゚ãĢäŋæŒ", + "keep_on_device_hint": "こぎデバイ゚ãĢäŋæŒã—ãŸã„é …į›Žã‚’é¸æŠžã—ãžã™", "keep_this_delete_others": "これを掋しãĻãģかを削除する", + "keeping": "äŋæŒã™ã‚‹é …į›Žæ•°: {items}", "kept_this_deleted_others": "ã“ãŽå†™įœŸ/動į”ģを掋しãĻ{count, plural, other {#äģļ}}を削除する", "keyboard_shortcuts": "キãƒŧボãƒŧãƒ‰ã‚ˇãƒ§ãƒŧトã‚Ģット", "language": "言čĒž", @@ -1271,12 +1390,13 @@ "local": "ロãƒŧã‚ĢãƒĢ", "local_asset_cast_failed": "ã‚ĩãƒŧバãƒŧãĢã‚ĸップロãƒŧドされãĻいãĒã„é …į›Žã¯ã‚­ãƒŖã‚šãƒˆã§ããžã›ã‚“", "local_assets": "ロãƒŧã‚ĢãƒĢãŽé …į›Ž", + "local_id": "ロãƒŧã‚ĢãƒĢID", "local_media_summary": "ロãƒŧã‚ĢãƒĢãƒĄãƒ‡ã‚Ŗã‚ĸぎぞとめ", "local_network": "ロãƒŧã‚ĢãƒĢネットワãƒŧク", "local_network_sheet_info": "ã‚ĸプãƒĒは指厚されたWi-FiãĢįš‹ãŒãŖãĻいる時ã‚ĩãƒŧバãƒŧへぎæŽĨįļšã‚’ä¸‹č¨˜ãŽURLã§čĄŒã„ãžã™", "location": "äŊįŊŽæƒ…å ą", "location_permission": "äŊįŊŽæƒ…å ąæ¨Šé™", - "location_permission_content": "č‡Ē動URL切りæ›ŋえをäŊŋį”¨ã™ã‚‹ãĢはWi-Fiぎ名前(SSID)を取垗するåŋ…čĻãŒã‚ã‚Šã€æ­Ŗå¸¸ãĢ抟čƒŊするãĢはã‚ĸプãƒĒが常ãĢčŠŗį´°ãĒäŊįŊŽæƒ…å ąãĢã‚ĸクã‚ģ゚できるåŋ…čĻãŒã‚ã‚Šãžã™", + "location_permission_content": "č‡Ē動URL切りæ›ŋえをäŊŋį”¨ã™ã‚‹ãĢã¯įžåœ¨ãŽWi-Fi名を取垗するåŋ…čĻãŒã‚ã‚Šã€ã‚ĸプãƒĒが常ãĢčŠŗį´°ãĒäŊįŊŽæƒ…å ąãĢã‚ĸクã‚ģ゚できるåŋ…čĻãŒã‚ã‚Šãžã™", "location_picker_choose_on_map": "マップを選択", "location_picker_latitude_error": "有劚ãĒ᎝åēĻをå…Ĩ力しãĻください", "location_picker_latitude_hint": "᎝åēĻをå…Ĩ力", @@ -1322,10 +1442,28 @@ "loop_videos_description": "有劚ãĢã™ã‚‹ã¨čŠŗį´°čĄ¨į¤ēでč‡Ēå‹•įš„ãĢ動į”ģがãƒĢãƒŧプしぞす。", "main_branch_warning": "開į™ēį‰ˆã‚’äŊŋãŖãĻいるようです。ãƒĒãƒĒãƒŧã‚šį‰ˆãŽäŊŋį”¨ã‚’åŧˇãæŽ¨åĨ¨ã—ぞす!", "main_menu": "ãƒĄã‚¤ãƒŗãƒĄãƒ‹ãƒĨãƒŧ", + "maintenance_action_restore": "デãƒŧã‚ŋベãƒŧ゚を垊元", "maintenance_description": "Immich は ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧド中です。", "maintenance_end": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドをįĩ‚äē†ã™ã‚‹", "maintenance_end_error": "ãƒĄãƒŗãƒ†ãƒŠãƒŗã‚šãƒĸãƒŧドぎįĩ‚äē†ãĢå¤ąæ•—ã—ãžã—ãŸã€‚", "maintenance_logged_in_as": "įžåœ¨ {user}としãĻãƒ­ã‚°ã‚¤ãƒŗã—ãĻいぞす", + "maintenance_restore_from_backup": "バックã‚ĸップから垊元", + "maintenance_restore_library": "あãĒたぎナイブナãƒĒを垊元", + "maintenance_restore_library_confirm": "ã“ãĄã‚‰ãŒæ­Ŗã—ã„ã“ã¨ã‚’įĸēčĒã—ãŸä¸Šã§ã€ãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—ãŽåžŠå…ƒã‚’é€˛ã‚ãĻくださいīŧ", + "maintenance_restore_library_description": "デãƒŧã‚ŋベãƒŧ゚を垊元", + "maintenance_restore_library_folder_has_files": "{folder}は{count}個ぎフりãƒĢダをåĢãŋぞす", + "maintenance_restore_library_folder_no_files": "{folder}ãĢãƒ•ã‚Ąã‚¤ãƒĢがありぞせんīŧ", + "maintenance_restore_library_folder_pass": "čĒ­ãŋčžŧãŋ可čƒŊかつ書きčžŧãŋ可čƒŊ", + "maintenance_restore_library_folder_read_fail": "čĒ­ãŋčžŧãŋ不čƒŊ", + "maintenance_restore_library_folder_write_fail": "書きčžŧãŋ不čƒŊ", + "maintenance_restore_library_hint_missing_files": "重čρãĒãƒ•ã‚Ąã‚¤ãƒĢãŒå¤ąã‚ã‚Œã‚‹å¯čƒŊ性がありぞす", + "maintenance_restore_library_hint_regenerate_later": "ã“ãŽč¨­åŽšã¯ã‚ã¨ã‹ã‚‰å†į”Ÿæˆã§ããžã™", + "maintenance_restore_library_hint_storage_template_missing_files": "゚トãƒŦãƒŧã‚¸ãƒ†ãƒŗãƒ—ãƒŦãƒŧトをäŊŋいぞすかīŧŸé‡čρãĒãƒ•ã‚Ąã‚¤ãƒĢãŒå¤ąã‚ã‚Œã‚‹å¯čƒŊ性がありぞす", + "maintenance_restore_library_loading": "整合性ぎチェックとヒãƒĨãƒŧãƒĒã‚šãƒ†ã‚Ŗãƒƒã‚¯ã‚’čĒ­ãŋčžŧんでいぞすâ€Ļ", + "maintenance_task_backup": "æ—ĸ存ぎデãƒŧã‚ŋベãƒŧ゚ぎバックã‚ĸップをäŊœæˆã—ãĻいぞすâ€Ļ", + "maintenance_task_migrations": "デãƒŧã‚ŋベãƒŧ゚ぎマイグãƒŦãƒŧã‚ˇãƒ§ãƒŗã‚’åŽŸčĄŒã—ãĻいぞすâ€Ļ", + "maintenance_task_restore": "選択したバックã‚ĸップを垊元しãĻいぞすâ€Ļ", + "maintenance_task_rollback": "垊元ãĢå¤ąæ•—ã—ãŸãŸã‚ã€åžŠå…ƒãƒã‚¤ãƒŗãƒˆã¸ãƒ­ãƒŧãƒĢバックしぞすâ€Ļ", "maintenance_title": "ä¸€æ™‚įš„ãĢåˆŠį”¨ä¸å¯čƒŊ", "make": "ãƒĄãƒŧã‚Ģãƒŧ", "manage_geolocation": "äŊįŊŽæƒ…å ąã‚’įˇ¨é›†", @@ -1387,6 +1525,8 @@ "minimize": "最小化", "minute": "分", "minutes": "分", + "mirror_horizontal": "æ°´åšŗ", + "mirror_vertical": "åž‚į›´", "missing": "æŦ čŊ", "mobile_app": "ãƒĸバイãƒĢã‚ĸプãƒĒ", "mobile_app_download_onboarding_note": "äģĨ下ぎã‚Ēãƒ—ã‚ˇãƒ§ãƒŗã‚’äŊŋį”¨ã—ãĻã‚ŗãƒŗãƒ‘ãƒ‹ã‚ĒãƒŗãƒĸバイãƒĢã‚ĸプãƒĒをダã‚Ļãƒŗãƒ­ãƒŧドしãĻください", @@ -1395,11 +1535,14 @@ "monthly_title_text_date_format": "yyyy MM", "more": "ã‚‚ãŖã¨čĄ¨į¤ē", "move": "į§ģ動", + "move_down": "下へ", "move_off_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧからå‡ēす", - "move_to": "æŦĄãĢį§ģ動:", + "move_to": "į§ģ動する", + "move_to_device_trash": "ãƒ‡ãƒã‚¤ã‚šãŽã‚´ãƒŸįŽąã¸į§ģ動", "move_to_lock_folder_action_prompt": "{count}é …į›Žã‚’éĩäģ˜ããƒ•りãƒĢダãƒŧãĢčŋŊ加しぞした", "move_to_locked_folder": "éĩäģ˜ããƒ•りãƒĢダãƒŧへį§ģ動", "move_to_locked_folder_confirmation": "ã“ã‚Œã‚‰ãŽå†™įœŸã‚„å‹•į”ģはすずãĻぎã‚ĸãƒĢバムから外され、éĩäģ˜ããƒ•りãƒĢダãƒŧ内でぎãŋ閲čĻ§å¯čƒŊãĢãĒりぞす", + "move_up": "上へ", "moved_to_archive": "{count, plural, one {#} other {#}}é …į›Žã‚’ã‚ĸãƒŧã‚Ģイブしぞした", "moved_to_library": "{count, plural, one {#} other {#}}é …į›Žã‚’ãƒŠã‚¤ãƒ–ãƒŠãƒĒãĢį§ģ動しぞした", "moved_to_trash": "ã‚´ãƒŸįŽąãĢį§ģ動しぞした", @@ -1409,8 +1552,9 @@ "my_albums": "į§ãŽã‚ĸãƒĢバム", "name": "名前", "name_or_nickname": "名前ぞたはニックネãƒŧム", + "name_required": "名前はåŋ…é ˆé …į›Žã§ã™", "navigate": "ãƒŠãƒ“ã‚˛ãƒŧト", - "navigate_to_time": "時間ãĢį§ģ動", + "navigate_to_time": "į‰šåŽšãŽæ™‚é–“ãĢį§ģ動", "network_requirement_photos_upload": "ãƒĸバイãƒĢ通äŋĄã‚’äŊŋį”¨ã—ãĻå†™įœŸãŽãƒãƒƒã‚¯ã‚ĸãƒƒãƒ—ã‚’čĄŒã†", "network_requirement_videos_upload": "ãƒĸバイãƒĢ通äŋĄã‚’äŊŋį”¨ã—ãĻ動į”ģぎバックã‚ĸãƒƒãƒ—ã‚’čĄŒã†", "network_requirements": "ネットワãƒŧクぎčρäģļ", @@ -1433,6 +1577,8 @@ "next": "æŦĄ", "next_memory": "æŦĄãŽãƒĄãƒĸãƒĒãƒŧ", "no": "いいえ", + "no_actions_added": "ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗãŒã‚ã‚Šãžã›ã‚“", + "no_albums_found": "ã‚ĸãƒĢバムがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", "no_albums_message": "ã‚ĸãƒĢバムをäŊœæˆã—ãĻå†™įœŸã‚„å‹•į”ģã‚’æ•´į†ã—ãžã—ã‚‡ã†", "no_albums_with_name_yet": "こぎ名前ぎã‚ĸãƒĢバムはぞだãĒいようです。", "no_albums_yet": "ぞだã‚ĸãƒĢバムがãĒいようです。", @@ -1442,11 +1588,13 @@ "no_cast_devices_found": "ã‚­ãƒŖã‚šãƒˆå…ˆãŽãƒ‡ãƒã‚¤ã‚šãŒčĻ‹ã¤ã‹ã‚Šãžã›ã‚“", "no_checksum_local": "チェックã‚ĩムがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ - ãƒ‡ãƒã‚¤ã‚šä¸ŠãŽé …į›Žã‚’å–åž—ã§ããĒいようです", "no_checksum_remote": "チェックã‚ĩムがčĻ‹ã¤ã‹ã‚Šãžã›ã‚“ - ã‚ĩãƒŧバãƒŧä¸ŠãŽé …į›Žã‚’å–åž—ã§ããĒいようです", + "no_configuration_needed": "č¨­åŽšã¯ä¸čĻã§ã™", "no_devices": "č¨ąå¯ã•ã‚ŒãŸãƒ‡ãƒã‚¤ã‚šãŒã‚ã‚Šãžã›ã‚“", "no_duplicates_found": "é‡č¤‡ã¯čĻ‹ã¤ã‹ã‚Šãžã›ã‚“ã§ã—ãŸã€‚", "no_exif_info_available": "exifæƒ…å ąãŒåˆŠį”¨ã§ããžã›ã‚“", "no_explore_results_message": "ã‚ŗãƒŦã‚¯ã‚ˇãƒ§ãƒŗã‚’æŽĸį´ĸするãĢはさらãĢå†™įœŸã‚’ã‚ĸップロãƒŧドしãĻください。", "no_favorites_message": "お気ãĢå…Ĩりį™ģéŒ˛ã™ã‚‹ã¨åĨŊきãĒå†™įœŸã‚„å‹•į”ģをすぐãĢčĻ‹ã¤ã‘ã‚‰ã‚Œãžã™", + "no_filters_added": "ãžã ãƒ•ã‚ŖãƒĢã‚ŋãƒŧがčŋŊ加されãĻいぞせん", "no_libraries_message": "あãĒãŸãŽå†™įœŸã‚„å‹•į”ģã‚’čĄ¨į¤ēするためぎ外部ナイブナãƒĒをäŊœæˆã—ぞしょう", "no_local_assets_found": "こぎチェックã‚ĩãƒ ãŽé …į›Žã¯ãƒ‡ãƒã‚¤ã‚šä¸ŠãĢ存在しぞせん", "no_location_set": "äŊįŊŽæƒ…å ąãŒæŒ‡åŽšã•ã‚ŒãĻいぞせん", @@ -1460,11 +1608,11 @@ "no_results_description": "åŒįžŠčĒžã‚„ã‚ˆã‚Šä¸€čˆŦįš„ãĒキãƒŧワãƒŧドをčŠĻしãĻください", "no_shared_albums_message": "ã‚ĸãƒĢバムをäŊœæˆã—ãĻå†™įœŸã‚„å‹•į”ģã‚’å…ąæœ‰ã—ãžã—ã‚‡ã†", "no_uploads_in_progress": "ã‚ĸップロãƒŧãƒ‰ã¯čĄŒã‚ã‚ŒãĻいぞせん", + "none": "ãĒし", "not_allowed": "č¨ąå¯ã•ã‚ŒãĻいぞせん", "not_available": "éŠį”¨ãĒし", "not_in_any_album": "おぎã‚ĸãƒĢバムãĢもå…ĨãŖãĻいãĒい", "not_selected": "選択ãĒし", - "note_apply_storage_label_to_previously_uploaded assets": "æŗ¨æ„: äģĨ前ãĢã‚ĸップロãƒŧドしたã‚ĸã‚ģットãĢ゚トãƒŦãƒŧジナベãƒĢã‚’éŠį”¨ã™ã‚‹ãĢはäģĨä¸‹ã‚’åŽŸčĄŒã—ãĻください", "notes": "æŗ¨æ„", "nothing_here_yet": "ぞだäŊ•ã‚‚į„Ąã„ã‚ˆã†ã§ã™", "notification_permission_dialog_content": "通įŸĨã‚’č¨ąå¯ã™ã‚‹ãĢã¯č¨­åŽšã‚’é–‹ã„ãĻã‚ĒãƒŗãĢしãĻください", @@ -1509,6 +1657,7 @@ "other_variables": "そぎäģ–ぎ変数", "owned": "所有中", "owner": "ã‚Ēãƒŧナãƒŧ", + "page": "ペãƒŧジ", "partner": "パãƒŧトナãƒŧ", "partner_can_access": "{partner} がã‚ĸクã‚ģ゚できぞす", "partner_can_access_assets": "ã‚ĸãƒŧã‚Ģイブ済ãŋぎもぎと削除済ãŋぎもぎを除いた全ãĻãŽå†™įœŸã¨å‹•į”ģ", @@ -1541,6 +1690,7 @@ "people": "äēēį‰Š", "people_edits_count": "{count, plural, one {#äēē} other {#äēē}}ãŒįˇ¨é›†æ¸ˆ", "people_feature_description": "äēēį‰Šã§ã‚°ãƒĢãƒŧãƒ—åŒ–ã•ã‚ŒãŸå†™įœŸã¨å‹•į”ģã‚’é–˛čĻ§ã™ã‚‹", + "people_selected": "{count, plural, one {# äēēį‰Šã‚’é¸æŠžä¸­} other {# äēēį‰Šã‚’é¸æŠžä¸­}}", "people_sidebar_description": "äēēį‰Šã¸ãŽãƒĒãƒŗã‚¯ã‚’ã‚ĩイドバãƒŧãĢ襨į¤ē", "permanent_deletion_warning": "永䚅削除ぎč­Ļ告", "permanent_deletion_warning_setting_description": "ã‚ĸã‚ģットを厌全ãĢ削除するときãĢč­Ļå‘Šã‚’čĄ¨į¤ēする", @@ -1565,11 +1715,14 @@ "person_age_years": "{years, plural, other {# æ­ŗ}}", "person_birthdate": "{date}į”Ÿãžã‚Œ", "person_hidden": "{name}{hidden, select, true { (非表į¤ē)} other {}}", + "person_recognized": "äēēį‰ŠãŒčĒč­˜ã•ã‚ŒãĻいぞす", + "person_selected": "äēēį‰ŠãŒé¸æŠžã•ã‚ŒãĻいぞす", "photo_shared_all_users": "å†™įœŸã‚’ã™ãšãĻぎãƒĻãƒŧã‚ļãƒŧã¨å…ąæœ‰ã—ãŸã‹ã€å…ąæœ‰ã™ã‚‹ãƒĻãƒŧã‚ļãƒŧがいãĒいようです。", "photos": "å†™įœŸ", "photos_and_videos": "å†™įœŸã¨å‹•į”ģ", "photos_count": "{count, plural, one {{count, number}æžšãŽå†™įœŸ} other {{count, number}æžšãŽå†™įœŸ}}", "photos_from_previous_years": "äģĨå‰ãŽåš´ãŽå†™įœŸ", + "photos_only": "å†™įœŸãŽãŋ", "pick_a_location": "場所を選択", "pick_custom_range": "期間を指厚", "pick_date_range": "æ—Ĩäģ˜į¯„å›˛ãŽé¸æŠž", @@ -1645,10 +1798,12 @@ "purchase_settings_server_activated": "ã‚ĩãƒŧバãƒŧぎプロダクトキãƒŧã¯įŽĄį†č€…ãĢįŽĄį†ã•ã‚ŒãĻいぞす", "query_asset_id": "順į•Ēåž…ãĄãŽé …į›ŽID", "queue_status": "順į•Ēåž…ãĄä¸­ {count}/{total}", + "rate_asset": "é …į›Žã‚’čŠ•äžĄã™ã‚‹", "rating": "æ˜Ÿã§ãŽčŠ•äžĄ", "rating_clear": "čŠ•äžĄã‚’å–ã‚Šæļˆã™", "rating_count": "星{count, plural, one {#つ} other {#つ}}", "rating_description": "æƒ…å ąæŦ„ãĢEXIFãŽčŠ•äžĄã‚’čĄ¨į¤ē", + "rating_set": "お気ãĢå…ĨりåēĻ {rating, plural, one {# ツ星} other {# ツ星}}", "reaction_options": "ãƒĒã‚ĸã‚¯ã‚ˇãƒ§ãƒŗãŽé¸æŠž", "read_changelog": "変更åąĨ歴をčĒ­ã‚€", "readonly_mode_disabled": "čĒ­ãŋå–ã‚Šå°‚į”¨ãƒĸãƒŧãƒ‰į„ĄåŠš", @@ -1659,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {#個} other {#個}}ãŽå†™įœŸ/動į”ģを新しいäēēį‰ŠãĢå‰˛ã‚ŠåŊ“ãĻぞした", "reassing_hint": "é¸æŠžã•ã‚ŒãŸå†™įœŸ/動į”ģをæ—ĸ存ぎäēēį‰ŠãĢå‰˛ã‚ŠåŊ“ãĻ", "recent": "最čŋ‘", - "recent-albums": "最čŋ‘ぎã‚ĸãƒĢバム", + "recent_albums": "最čŋ‘ぎã‚ĸãƒĢバム", "recent_searches": "最čŋ‘ぎ検į´ĸ", "recently_added": "最čŋ‘čŋŊåŠ ã•ã‚ŒãŸé …į›Ž", "recently_added_page_title": "最čŋ‘", @@ -1748,9 +1903,11 @@ "saved_settings": "č¨­åŽšã‚’äŋå­˜ã—ぞした", "say_something": "äŊ•か書きčžŧãŋぞしょう", "scaffold_body_error_occurred": "エナãƒŧがį™ēį”Ÿã—ãžã—ãŸ", + "scan": "ã‚šã‚­ãƒŖãƒŗ", "scan_all_libraries": "全ãĻぎナイブナãƒĒã‚’ã‚šã‚­ãƒŖãƒŗ", "scan_library": "ã‚šã‚­ãƒŖãƒŗ", "scan_settings": "ã‚šã‚­ãƒŖãƒŗč¨­åŽš", + "scanning": "ã‚šã‚­ãƒŖãƒŗä¸­", "scanning_for_album": "ã‚ĸãƒĢãƒãƒ ã‚’ã‚šã‚­ãƒŖãƒŗä¸­â€Ļ", "search": "検į´ĸ", "search_albums": "ã‚ĸãƒĢバムを検į´ĸ", @@ -1780,6 +1937,7 @@ "search_filter_media_type_title": "ãƒĄãƒ‡ã‚Ŗã‚ĸãŽį¨ŽéĄžã‚’é¸æŠž", "search_filter_ocr": "OCRで検į´ĸ", "search_filter_people_title": "äēēį‰Šã‚’é¸æŠž", + "search_filter_star_rating": "æ˜ŸčŠ•äžĄ", "search_for": "検į´ĸ", "search_for_existing_person": "æ—ĸ存ぎäēēį‰Šã‚’æ¤œį´ĸ", "search_no_more_result": "検į´ĸįĩæžœäģĨ上", @@ -1814,17 +1972,23 @@ "second": "į§’", "see_all_people": "全ãĻぎäēēį‰Šã‚’čĻ‹ã‚‹", "select": "選択", + "select_album": "ã‚ĸãƒĢバム選択", "select_album_cover": "ã‚ĸãƒĢバムã‚Ģバãƒŧを選択", + "select_albums": "ã‚ĸãƒĢバム選択", "select_all": "全ãĻ選択", "select_all_duplicates": "全ãĻãŽé‡č¤‡ã‚’é¸æŠž", "select_all_in": "{group}ぎすずãĻを選択", "select_avatar_color": "ã‚ĸバã‚ŋãƒŧãŽč‰˛ã‚’é¸æŠž", + "select_count": "{count, plural, one {# 選択中} other {# 選択中}}", + "select_cutoff_date": "æ‰“ãĄåˆ‡ã‚ŠæœŸé–“ã‚’é¸æŠž", "select_face": "éĄ”ã‚’é¸æŠž", "select_featured_photo": "äēēį‰Šå†™įœŸã‚’é¸æŠž", "select_from_computer": "PCから選択", "select_keep_all": "全ãĻäŋæŒ", "select_library_owner": "ナイブナãƒĒæ‰€æœ‰č€…ã‚’é¸æŠž", "select_new_face": "æ–°ã—ã„éĄ”ã‚’é¸æŠž", + "select_people": "äēēį‰Šã‚’é¸æŠž", + "select_person": "äēēį‰Šã‚’é¸æŠž", "select_person_to_tag": "ã‚ŋグをäģ˜ã‘ã‚‹äēēį‰Šã‚’é¸ã‚“ã§ãã ã•ã„", "select_photos": "å†™įœŸã‚’é¸æŠž", "select_trash_all": "全ãĻ削除", @@ -1960,6 +2124,7 @@ "show_password": "パ゚ワãƒŧãƒ‰ã‚’čĄ¨į¤ē", "show_person_options": "äēēį‰Šč¨­åŽšã‚’čĄ¨į¤ē", "show_progress_bar": "プログãƒŦ゚バãƒŧã‚’čĄ¨į¤ē", + "show_schema": "゚キãƒŧãƒžã‚’čĄ¨į¤ē", "show_search_options": "検į´ĸã‚Ēãƒ—ã‚ˇãƒ§ãƒŗã‚’čĄ¨į¤ē", "show_shared_links": "å…ąæœ‰ãƒĒãƒŗã‚¯ã‚’čĄ¨į¤ē", "show_slideshow_transition": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧãŽãƒˆãƒŠãƒŗã‚¸ã‚ˇãƒ§ãƒŗã‚’čĄ¨į¤ē", @@ -1977,6 +2142,8 @@ "skip_to_folders": "フりãƒĢダぺキップ", "skip_to_tags": "ã‚ŋグぺキップ", "slideshow": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧ", + "slideshow_repeat": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧã‚’įš°ã‚Ščŋ”す", + "slideshow_repeat_description": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧがįĩ‚ã‚ãŖãŸã‚‰å§‹ã‚ãĢæˆģりぞす", "slideshow_settings": "ã‚šãƒŠã‚¤ãƒ‰ã‚ˇãƒ§ãƒŧč¨­åŽš", "sort_albums_by": "こぎ順åēã§ã‚ĸãƒĢバムをã‚Ŋãƒŧトâ€Ļ", "sort_created": "äŊœæˆæ—Ĩ", @@ -2053,6 +2220,7 @@ "theme_setting_theme_subtitle": "テãƒŧãƒžč¨­åŽš", "theme_setting_three_stage_loading_subtitle": "三æŽĩ階čĒ­ãŋčžŧãŋを有劚ãĢすると、パフりãƒŧãƒžãƒŗã‚šãŒæ”šå–„ã™ã‚‹å¯čƒŊ性がありぞすが、ネットワãƒŧã‚¯č˛ čˇãŒč‘—ã—ãåĸ—加しぞす。", "theme_setting_three_stage_loading_title": "三æŽĩ階čĒ­ãŋčžŧãŋをã‚ĒãƒŗãĢする", + "then": "そぎとき", "they_will_be_merged_together": "ã“ã‚Œã‚‰ã¯ä¸€įˇ’ãĢįĩąåˆã•れぞす", "third_party_resources": "ã‚ĩãƒŧドパãƒŧãƒ†ã‚ŖãƒŧãƒĒã‚Ŋãƒŧ゚", "time": "時åˆģ", @@ -2069,6 +2237,7 @@ "to_select": "選択", "to_trash": "ã‚´ãƒŸįŽą", "toggle_settings": "č¨­åŽšã‚’ãƒˆã‚°ãƒĢ", + "toggle_theme_description": "テãƒŧマを切りæ›ŋえ", "total": "合荈", "total_usage": "įˇäŊŋį”¨é‡", "trash": "ã‚´ãƒŸįŽą", @@ -2086,6 +2255,13 @@ "trash_page_select_assets_btn": "é …į›Žã‚’é¸æŠž", "trash_page_title": "ã‚´ãƒŸįŽą ({count})", "trashed_items_will_be_permanently_deleted_after": "ã‚´ãƒŸįŽąãĢå…Ĩれられたã‚ĸイテムは{days, plural, one {#æ—Ĩ} other {#æ—Ĩ}}垌ãĢ厌全ãĢ削除されぞす。", + "trigger": "トãƒĒã‚Ŧãƒŧ", + "trigger_asset_uploaded": "ã‚ĸã‚ģットがã‚ĸップロãƒŧド", + "trigger_asset_uploaded_description": "æ–°ã—ã„é …į›ŽãŒã‚ĸップロãƒŧドされたときãĢトãƒĒã‚Ŧãƒŧされぞす", + "trigger_description": "ワãƒŧクフロãƒŧã‚’é–‹å§‹ã™ã‚‹ã‚¤ãƒ™ãƒŗãƒˆ", + "trigger_person_recognized": "čĒč­˜ã•ã‚ŒãŸäēēį‰Š", + "trigger_person_recognized_description": "äēēį‰ŠãŒæ¤œįŸĨされた際ぎトãƒĒã‚Ŧãƒŧ", + "trigger_type": "トãƒĒã‚Ŧãƒŧã‚ŋイプ", "troubleshoot": "トナブãƒĢã‚ˇãƒĨãƒŧãƒ†ã‚Ŗãƒŗã‚°", "type": "ã‚ŋイプ", "unable_to_change_pin_code": "PINã‚ŗãƒŧドを変更できぞせんでした", @@ -2100,6 +2276,7 @@ "unhide_person": "äēēį‰ŠãŽéžčĄ¨į¤ēã‚’č§Ŗé™¤", "unknown": "不明", "unknown_country": "不明ãĒå›Ŋ", + "unknown_date": "不明ãĒæ—Ĩäģ˜", "unknown_year": "不明ãĒåš´", "unlimited": "į„Ąåˆļ限", "unlink_motion_video": "ãƒĸãƒŧã‚ˇãƒ§ãƒŗãƒ“ãƒ‡ã‚ĒぎãƒĒãƒŗã‚¯ã‚’č§Ŗé™¤", @@ -2116,17 +2293,19 @@ "unstack": "゚ã‚ŋãƒƒã‚¯ã‚’č§Ŗé™¤", "unstack_action_prompt": "{count}é …į›ŽãŽé‡ã­åˆã‚ã›ã‚’č§Ŗé™¤", "unstacked_assets_count": "{count, plural, one {#個} other {#個}}ãŽå†™įœŸ/動į”ģを゚ã‚ŋãƒƒã‚¯ã‹ã‚‰č§Ŗé™¤ã—ãžã—ãŸ", + "unsupported_field_type": "ã‚ĩポãƒŧトされãĻいãĒã„ãƒ•ã‚ŖãƒŧãƒĢドã‚ŋイプ", "untagged": "ã‚ŋã‚°ã‚’č§Ŗé™¤", + "untitled_workflow": "į„ĄéĄŒãŽãƒ¯ãƒŧクフロãƒŧ", "up_next": "æŦĄã¸", "update_location_action_prompt": "{count}é …į›Žã‚’åŗč¨˜ãŽäŊįŊŽæƒ…å ąãĢã‚ĸップデãƒŧトしぞす:", "updated_at": "更新", "updated_password": "パ゚ワãƒŧドを更新しぞした", "upload": "ã‚ĸップロãƒŧド", - "upload_action_prompt": "{count}é …į›ŽãŒã‚ĸップロãƒŧドぎ順į•Ēåž…ãĄä¸­", "upload_concurrency": "ã‚ĸップロãƒŧãƒ‰ãŽåŒæ™‚åŽŸčĄŒæ•°", "upload_details": "ã‚ĸップロãƒŧãƒ‰ãŽčŠŗį´°", "upload_dialog_info": "é¸æŠžã—ãŸé …į›ŽãŽãƒãƒƒã‚¯ã‚ĸップをしぞすかīŧŸ", "upload_dialog_title": "ã‚ĸップロãƒŧド", + "upload_error_with_count": "{count, plural, one {#å€‹ãŽå†™įœŸ/動į”ģ} other {#å€‹ãŽå†™įœŸ/動į”ģ}}ãĢついãĻã‚ĸップロãƒŧドエナãƒŧがį™ēį”Ÿã—ãžã—ãŸ", "upload_errors": "ã‚ĸップロãƒŧドは{count, plural, one {#個} other {#個}}ぎエナãƒŧで厌äē†ã—ぞした、新しくã‚ĸップロãƒŧドされたã‚ĸã‚ģットをčĻ‹ã‚‹ãĢはペãƒŧジを更新しãĻください。", "upload_finished": "ã‚ĸップロãƒŧド厌äē†", "upload_progress": "掋り {remaining, number} - {processed, number}/{total, number} å‡Ļį†æ¸ˆãŋ", @@ -2162,6 +2341,7 @@ "utilities": "ãƒĻãƒŧãƒ†ã‚ŖãƒĒãƒ†ã‚Ŗ", "validate": "čĒč¨ŧ", "validate_endpoint_error": "有劚ãĒURLをå…Ĩ力しãĻください", + "validation_error": "バãƒĒデãƒŧã‚ˇãƒ§ãƒŗã‚¨ãƒŠãƒŧ", "variables": "変数", "version": "バãƒŧã‚¸ãƒ§ãƒŗ", "version_announcement_closing": "あãĒたぎ友äēē、Alex", @@ -2173,10 +2353,12 @@ "video_hover_setting_description": "マã‚Ļã‚šãŒé …į›ŽãŽä¸ŠãĢあるときãĢ動į”ģぎã‚ĩムネイãƒĢã‚’å†į”Ÿã—ãžã™ã€‚į„ĄåŠšæ™‚ã§ã‚‚å†į”Ÿã‚ĸã‚¤ã‚ŗãƒŗãĢã‚Ģãƒŧã‚ŊãƒĢã‚’åˆã‚ã›ã‚‹ã¨å†į”Ÿã‚’é–‹å§‹ã§ããžã™ã€‚", "videos": "ビデã‚Ē", "videos_count": "{count, plural, one {#個} other {#個}}ぎ動į”ģ", + "videos_only": "動į”ģぎãŋ", "view": "čĻ‹ã‚‹", "view_album": "ã‚ĸãƒĢバムをčĻ‹ã‚‹", "view_all": "すずãĻčĻ‹ã‚‹", "view_all_users": "全ãĻぎãƒĻãƒŧã‚ļãƒŧをįĸēčĒã™ã‚‹", + "view_asset_owners": "ã‚ĸã‚ģãƒƒãƒˆãŽæ‰€æœ‰č€…ã‚’é–˛čϧ", "view_details": "čŠŗį´°ã‚’čĄ¨į¤ē", "view_in_timeline": "ã‚ŋã‚¤ãƒ ãƒŠã‚¤ãƒŗã§čĻ‹ã‚‹", "view_link": "ãƒĒãƒŗã‚¯ã‚’čĻ‹ã‚‹", @@ -2192,19 +2374,36 @@ "viewer_stack_use_as_main_asset": "ãƒĄã‚¤ãƒŗãŽį”ģ像としãĻäŊŋį”¨ã™ã‚‹", "viewer_unstack": "゚ã‚ŋãƒƒã‚¯ã‚’č§Ŗé™¤", "visibility_changed": "{count, plural, one {#äēē} other {#äēē}}ぎäēēį‰ŠãŽéžčĄ¨į¤ēč¨­åŽšãŒå¤‰æ›´ã•ã‚Œãžã—ãŸ", + "visual": "ビジãƒĨã‚ĸãƒĢ", + "visual_builder": "ビジãƒĨã‚ĸãƒĢビãƒĢダãƒŧ", "waiting": "垅抟中", + "waiting_count": "垅抟中: {count}", "warning": "č­Ļ告", "week": "週", "welcome": "ようこそ", "welcome_to_immich": "ImmichãĢようこそ", + "width": "åš…", "wifi_name": "Wi-Fiぎ名前(SSID)", - "workflow": "ワãƒŧクフロãƒŧ", + "workflow_delete_prompt": "こぎワãƒŧクフロãƒŧをãģんとうãĢ削除しぞすかīŧŸ", + "workflow_deleted": "ワãƒŧクフロãƒŧ削除厌äē†", + "workflow_description": "ワãƒŧクフロãƒŧぎčĒŦ明文", + "workflow_info": "ワãƒŧクフロãƒŧãŽæƒ…å ą", + "workflow_json": "ワãƒŧクフロãƒŧJSON", + "workflow_json_help": "JSONフりãƒŧマットでワãƒŧクフロãƒŧã‚’įˇ¨é›† (įˇ¨é›†å†…åŽšã¯ãƒ“ã‚¸ãƒĨã‚ĸãƒĢビãƒĢダãƒŧãĢも反映されぞす)", + "workflow_name": "ワãƒŧクフロãƒŧåį§°", + "workflow_navigation_prompt": "変更内厚をäŋå­˜ã›ãšãĢįĩ‚äē†ã—ぞすかīŧŸ", + "workflow_summary": "ワãƒŧクフロãƒŧぎã‚ĩマãƒĒ", + "workflow_update_success": "ワãƒŧクフロãƒŧぎ更新ãĢ成功しぞした", + "workflow_updated": "ワãƒŧクフロãƒŧが更新されぞした", + "workflows": "ワãƒŧクフロãƒŧ", + "workflows_help_text": "ワãƒŧクフロãƒŧはあãĒたぎã‚ĸã‚ģットãĢ寞し、トãƒĒã‚Ŧãƒŧã‚„ãƒ•ã‚ŖãƒĢã‚ŋãƒŧã‚’č¨­åŽšã™ã‚‹ã“ã¨ã§ã‚ĸã‚¯ã‚ˇãƒ§ãƒŗã‚’č‡Ē動化しぞす", "wrong_pin_code": "PINã‚ŗãƒŧãƒ‰ãŒé–“é•ãŖãĻいぞす", "year": "åš´", "years_ago": "{years, plural, one {#åš´} other {#åš´}}前", "yes": "はい", "you_dont_have_any_shared_links": "å…ąæœ‰ãƒĒãƒŗã‚¯ã¯ã‚ã‚Šãžã›ã‚“", "your_wifi_name": "Wi-Fiぎ名前(SSID)", + "zero_to_clear_rating": "0をæŠŧã™ã¨é …į›ŽãŽčŠ•äžĄã‚’å‰Šé™¤ã§ããžã™", "zoom_image": "į”ģåƒã‚’æ‹Ąå¤§", "zoom_to_bounds": "į”ģéĸįĢ¯ãžã§ã‚ēãƒŧム" } diff --git a/i18n/ka.json b/i18n/ka.json index dd15cdd721..f386a1e357 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -5,8 +5,10 @@ "acknowledge": "მიáƒĻება", "action": "áƒĨმედება", "action_common_update": "განაახლე", + "action_description": "მოáƒĨმედებები გაფილáƒĸáƒ áƒŖáƒš áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ”áƒ‘áƒ–áƒ”", "actions": "áƒĨმედებები", "active": "აáƒĨáƒĸáƒ˜áƒŖáƒ áƒ˜", + "active_count": "aáƒĨáƒĸáƒ˜áƒŖáƒ áƒ˜: {count}", "activity": "აáƒĨáƒĸივობა", "activity_changed": "აáƒĨáƒĸივობა {enabled, select, true {áƒŠáƒáƒ áƒ—áƒŖáƒšáƒ˜} other {áƒ’áƒáƒ›áƒáƒ áƒ—áƒŖáƒšáƒ˜}}", "add": "დაამაáƒĸე", @@ -14,9 +16,14 @@ "add_a_location": "დაამაáƒĸე ადგილი", "add_a_name": "დაამაáƒĸე სახელი", "add_a_title": "áƒ“áƒáƒáƒĄáƒáƒ—áƒáƒŖáƒ áƒ”", + "add_action": "დაამაáƒĸე მოáƒĨმედება", + "add_action_description": "დააჭირე რომ დაამაáƒĸო მოáƒĨმედება", + "add_assets": "áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ˜áƒĄ აáƒĸვირთვა", "add_birthday": "დაბადების დáƒĻიქ დამაáƒĸება", "add_endpoint": "ბოლოáƒŦერáƒĸილის დამაáƒĸება", "add_exclusion_pattern": "დაამაáƒĸე გამონაკლისი áƒœáƒ˜áƒ›áƒŖáƒ¨áƒ˜", + "add_filter": "დაამაáƒĸე ფილáƒĸრი", + "add_filter_description": "დააჭირე ფილáƒĸრიქ დასამაáƒĸებლად", "add_location": "დაამაáƒĸე ადგილი", "add_more_users": "დაამაáƒĸე მომხმარებლები", "add_partner": "დაამაáƒĸე პარáƒĸნიორი", @@ -27,8 +34,10 @@ "add_to_album": "დაამაáƒĸე ალბომში", "add_to_album_bottom_sheet_added": "დამაáƒĸáƒ”áƒ‘áƒŖáƒšáƒ˜áƒ {album}-ში", "add_to_album_bottom_sheet_already_exists": "{album}-ში áƒŖáƒ™áƒ•áƒ” არსებობს", + "add_to_album_bottom_sheet_some_local_assets": "ზოგიერთი áƒšáƒáƒ™áƒáƒšáƒŖáƒ áƒ˜ áƒ áƒ”áƒĄáƒŖáƒ áƒĄáƒ˜ ვერ დაემაáƒĸა ალბომში", "add_to_albums": "დაამაáƒĸე ალბომებში", "add_to_albums_count": "დაამაáƒĸე ალბომში ({count})", + "add_to_bottom_bar": "დამაáƒĸება სად", "add_to_shared_album": "დაამაáƒĸე საზიარო ალბომში", "add_url": "დაამაáƒĸე URL", "added_to_archive": "დაარáƒĨივდა", @@ -36,7 +45,7 @@ "added_to_favorites_count": "{count, number} დაემაáƒĸა áƒ áƒŠáƒ”áƒŖáƒšáƒ”áƒ‘áƒ¨áƒ˜", "admin": { "admin_user": "ადმინ მომხმარებელი", - "asset_offline_description": "ეს საგარეო ბიბლიოთეკის აáƒĨáƒĸივი დისკზე ვერ მოიáƒĢებნა და სანაგვეში იáƒĨნა áƒ›áƒáƒ—áƒáƒ•áƒĄáƒ”áƒ‘áƒŖáƒšáƒ˜. áƒ—áƒŖ ფაილი ბიბლიოთეკის შიგნით მდებარეობს, შეამოáƒŦმეთ შესაბამისი აáƒĨáƒĸივი áƒĸაიმლაინზე. ამ აáƒĨáƒĸივის აáƒĻსადგენად, დარáƒŦáƒ›áƒŖáƒœáƒ“áƒ˜áƒ— რომ áƒĨვემოთ მოáƒĒáƒ”áƒ›áƒŖáƒšáƒ˜ ფაილის მისამართი Immich-იქ მიერ áƒŦვდომადია და დაასკანერეთ ბიბლიოთეკა.", + "asset_offline_description": "ეს გარე ბიბლიოთეკის აáƒĨáƒĸივი დისკზე ვერ მოიáƒĢებნა და გადაáƒĸანილი იáƒĨნა ნაგვის áƒ§áƒŖáƒ—áƒ¨áƒ˜. áƒ—áƒŖ ფაილი ბიბლიოთეკის შიგნით იáƒĨნა გადაáƒĸანილი, შეამოáƒŦმეთ შესაბამისი აáƒĨáƒĸივი დროის ხაზზე. ამ აáƒĨáƒĸივის აáƒĻსადგენად, დარáƒŦáƒ›áƒŖáƒœáƒ“áƒ˜áƒ—, რომ áƒĨვემოთ მოáƒĒáƒ”áƒ›áƒŖáƒšáƒ˜ ფაილის მისამართი Immich-იქ მიერ áƒŦვდომადია და დაასკანერეთ ბიბლიოთეკა.", "authentication_settings": "ავთენáƒĸიკაáƒĒიიქ პარამეáƒĸრები", "authentication_settings_description": "პაროლის, OAuth-იქ და სხვა ავáƒĸენთიფიკაáƒĒიიქ პარამეáƒĸრების მართვა", "authentication_settings_disable_all": "ნამდვილად გინდა ავáƒĸორიზაáƒĒიიქ ყველა მეთოდის გამორთვა? ავáƒĸორიზაáƒĒიაქ ვეáƒĻარანაირად შეáƒĢლებ.", @@ -45,12 +54,13 @@ "backup_database": "ბაზის დამპის შეáƒĨმნა", "backup_database_enable_description": "ბაზის დამპების ჩართვა", "backup_keep_last_amount": "áƒŦინა დამპების áƒ¨áƒ”áƒĄáƒáƒœáƒáƒ áƒŠáƒŖáƒœáƒ”áƒ‘áƒ”áƒšáƒ˜ რაოდენობა", + "backup_onboarding_title": "მარáƒĨაფები", "backup_settings": "მონაáƒĒემთა ბაზის დამპის მორგება", - "backup_settings_description": "მონაáƒĒემთა ბაზის ასლის შეáƒĨმნის პარამეáƒĸრების მრთვა.", + "backup_settings_description": "მონაáƒĒემთა ბაზის დამპის პარამეáƒĸრების მართვა.", "cleared_jobs": "დავალებები {job}-ისათვის გაáƒŦმენდილია", "config_set_by_file": "მიმდინარე áƒ™áƒáƒœáƒ¤áƒ˜áƒ’áƒŖáƒ áƒáƒĒია ფაილის მიერ არიქ áƒ“áƒáƒ§áƒ”áƒœáƒ”áƒ‘áƒŖáƒšáƒ˜", "confirm_delete_library": "ნამდვილად გინდა {library} ბიბლიოთეკის áƒŦაშლა?", - "confirm_delete_library_assets": "მართლა áƒ’áƒĄáƒŖáƒ áƒ— ამ ბიბლიოთეკის áƒŦაშლა? ეს áƒĨმედება Immich-იდან áƒŦაშლის ყველა áƒ›áƒáƒœáƒ˜áƒ¨áƒœáƒŖáƒš აáƒĨáƒĸივს და áƒ¨áƒ”áƒŖáƒĨáƒĒევადია. ფაილები მყარ დისკზე áƒŽáƒ”áƒšáƒŖáƒŽáƒšáƒ”áƒ‘áƒ”áƒšáƒ˜ დარჩება.", + "confirm_delete_library_assets": "მართლა áƒ’áƒĄáƒŖáƒ áƒ— ამ ბიბლიოთეკის áƒŦაშლა? ეს áƒĨმედება Immich-იდან áƒŦაშლის{count, plural, one {# áƒáƒ áƒĄáƒ”áƒ‘áƒŖáƒš აáƒĨáƒĸივს} other {ყველა # áƒáƒ áƒ”áƒ‘áƒŖáƒš აáƒĨáƒĸივს}} და ეს áƒĨმედება áƒ¨áƒ”áƒŖáƒĨáƒĒევადია. ფაილები დისკზე áƒ¨áƒ”áƒœáƒáƒ áƒŠáƒŖáƒœáƒ”áƒ‘áƒŖáƒšáƒ˜ იáƒĨნება.", "confirm_email_below": "დასადასáƒĸáƒŖáƒ áƒ”áƒ‘áƒšáƒáƒ“, áƒĨვემოთ აკრიფე \"{email}\"", "confirm_reprocess_all_faces": "მართლა áƒ’áƒĄáƒŖáƒ áƒ— ყველა ქა჎იქ თავიდან áƒ“áƒáƒ›áƒŖáƒ¨áƒáƒ•áƒ”áƒ‘áƒ? ეს áƒĨმედება ხალხისათვის áƒ›áƒ˜áƒœáƒ˜áƒ­áƒ”áƒ‘áƒŖáƒš სახელებს გაáƒŦმენდს.", "confirm_user_password_reset": "ნამდვილად გინდა {user}-(ი)ქ პაროლის დარესეáƒĸება?", @@ -75,36 +85,67 @@ "library_settings": "გარე ბიბლიოთეკა", "library_settings_description": "გარე ბიბლიოთეკების პარამეáƒĸრების მართვა", "logging_settings": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜", + "machine_learning_ocr": "OCR", "map_settings": "áƒ áƒŖáƒ™áƒ", "migration_job": "მიგრაáƒĒია", + "notification_email_secure": "SMTPS", "oauth_settings": "OAuth", "template_email_preview": "მინიაáƒĸáƒŖáƒ áƒ", "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_hardware_acceleration": "áƒ°áƒáƒ áƒ“áƒ•áƒ”áƒáƒ áƒŖáƒšáƒ˜ ამაჩáƒĨარებელი", + "transcoding_policy": "áƒĸრანსკოდირების პოლიáƒĸიკა", "transcoding_threads": "ნაკადები", "transcoding_tone_mapping": "áƒĸონების ასახვა" }, "administration": "ადმინისáƒĸრაáƒĒია", "advanced": "დამაáƒĸებით", + "advanced_settings_troubleshooting_title": "პრობლემების გადაáƒŦყვეáƒĸა", + "album_info_card_backup_album_excluded": "ამოáƒĻáƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "album_info_card_backup_album_included": "áƒŠáƒáƒĄáƒ›áƒŖáƒšáƒ˜áƒ", "albums": "ალბომები", "all": "ყველა", + "allowed": "áƒ“áƒáƒ¨áƒ•áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", "anti_clockwise": "საათის იქრიქ ქაáƒŦინააáƒĻმდეგო", + "app_bar_signout_dialog_ok": "დიახ", "archive": "არáƒĨივი", + "archived": "დაარáƒĨáƒ˜áƒ•áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", "asset_hashing": "დაჰეშვა.â€Ļ", + "asset_list_layout_settings_group_automatically": "ავáƒĸომაáƒĸáƒŖáƒ áƒ˜", + "asset_list_layout_sub_title": "განლაგება", "asset_skipped": "გამოáƒĸáƒáƒ•áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", "asset_uploaded": "აáƒĸáƒ•áƒ˜áƒ áƒ—áƒŖáƒšáƒ˜áƒ", "asset_uploading": "მიმდინარეობს აáƒĸვირთვაâ€Ļ", "assets": "ობიეáƒĨáƒĸები", "back": "áƒŖáƒ™áƒáƒœ", + "backup": "მარáƒĨაფი", + "backup_all": "ყველა", + "backup_controller_page_background_battery_info_ok": "დიახ", + "backup_controller_page_backup": "მარáƒĨაფი", + "backup_controller_page_backup_selected": "áƒáƒ áƒŠáƒ”áƒŖáƒšáƒ˜áƒ: ", + "backup_controller_page_excluded": "ამოáƒĻáƒ”áƒ‘áƒŖáƒšáƒ˜áƒ: ", + "backup_controller_page_remainder": "დარჩენილია", + "backup_info_card_assets": "აáƒĨáƒĸივები", + "backup_manual_cancelled": "áƒ’áƒáƒŖáƒĨáƒ›áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "backup_manual_success": "áƒŦარმაáƒĸება", "backward": "áƒŖáƒ™áƒáƒœ გადასვლა", "build": "აგება", + "cache_settings_duplicated_assets_clear_button": "áƒ’áƒáƒĄáƒŖáƒ¤áƒ—áƒáƒ•áƒ”áƒ‘áƒ", + "cache_settings_statistics_thumbnail": "მინიაáƒĸáƒŖáƒ áƒ”áƒ‘áƒ˜", "camera": "კამერა", "cancel": "áƒ’áƒáƒŖáƒĨმება", + "canceled": "áƒ’áƒáƒŖáƒĨáƒ›áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "canceling": "áƒŖáƒĨმდება", + "cast": "áƒĸრანსლაáƒĒია", + "charging": "იáƒĸენება", "city": "áƒĨალაáƒĨი", "clear": "áƒ’áƒáƒĄáƒŖáƒ¤áƒ—áƒáƒ•áƒ”áƒ‘áƒ", + "client_cert_dialog_msg_confirm": "დიახ", + "client_cert_import": "შემოáƒĸანა", "clockwise": "საათის იქრიქ áƒ›áƒ˜áƒ›áƒáƒ áƒ—áƒŖáƒšáƒ”áƒ‘áƒ˜áƒ—", "close": "áƒ“áƒáƒŽáƒŖáƒ áƒ•áƒ", "collapse": "აკეáƒĒვა", "color": "ფერი", + "completed": "áƒ“áƒáƒĄáƒ áƒŖáƒšáƒ“áƒ", "confirm": "დასáƒĸáƒŖáƒ áƒ˜", "contain": "შეიáƒĒავს", "context": "კონáƒĸეáƒĨქáƒĸი", @@ -113,14 +154,21 @@ "cover": "ყდა", "covers": "ყდები", "create": "შეáƒĨმნა", + "create_album_page_untitled": "áƒŖáƒĄáƒáƒŽáƒ”áƒšáƒ", "created": "შეáƒĨმნილია", + "created_at": "შეიáƒĨმნა", + "crop": "ამოჭრა", + "curated_object_page_title": "ნივთები", "dark": "áƒ›áƒŖáƒĨი", + "date": "თარიáƒĻი", "day": "დáƒĻე", + "days": "დáƒĻე", "delete": "áƒŦაშლა", "description": "აáƒĻáƒŦერა", "details": "დეáƒĸალები", "direction": "áƒ›áƒ˜áƒ›áƒáƒ áƒ—áƒŖáƒšáƒ”áƒ‘áƒ", "disabled": "áƒ’áƒáƒ—áƒ˜áƒ¨áƒŖáƒšáƒ˜áƒ", + "discord": "Discord", "discover": "აáƒĻმოჩენა", "documentation": "áƒ“áƒáƒ™áƒŖáƒ›áƒ”áƒœáƒĸაáƒĒია", "done": "მზადაა", @@ -130,12 +178,18 @@ "duplicates": "áƒ“áƒŖáƒ‘áƒšáƒ˜áƒ™áƒáƒĸები", "duration": "ხანგრáƒĢლივობა", "edit": "჊აქáƒŦორება", + "edit_location_dialog_title": "მდებარეობა", "editor": "რედაáƒĨáƒĸორი", - "editor_crop_tool_h2_rotation": "áƒĸრიალი", "email": "ელფოსáƒĸა", "enable": "ჩართვა", "enabled": "áƒŠáƒáƒ áƒ—áƒŖáƒšáƒ˜áƒ", + "enqueued": "რიგში áƒŠáƒáƒĄáƒ›áƒŖáƒšáƒ˜áƒ", "error": "შეáƒĒდომა", + "exif": "Exif", + "exif_bottom_sheet_details": "დეáƒĸალები", + "exif_bottom_sheet_location": "მდებარეობა", + "exif_bottom_sheet_people": "ხალხი", + "experimental_settings_title": "ქაáƒĒდელი", "expired": "ვადაამოáƒŦáƒŖáƒ áƒŖáƒšáƒ˜áƒ", "explore": "დათვალიერება", "explorer": "გამáƒĒილებელი", @@ -143,37 +197,232 @@ "extension": "გაფართოება", "external": "გარე", "face_unassigned": "áƒ›áƒ˜áƒŖáƒœáƒ˜áƒ­áƒ”áƒ‘áƒ”áƒšáƒ˜", + "failed": "ჩავარდა", "favorite": "áƒ áƒŠáƒ”áƒŖáƒšáƒ˜", "favorites": "áƒ áƒŠáƒ”áƒŖáƒšáƒ”áƒ‘áƒ˜", "features": "თვისებები", "filename": "ფაილის სახელი", "filetype": "ფაილის áƒĸიპი", + "filter": "ფილáƒĸრი", + "first": "პირველი", + "folder": "ქაáƒĨაáƒĻალდე", "folders": "ქაáƒĨაáƒĻალდეები", "forward": "áƒŦინ", "general": "ზოგადი", + "gps": "GPS", + "hashing": "დაჰეშვა", "host": "ჰოსáƒĸი", "hour": "საათი", + "hours": "საათი", + "id": "ID", + "idle": "áƒŖáƒĨმე", "image": "áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ", "info": "ინფორმაáƒĒია", "jobs": "დავალებები", "keep": "áƒ¨áƒ”áƒœáƒáƒ áƒŠáƒŖáƒœáƒ”áƒ‘áƒ", "language": "ენა", + "last": "ბოლო", "latitude": "განედი", "leave": "გასვლა", "level": "დონე", "library": "ბიბლიოთეკა", + "licenses": "ლიáƒĒენზიები", "light": "áƒĻია", + "like": "მოáƒŦონება", "list": "ქია", "loading": "჊აáƒĸვირთვა", + "local": "áƒšáƒáƒ™áƒáƒšáƒŖáƒ áƒ˜", + "location": "მდებარეობა", + "lock": "დაბლოკვა", "login": "შესვლა", + "login_form_back_button_text": "áƒŖáƒ™áƒáƒœ", + "login_form_email_hint": "youremail@email.com", + "login_form_endpoint_hint": "http://your-server-ip:პორáƒĸი", + "login_form_password_hint": "პაროლი", + "logs": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜", "longitude": "გრáƒĢედი", "look": "შეხედვა", "make": "მáƒŦარმოებელი", "map": "áƒ áƒŖáƒ™áƒ", + "map_location_dialog_yes": "დიახ", "matches": "დამთხვევები", "memories": "მოგონებები", "memory": "მეხსიერება", "menu": "áƒ›áƒ”áƒœáƒ˜áƒŖ", "merge": "შერáƒŦყმა", - "minimize": "დაპაáƒĸარავება" + "minimize": "დაპაáƒĸარავება", + "minute": "áƒŦáƒŖáƒ—áƒ˜", + "minutes": "áƒŦáƒŖáƒ—áƒ˜", + "missing": "აკლია", + "model": "მოდელი", + "month": "თვე", + "more": "მეáƒĸი", + "move": "გადაáƒĸანა", + "name": "სახელი", + "navigate": "ნავიგაáƒĒია", + "networking_settings": "áƒĨსელი", + "never": "არასდროს", + "next": "შემდეგი", + "no": "არა", + "not_available": "N/A", + "notes": "შენშვნები", + "notifications": "გაფრთხილებები", + "oauth": "OAuth", + "ocr": "OCR", + "offline": "ინáƒĸერნეáƒĸიქ გარეშე", + "offset": "áƒŦანაáƒĒვლება", + "ok": "დიახ", + "onboarding": "áƒĄáƒáƒ›áƒŖáƒ¨áƒáƒáƒĄ დაáƒŦყება", + "online": "ხაზზეა", + "open": "გახსნა", + "options": "მორგება", + "or": "ან", + "original": "ორიგინალი", + "other": "სხვა", + "owned": "áƒĄáƒáƒ™áƒŖáƒ—áƒáƒ áƒ˜", + "owner": "მფლობელი", + "partner": "პარáƒĸნიორი", + "partners": "პარáƒĸნიორები", + "password": "პაროლი", + "path": "ბილიკი", + "pattern": "შაბლონი", + "pause": "áƒžáƒáƒŖáƒ–áƒ", + "paused": "áƒ“áƒáƒžáƒáƒŖáƒ–áƒ”áƒ‘áƒŖáƒšáƒ˜", + "pending": "რიგშია", + "people": "ხალხი", + "permission": "áƒŦვდომა", + "permission_onboarding_back": "áƒŖáƒ™áƒáƒœ", + "person": "პიროვნება", + "photos": "ფოáƒĸოები", + "place": "ადგილი", + "places": "ადგილები", + "play": "დაკვრა", + "port": "პორáƒĸი", + "preferences_settings_title": "მორგება", + "preparing": "მომზადება", + "preset": "პრესეáƒĸი", + "preview": "მინიაáƒĸáƒŖáƒ áƒ", + "previous": "áƒŦინა", + "primary": "áƒĢირითადი", + "privacy": "კონფიდენáƒĒიალობა", + "profile": "პროფილი", + "profile_drawer_app_logs": "áƒŸáƒŖáƒ áƒœáƒáƒšáƒ˜", + "profile_drawer_github": "GitHub", + "purchase_account_info": "მხარდამჭერი", + "purchase_button_activate": "გააáƒĨáƒĸáƒ˜áƒŖáƒ áƒ”áƒ‘áƒ", + "purchase_button_buy": "ყიდვა", + "purchase_button_select": "არჩევა", + "purchase_individual_title": "áƒ˜áƒœáƒ“áƒ˜áƒ•áƒ˜áƒ“áƒŖáƒáƒšáƒŖáƒ áƒ˜", + "purchase_server_title": "სერვერი", + "reassign": "თავიდან მინიჭება", + "recent": "áƒŖáƒáƒŽáƒšáƒ”áƒĄáƒ˜", + "refresh": "განახლება", + "refreshed": "áƒ’áƒáƒœáƒáƒŽáƒšáƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "remote": "áƒ“áƒáƒ¨áƒáƒ áƒ”áƒ‘áƒŖáƒšáƒ˜", + "remove": "áƒŦაშლა", + "rename": "სახელის გადარáƒĨმევა", + "repair": "შეკეთება", + "repository": "რეპოზიáƒĸორია", + "rescan": "თავიდან სკანირება", + "reset": "ჩამოყრა", + "resolution": "გაფართოება", + "restore": "აáƒĻდგენა", + "resume": "გაგრáƒĢელება", + "role": "როლი", + "role_editor": "რედაáƒĨáƒĸორი", + "role_viewer": "დამთვალიერებელი", + "running": "áƒ’áƒáƒ¨áƒ•áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "save": "შენახვა", + "saved": "áƒ¨áƒ”áƒœáƒáƒŽáƒŖáƒšáƒ˜áƒ", + "scan_library": "სკანირება", + "search": "áƒĢებნა", + "search_by_ocr_example": "ლაáƒĸე", + "search_filter_date": "თარიáƒĻი", + "search_filter_location": "მდებარეობა", + "search_page_categories": "კაáƒĸეგორიები", + "search_page_screenshots": "ეკრანის ანაბეჭდები", + "search_page_selfies": "სელფიები", + "search_page_things": "ნივთები", + "search_suggestion_list_smart_search_hint_2": "m:თáƒĨვენი-ქაáƒĢებნი-ქáƒĸრიáƒĨონი", + "second": "áƒŦამი", + "select": "აირჩიეთ", + "selected": "áƒáƒ áƒŠáƒ”áƒŖáƒšáƒ˜áƒ", + "set": "დაყენება", + "setting_image_viewer_title": "áƒ’áƒáƒ›áƒáƒĄáƒáƒŽáƒŖáƒšáƒ”áƒ‘áƒ”áƒ‘áƒ˜", + "setting_languages_apply": "გადაáƒĸარება", + "setting_notifications_notify_immediately": "áƒ“áƒáƒŖáƒ§áƒáƒ•áƒœáƒ”áƒ‘áƒšáƒ˜áƒ•", + "setting_notifications_notify_never": "არასდროს", + "setting_video_viewer_looping_title": "áƒŦáƒ áƒ˜áƒŖáƒšáƒáƒ“", + "settings": "მორგება", + "share": "გაზიარება", + "share_dialog_preparing": "მომზადება...", + "shared": "áƒ’áƒáƒ–áƒ˜áƒáƒ áƒ”áƒ‘áƒŖáƒšáƒ˜áƒ", + "shared_album_section_people_title": "ხალხი", + "shared_link_info_chip_metadata": "EXIF", + "sharing": "გაზიარებები", + "shuffle": "შემთხვევით", + "sidebar": "გვერდითი პანელი", + "size": "ზომა", + "slideshow": "áƒĄáƒšáƒáƒ˜áƒ“áƒ¨áƒáƒŖ", + "sort_title": "áƒĄáƒáƒ—áƒáƒŖáƒ áƒ˜", + "source": "áƒŦყარო", + "stack": "áƒ“áƒáƒ¯áƒ’áƒŖáƒ¤áƒ”áƒ‘áƒ", + "stacktrace": "áƒ¯áƒ’áƒŖáƒ¤áƒ˜áƒĄ áƒĸრეისი", + "start": "გაშვება", + "state": "მდგომარეობა", + "status": "ქáƒĸაáƒĸáƒŖáƒĄáƒ˜", + "submit": "გადაáƒĒემა", + "success": "áƒŦარმაáƒĸება", + "suggestions": "რჩევები", + "support": "მხარდაჭერა", + "sync": "სინáƒĨრონიზაáƒĒია", + "tag": "ჭდე", + "tags": "ჭდეები", + "template": "áƒœáƒ˜áƒ›áƒŖáƒ¨áƒ˜", + "theme": "თემა", + "time": "დრო", + "timeline": "áƒĨრონოლოგია", + "timezone": "დროის ქარáƒĸყელი", + "to_archive": "არáƒĨივი", + "to_favorite": "áƒ áƒŠáƒ”áƒŖáƒšáƒ˜", + "to_login": "შესვლა", + "to_trash": "ნაგვის áƒ§áƒŖáƒ—áƒ˜", + "total": "ჯამი", + "trash": "ნაგვის áƒ§áƒŖáƒ—áƒ˜", + "troubleshoot": "პრობლემების გადაჭრა", + "type": "áƒĸიპი", + "unarchive": "არáƒĨივიდან ამოáƒĻება", + "undo": "áƒ’áƒáƒŖáƒĨმება", + "unfavorite": "áƒ áƒŠáƒ”áƒŖáƒšáƒ”áƒ‘áƒ˜áƒ“áƒáƒœ áƒŦაშლა", + "unknown": "áƒŖáƒĒნობი", + "unlimited": "áƒ¨áƒ”áƒŖáƒ–áƒĻáƒŖáƒ“áƒáƒ•áƒ˜", + "unstack": "áƒ’áƒáƒœáƒ¯áƒ’áƒŖáƒ¤áƒ”áƒ‘áƒ", + "untagged": "ჭდის გარეშე", + "updated_at": "განახლდა", + "upload": "აáƒĸვირთვა", + "upload_status_duplicates": "áƒ“áƒŖáƒ‘áƒšáƒ˜áƒ™áƒáƒĸები", + "upload_status_errors": "შეáƒĒდომები", + "upload_status_uploaded": "აáƒĸáƒ•áƒ˜áƒ áƒ—áƒŖáƒšáƒ˜áƒ", + "uploading": "მიმდინარეობს აáƒĸვირთვა", + "url": "URL", + "usage": "გამოყენება", + "user": "მომხმარებელი", + "user_purchase_settings": "შეáƒĢენა", + "username": "მომხმარებლის სახელი", + "users": "მომხმარებლები", + "utilities": "ხელსაáƒŦყოები", + "validate": "გადამოáƒŦმება", + "variables": "áƒĒვლადები", + "version": "ვერსია", + "video": "ვიდეო", + "videos": "ვიდეოები", + "view": "დათვალიერება", + "view_name": "ხედი", + "viewer_unstack": "áƒ’áƒáƒœáƒ¯áƒ’áƒŖáƒ¤áƒ”áƒ‘áƒ", + "waiting": "მოლოდინი", + "warning": "გაფრთხილება", + "week": "კვირა", + "welcome": "მოგესალმებით", + "year": "áƒŦელი", + "yes": "დიახ" } diff --git a/i18n/kn.json b/i18n/kn.json index 6bef39c34c..f6dde7bf8f 100644 --- a/i18n/kn.json +++ b/i18n/kn.json @@ -5,8 +5,10 @@ "acknowledge": "➅➂➗⺀➕➰ā˛ŋ➏ā˛ŋ", "action": "ā˛•ā˛žā˛°āŗā˛¯", "action_common_update": "➍ā˛ĩ⺀➕➰ā˛ŋ➏ā˛ŋ", + "action_description": "ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗ ā˛Žāŗ‡ā˛˛āŗ† ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛Ŧāŗ‡ā˛•ā˛žā˛Ļ ā˛•āŗā˛°ā˛ŋ➝⺆➗➺ ā˛¸āŗ†ā˛Ÿāŗ", "actions": "ā˛•āŗā˛°ā˛ŋ➝⺆➗➺⺁", "active": "ā˛¸ā˛•āŗā˛°ā˛ŋ➝", + "active_count": "ā˛¸ā˛•āŗā˛°ā˛ŋ➝: {count}", "activity": "➚➟⺁ā˛ĩ➟ā˛ŋ➕⺆", "activity_changed": "➚➟⺁ā˛ĩ➟ā˛ŋ➕⺆ {enabled, select, true{ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†} other {➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†}}", "add": "➏⺇➰ā˛ŋ➏ā˛ŋ", @@ -14,15 +16,306 @@ "add_a_location": "ā˛¸āŗā˛Ĩ➺ā˛ĩā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_a_name": "ā˛šāŗ†ā˛¸ā˛°ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_a_title": "ā˛ļāŗ€ā˛°āŗā˛ˇā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_action": "ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_action_description": "➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏➞⺁ ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛•āŗā˛˛ā˛ŋā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "add_assets": "ā˛†ā˛¸āŗā˛¤ā˛ŋ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_birthday": "ā˛œā˛¨āŗā˛Žā˛Ļā˛ŋ➍ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_endpoint": "ā˛Žā˛‚ā˛Ąāŗâ€Œā˛Ēā˛žā˛¯ā˛ŋā˛‚ā˛Ÿāŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_exclusion_pattern": "ā˛šāŗŠā˛°ā˛—ā˛ŋā˛Ąāŗā˛ĩā˛ŋ➕⺆ ā˛Žā˛žā˛Ļ➰ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_filter": "ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_filter_description": "ā˛Ģā˛ŋā˛˛āŗā˛Ÿā˛°āŗ ā˛¸āŗā˛Ĩā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏➞⺁ ā˛•āŗā˛˛ā˛ŋā˛•āŗ ā˛Žā˛žā˛Ąā˛ŋ", "add_location": "ā˛¸āŗā˛Ĩ➺ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_more_users": "ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_partner": "ā˛Ēā˛žā˛˛āŗā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_path": "ā˛šā˛žā˛Ļā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_photos": "ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_tag": "ā˛Ÿāŗā˛¯ā˛žā˛—āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", "add_to": "➏⺇➰ā˛ŋ➏ā˛ŋâ€Ļ", "add_to_album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ➏⺇➰ā˛ŋ➏ā˛ŋ", - "add_to_album_bottom_sheet_added": "{album}➗⺆ ➏⺇➰ā˛ŋ➏ā˛ŋā˛Ļāŗ†" + "add_to_album_bottom_sheet_added": "{album} ➗⺆ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "add_to_album_bottom_sheet_already_exists": "ā˛ˆā˛—ā˛žā˛—ā˛˛āŗ‡ {album} ā˛¨ā˛˛āŗā˛˛ā˛ŋā˛Ļāŗ†", + "add_to_album_bottom_sheet_some_local_assets": "➕⺆➞ā˛ĩ⺁ ā˛¸āŗā˛Ĩ➺⺀➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ➏⺇➰ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—ā˛˛ā˛ŋā˛˛āŗā˛˛", + "add_to_album_toggle": "{album}ā˛—ā˛žā˛—ā˛ŋ ā˛†ā˛¯āŗā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ÿā˛žā˛—ā˛˛āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "add_to_albums": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_to_albums_count": "({count}) ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—ā˛ŗā˛ŋ➗⺆ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_to_bottom_bar": "➗⺆ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_to_shared_album": "ā˛šā˛‚ā˛šā˛ŋā˛Ļ ā˛†ā˛˛āŗā˛Ŧā˛Žāŗâ€Œā˛—āŗ† ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_upload_to_stack": "ā˛¸āŗā˛Ÿāŗā˛¯ā˛žā˛•āŗâ€Œā˛—āŗ† ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_url": "URL ➏⺇➰ā˛ŋ➏ā˛ŋ", + "add_workflow_step": "➕⺆➞➏ā˛Ļ ā˛šā˛°ā˛ŋā˛ĩā˛ŋ➍ ā˛šā˛‚ā˛¤ā˛ĩā˛¨āŗā˛¨āŗ ➏⺇➰ā˛ŋ➏ā˛ŋ", + "added_to_archive": "ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗâ€Œā˛—āŗ† ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "added_to_favorites": "ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛ĩ⺁➗➺ā˛ŋ➗⺆ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "added_to_favorites_count": "{count, number} ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛ĩ⺁➗➺ā˛ŋ➗⺆ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "admin": { + "admin_user": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°", + "asset_offline_description": "➈ ā˛Ŧā˛žā˛šāŗā˛¯ ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗ ā˛‡ā˛¨āŗā˛¨āŗ ā˛Žāŗā˛‚ā˛Ļāŗ† ā˛Ąā˛ŋā˛¸āŗā˛•āŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ā˛•ā˛‚ā˛Ąāŗā˛Ŧ➰⺁ā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛ ā˛Žā˛¤āŗā˛¤āŗ ➅ā˛Ļā˛¨āŗā˛¨āŗ ➅➍⺁ā˛Ēā˛¯āŗā˛•āŗā˛¤ā˛•āŗā˛•āŗ† ➏➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†. ā˛Ģāŗˆā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯āŗŠā˛ŗā˛—āŗ† ➏➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†ā˛¯ā˛žā˛Ļ➰⺆, ā˛šāŗŠā˛¸ ➅➍⺁➗⺁➪ā˛ĩā˛žā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛ŋā˛—ā˛žā˛—ā˛ŋ ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ÿāŗˆā˛Žāŗâ€Œā˛˛āŗˆā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ. ➈ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛¨ā˛ƒā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏➞⺁, ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ➕⺆➺➗ā˛ŋ➍ ā˛Ģāŗˆā˛˛āŗ ā˛Žā˛žā˛°āŗā˛—ā˛ĩā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛Ēāŗā˛°ā˛ĩāŗ‡ā˛ļā˛ŋ➏ā˛Ŧā˛šāŗā˛Ļ⺆➂ā˛Ļ⺁ ā˛–ā˛šā˛ŋ➤ā˛Ēā˛Ąā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ.", + "authentication_settings": "ā˛Ļ⺃ā˛ĸ⺀➕➰➪ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "authentication_settings_description": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ, ā˛’ā˛”ā˛¤āŗ ā˛Žā˛¤āŗā˛¤āŗ ➇➤➰ ā˛Ļ⺃ā˛ĸ⺀➕➰➪ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "authentication_settings_disable_all": "➍⺀ā˛ĩ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛ĩā˛ŋā˛§ā˛žā˛¨ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ➏➂ā˛Ēāŗ‚ā˛°āŗā˛Ŗā˛ĩā˛žā˛—ā˛ŋ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "authentication_settings_reenable": "ā˛Žā˛°āŗ-ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁, ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛•ā˛Žā˛žā˛‚ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ.", + "background_task_job": "ā˛šā˛ŋā˛¨āŗā˛¨āŗ†ā˛˛āŗ† ā˛•ā˛žā˛°āŗā˛¯ā˛—ā˛ŗāŗ", + "backup_database": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ąā˛‚ā˛Ēāŗ ➰➚ā˛ŋ➏ā˛ŋ", + "backup_database_enable_description": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ąā˛‚ā˛Ēāŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "backup_keep_last_amount": "ā˛šā˛ŋ➂ā˛Ļāŗ† ā˛‡ā˛Ąā˛Ŧāŗ‡ā˛•ā˛žā˛Ļ ā˛Ąā˛‚ā˛Ēāŗ ➗➺ ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗ", + "backup_onboarding_1_description": "ā˛•āŗā˛˛āŗŒā˛Ąāŗâ€Œā˛¨ā˛˛āŗā˛˛ā˛ŋ ➅ā˛Ĩā˛ĩā˛ž ā˛‡ā˛¨āŗā˛¨āŗŠā˛‚ā˛Ļ⺁ ➭⺌➤ā˛ŋ➕ ā˛¸āŗā˛Ĩ➺ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➆ā˛Ģāŗâ€Œā˛¸āŗˆā˛Ÿāŗ ➍➕➞⺁.", + "backup_onboarding_2_description": "ā˛ĩā˛ŋā˛ĩā˛ŋ➧ ā˛¸ā˛žā˛§ā˛¨ā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛¸āŗā˛Ĩ➺⺀➝ ā˛Ēāŗā˛°ā˛¤ā˛ŋ➗➺⺁. ➇ā˛Ļ⺁ ā˛Žāŗā˛–āŗā˛¯ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ➆ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗ ā˛¸āŗā˛Ĩ➺⺀➝ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "backup_onboarding_3_description": "ā˛Žāŗ‚ā˛˛ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛‚ā˛¤āŗ† ➍ā˛ŋā˛Žāŗā˛Ž ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ļ ā˛’ā˛Ÿāŗā˛Ÿāŗ ā˛Ēāŗā˛°ā˛¤ā˛ŋ➗➺⺁. ➇ā˛Ļā˛°ā˛˛āŗā˛˛ā˛ŋ 1 ➆ā˛Ģāŗâ€Œā˛¸āŗˆā˛Ÿāŗ ā˛Ēāŗā˛°ā˛¤ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ 2 ā˛¸āŗā˛Ĩ➺⺀➝ ā˛Ēāŗā˛°ā˛¤ā˛ŋ➗➺⺁ ➏⺇➰ā˛ŋā˛ĩāŗ†.", + "backup_onboarding_footer": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛žā˛Ąāŗā˛ĩ ā˛Ŧā˛—āŗā˛—āŗ† ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛—ā˛žā˛—ā˛ŋ, ā˛Ļ➝ā˛ĩā˛ŋā˛Ÿāŗā˛Ÿāŗ ā˛Ąā˛žā˛•āŗā˛¯āŗā˛Žāŗ†ā˛‚ā˛Ÿāŗ‡ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¨āŗ‹ā˛Ąā˛ŋ.", + "backup_onboarding_parts_title": "3-2-1 ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➇ā˛ĩāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛ŋā˛Ļāŗ†:", + "backup_onboarding_title": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗâ€Œā˛—ā˛ŗāŗ", + "backup_settings": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ąā˛‚ā˛Ēāŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "backup_settings_description": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ąā˛‚ā˛Ēāŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ.", + "cleared_jobs": "{job} ā˛—ā˛žā˛—ā˛ŋ ➉ā˛Ļāŗā˛¯āŗ‹ā˛—ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "config_set_by_file": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛Ģāŗˆā˛˛āŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛‚ā˛Ļā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "confirm_delete_library": "➍⺀ā˛ĩ⺁ {library} ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "confirm_email_below": "ā˛Ļ⺃ā˛ĸ⺀➕➰ā˛ŋ➏➞⺁, ➕⺆➺➗⺆ \"{email}\" ā˛Žā˛‚ā˛Ļ⺁ ➟⺈ā˛Ēāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "confirm_reprocess_all_faces": "➍⺀ā˛ĩ⺁ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Žāŗā˛–ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛Ēāŗā˛°ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž? ➇ā˛Ļ⺁ ā˛šāŗ†ā˛¸ā˛°ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ā˛œā˛¨ā˛°ā˛¨āŗā˛¨āŗ ā˛¸ā˛š ➤⺆➰ā˛ĩāŗā˛—āŗŠā˛ŗā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "confirm_user_password_reset": "➍⺀ā˛ĩ⺁ {user} ➅ā˛ĩ➰ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "confirm_user_pin_code_reset": "➍⺀ā˛ĩ⺁ {user} ➅ā˛ĩ➰ ā˛Ēā˛ŋā˛¨āŗ ā˛•āŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛šāŗŠā˛‚ā˛Ļā˛ŋ➏➞⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "copy_config_to_clipboard_description": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ JSON ➆ā˛Ŧāŗā˛œāŗ†ā˛•āŗā˛Ÿāŗ ➆➗ā˛ŋ ā˛•āŗā˛˛ā˛ŋā˛Ēāŗâ€Œā˛Ŧāŗ‹ā˛°āŗā˛Ąāŗâ€Œā˛—āŗ† ➍➕➞ā˛ŋ➏ā˛ŋ", + "create_job": "➉ā˛Ļāŗā˛¯āŗ‹ā˛— ➰➚ā˛ŋ➏ā˛ŋ", + "cron_expression_presets": "ā˛•āŗā˛°ā˛žā˛¨āŗ ➅➭ā˛ŋā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ ā˛Ēāŗ‚ā˛°āŗā˛ĩ➍ā˛ŋ➗ā˛Ļā˛ŋ➗➺⺁", + "disable_login": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "duplicate_detection_job_description": "➒➂ā˛Ļāŗ‡ ➰⺀➤ā˛ŋ➝ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēā˛¤āŗā˛¤āŗ†ā˛šā˛šāŗā˛šā˛˛āŗ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗ ā˛Žāŗ‡ā˛˛āŗ† ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛°ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ. ā˛¸āŗā˛Žā˛žā˛°āŗā˛Ÿāŗ ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿā˛ĩā˛¨āŗā˛¨āŗ ➅ā˛ĩ➞➂ā˛Ŧā˛ŋ➏ā˛ŋā˛Ļāŗ†", + "export_config_as_json_description": "ā˛Ēāŗā˛°ā˛¸āŗā˛¤āŗā˛¤ ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ JSON ā˛Ģāŗˆā˛˛āŗ ➆➗ā˛ŋ ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "external_libraries_page_description": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ā˛Ŧā˛žā˛šāŗā˛¯ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ ā˛Ē⺁➟", + "face_detection": "ā˛Žāŗā˛– ā˛Ēā˛¤āŗā˛¤āŗ†", + "failed_job_command": "{job} ā˛Žā˛‚ā˛Ŧ ā˛•āŗ†ā˛˛ā˛¸ā˛•āŗā˛•āŗ† {command} ā˛†ā˛œāŗā˛žāŗ† ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†", + "force_delete_user_warning": "ā˛Žā˛šāŗā˛šā˛°ā˛ŋ➕⺆: ➇ā˛Ļ⺁ ā˛Ŧ➺➕⺆ā˛Ļā˛žā˛°ā˛°ā˛¨āŗā˛¨āŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛˛āŗā˛˛ā˛ž ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¤ā˛•āŗā˛ˇā˛Ŗā˛ĩāŗ‡ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ➇ā˛Ļā˛¨āŗā˛¨āŗ ➰ā˛Ļāŗā˛Ļāŗā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛Ēā˛Ąāŗ†ā˛¯ā˛˛āŗ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛ŋā˛˛āŗā˛˛.", + "image_format": "ā˛¸āŗā˛ĩ➰⺂ā˛Ē", + "image_format_description": "WebP, JPEG ➗ā˛ŋ➂➤ ➚ā˛ŋā˛•āŗā˛• ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‰ā˛¤āŗā˛Ēā˛žā˛Ļā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†, ➆ā˛Ļ➰⺆ ā˛Žā˛¨āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛āŗ ➍ā˛ŋā˛§ā˛žā˛¨ā˛ĩā˛žā˛—ā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "image_fullsize_description": "ā˛āŗ‚ā˛Žāŗ ā˛‡ā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļā˛žā˛— ā˛Ŧā˛ŗā˛¸ā˛˛ā˛žā˛Ļ, ā˛¸āŗā˛Ÿāŗā˛°ā˛ŋā˛Ēāŗā˛Ąāŗ ā˛Žāŗ†ā˛Ÿā˛žā˛Ąāŗ‡ā˛Ÿā˛ž ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➰⺁ā˛ĩ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ➚ā˛ŋā˛¤āŗā˛°", + "image_fullsize_enabled": "ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ➚ā˛ŋā˛¤āŗā˛° ā˛°ā˛šā˛¨āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "image_fullsize_enabled_description": "ā˛ĩāŗ†ā˛Ŧāŗ ā˛¸āŗā˛¨āŗ‡ā˛šā˛ŋā˛¯ā˛˛āŗā˛˛ā˛Ļ ā˛¸āŗā˛ĩ➰⺂ā˛Ē➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ➚ā˛ŋā˛¤āŗā˛°ā˛ĩā˛¨āŗā˛¨āŗ ➰➚ā˛ŋ➏ā˛ŋ. \"ā˛Žā˛‚ā˛Ŧāŗ†ā˛Ąāŗ†ā˛Ąāŗ ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➆ā˛Ļāŗā˛¯ā˛¤āŗ† ā˛¨āŗ€ā˛Ąā˛ŋ\" ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļā˛žā˛—, ā˛Žā˛‚ā˛Ŧāŗ†ā˛Ąāŗ†ā˛Ąāŗ ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ĩā˛°āŗā˛¤ā˛¨āŗ† ā˛‡ā˛˛āŗā˛˛ā˛Ļāŗ† ➍⺇➰ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧā˛ŗā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†. JPEG ā˛¨ā˛‚ā˛¤ā˛š ā˛ĩāŗ†ā˛Ŧāŗ ā˛¸āŗā˛¨āŗ‡ā˛šā˛ŋ ā˛¸āŗā˛ĩ➰⺂ā˛Ē➗➺ ā˛Žāŗ‡ā˛˛āŗ† ā˛Ē➰ā˛ŋā˛Ŗā˛žā˛Ž ā˛Ŧ⺀➰⺁ā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "image_fullsize_quality_description": "1-100 ➰ā˛ĩ➰⺆➗ā˛ŋ➍ ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ➚ā˛ŋā˛¤āŗā˛°ā˛Ļ ā˛—āŗā˛Ŗā˛Žā˛Ÿāŗā˛Ÿ. ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛Ļ⺁ ā˛‰ā˛¤āŗā˛¤ā˛Ž, ➆ā˛Ļ➰⺆ ā˛ĻāŗŠā˛Ąāŗā˛Ą ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‰ā˛¤āŗā˛Ēā˛žā˛Ļā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "image_fullsize_title": "ā˛Ēāŗ‚ā˛°āŗā˛Ŗ-ā˛—ā˛žā˛¤āŗā˛°ā˛Ļ ➚ā˛ŋā˛¤āŗā˛° ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "image_prefer_embedded_preview": "ā˛Žā˛‚ā˛Ŧāŗ†ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛—āŗ† ➆ā˛Ļāŗā˛¯ā˛¤āŗ† ā˛¨āŗ€ā˛Ąā˛ŋ", + "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": "ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ† ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "image_progressive": "ā˛Ēāŗā˛°ā˛—ā˛¤ā˛ŋā˛Ē➰", + "image_progressive_description": "ā˛•āŗā˛°ā˛Žāŗ‡ā˛Ŗ ā˛˛āŗ‹ā˛Ąā˛ŋā˛‚ā˛—āŗ ā˛Ēāŗā˛°ā˛Ļā˛°āŗā˛ļā˛¨ā˛•āŗā˛•ā˛žā˛—ā˛ŋ JPEG ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛šā˛‚ā˛¤ā˛šā˛‚ā˛¤ā˛ĩā˛žā˛—ā˛ŋ ā˛Žā˛¨āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ. ➇ā˛Ļ⺁ WebP ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗ ā˛Žāŗ‡ā˛˛āŗ† ā˛¯ā˛žā˛ĩ⺁ā˛Ļāŗ‡ ā˛Ē➰ā˛ŋā˛Ŗā˛žā˛Ž ā˛Ŧ⺀➰⺁ā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "image_quality": "ā˛—āŗā˛Ŗā˛Žā˛Ÿāŗā˛Ÿ", + "image_resolution": "ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ļā˛¨āŗ", + "image_settings": "➚ā˛ŋā˛¤āŗā˛° ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "image_settings_description": "➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗ ā˛—āŗā˛Ŗā˛Žā˛Ÿāŗā˛Ÿ ā˛Žā˛¤āŗā˛¤āŗ ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "image_thumbnail_description": "ā˛Žāŗā˛–āŗā˛¯ ā˛Ÿāŗˆā˛Žāŗâ€Œā˛˛āŗˆā˛¨āŗâ€Œā˛¨ā˛‚ā˛¤ā˛š ā˛Ģāŗ‹ā˛Ÿāŗ‹ā˛—ā˛ŗ ➗⺁➂ā˛Ēāŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Ŧā˛ŗā˛¸ā˛˛ā˛žā˛—āŗā˛ĩ ā˛¸āŗā˛Ÿāŗā˛°ā˛ŋā˛Ēāŗā˛Ąāŗ ā˛Žāŗ†ā˛Ÿā˛žā˛Ąāŗ‡ā˛Ÿā˛ž ā˛šāŗŠā˛‚ā˛Ļā˛ŋ➰⺁ā˛ĩ ā˛¸ā˛Ŗāŗā˛Ŗ ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ", + "image_thumbnail_quality_description": "ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛—āŗā˛Ŗā˛Žā˛Ÿāŗā˛Ÿ 1 ➰ā˛ŋ➂ā˛Ļ 100 ➰ā˛ĩ➰⺆➗⺆. ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛Ļ⺁ ā˛‰ā˛¤āŗā˛¤ā˛Ž, ➆ā˛Ļ➰⺆ ā˛ĻāŗŠā˛Ąāŗā˛Ą ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‰ā˛¤āŗā˛Ēā˛žā˛Ļā˛ŋā˛¸āŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ➅ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Ēāŗā˛°ā˛¤ā˛ŋā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛•ā˛Ąā˛ŋā˛Žāŗ† ā˛Žā˛žā˛Ąāŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "image_thumbnail_title": "ā˛Ĩ➂ā˛Ŧāŗâ€Œā˛¨āŗ‡ā˛˛āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "import_config_from_json_description": "JSON ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛Ģāŗˆā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąāŗā˛ĩ ā˛Žāŗ‚ā˛˛ā˛• ➏ā˛ŋā˛¸āŗā˛Ÿā˛Žāŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋ➗➰⺇ā˛ļā˛¨āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛Ąā˛ŋ", + "job_concurrency": "{job} ā˛¸ā˛šā˛ĩā˛°āŗā˛¤ā˛ŋā˛¤āŗā˛ĩ", + "job_created": "➕⺆➞➏ā˛ĩā˛¨āŗā˛¨āŗ ➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "job_not_concurrency_safe": "➈ ➕⺆➞➏ā˛ĩ⺁ ā˛¸ā˛šā˛ĩā˛°āŗā˛¤ā˛ŋā˛¤āŗā˛ĩā˛•āŗā˛•āŗ† ā˛¸āŗā˛°ā˛•āŗā˛ˇā˛ŋ➤ā˛ĩā˛˛āŗā˛˛.", + "job_settings": "➕⺆➞➏ā˛Ļ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "job_settings_description": "➕⺆➞➏ā˛Ļ ā˛¸ā˛Žā˛•ā˛žā˛˛āŗ€ā˛¨ā˛¤āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "jobs_over_time": "ā˛•ā˛žā˛˛ā˛žā˛¨ā˛‚ā˛¤ā˛°ā˛Ļ ➉ā˛Ļāŗā˛¯āŗ‹ā˛—ā˛—ā˛ŗāŗ", + "library_created": "➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛Ļ ➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋ: {library}", + "library_deleted": "➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "library_details": "➞⺈ā˛Ŧāŗā˛°ā˛°ā˛ŋ➝ ā˛ĩā˛ŋā˛ĩ➰➗➺⺁", + "library_folder_description": "ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛Ąā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛˛āŗ ➒➂ā˛Ļ⺁ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛Ļā˛ŋā˛ˇāŗā˛Ÿā˛Ēā˛Ąā˛ŋ➏ā˛ŋ. ➉ā˛Ē ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛’ā˛ŗā˛—āŗŠā˛‚ā˛Ąā˛‚ā˛¤āŗ† ➈ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛ĩāŗ€ā˛Ąā˛ŋā˛¯āŗŠā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "library_remove_exclusion_pattern_prompt": "➈ ā˛šāŗŠā˛°ā˛—ā˛ŋā˛Ąāŗā˛ĩ ā˛Žā˛žā˛Ļ➰ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "library_remove_folder_prompt": "➈ ā˛†ā˛Žā˛Ļ⺁ ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗ ā˛…ā˛¨āŗā˛¨āŗ ➤⺆➗⺆ā˛Ļāŗā˛šā˛žā˛•ā˛˛āŗ ➍⺀ā˛ĩ⺁ ā˛–ā˛šā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ŧ➝➏⺁ā˛ĩā˛ŋā˛°ā˛ž?", + "library_scanning": "➆ā˛ĩā˛°āŗā˛¤ā˛• ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨ā˛ŋā˛‚ā˛—āŗ", + "library_scanning_description": "➆ā˛ĩā˛°āŗā˛¤ā˛• ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨ā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛•ā˛žā˛¨āŗā˛Ģā˛ŋā˛—ā˛°āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "library_scanning_enable_description": "➆ā˛ĩā˛°āŗā˛¤ā˛• ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨ā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "library_settings": "ā˛Ŧā˛žā˛šāŗā˛¯ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯", + "library_settings_description": "ā˛Ŧā˛žā˛šāŗā˛¯ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "library_tasks_description": "ā˛šāŗŠā˛¸ ā˛Žā˛¤āŗā˛¤āŗ/➅ā˛Ĩā˛ĩā˛ž ā˛Ŧā˛Ļā˛˛ā˛žā˛Ļ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Ŧā˛žā˛šāŗā˛¯ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛•āŗā˛¯ā˛žā˛¨āŗ ā˛Žā˛žā˛Ąā˛ŋ", + "library_updated": "➍ā˛ĩ⺀➕➰ā˛ŋ➏ā˛ŋā˛Ļ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯", + "library_watching_enable_description": "ā˛Ģāŗˆā˛˛āŗ ā˛Ŧā˛Ļā˛˛ā˛žā˛ĩ➪⺆➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Ŧā˛žā˛šāŗā˛¯ ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "library_watching_settings": "ā˛—āŗā˛°ā˛‚ā˛Ĩā˛žā˛˛ā˛¯ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ† [ā˛Ēāŗā˛°ā˛žā˛¯āŗ‹ā˛—ā˛ŋ➕]", + "library_watching_settings_description": "ā˛Ŧā˛Ļā˛˛ā˛žā˛Ļ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛ĩāŗ€ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "logging_enable_description": "ā˛˛ā˛žā˛—ā˛ŋā˛‚ā˛—āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "logging_level_description": "ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļā˛žā˛—, ā˛¯ā˛žā˛ĩ ā˛˛ā˛žā˛—āŗ ā˛Žā˛Ÿāŗā˛Ÿā˛ĩā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛Ŧ⺇➕⺁.", + "logging_settings": "ā˛˛ā˛žā˛—ā˛ŋā˛‚ā˛—āŗ", + "machine_learning_availability_checks": "ā˛˛ā˛­āŗā˛¯ā˛¤āŗ† ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆➗➺⺁", + "machine_learning_availability_checks_description": "ā˛˛ā˛­āŗā˛¯ā˛ĩā˛ŋ➰⺁ā˛ĩ ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋ➕⺆ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛ĩā˛¯ā˛‚ā˛šā˛žā˛˛ā˛ŋ➤ā˛ĩā˛žā˛—ā˛ŋ ā˛Ēā˛¤āŗā˛¤āŗ†ā˛šā˛šāŗā˛šā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➆ā˛Ļāŗā˛¯ā˛¤āŗ† ā˛¨āŗ€ā˛Ąā˛ŋ", + "machine_learning_availability_checks_enabled": "ā˛˛ā˛­āŗā˛¯ā˛¤āŗ† ā˛Ē➰ā˛ŋā˛ļāŗ€ā˛˛ā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_availability_checks_interval": "ā˛Žā˛§āŗā˛¯ā˛‚ā˛¤ā˛°ā˛ĩā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", + "machine_learning_availability_checks_interval_description": "ā˛˛ā˛­āŗā˛¯ā˛¤āŗ† ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆➗➺ ā˛¨ā˛Ąāŗā˛ĩā˛ŋ➍ ā˛Žā˛§āŗā˛¯ā˛‚ā˛¤ā˛° ā˛Žā˛ŋ➞ā˛ŋā˛¸āŗ†ā˛•āŗ†ā˛‚ā˛Ąāŗā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ", + "machine_learning_availability_checks_timeout": "ā˛ĩā˛ŋ➍➂➤ā˛ŋ ➅ā˛ĩ➧ā˛ŋ ā˛Žāŗ€ā˛°ā˛ŋā˛Ļāŗ†", + "machine_learning_availability_checks_timeout_description": "ā˛˛ā˛­āŗā˛¯ā˛¤āŗ† ā˛Ē➰ā˛ŋā˛ļ⺀➞➍⺆➗➺ā˛ŋā˛—ā˛žā˛—ā˛ŋ ā˛Žā˛ŋ➞ā˛ŋā˛¸āŗ†ā˛•āŗ†ā˛‚ā˛Ąāŗā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛¸ā˛Žā˛¯ ā˛Žāŗ€ā˛°ā˛ŋā˛Ļāŗ†", + "machine_learning_clip_model": "CLIP ā˛Žā˛žā˛Ļ➰ā˛ŋ", + "machine_learning_duplicate_detection": "➍➕➞⺁ ā˛Ēā˛¤āŗā˛¤āŗ†", + "machine_learning_duplicate_detection_enabled": "➍➕➞⺁ ā˛Ēā˛¤āŗā˛¤āŗ†ā˛šā˛šāŗā˛šāŗā˛ĩā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_duplicate_detection_enabled_description": "➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ➰⺆, ➍ā˛ŋ➖➰ā˛ĩā˛žā˛—ā˛ŋ ➒➂ā˛Ļāŗ‡ ➰⺀➤ā˛ŋ➝ ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛‡ā˛¨āŗā˛¨āŗ‚ ā˛Ąā˛ŋ-ā˛Ąāŗ‚ā˛Ēāŗā˛˛ā˛ŋ➕⺇ā˛ļā˛¨āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "machine_learning_duplicate_detection_setting_description": "ā˛¸ā˛‚ā˛­ā˛žā˛ĩāŗā˛¯ ā˛¨ā˛•ā˛˛āŗā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛•ā˛‚ā˛Ąāŗā˛šā˛ŋā˛Ąā˛ŋ➝➞⺁ CLIP ā˛Žā˛‚ā˛Ŧāŗ†ā˛Ąā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", + "machine_learning_enabled": "ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_enabled_description": "➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ➰⺆, ➕⺆➺➗ā˛ŋ➍ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛˛āŗ†ā˛•āŗā˛•ā˛ŋ➏ā˛Ļāŗ† ā˛Žā˛˛āŗā˛˛ā˛ž ML ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "machine_learning_facial_recognition": "ā˛Žāŗā˛– ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆", + "machine_learning_facial_recognition_description": "➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ ā˛Žāŗā˛–ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ēā˛¤āŗā˛¤āŗ† ā˛Žā˛žā˛Ąā˛ŋ, ➗⺁➰⺁➤ā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➗⺁➂ā˛Ē⺁ ā˛Žā˛žā˛Ąā˛ŋ", + "machine_learning_facial_recognition_model": "ā˛Žāŗā˛– ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆ ā˛Žā˛žā˛Ļ➰ā˛ŋ", + "machine_learning_facial_recognition_setting": "ā˛Žāŗā˛– ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_facial_recognition_setting_description": "➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ➰⺆, ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Žāŗā˛– ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋā˛•āŗ†ā˛—ā˛žā˛—ā˛ŋ ā˛Žā˛¨āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛ ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛•āŗā˛¸āŗâ€Œā˛Ēāŗā˛˛āŗ‹ā˛°āŗ ā˛Ē⺁➟ā˛Ļā˛˛āŗā˛˛ā˛ŋ ➜➍➰ ā˛ĩā˛ŋā˛­ā˛žā˛—ā˛ĩā˛¨āŗā˛¨āŗ ā˛­ā˛°āŗā˛¤ā˛ŋ ā˛Žā˛žā˛Ąāŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "machine_learning_max_detection_distance": "➗➰ā˛ŋā˛ˇāŗā˛  ā˛Ēā˛¤āŗā˛¤āŗ† ā˛Ļ⺂➰", + "machine_learning_max_recognition_distance": "➗➰ā˛ŋā˛ˇāŗā˛  ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆ ā˛Ļ⺂➰", + "machine_learning_min_detection_score": "➕➍ā˛ŋā˛ˇāŗā˛  ā˛Ēā˛¤āŗā˛¤āŗ† ā˛¸āŗā˛•āŗ‹ā˛°āŗ", + "machine_learning_min_recognized_faces": "➕➍ā˛ŋā˛ˇāŗā˛  ➗⺁➰⺁➤ā˛ŋā˛¸ā˛˛āŗā˛Ēā˛Ÿāŗā˛Ÿ ā˛Žāŗā˛–ā˛—ā˛ŗāŗ", + "machine_learning_ocr": "➓➏ā˛ŋā˛†ā˛°āŗ", + "machine_learning_ocr_description": "➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛˛āŗā˛˛ā˛ŋ➍ ā˛Ēā˛ āŗā˛¯ā˛ĩā˛¨āŗā˛¨āŗ ➗⺁➰⺁➤ā˛ŋ➏➞⺁ ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋā˛•āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋ", + "machine_learning_ocr_enabled": "OCR ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_ocr_enabled_description": "➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ➰⺆, ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗāŗ ā˛Ēā˛ āŗā˛¯ ➗⺁➰⺁➤ā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆➗⺆ ā˛’ā˛ŗā˛—ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "machine_learning_ocr_max_resolution": "➗➰ā˛ŋā˛ˇāŗā˛  ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ˇā˛¨āŗ", + "machine_learning_ocr_max_resolution_description": "➈ ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ˇā˛¨āŗ ā˛Žāŗ‡ā˛˛ā˛ŋ➍ ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛•ā˛žā˛° ➅➍⺁ā˛Ēā˛žā˛¤ā˛ĩā˛¨āŗā˛¨āŗ ā˛¸ā˛‚ā˛°ā˛•āŗā˛ˇā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Žā˛°āŗā˛—ā˛žā˛¤āŗā˛°ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛ŽāŗŒā˛˛āŗā˛¯ā˛—ā˛ŗāŗ ā˛šāŗ†ā˛šāŗā˛šāŗ ➍ā˛ŋ➖➰ā˛ĩā˛žā˛—ā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛ĩāŗ†, ➆ā˛Ļ➰⺆ ā˛Ēāŗā˛°ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛Žā˛¤āŗā˛¤āŗ ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛Žāŗ†ā˛ŽāŗŠā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏➞⺁ ā˛šāŗ†ā˛šāŗā˛šāŗ ā˛¸ā˛Žā˛¯ ➤⺆➗⺆ā˛Ļāŗā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "machine_learning_ocr_min_detection_score": "➕➍ā˛ŋā˛ˇāŗā˛  ā˛Ēā˛¤āŗā˛¤āŗ† ā˛¸āŗā˛•āŗ‹ā˛°āŗ", + "machine_learning_ocr_min_recognition_score": "➕➍ā˛ŋā˛ˇāŗā˛  ā˛Ąā˛ŋā˛Ÿāŗ†ā˛•āŗā˛ˇā˛¨āŗ ➅➂➕", + "machine_learning_ocr_model": "OCR ā˛Žā˛žā˛Ąāŗ†ā˛˛āŗ", + "machine_learning_ocr_model_description": "ā˛¸ā˛°āŗā˛ĩā˛°āŗ ā˛Žāŗ‹ā˛Ąāŗ†ā˛˛āŗā˛—ā˛ŗāŗ ā˛ŽāŗŠā˛Ŧāŗˆā˛˛āŗ ā˛Žāŗ‹ā˛Ąāŗ†ā˛˛āŗā˛—ā˛ŗā˛ŋ➗ā˛ŋ➂➤ ā˛šāŗ†ā˛šāŗā˛šāŗ ➍ā˛ŋ➖➰ā˛ĩā˛žā˛—ā˛ŋā˛°āŗā˛¤āŗā˛¤ā˛ĩāŗ†, ➆ā˛Ļ➰⺆ ā˛šāŗ†ā˛šāŗā˛šā˛ŋ➍ ā˛Žāŗ†ā˛ŽāŗŠā˛°ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛•āŗā˛°ā˛ŋā˛¯āŗ†ā˛—āŗŠā˛ŗā˛ŋ➏➞⺁ ā˛Žā˛¤āŗā˛¤āŗ ā˛Ŧ➺➏➞⺁ ā˛šāŗ†ā˛šāŗā˛šāŗ ā˛¸ā˛Žā˛¯ ➤⺆➗⺆ā˛Ļāŗā˛•āŗŠā˛ŗāŗā˛ŗāŗā˛¤āŗā˛¤ā˛ĩāŗ†.", + "machine_learning_settings": "ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋ➕⺆ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "machine_learning_settings_description": "ā˛¯ā˛‚ā˛¤āŗā˛° ➕➞ā˛ŋ➕⺆ ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "machine_learning_smart_search": "ā˛¸āŗā˛Žā˛žā˛°āŗā˛Ÿāŗ ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿ", + "machine_learning_smart_search_description": "CLIP ā˛Žā˛‚ā˛Ŧāŗ†ā˛Ąā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ŧ➺➏ā˛ŋā˛•āŗŠā˛‚ā˛Ąāŗ ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛…ā˛°āŗā˛Ĩā˛Ēāŗ‚ā˛°āŗā˛Ŗā˛ĩā˛žā˛—ā˛ŋ ā˛šāŗā˛Ąāŗā˛•ā˛ŋ", + "machine_learning_smart_search_enabled": "ā˛¸āŗā˛Žā˛žā˛°āŗā˛Ÿāŗ ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿā˛ĩā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "machine_learning_smart_search_enabled_description": "➍ā˛ŋā˛ˇāŗā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋā˛Ļ➰⺆, ➚ā˛ŋā˛¤āŗā˛°ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸āŗā˛Žā˛žā˛°āŗā˛Ÿāŗ ā˛šāŗā˛Ąāŗā˛•ā˛žā˛Ÿā˛•āŗā˛•ā˛žā˛—ā˛ŋ ā˛Žā˛¨āŗâ€Œā˛•āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛.", + "maintenance_delete_backup": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➅➺ā˛ŋ➏ā˛ŋ", + "maintenance_delete_backup_description": "➈ ā˛Ģāŗˆā˛˛āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛ļā˛žā˛ļāŗā˛ĩ➤ā˛ĩā˛žā˛—ā˛ŋ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "maintenance_delete_error": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➅➺ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†.", + "maintenance_restore_backup": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏ā˛ŋ", + "maintenance_restore_backup_description": "ā˛†ā˛¯āŗā˛•āŗ† ā˛Žā˛žā˛Ąā˛ŋā˛Ļ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗâ€Œā˛¨ā˛ŋ➂ā˛Ļ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ➅➺ā˛ŋ➏ā˛ŋā˛šā˛žā˛•ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ† ā˛Žā˛¤āŗā˛¤āŗ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†. ā˛Žāŗā˛‚ā˛Ļ⺁ā˛ĩ➰ā˛ŋ➝⺁ā˛ĩ ā˛ŽāŗŠā˛Ļ➞⺁ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛…ā˛¨āŗā˛¨āŗ ➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛Ļāŗ†.", + "maintenance_restore_backup_different_version": "➈ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗâ€Œā˛¨ ā˛ĩā˛ŋ➭ā˛ŋā˛¨āŗā˛¨ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋā˛¯āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ➰➚ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†!", + "maintenance_restore_backup_unknown_version": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ➆ā˛ĩāŗƒā˛¤āŗā˛¤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛§ā˛°ā˛ŋ➏➞⺁ ā˛¸ā˛žā˛§āŗā˛¯ā˛ĩā˛žā˛—ā˛˛ā˛ŋā˛˛āŗā˛˛.", + "maintenance_restore_database_backup": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Žā˛°āŗā˛¸āŗā˛Ĩā˛žā˛Ēā˛ŋ➏ā˛ŋ", + "maintenance_restore_database_backup_description": "ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Ģāŗˆā˛˛āŗ ā˛Ŧ➺➏ā˛ŋ ā˛šā˛ŋ➂ā˛Ļā˛ŋ➍ ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛¸āŗā˛Ĩā˛ŋ➤ā˛ŋ➗⺆ ā˛šā˛ŋ➂➤ā˛ŋ➰⺁➗ā˛ŋ", + "maintenance_settings": "➍ā˛ŋā˛°āŗā˛ĩā˛šā˛Ŗāŗ†", + "maintenance_settings_description": "ā˛‡ā˛Žāŗā˛Žā˛ŋā˛šāŗ ā˛…ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛Ŗā˛ž ā˛•āŗā˛°ā˛Žā˛•āŗā˛•āŗ† ➇➰ā˛ŋ➏ā˛ŋ.", + "maintenance_start": "➍ā˛ŋā˛°āŗā˛ĩā˛šā˛Ŗā˛ž ā˛Žāŗ‹ā˛Ąāŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏ā˛ŋ", + "maintenance_start_error": "➍ā˛ŋā˛°āŗā˛ĩā˛šā˛Ŗā˛ž ā˛•āŗā˛°ā˛Žā˛ĩā˛¨āŗā˛¨āŗ ā˛Ēāŗā˛°ā˛žā˛°ā˛‚ā˛­ā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†.", + "maintenance_upload_backup": "ā˛Ąāŗ‡ā˛Ÿā˛žā˛Ŧāŗ‡ā˛¸āŗ ā˛Ŧāŗā˛¯ā˛žā˛•ā˛Ēāŗ ā˛Ģāŗˆā˛˛āŗ ➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "manage_concurrency": "ā˛ā˛•ā˛•ā˛žā˛˛ā˛ŋā˛•ā˛¤āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "manage_concurrency_description": "➉ā˛Ļāŗā˛¯āŗ‹ā˛— ā˛¸ā˛Žā˛•ā˛žā˛˛āŗ€ā˛¨ā˛¤āŗ†ā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏➞⺁ ➉ā˛Ļāŗā˛¯āŗ‹ā˛—ā˛—ā˛ŗ ā˛Ēāŗā˛Ÿā˛•āŗā˛•āŗ† ā˛¨āŗā˛¯ā˛žā˛ĩā˛ŋā˛—āŗ‡ā˛Ÿāŗ ā˛Žā˛žā˛Ąā˛ŋ", + "manage_log_settings": "ā˛˛ā˛žā˛—āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "map_dark_style": "ā˛Ąā˛žā˛°āŗā˛•āŗ ā˛ļ⺈➞ā˛ŋ", + "map_enable_description": "ā˛¨ā˛•āŗā˛ˇāŗ† ā˛ĩ⺈ā˛ļā˛ŋā˛ˇāŗā˛Ÿāŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "map_gps_settings": "ā˛¨ā˛•āŗā˛ˇāŗ† ā˛Žā˛¤āŗā˛¤āŗ GPS ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "map_reverse_geocoding": "➰ā˛ŋā˛ĩā˛°āŗā˛¸āŗ ➜ā˛ŋā˛¯āŗ‹ā˛•āŗ‹ā˛Ąā˛ŋā˛‚ā˛—āŗ", + "map_reverse_geocoding_enable_description": "➰ā˛ŋā˛ĩā˛°āŗā˛¸āŗ ➜ā˛ŋā˛¯āŗ‹ā˛•āŗ‹ā˛Ąā˛ŋā˛‚ā˛—āŗ ā˛…ā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "map_reverse_geocoding_settings": "➰ā˛ŋā˛ĩā˛°āŗā˛¸āŗ ➜ā˛ŋā˛¯āŗ‹ā˛•āŗ‹ā˛Ąā˛ŋā˛‚ā˛—āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "map_settings": "ā˛¨ā˛•āŗā˛ˇāŗ†", + "map_settings_description": "ā˛¨ā˛•āŗā˛ˇāŗ† ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "memory_cleanup_job": "ā˛Žāŗ†ā˛ŽāŗŠā˛°ā˛ŋ ā˛¸āŗā˛ĩā˛šāŗā˛›ā˛—āŗŠā˛ŗā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆", + "memory_generate_job": "ā˛¸āŗā˛Žāŗƒā˛¤ā˛ŋ ā˛‰ā˛¤āŗā˛Ēā˛žā˛Ļ➍⺆", + "metadata_extraction_job": "ā˛Žāŗ†ā˛Ÿā˛žā˛Ąāŗ‡ā˛Ÿā˛žā˛ĩā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛°ā˛¤āŗ†ā˛—āŗ†ā˛¯ā˛ŋ➰ā˛ŋ", + "metadata_extraction_job_description": "GPS, ā˛Žāŗā˛–ā˛—ā˛ŗāŗ ā˛Žā˛¤āŗā˛¤āŗ ā˛°āŗ†ā˛¸ā˛˛āŗā˛¯āŗ‚ā˛ļā˛¨āŗâ€Œā˛¨ā˛‚ā˛¤ā˛š ā˛Ēāŗā˛°ā˛¤ā˛ŋ ā˛¸āŗā˛ĩā˛¤āŗā˛¤ā˛ŋ➍ā˛ŋ➂ā˛Ļ ā˛Žāŗ†ā˛Ÿā˛žā˛Ąāŗ‡ā˛Ÿā˛ž ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ā˛šāŗŠā˛°ā˛¤āŗ†ā˛—āŗ†ā˛¯ā˛ŋ➰ā˛ŋ", + "metadata_faces_import_setting": "ā˛Žāŗā˛– ā˛†ā˛Žā˛Ļ⺁ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "metadata_faces_import_setting_description": "ā˛‡ā˛Žāŗ‡ā˛œāŗ EXIF ā˛Ąāŗ‡ā˛Ÿā˛ž ā˛Žā˛¤āŗā˛¤āŗ ā˛¸āŗˆā˛Ąāŗâ€Œā˛•ā˛žā˛°āŗ ā˛Ģāŗˆā˛˛āŗâ€Œā˛—ā˛ŗā˛ŋ➂ā˛Ļ ā˛Žāŗā˛–ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛†ā˛Žā˛Ļ⺁ ā˛Žā˛žā˛Ąā˛ŋ", + "metadata_settings": "ā˛Žāŗ†ā˛Ÿā˛žā˛Ąāŗ‡ā˛Ÿā˛ž ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "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_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_cannot_be_changed_later": "ā˛—ā˛Žā˛¨ā˛ŋ➏ā˛ŋ: ➇ā˛Ļā˛¨āŗā˛¨āŗ ➍➂➤➰ ā˛Ŧā˛Ļā˛˛ā˛žā˛¯ā˛ŋā˛¸ā˛˛ā˛žā˛—āŗā˛ĩ⺁ā˛Ļā˛ŋā˛˛āŗā˛˛!", + "notification_email_from_address": "ā˛ĩā˛ŋā˛ŗā˛žā˛¸ā˛Ļā˛ŋ➂ā˛Ļ", + "notification_email_ignore_certificate_errors": "ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛Ļāŗ‹ā˛ˇā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛˛ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ", + "notification_email_ignore_certificate_errors_description": "TLS ā˛Ēāŗā˛°ā˛Žā˛žā˛Ŗā˛Ēā˛¤āŗā˛° ā˛ŽāŗŒā˛˛āŗā˛¯āŗ€ā˛•ā˛°ā˛Ŗ ā˛Ļāŗ‹ā˛ˇā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛˛ā˛•āŗā˛ˇā˛ŋ➏ā˛ŋ (ā˛ļā˛ŋā˛Ģā˛žā˛°ā˛¸āŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛˛āŗā˛˛)", + "notification_email_password_description": "ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨āŗŠā˛‚ā˛Ļā˛ŋ➗⺆ ā˛Ļ⺃ā˛ĸ⺀➕➰ā˛ŋ➏⺁ā˛ĩā˛žā˛— ā˛Ŧ➺➏ā˛Ŧāŗ‡ā˛•ā˛žā˛Ļ ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ", + "notification_email_port_description": "ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛¸ā˛°āŗā˛ĩā˛°āŗâ€Œā˛¨ ā˛Ēāŗ‹ā˛°āŗā˛Ÿāŗ (➉ā˛Ļā˛ž. 25, 465, ➅ā˛Ĩā˛ĩā˛ž 587)", + "notification_email_secure": "ā˛Žā˛¸āŗâ€Œā˛Žā˛‚ā˛Ÿā˛ŋā˛Ēā˛ŋā˛Žā˛¸āŗ", + "notification_email_sent_test_email_button": "ā˛Ēā˛°āŗ€ā˛•āŗā˛ˇā˛ž ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛•ā˛ŗāŗā˛šā˛ŋ➏ā˛ŋ ā˛Žā˛¤āŗā˛¤āŗ ➉➺ā˛ŋ➏ā˛ŋ", + "notification_email_setting_description": "ā˛‡ā˛Žāŗ‡ā˛˛āŗ ➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛•ā˛ŗāŗā˛šā˛ŋ➏➞⺁ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "notification_email_test_email": "ā˛Ēā˛°āŗ€ā˛•āŗā˛ˇā˛ž ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛•ā˛ŗāŗā˛šā˛ŋ➏ā˛ŋ", + "notification_email_test_email_failed": "ā˛Ēā˛°āŗ€ā˛•āŗā˛ˇā˛ž ā˛‡ā˛Žāŗ‡ā˛˛āŗ ā˛•ā˛ŗāŗā˛šā˛ŋ➏➞⺁ ā˛ĩā˛ŋā˛Ģ➞ā˛ĩā˛žā˛—ā˛ŋā˛Ļāŗ†, ➍ā˛ŋā˛Žāŗā˛Ž ā˛ŽāŗŒā˛˛āŗā˛¯ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛Ē➰ā˛ŋā˛ļ⺀➞ā˛ŋ➏ā˛ŋ", + "notification_enable_email_notifications": "ā˛‡ā˛Žāŗ‡ā˛˛āŗ ➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ†ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "notification_settings": "➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ† ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗāŗ", + "notification_settings_description": "ā˛‡ā˛Žāŗ‡ā˛˛āŗ ➏⺇➰ā˛ŋā˛Ļ➂➤⺆ ➅➧ā˛ŋā˛¸āŗ‚ā˛šā˛¨āŗ† ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "oauth_auto_launch": "ā˛¸āŗā˛ĩ➝➂ ā˛‰ā˛Ąā˛žā˛ĩ➪⺆", + "oauth_storage_quota_claim": "ā˛¸ā˛‚ā˛—āŗā˛°ā˛šā˛Ŗāŗ† ā˛•āŗ‹ā˛Ÿā˛ž ā˛šā˛•āŗā˛•āŗ", + "password_settings": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ", + "password_settings_description": "ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ ā˛¸āŗ†ā˛Ÿāŗā˛Ÿā˛ŋā˛‚ā˛—āŗâ€Œā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➍ā˛ŋā˛°āŗā˛ĩā˛šā˛ŋ➏ā˛ŋ", + "paths_validated_successfully": "ā˛Žā˛˛āŗā˛˛ā˛ž ā˛Žā˛žā˛°āŗā˛—ā˛—ā˛ŗā˛¨āŗā˛¨āŗ ➝ā˛ļā˛¸āŗā˛ĩā˛ŋā˛¯ā˛žā˛—ā˛ŋ ā˛ŽāŗŒā˛˛āŗā˛¯āŗ€ā˛•ā˛°ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "person_cleanup_job": "ā˛ĩāŗā˛¯ā˛•āŗā˛¤ā˛ŋ ā˛ļ⺁➚ā˛ŋā˛—āŗŠā˛ŗā˛ŋ➏⺁ā˛ĩā˛ŋ➕⺆", + "queue_details": "➏➰ā˛Ļā˛ŋ ā˛ĩā˛ŋā˛ĩ➰➗➺⺁", + "queues": "➕⺆➞➏ā˛Ļ ➏➰➤ā˛ŋ ā˛¸ā˛žā˛˛āŗā˛—ā˛ŗāŗ", + "queues_page_description": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ➕⺆➞➏ā˛Ļ ➏➰➤ā˛ŋ ā˛Ē⺁➟", + "template_email_preview": "ā˛Ēāŗ‚ā˛°āŗā˛ĩā˛ĩāŗ€ā˛•āŗā˛ˇā˛Ŗāŗ†", + "transcoding_tone_mapping": "ā˛Ÿāŗ‹ā˛¨āŗ-ā˛Žāŗā˛¯ā˛žā˛Ēā˛ŋā˛‚ā˛—āŗ" + }, + "admin_email": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ā˛‡ā˛Žāŗ‡ā˛˛āŗ", + "admin_password": "➍ā˛ŋā˛°āŗā˛ĩā˛žā˛šā˛• ā˛Ēā˛žā˛¸āŗâ€Œā˛ĩā˛°āŗā˛Ąāŗ", + "administration": "ā˛†ā˛Ąā˛ŗā˛ŋ➤", + "advanced": "ā˛¸āŗā˛§ā˛žā˛°ā˛ŋ➤", + "album": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ", + "album_added": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➏⺇➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "album_cover_updated": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➕ā˛ĩā˛°āŗ ➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "album_deleted": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ➅➺ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "album_info_updated": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋā˛¯ā˛¨āŗā˛¨āŗ ➍ā˛ĩ⺀➕➰ā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "album_name": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛šāŗ†ā˛¸ā˛°āŗ", + "album_summary": "ā˛†ā˛˛āŗā˛Ŧā˛Žāŗ ā˛¸ā˛žā˛°ā˛žā˛‚ā˛ļ", + "albums": "ā˛†ā˛˛āŗā˛Ŧ➂➗➺⺁", + "all": "ā˛Žā˛˛āŗā˛˛ā˛ĩāŗ‚", + "anti_clockwise": "➅ā˛Ēāŗā˛°ā˛Ļā˛•āŗā˛ˇā˛ŋā˛Ŗā˛žā˛•ā˛žā˛°ā˛ĩā˛žā˛—ā˛ŋ", + "archive": "ā˛†ā˛°āŗā˛•āŗˆā˛ĩāŗ", + "asset_uploaded": "➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "asset_uploading": "➅ā˛Ēāŗâ€Œā˛˛āŗ‹ā˛Ąāŗ ā˛Žā˛žā˛Ąā˛˛ā˛žā˛—āŗā˛¤āŗā˛¤ā˛ŋā˛Ļāŗ†â€Ļ", + "assets": "ā˛¸āŗā˛ĩā˛¤āŗā˛¤āŗā˛—ā˛ŗāŗ", + "back": "ā˛šā˛ŋ➂ā˛Ļāŗ†", + "backward": "ā˛šā˛ŋ➂ā˛Ļā˛•āŗā˛•āŗ†", + "build": "➍ā˛ŋā˛°āŗā˛Žā˛žā˛Ŗ", + "camera": "ā˛•āŗā˛¯ā˛žā˛Žāŗ†ā˛°ā˛ž", + "cancel": "➰ā˛Ļāŗā˛Ļāŗā˛Žā˛žā˛Ąā˛ŋ", + "city": "➍➗➰", + "close": "ā˛Žāŗā˛šāŗā˛šā˛ŋ", + "collapse": "ā˛•āŗā˛—āŗā˛—ā˛ŋ➏⺁", + "color": "ā˛Ŧā˛Ŗāŗā˛Ŗ", + "confirm": "ā˛Ļ⺃ā˛ĸ⺀➕➰ā˛ŋ➏ā˛ŋ", + "context": "➏➂ā˛Ļā˛°āŗā˛­", + "continue": "ā˛Žāŗā˛‚ā˛Ļ⺁ā˛ĩ➰ā˛ŋ➏ā˛ŋ", + "country": "ā˛Ļāŗ‡ā˛ļ", + "cover": "➕ā˛ĩā˛°āŗ", + "covers": "➕ā˛ĩā˛°āŗâ€Œā˛—ā˛ŗāŗ", + "create": "➰➚ā˛ŋ➏ā˛ŋ", + "dark": "ā˛•ā˛¤āŗā˛¤ā˛˛āŗ", + "day": "ā˛Ļā˛ŋ➍", + "delete": "➅➺ā˛ŋ➏ā˛ŋ", + "description": "ā˛ĩā˛ŋā˛ĩ➰➪⺆", + "details": "ā˛ĩā˛ŋā˛ĩ➰➗➺⺁", + "direction": "➍ā˛ŋā˛°āŗā˛Ļāŗ‡ā˛ļ➍", + "documentation": "ā˛Ļā˛¸āŗā˛¤ā˛žā˛ĩāŗ‡ā˛œāŗ€ā˛•ā˛°ā˛Ŗ", + "done": "ā˛Žāŗā˛—ā˛ŋā˛Ļā˛ŋā˛Ļāŗ†", + "download": "ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ", + "download_settings": "ā˛ĄāŗŒā˛¨āŗâ€Œā˛˛āŗ‹ā˛Ąāŗ", + "duration": "➅ā˛ĩ➧ā˛ŋ", + "email": "ā˛‡ā˛Žāŗ†āŗ•ā˛˛āŗ", + "enable": "ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋ➏ā˛ŋ", + "enabled": "ā˛¸ā˛•āŗā˛°ā˛ŋā˛¯ā˛—āŗŠā˛ŗā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛Ļāŗ†", + "error": "ā˛Ļ⺋➎", + "exif": "ā˛Žā˛•āŗā˛¸ā˛ŋā˛Ģāŗ", + "face_unassigned": "➍ā˛ŋā˛¯āŗ‹ā˛œā˛ŋā˛¸ā˛˛ā˛žā˛—ā˛ŋā˛˛āŗā˛˛", + "favorites": "ā˛Žāŗ†ā˛šāŗā˛šā˛ŋ➍ā˛ĩ⺁➗➺⺁", + "filename": "ā˛Ģāŗˆā˛˛āŗ ā˛šāŗ†ā˛¸ā˛°āŗ", + "filetype": "ā˛Ģāŗˆā˛˛āŗ ā˛Ēāŗā˛°ā˛•ā˛žā˛°", + "folders": "ā˛Ģāŗ‹ā˛˛āŗā˛Ąā˛°āŗâ€Œā˛—ā˛ŗāŗ", + "forward": "ā˛Žāŗā˛‚ā˛Ļāŗ†", + "general": "ā˛œā˛¨ā˛°ā˛˛āŗ", + "host": "ā˛šāŗ‹ā˛¸āŗā˛Ÿāŗ", + "hour": "ā˛—ā˛‚ā˛Ÿāŗ†", + "image": "➚ā˛ŋā˛¤āŗā˛°", + "info": "ā˛Žā˛žā˛šā˛ŋ➤ā˛ŋ", + "jobs": "➉ā˛Ļāŗā˛¯āŗ‹ā˛—ā˛—ā˛ŗāŗ", + "keep": "➇➰ā˛ŋ➏ā˛ŋā˛•āŗŠā˛ŗāŗā˛ŗā˛ŋ", + "language": "ā˛­ā˛žā˛ˇāŗ†", + "leave": "ā˛Ŧā˛ŋā˛Ąā˛ŋ", + "level": "ā˛Žā˛Ÿāŗā˛Ÿ", + "light": "ā˛Ŧ⺆➺➕⺁", + "list": "ā˛Ēā˛Ÿāŗā˛Ÿā˛ŋ", + "login": "ā˛˛ā˛žā˛—ā˛ŋā˛¨āŗ", + "make": "ā˛Žā˛žā˛Ąā˛ŋ", + "map": "ā˛¨ā˛•āŗā˛ˇāŗ†", + "memories": "➍⺆➍ā˛Ē⺁➗➺⺁", + "memory": "➍⺆➍ā˛Ē⺁" } diff --git a/i18n/ko.json b/i18n/ko.json index d13416684b..5449bf1e44 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -5,6 +5,7 @@ "acknowledge": "í™•ė¸", "action": "ėž‘ė—…", "action_common_update": "ė—…ë°ė´íŠ¸", + "action_description": "필터링된 ėžė‚°ė— 대해 ėˆ˜í–‰í•  ėŧë ¨ė˜ ėž‘ė—…", "actions": "ėž‘ė—…", "active": "í™œė„ą", "active_count": "í™œė„ą: {count}", @@ -15,9 +16,14 @@ "add_a_location": "ėœ„ėš˜ ėļ”ę°€", "add_a_name": "ė´ëĻ„ ėļ”ę°€", "add_a_title": "렜ëĒŠ ėļ”ę°€", + "add_action": "ėž‘ė—… ėļ”ę°€", + "add_action_description": "클ëĻ­í•˜ė—Ŧ ėˆ˜í–‰í•  ėž‘ė—…ė„ ėļ”ę°€í•˜ė„¸ėš”", + "add_assets": "항ëĒŠ ėļ”ę°€", "add_birthday": "ėƒėŧ ėļ”ę°€", "add_endpoint": "ė—”ë“œíŦė¸íŠ¸ ėļ”ę°€", "add_exclusion_pattern": "ė œė™¸ ęˇœėš™ ėļ”ę°€", + "add_filter": "필터 ėļ”ę°€", + "add_filter_description": "필터 ėĄ°ęą´ė„ ėļ”ę°€í•˜ë ¤ëŠ´ 클ëĻ­í•˜ė„¸ėš”", "add_location": "ėœ„ėš˜ ėļ”ę°€", "add_more_users": "다ëĨ¸ ė‚ŦėšŠėž ėļ”ę°€", "add_partner": "파트너 ėļ”ę°€", @@ -36,6 +42,7 @@ "add_to_shared_album": "ęŗĩ뜠 ė•¨ë˛”ė— ėļ”ę°€", "add_upload_to_stack": "ėŠ¤íƒė— 항ëĒŠ ė—…ëĄœë“œ", "add_url": "URL ėļ”ę°€", + "add_workflow_step": "ė›ŒíŦ플로 ë‹¨ęŗ„ ėļ”ę°€", "added_to_archive": "ëŗ´ę´€í•¨ėœŧ로 ė´ë™ë˜ė—ˆėŠĩ니다.", "added_to_favorites": "ėĻę˛¨ė°žę¸°ė— ėļ”ę°€ë˜ė—ˆėŠĩ니다.", "added_to_favorites_count": "ėĻę˛¨ė°žę¸°ė— 항ëĒŠ {count, number}氜 ėļ”가됨", @@ -77,6 +84,7 @@ "duplicate_detection_job_description": "揰溄 학ėŠĩėœŧ로 뜠ė‚Ŧ한 ė´ë¯¸ė§€ëĨŧ ę°ė§€í•Šë‹ˆë‹¤. ėŠ¤ë§ˆíŠ¸ ę˛€ėƒ‰ė´ í™œė„ąí™”ë˜ė–´ ėžˆė–´ė•ŧ 합니다.", "exclusion_pattern_description": "ëŧė´ë¸ŒëŸŦëĻŦ 늤ėē”ė—ė„œ ė œė™¸í•  파ėŧė´ë‚˜ 폴더 ęˇœėš™ė„ ė„¤ė •í•Šë‹ˆë‹¤. í´ë”ė— ė›í•˜ė§€ ė•ŠëŠ” 파ėŧ(RAW 파ėŧ 등)ė´ 함ęģ˜ ėĄ´ėžŦ하는 ę˛Ŋ뚰 ėœ ėšŠí•Šë‹ˆë‹¤.", "export_config_as_json_description": "현ėžŦ ė‹œėŠ¤í…œ ęĩŦė„ąė„ JSON 파ėŧ로 ë‹¤ėš´ëĄœë“œí•Šë‹ˆë‹¤.", + "external_libraries_page_description": "뙏ëļ€ ëŧė´ë¸ŒëŸŦëĻŦ íŽ˜ė´ė§€ 관ëĻŦ", "face_detection": "ė–ŧęĩ´ 氐맀", "face_detection_description": "揰溄 학ėŠĩėœŧ로 항ëĒŠė—ė„œ ė–ŧęĩ´ė„ ę°ė§€í•Šë‹ˆë‹¤. ë™ė˜ėƒė˜ ę˛Ŋ뚰 ė„Ŧ네ėŧ만 ëļ„ė„ė— ė‚ŦėšŠëŠë‹ˆë‹¤. \"ėƒˆëĄœęŗ ėš¨\"ė€ ëĒ¨ë“  항ëĒŠė„ (ėžŦ)래ëĻŦ하며, \"ė´ˆę¸°í™”\"는 현ėžŦ ëĒ¨ë“  ė–ŧęĩ´ ë°ė´í„°ëĨŧ ėļ”ę°€ëĄœ ė‚­ė œí•Šë‹ˆë‹¤. \"누ëŊ\"ė€ 땄링 래ëĻŦë˜ė§€ ė•Šė€ 항ëĒŠė„ ëŒ€ę¸°ė—´ė— ėļ”ę°€í•Šë‹ˆë‹¤. ė–ŧęĩ´ 氐맀氀 ė™„ëŖŒë˜ëŠ´ ė–ŧęĩ´ ė¸ė‹ ë‹¨ęŗ„ëĄœ ë„˜ė–´ę°€ ę¸°ėĄ´ ė¸ëŦŧė´ë‚˜ ėƒˆëĄœėš´ ė¸ëŦŧ로 ęˇ¸ëŖší™”í•Šë‹ˆë‹¤.", "facial_recognition_job_description": "ę°ė§€ëœ ė–ŧęĩ´ė„ ė¸ëŦŧëŗ„ëĄœ ęˇ¸ëŖší™”í•Šë‹ˆë‹¤. ė´ ėž‘ė—…ė€ ė–ŧęĩ´ 氐맀 ėž‘ė—…ė´ ė™„ëŖŒëœ 후 ė§„í–‰ëŠë‹ˆë‹¤. \"ė´ˆę¸°í™”\"는 ëĒ¨ë“  ė–ŧęĩ´ė„ ë‹¤ė‹œ ęˇ¸ëŖší™”í•Šë‹ˆë‹¤. \"누ëŊ\"ė€ ęˇ¸ëŖší™”ë˜ė§€ ė•Šė€ ė–ŧęĩ´ė„ ëŒ€ę¸°ė—´ė— ėļ”ę°€í•Šë‹ˆë‹¤.", @@ -96,6 +104,8 @@ "image_preview_description": "ëŠ”íƒ€ë°ė´í„°ę°€ ė œęą°ëœ 뤑氄 íŦ기 ė´ë¯¸ė§€. 揰溄 학ėŠĩ 또는 ę°œëŗ„ 항ëĒŠė„ í‘œė‹œí•  때 ė‚ŦėšŠëŠë‹ˆë‹¤.", "image_preview_quality_description": "미ëĻŦëŗ´ę¸°ė˜ í’ˆė§ˆė„ 1ė—ė„œ 100 ė‚Ŧė´ëĄœ ė„¤ė •í•Šë‹ˆë‹¤. ę°’ė„ ë†’ė´ëŠ´ í’ˆė§ˆė´ ėĸ‹ė•„ė§€ė§€ë§Œ 파ėŧ íŦ기가 ėģ¤ė§€ęŗ  ė•ą ë°˜ė‘ ė†ë„ę°€ ëŠë ¤ė§ˆ 눘 ėžˆėŠĩ니다. 너ëŦ´ ë‚Žė€ ę°’ė€ 揰溄 학ėŠĩ뗐 똁í–Ĩė„ 뤄 눘 ėžˆėŠĩ니다.", "image_preview_title": "미ëĻŦëŗ´ę¸° 네렕", + "image_progressive": "렐맄렁 로딩", + "image_progressive_description": "JPEG ė´ë¯¸ė§€ëĨŧ 렐맄렁ėœŧ로 í‘œė‹œí•  눘 ėžˆę˛Œ ë‹¨ęŗ„ė ėœŧ로 ė¸ėŊ”딊핊니다. WebP ė´ë¯¸ė§€ė—ëŠ” 똁í–Ĩė´ ė—†ėŠĩ니다.", "image_quality": "í’ˆė§ˆ", "image_resolution": "í•´ėƒë„", "image_resolution_description": "í•´ėƒë„ę°€ 높ėœŧ늴 넏ëļ€ ė •ëŗ´ę°€ ëŗ´ėĄ´ë˜ė§€ë§Œ, ė¸ėŊ”ë”Šė— 더 ë§Žė€ ė‹œę°„ė´ ė†Œėš”ë˜ęŗ  파ėŧ íŦ기가 ėģ¤ė ¸ ė•ą ë°˜ė‘ ė†ë„ę°€ ëŠë ¤ė§ˆ 눘 ėžˆėŠĩ니다.", @@ -112,11 +122,13 @@ "job_settings_description": "각 ėž‘ė—…ė—ė„œ ë™ė‹œė— 래ëĻŦ할 항ëĒŠ 눘ëĨŧ ė§€ė •í•Šë‹ˆë‹¤.", "jobs_delayed": "{jobCount, plural, other {#氜}} 맀뗰", "jobs_failed": "{jobCount, plural, other {#氜}} ė‹¤íŒ¨", + "jobs_over_time": "ėž‘ė—… ë§ŒëŖŒ ė‹œę°„", "library_created": "{library} ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ėƒė„ąí–ˆėŠĩ니다.", "library_deleted": "ëŧė´ë¸ŒëŸŦëĻŦ가 ė‚­ė œë˜ė—ˆėŠĩ니다.", "library_details": "ëŧė´ë¸ŒëŸŦëĻŦ ėƒė„¸", "library_folder_description": "氀렏ė˜Ŧ 폴더ëĨŧ ė§€ė •í•Šë‹ˆë‹¤. 해당 폴더ëĨŧ íŦ함한 ëĒ¨ë“  í•˜ėœ„ í´ë”ė—ė„œ ė´ë¯¸ė§€ 및 ë™ė˜ėƒė„ 늤ėē”핊니다.", "library_remove_exclusion_pattern_prompt": "ė´ ė œė™¸ ęˇœėš™ė„ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", + "library_remove_folder_prompt": "ė´ ę°€ė ¸ė˜¤ę¸° 폴더ëĨŧ ė •ë§ëĄœ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", "library_scanning": "ėŖŧę¸°ė ė¸ 늤ėē”", "library_scanning_description": "ėŖŧę¸°ė ė¸ ëŧė´ë¸ŒëŸŦëĻŦ 늤ėē”ė„ ęĩŦė„ąí•Šë‹ˆë‹¤.", "library_scanning_enable_description": "ėŖŧę¸°ė ė¸ ëŧė´ë¸ŒëŸŦëĻŦ 늤ėē” í™œė„ąí™”", @@ -178,7 +190,23 @@ "machine_learning_smart_search_enabled": "ėŠ¤ë§ˆíŠ¸ ę˛€ėƒ‰ í™œė„ąí™”", "machine_learning_smart_search_enabled_description": "ëš„í™œė„ąí™”í•˜ëŠ´ ėŠ¤ë§ˆíŠ¸ ę˛€ėƒ‰ė„ ėœ„í•œ ė´ë¯¸ė§€ 래ëĻŦëĨŧ ė§„í–‰í•˜ė§€ ė•ŠėŠĩ니다.", "machine_learning_url_description": "揰溄 학ėŠĩ ė„œë˛„ė˜ URLė„ ė„¤ė •í•Šë‹ˆë‹¤. ė—ŦëŸŦ 개가 ėž…ë Ĩ되늴 ė˛Ģ ë˛ˆė§¸ëļ€í„° 한 ë˛ˆė— í•˜ë‚˜ė”Š ėˆœė„œëŒ€ëĄœ ė‘ë‹ĩ하는 ė„œë˛„ëĨŧ ė°žė„ ë•ŒęšŒė§€ ėš”ė˛­ė„ ė‹œë„í•Šë‹ˆë‹¤. ė‘ë‹ĩí•˜ė§€ ė•ŠëŠ” ė„œë˛„ëŠ” ë‹¤ė‹œ ė‚ŦėšŠ 가ëŠĨ할 ë•ŒęšŒė§€ ėŧė‹œė ėœŧ로 ė œė™¸ëŠë‹ˆë‹¤.", + "maintenance_delete_backup": "ë°ąė—… ė‚­ė œ", + "maintenance_delete_backup_description": "ė´ 파ėŧė€ 똁ęĩŦ렁ėœŧ로 ė‚­ė œëŠë‹ˆë‹¤.", + "maintenance_delete_error": "ë°ąė—… ė‚­ė œ ė‹¤íŒ¨.", + "maintenance_restore_backup": "ë°ąė—… ëŗĩ뛐", + "maintenance_restore_backup_description": "Immich가 ė‚­ė œë˜ęŗ  ė„ íƒí•œ ë°ąė—…ė—ė„œ ëŗĩė›ëŠë‹ˆë‹¤. ęŗ„ė†í•˜ę¸° 렄뗐 ë°ąė—…ė´ ėƒė„ąëŠë‹ˆë‹¤.", + "maintenance_restore_backup_different_version": "ė´ ë°ąė—…ė€ 다ëĨ¸ ë˛„ė „ė˜ Immichė—ė„œ ėƒė„ąë˜ė—ˆėŠĩ니다!", + "maintenance_restore_backup_unknown_version": "ë°ąė—… ë˛„ė „ė„ í™•ė¸í•  눘 ė—†ėŠĩ니다.", + "maintenance_restore_database_backup": "ë°ė´í„°ë˛ ė´ėŠ¤ ë°ąė—… ëŗĩ뛐", + "maintenance_restore_database_backup_description": "ë°ąė—… 파ėŧė„ ė‚ŦėšŠí•´ ė´ė „ ë°ė´í„°ë˛ ė´ėŠ¤ ėƒíƒœëĄœ 륤밹", + "maintenance_settings": "ėœ ė§€ëŗ´ėˆ˜", + "maintenance_settings_description": "ImmichëĨŧ ėœ ė§€ ëŗ´ėˆ˜ ëĒ¨ë“œëĄœ ė „í™˜í•˜ę¸°.", + "maintenance_start": "ėœ ė§€ ëŗ´ėˆ˜ ëĒ¨ë“œëĄœ ė „í™˜", + "maintenance_start_error": "ėœ ė§€ ëŗ´ėˆ˜ ëĒ¨ë“œ ė‹œėž‘ė— ė‹¤íŒ¨í•¨.", + "maintenance_upload_backup": "ë°ė´í„°ë˛ ė´ėŠ¤ ë°ąė—… 파ėŧ ė—…ëĄœë“œ", + "maintenance_upload_backup_error": "ë°ąė—…ė„ ė—…ëĄœë“œí•  눘 ė—†ėŠĩ니다, .sql/.sql.gz 파ėŧė´ 맞ėŠĩ니까?", "manage_concurrency": "ë™ė‹œė„ą 관ëĻŦ", + "manage_concurrency_description": "ėž‘ė—… íŽ˜ė´ė§€ëĄœ ė´ë™í•˜ė—Ŧ ėž‘ė—… ë™ė‹œ ė§„í–‰ ėƒí™Šė„ 관ëĻŦí•˜ė„¸ėš”", "manage_log_settings": "로그 기록 ė„¤ė •ė„ 관ëĻŦ합니다.", "map_dark_style": "다íŦ ėŠ¤íƒ€ėŧ", "map_enable_description": "ė§€ë„ 기ëŠĨ í™œė„ąí™”", @@ -244,7 +272,7 @@ "oauth_auto_register": "ėžë™ 등록", "oauth_auto_register_description": "OAuth ëĄœęˇ¸ė¸ 후 냈 ė‚ŦėšŠėžëĨŧ ėžë™ėœŧ로 등록합니다.", "oauth_button_text": "버íŠŧ í…ėŠ¤íŠ¸", - "oauth_client_secret_description": "OAuth 렜ęŗĩėžę°€ PKCE(Proof Key for Code Exchange, ėŊ”드 ęĩí™˜ėšŠ 검ėĻ 키)ëĨŧ ė§€ė›í•˜ė§€ ė•ŠëŠ” ę˛Ŋ뚰 í•„ėš”í•Šë‹ˆë‹¤.", + "oauth_client_secret_description": "비ęŗĩ氜 클ëŧė´ė–¸íŠ¸ 또는 ęŗĩ氜 클ëŧė´ė–¸íŠ¸ę°€ PKCE(Proof Key for Code Exchange, ėŊ”드 ęĩí™˜ėšŠ 검ėĻ 키)ëĨŧ ė§€ė›í•˜ė§€ ė•ŠëŠ” ę˛Ŋ뚰 í•„ėš”í•Šë‹ˆë‹¤.", "oauth_enable_description": "OAuth ëĄœęˇ¸ė¸", "oauth_mobile_redirect_uri": "ëĒ¨ë°”ėŧ ëĻŦë‹¤ė´ë ‰íŠ¸ URI", "oauth_mobile_redirect_uri_override": "ëĒ¨ë°”ėŧ ëĻŦë‹¤ė´ë ‰íŠ¸ URI ė˜¤ë˛„ëŧė´ë“œ", @@ -270,6 +298,7 @@ "person_cleanup_job": "ė¸ëŦŧ ė •ëĻŦ", "queue_details": "ëŒ€ę¸°ė—´ ėƒė„¸", "queues": "ėž‘ė—… ëŒ€ę¸°ė—´", + "queues_page_description": "관ëĻŦėž ėž‘ė—… ëŒ€ę¸°ė—´ íŽ˜ė´ė§€", "quota_size_gib": "할당량 (GiB)", "refreshing_all_libraries": "ëĒ¨ë“  ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ėƒˆëĄœęŗ ėš¨í•Šë‹ˆë‹¤.", "registration": "관ëĻŦėž 등록", @@ -287,8 +316,10 @@ "server_public_users_description": "ė‚ŦėšŠėžëĨŧ ęŗĩ뜠 ė•¨ë˛”ė— ėļ”가할 때 ëĒ¨ë“  ė‚ŦėšŠėž(ė´ëĻ„ęŗŧ ė´ëŠ”ėŧ)가 í‘œė‹œëŠë‹ˆë‹¤. ëš„í™œė„ąí™”í•˜ëŠ´ 관ëĻŦėžë§Œ ëĒŠëĄė„ ëŗŧ 눘 ėžˆėŠĩ니다.", "server_settings": "ė„œë˛„ 네렕", "server_settings_description": "ė„œë˛„ ė„¤ė •ė„ 관ëĻŦ합니다.", + "server_stats_page_description": "관ëĻŦėž ė„œë˛„ í†ĩęŗ„ íŽ˜ė´ė§€", "server_welcome_message": "í™˜ė˜ ëŠ”ė‹œė§€", "server_welcome_message_description": "ëĄœęˇ¸ė¸ íŽ˜ė´ė§€ė— í‘œė‹œë˜ëŠ” ëŠ”ė‹œė§€ėž…ë‹ˆë‹¤.", + "settings_page_description": "관ëĻŦėž 네렕 íŽ˜ė´ė§€", "sidecar_job": "ė‚Ŧė´ë“œėš´ ëŠ”íƒ€ë°ė´í„°", "sidecar_job_description": "파ėŧ ė‹œėŠ¤í…œė—ė„œ ė‚Ŧė´ë“œėš´ ëŠ”íƒ€ë°ė´í„° 파ėŧ íƒėƒ‰ 및 동기화", "slideshow_duration_description": "ę°œëŗ„ ė‚Ŧė§„ė´ í‘œė‹œë˜ëŠ” 봈 ë‹¨ėœ„ė˜ ė‹œę°„", @@ -324,7 +355,7 @@ "theme_settings": "테마 네렕", "theme_settings_description": "Immich ė›š ė¸í„°íŽ˜ė´ėŠ¤ëĨŧ ė‚ŦėšŠėž ė •ė˜í•Šë‹ˆë‹¤.", "thumbnail_generation_job": "ė„Ŧ네ėŧ ėƒė„ą", - "thumbnail_generation_job_description": "각 항ëĒŠ 및 ė¸ëŦŧ뗐 대해 íŦęŗ  ėž‘ė€ ė¸ë„¤ėŧ, 흐ëĻŋ한 ė¸ë„¤ėŧ ėƒė„ą", + "thumbnail_generation_job_description": "각 항ëĒŠ 및 ė¸ëŦŧ뗐 대해 íŦęŗ  ėž‘ė€ ė¸ë„¤ėŧ, 흐ëĻŋ한 ė„Ŧ네ėŧ ėƒė„ą", "transcoding_acceleration_api": "ę°€ė† API", "transcoding_acceleration_api_description": "íŠ¸ëžœėŠ¤ėŊ”딊 ę°€ė†ė— ė‚ŦėšŠí•  APIëĨŧ ė§€ė •í•Šë‹ˆë‹¤. ė´ ė„¤ė •ė€ 'best effort' ë°Šė‹ėœŧ로 ë™ėž‘í•˜ëŠ°, ė‹¤íŒ¨ ė‹œ ė†Œí”„íŠ¸ė›¨ė–´ íŠ¸ëžœėŠ¤ėŊ”딊ėœŧ로 ė „í™˜ëŠë‹ˆë‹¤. í•˜ë“œė›¨ė–´ė— 따ëŧ VP9ė€ ė§€ė›ë˜ė§€ ė•Šė„ 눘 ėžˆėŠĩ니다.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU í•„ėš”)", @@ -407,6 +438,8 @@ "user_restore_scheduled_removal": "{date, date, long}뗐 똈ė•Ŋ된 ė‚ŦėšŠėž ė‚­ė œ ėˇ¨ė†Œ", "user_settings": "ė‚ŦėšŠėž 네렕", "user_settings_description": "ė‚ŦėšŠėž ė„¤ė •ė„ 관ëĻŦ합니다.", + "user_successfully_removed": "ė‚ŦėšŠėž {email}ë‹˜ė´ ė„ąęŗĩ렁ėœŧ로 ė‚­ė œë˜ė—ˆėŠĩ니다.", + "users_page_description": "관ëĻŦėž ė‚ŦėšŠėž íŽ˜ė´ė§€", "version_check_enabled_description": "ë˛„ė „ í™•ė¸ í™œė„ąí™”", "version_check_implications": "ėŖŧ揰렁ėœŧ로 Github뗐 ėš”ė˛­ė„ ëŗ´ë‚´ 냈 ë˛„ė „ė„ í™•ė¸í•Šë‹ˆë‹¤.", "version_check_settings": "ë˛„ė „ í™•ė¸", @@ -418,6 +451,9 @@ "admin_password": "관ëĻŦėž 비밀번호", "administration": "관ëĻŦ", "advanced": "溠揉", + "advanced_settings_clear_image_cache": "ė´ë¯¸ė§€ ėēė‹œ ė§€ėš°ę¸°", + "advanced_settings_clear_image_cache_error": "ė´ë¯¸ė§€ ėēė‹œ ė‚­ė œ ė‹¤íŒ¨", + "advanced_settings_clear_image_cache_success": "{size}가 ė„ąęŗĩ렁ėœŧ로 ė •ëĻŦ됨", "advanced_settings_enable_alternate_media_filter_subtitle": "ė´ ė˜ĩė…˜ė„ ė‚ŦėšŠí•˜ëŠ´ 동기화 뤑 ë¯¸ë””ė–´ëĨŧ ëŒ€ė˛´ 揰뤀ėœŧ로 필터링할 눘 ėžˆėŠĩ니다. ė•ąė´ ëĒ¨ë“  ė•¨ë˛”ė„ ė œëŒ€ëĄœ ę°ė§€í•˜ė§€ ëĒģ할 때만 ė‚ŦėšŠí•˜ė„¸ėš”.", "advanced_settings_enable_alternate_media_filter_title": "ëŒ€ė˛´ 기기 ė•¨ë˛” 동기화 필터 ė‚ŦėšŠ (ė‹¤í—˜ė )", "advanced_settings_log_level_title": "로그 레벨: {level}", @@ -454,10 +490,12 @@ "album_remove_user": "ė‚ŦėšŠėžëĨŧ ė œęą°í•˜ė‹œę˛ ėŠĩ니까?", "album_remove_user_confirmation": "{user}ë‹˜ė„ ė•¨ë˛”ė—ė„œ ė œęą°í•˜ė‹œę˛ ėŠĩ니까?", "album_search_not_found": "ę˛€ėƒ‰ 결ęŗŧ뗐 해당하는 ė•¨ë˛”ė´ ė—†ėŠĩ니다.", + "album_selected": "ė„ íƒëœ ė•¨ë˛”", "album_share_no_users": "ė´ë¯¸ ëĒ¨ë“  ė‚ŦėšŠėžė™€ ė•¨ë˛”ė„ ęŗĩėœ í–ˆęą°ë‚˜ ęŗĩėœ í•  ė‚ŦėšŠėžę°€ ė—†ėŠĩ니다.", "album_summary": "ė•¨ë˛” ėš”ė•Ŋ", "album_updated": "항ëĒŠ ėļ”ę°€ ė•ŒëĻŧ", "album_updated_setting_description": "ęŗĩ뜠 ė•¨ë˛”ė— 항ëĒŠė´ ėļ”ę°€ëœ ę˛Ŋ뚰 ė´ëŠ”ėŧ ė•ŒëĻŧ 받기", + "album_upload_assets": "ėģ´í“¨í„°ė—ė„œ 항ëĒŠė„ ė—…ëĄœë“œí•˜ęŗ  ė•¨ë˛”ė— ėļ”ę°€", "album_user_left": "{album} ė•¨ë˛”ė—ė„œ ë‚˜ė˜´", "album_user_removed": "{user}ë‹˜ė„ ė•¨ë˛”ė—ė„œ ė œęą°í•¨", "album_viewer_appbar_delete_confirm": "ė´ ė•¨ë˛”ė„ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", @@ -475,9 +513,11 @@ "albums_default_sort_order_description": "냈 ė•¨ë˛” ėƒė„ą ė‹œ ė ėšŠë˜ëŠ” ę¸°ëŗ¸ ė •ë Ŧė„ ė„¤ė •í•Šë‹ˆë‹¤.", "albums_feature_description": "ė—ŦëŸŦ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ í•œęŗŗė— ëǍ땄 둘 눘 ėžˆėŠĩ니다.", "albums_on_device_count": "ę¸°ę¸°ė˜ ė•¨ë˛” ({count}氜)", + "albums_selected": "{count, plural, one {#氜} other {#氜}} ė•¨ë˛” ė„ íƒë¨", "all": "ëĒ¨ë‘", "all_albums": "ëĒ¨ë“  ė•¨ë˛”", "all_people": "ëĒ¨ë“  ė¸ëŦŧ", + "all_photos": "ëĒ¨ë“  ė‚Ŧė§„", "all_videos": "ëĒ¨ë“  ë™ė˜ėƒ", "allow_dark_mode": "다íŦ ëĒ¨ë“œ ė‚ŦėšŠ", "allow_edits": "íŽ¸ė§‘ėžëĄœ 네렕", @@ -485,6 +525,9 @@ "allow_public_user_to_upload": "ëĒ¨ë“  ė‚ŦėšŠėžė˜ ė—…ëĄœë“œ í—ˆėšŠ", "allowed": "í—ˆėšŠë¨", "alt_text_qr_code": "QR ėŊ”드 ė´ë¯¸ė§€", + "always_keep": "í•­ėƒ ėœ ė§€", + "always_keep_photos_hint": "ė´ 揰揰뗐 ëĒ¨ë“  ė‚Ŧė§„ė´ ëŗ´ę´€ëŠë‹ˆë‹¤.", + "always_keep_videos_hint": "ė´ 揰揰뗐 ëĒ¨ë“  ë™ė˜ėƒė´ ëŗ´ę´€ëŠë‹ˆë‹¤.", "anti_clockwise": "ë°˜ė‹œęŗ„ ë°Ší–Ĩ", "api_key": "API 키", "api_key_description": "ė´ ę°’ė€ 한 번만 í‘œė‹œëŠë‹ˆë‹¤. ė°Ŋė„ ë‹Ģ기 ė „ ë°˜ë“œė‹œ ëŗĩė‚Ŧ해ėŖŧė„¸ėš”.", @@ -511,10 +554,12 @@ "archived_count": "ëŗ´ę´€í•¨ėœŧ로 항ëĒŠ {count, plural, other {#氜}} ė´ë™ë¨", "are_these_the_same_person": "동ėŧ한 ė¸ëŦŧė¸ę°€ėš”?", "are_you_sure_to_do_this": "ęŗ„ė† ė§„í–‰í•˜ė‹œę˛ ėŠĩ니까?", + "array_field_not_fully_supported": "ë°°ė—´ 필드는 JSONė„ ėˆ˜ë™ėœŧ로 íŽ¸ė§‘í•´ė•ŧ 합니다", "asset_action_delete_err_read_only": "ėŊ기 ė „ėšŠ 항ëĒŠė€ ė‚­ė œí•  눘 뗆떴 건너뜁니다.", "asset_action_share_err_offline": "ė˜¤í”„ëŧė¸ 항ëĒŠė€ ëļˆëŸŦė˜Ŧ 눘 뗆떴 건너뜁니다.", "asset_added_to_album": "ė•¨ë˛”ė— ėļ”ę°€ë˜ė—ˆėŠĩ니다.", "asset_adding_to_album": "ė•¨ë˛”ė— ėļ”ę°€ 뤑â€Ļ", + "asset_created": "ėžė‚° ėƒė„ąë¨", "asset_description_updated": "항ëĒŠ 네ëĒ…ė´ ė—…ë°ė´íŠ¸ë˜ė—ˆėŠĩ니다.", "asset_filename_is_offline": "{filename} 항ëĒŠ 누ëŊ됨", "asset_has_unassigned_faces": "항ëĒŠė— í• ë‹šë˜ė§€ ė•Šė€ ė–ŧęĩ´ė´ ėžˆėŒ", @@ -527,6 +572,9 @@ "asset_list_layout_sub_title": "ë ˆė´ė•„ė›ƒ", "asset_list_settings_subtitle": "ė‚Ŧė§„ ë°°ė—´ ë ˆė´ė•„ė›ƒ 네렕", "asset_list_settings_title": "ė‚Ŧė§„ ë°°ė—´", + "asset_not_found_on_device_android": "ę¸°ę¸°ė—ė„œ 항ëĒŠė„ ė°žė„ 눘 ė—†ėŒ", + "asset_not_found_on_device_ios": "ę¸°ę¸°ė—ė„œ 항ëĒŠė„ ė°žė„ 눘 ė—†ėŠĩ니다. iCloudëĨŧ ė‚ŦėšŠí•˜ëŠ” ę˛Ŋ뚰 ė €ėžĨ된 파ėŧė´ ė†ėƒë˜ė—ˆė„ 눘 ėžˆėŠĩ니다.", + "asset_not_found_on_icloud": "iCloudė—ė„œ 항ëĒŠė„ ė°žė„ 눘 ė—†ėŠĩ니다. iCloud뗐 ė €ėžĨ된 파ėŧė´ ė†ėƒë˜ė—ˆė„ 눘 ėžˆėŠĩ니다.", "asset_offline": "누ëŊ된 항ëĒŠ", "asset_offline_description": "ë””ėŠ¤íŦė—ė„œ 항ëĒŠė„ ë”ė´ėƒ ė°žė„ 눘 ė—†ėŠĩ니다. ė„œë˛„ 관ëĻŦėžė—ę˛Œ ė—°ëŊí•˜ė„¸ėš”.", "asset_restored_successfully": "항ëĒŠė´ ëŗĩė›ë˜ė—ˆėŠĩ니다.", @@ -693,10 +741,13 @@ "change_password_form_confirm_password": "현ėžŦ 비밀번호 ėž…ë Ĩ", "change_password_form_description": "ė•ˆë…•í•˜ė„¸ėš” {name}님,\n\nė˛˜ėŒ ëĄœęˇ¸ė¸í•˜ęą°ë‚˜ 비밀번호 ė´ˆę¸°í™” ėš”ė˛­ė´ ėžˆėŠĩ니다. 냈 비밀번호ëĨŧ ėž…ë Ĩí•˜ė„¸ėš”.", "change_password_form_log_out": "다ëĨ¸ ëĒ¨ë“  ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒ", + "change_password_form_log_out_description": "다ëĨ¸ ëĒ¨ë“  ę¸°ę¸°ė—ė„œ ëĄœęˇ¸ė•„ė›ƒí•˜ëŠ” ę˛ƒė´ ėĸ‹ėŠĩ니다", "change_password_form_new_password": "냈 비밀번호 ėž…ë Ĩ", "change_password_form_password_mismatch": "비밀번호가 ėŧėš˜í•˜ė§€ ė•ŠėŠĩ니다.", "change_password_form_reenter_new_password": "냈 비밀번호 í™•ė¸", "change_pin_code": "PIN ėŊ”드 ëŗ€ę˛Ŋ", + "change_trigger": "트ëĻŦęą° ëŗ€ę˛Ŋ", + "change_trigger_prompt": "트ëĻŦęą°ëĨŧ ëŗ€ę˛Ŋí•˜ė‹œę˛ ėŠĩ니까? ė´ë ‡ę˛Œ 하면 ę¸°ėĄ´ė˜ ëĒ¨ë“  ė•Ąė…˜ęŗŧ 필터가 ė œęą°ëŠë‹ˆë‹¤.", "change_your_password": "ė‚ŦėšŠėž ęŗ„ė •ė˜ 비밀번호ëĨŧ ëŗ€ę˛Ŋ합니다.", "changed_visibility_successfully": "ėˆ¨ęš€ ė—Ŧëļ€ę°€ ëŗ€ę˛Ŋë˜ė—ˆėŠĩ니다.", "charging": "ėļŠė „ 뤑", @@ -708,6 +759,18 @@ "checksum": "랴íŦė„Ŧ", "choose_matching_people_to_merge": "ëŗ‘í•Ší•  ė¸ëŦŧ ė„ íƒ", "city": "ë„ė‹œ", + "cleanup_confirm_description": "Immich ė„œë˛„ė—ė„œ ė•ˆė „í•˜ę˛Œ ë°ąė—…ëœ 항ëĒŠ {count}氜({date} ė´ė „ė— ėƒė„ąë¨)ëĨŧ ė°žė•˜ėŠĩ니다. ė´ ę¸°ę¸°ė—ė„œ 로ėģŦ ëŗĩė‚Ŧëŗĩė„ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", + "cleanup_confirm_prompt_title": "ė´ ę¸°ę¸°ė—ė„œ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", + "cleanup_deleted_assets": "{count}氜 항ëĒŠ íœ´ė§€í†ĩėœŧ로 ė´ë™ë¨", + "cleanup_deleting": "íœ´ė§€í†ĩėœŧ로 ė´ë™ 뤑...", + "cleanup_found_assets": "ë°ąė—…ëœ {count}ę°œė˜ 항ëĒŠ ė°žėŒ", + "cleanup_found_assets_with_size": "ë°ąė—…ëœ {count}ę°œė˜ 항ëĒŠ ė°žėŒ ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloudė˜ ęŗĩ뜠 ė•¨ë˛”ė€ 늤ėē” ëŒ€ėƒė—ė„œ ė œė™¸ëŠë‹ˆë‹¤", + "cleanup_no_assets_found": "ėœ„ ėĄ°ęą´ė— ėŧėš˜í•˜ëŠ” 항ëĒŠė´ ė—†ėŠĩ니다. ė €ėžĨ ęŗĩ간 í™•ëŗ´ 기ëŠĨė€ ė„œë˛„ė— ë°ąė—…ëœ 항ëĒŠë§Œ ė‚­ė œí•  눘 ėžˆėŠĩ니다.", + "cleanup_preview_title": "ė‚­ė œ ëŒ€ėƒ 항ëĒŠ ({count})", + "cleanup_step3_description": "ë°ąė—…ëœ 항ëĒŠė—ė„œ ë‚ ė§œ 및 ëŗ´ėĄ´ 네렕ęŗŧ ėŧėš˜í•˜ëŠ” 항ëĒŠė„ 늤ėē”핊니다.", + "cleanup_step4_summary": "({date} ė´ė „ė— ėƒė„ąëœ) {count} 항ëĒŠė´ 로ėģŦ ę¸°ę¸°ė—ė„œ ė‚­ė œëŠë‹ˆë‹¤. ė‚Ŧė§„ė€ Immich ė•ąė—ė„œ ęŗ„ė† ė•Ąė„¸ėŠ¤í•  눘 ėžˆėŠĩ니다.", + "cleanup_trash_hint": "ė €ėžĨ ęŗĩę°„ė„ ė™„ė „ížˆ í™•ëŗ´í•˜ë ¤ëŠ´ ė‹œėŠ¤í…œė˜ ę°¤ëŸŦëĻŦ ė•ąė„ ė—´ęŗ  íœ´ė§€í†ĩė„ ëš„ėš°ė„¸ėš”", "clear": "ė§€ėš°ę¸°", "clear_all": "ëĒ¨ë‘ ė§€ėš°ę¸°", "clear_all_recent_searches": "ę˛€ėƒ‰ 기록 렄랴 ė‚­ė œ", @@ -728,6 +791,7 @@ "collapse_all": "ëĒ¨ë‘ 렑揰", "color": "ėƒ‰ėƒ", "color_theme": "테마 ėƒ‰ėƒ", + "command": "ëĒ…ë š", "comment_deleted": "ëŒ“ę¸€ė´ ė‚­ė œë˜ė—ˆėŠĩ니다.", "comment_options": "댓글 ė˜ĩė…˜", "comments_and_likes": "댓글 및 ėĸ‹ė•„ėš”", @@ -772,6 +836,7 @@ "create_album": "ė•¨ë˛” ėƒė„ą", "create_album_page_untitled": "렜ëĒŠ ė—†ėŒ", "create_api_key": "API 키 ėƒė„ą", + "create_first_workflow": "ė˛Ģ ë˛ˆė§¸ ė›ŒíŦ플로ëĨŧ ėƒė„ąí•Šë‹ˆë‹¤", "create_library": "냈 ëŧė´ë¸ŒëŸŦëĻŦ", "create_link": "링íŦ ėƒė„ą", "create_link_to_share": "ęŗĩ뜠 링íŦ ėƒė„ą", @@ -786,21 +851,30 @@ "create_tag": "태그 ėƒė„ą", "create_tag_description": "냈 태그ëĨŧ ėƒė„ąí•Šë‹ˆë‹¤. í•˜ėœ„ íƒœęˇ¸ė˜ ę˛Ŋ뚰 /ëĨŧ íŦ함한 렄랴 태그ëĒ…ė„ ėž…ë Ĩí•˜ė„¸ėš”.", "create_user": "ė‚ŦėšŠėž ęŗ„ė • ėƒė„ą", + "create_workflow": "ė›ŒíŦ플로 ėƒė„ą", "created": "ėƒė„ąë¨", "created_at": "ėƒė„ąë¨", "creating_linked_albums": "ė—°ę˛°ëœ ė•¨ë˛” ėƒė„ą 뤑...", "crop": "ėžëĨ´ę¸°", + "crop_aspect_ratio_fixed": "ęŗ ė •", + "crop_aspect_ratio_free": "링렑 ėĄ°ė ˆ", + "crop_aspect_ratio_original": "ė›ëŗ¸", "curated_object_page_title": "ė‚ŦëŦŧ", "current_device": "현ėžŦ 기기", "current_pin_code": "현ėžŦ PIN ėŊ”드", "current_server_address": "현ėžŦ ė„œë˛„ ėŖŧė†Œ", + "custom_date": "ë‚ ė§œ ė„ íƒ", "custom_locale": "ė‚ŦėšŠėž 맀렕 로ėŧ€ėŧ", "custom_locale_description": "떏떴 및 맀뗭뗐 따ëĨ¸ ë‚ ė§œ 및 ėˆĢėž í˜•ė‹ 맀렕", "custom_url": "ė‚ŦėšŠėž 맀렕 URL", + "cutoff_date_description": "ė„ íƒí•œ ę¸°ę°„ė˜ ė‚Ŧė§„ė„ ėœ ė§€í•Šë‹ˆë‹¤â€Ļ", + "cutoff_day": "{count, plural, one {ėŧ} other {ėŧ}}", + "cutoff_year": "{count, plural, one {년} other {년}}", "daily_title_text_date": "Mė›” dėŧ EEEE", "daily_title_text_date_year": "yyyy년 Mė›” dėŧ EEEE", "dark": "다íŦ", "dark_theme": "다íŦ 테마 토글", + "date": "ë‚ ė§œ", "date_after": "ë‹¤ėŒ ë‚ ė§œ ė´í›„", "date_and_time": "ë‚ ė§œ 및 ė‹œę°„", "date_before": "ë‹¤ėŒ ë‚ ė§œ ė „", @@ -851,6 +925,7 @@ "deselect_all": "ëĒ¨ë‘ ė„ íƒ í•´ė œ", "details": "ėƒė„¸ ė •ëŗ´", "direction": "ë°Ší–Ĩ", + "disable": "ëš„í™œė„ąí™”", "disabled": "ëš„í™œė„ąí™”", "disallow_edits": "ëˇ°ė–´ëĄœ 네렕", "discord": "Discord", @@ -876,6 +951,7 @@ "download_include_embedded_motion_videos": "ëĒ¨ė…˜ íŦ토 똁냁", "download_include_embedded_motion_videos_description": "ëĒ¨ė…˜ íŦí† ė— íŦ함된 ë™ė˜ėƒė„ ëŗ„ë„ė˜ 파ėŧ로 ëļ„ëĻŦ해 ė €ėžĨ합니다.", "download_notfound": "ë‹¤ėš´ëĄœë“œí•  눘 ė—†ėŒ", + "download_original": "ė›ëŗ¸ ë‹¤ėš´ëĄœë“œ", "download_paused": "ë‹¤ėš´ëĄœë“œ ėŧė‹œ ė¤‘ė§€ë¨", "download_settings": "ë‹¤ėš´ëĄœë“œ", "download_settings_description": "파ėŧ ë‹¤ėš´ëĄœë“œ ė„¤ė •ė„ 관ëĻŦ합니다.", @@ -885,6 +961,7 @@ "download_waiting_to_retry": "ėžŦė‹œë„ 대기 뤑", "downloading": "ë‹¤ėš´ëĄœë“œ", "downloading_asset_filename": "{filename} ë‹¤ėš´ëĄœë“œ 뤑...", + "downloading_from_icloud": "iCloudė—ė„œ ë‹¤ėš´ëĄœë“œ 뤑", "downloading_media": "ë¯¸ë””ė–´ ë‹¤ėš´ëĄœë“œ 뤑", "drop_files_to_upload": "ė•„ëŦ´ ęŗŗė—ë‚˜ 파ėŧė„ 드롭하ė—Ŧ ė—…ëĄœë“œ", "duplicates": "ëš„ėŠˇí•œ 항ëĒŠ", @@ -913,11 +990,17 @@ "edit_tag": "태그 ėˆ˜ė •", "edit_title": "렜ëĒŠ ëŗ€ę˛Ŋ", "edit_user": "ė‚ŦėšŠėž ėˆ˜ė •", - "editor": "íŽ¸ė§‘ėž", + "edit_workflow": "ė›ŒíŦ플로 íŽ¸ė§‘", + "editor": "íŽ¸ė§‘ę¸°", "editor_close_without_save_prompt": "ëŗ€ę˛Ŋ ė‚Ŧí•­ė´ ė €ėžĨë˜ė§€ ė•ŠėŠĩ니다.", "editor_close_without_save_title": "íŽ¸ė§‘ė„ ėĸ…ëŖŒí•˜ė‹œę˛ ėŠĩ니까?", - "editor_crop_tool_h2_aspect_ratios": "ėĸ…횥뚄", - "editor_crop_tool_h2_rotation": "íšŒė „", + "editor_confirm_reset_all_changes": "ëĒ¨ë“  ėˆ˜ė •ė‚Ŧí•­ė„ ė´ˆę¸°í™”í•˜ė‹œę˛ ėŠĩ니까?", + "editor_flip_horizontal": "ėĸŒėš°ë°˜ė „", + "editor_flip_vertical": "ėƒí•˜ë°˜ė „", + "editor_orientation": "ë°Ší–Ĩ", + "editor_reset_all_changes": "íŽ¸ė§‘ë‚´ėšŠ ė´ˆę¸°í™”", + "editor_rotate_left": "ë°˜ė‹œęŗ„ ë°Ší–Ĩėœŧ로 90° íšŒė „", + "editor_rotate_right": "ė‹œęŗ„ ë°Ší–Ĩėœŧ로 90° íšŒė „", "email": "ė´ëŠ”ėŧ", "email_notifications": "ė´ëŠ”ėŧ ė•ŒëĻŧ", "empty_folder": "폴더가 ëš„ė–´ ėžˆėŒ", @@ -935,9 +1018,10 @@ "error": "똤ëĨ˜", "error_change_sort_album": "ė•¨ë˛” í‘œė‹œ ėˆœė„œ ëŗ€ę˛Ŋ ė‹¤íŒ¨", "error_delete_face": "항ëĒŠė—ė„œ ė–ŧęĩ´ ė‚­ė œ 뤑 똤ëĨ˜ ë°œėƒ", - "error_getting_places": "ėžĨė†Œ 로드 똤ëĨ˜", - "error_loading_image": "ė´ë¯¸ė§€ëĨŧ ëļˆëŸŦė˜¤ëŠ” 뤑 똤ëĨ˜ ë°œėƒ", - "error_loading_partners": "파트너 ëļˆëŸŦ똤揰 ė‹¤íŒ¨: {error}", + "error_getting_places": "ėžĨė†Œ 로딩 똤ëĨ˜", + "error_loading_albums": "ė•¨ë˛” 로딩 똤ëĨ˜", + "error_loading_image": "ė´ë¯¸ė§€ 로딩 똤ëĨ˜", + "error_loading_partners": "파트너 로딩 똤ëĨ˜: {error}", "error_saving_image": "똤ëĨ˜: {error}", "error_tag_face_bounding_box": "ė–ŧęĩ´ 태그 ė‹¤íŒ¨ - ė–ŧęĩ´ė˜ ėœ„ėš˜ëĨŧ 氀렏ė˜Ŧ 눘 ė—†ėŠĩ니다.", "error_title": "똤ëĨ˜ - ëŦ¸ė œę°€ ë°œėƒí–ˆėŠĩ니다", @@ -998,6 +1082,7 @@ "unable_to_complete_oauth_login": "OAuth ëĄœęˇ¸ė¸ė„ ė™„ëŖŒí•  눘 ė—†ėŠĩ니다.", "unable_to_connect": "ė—°ę˛°í•  눘 ė—†ėŒ", "unable_to_copy_to_clipboard": "클ëĻŊëŗ´ë“œė— ëŗĩė‚Ŧ할 눘 ė—†ėŠĩ니다. HTTPS로 ė ‘ė† ė¤‘ė¸ė§€ í™•ė¸í•˜ė„¸ėš”.", + "unable_to_create": "ė›ŒíŦ플로ëĨŧ ėƒė„ąí•  눘 ė—†ėŠĩ니다", "unable_to_create_admin_account": "관ëĻŦėž ęŗ„ė •ė„ ėƒė„ąí•  눘 ė—†ėŠĩ니다.", "unable_to_create_api_key": "냈 API 키ëĨŧ ėƒė„ąí•  눘 ė—†ėŠĩ니다.", "unable_to_create_library": "ëŧė´ë¸ŒëŸŦëĻŦëĨŧ ėƒė„ąí•  눘 ė—†ėŠĩ니다.", @@ -1008,6 +1093,7 @@ "unable_to_delete_exclusion_pattern": "ė œė™¸ ęˇœėš™ė„ ė‚­ė œí•  눘 ė—†ėŠĩ니다.", "unable_to_delete_shared_link": "ęŗĩ뜠 링íŦëĨŧ ė‚­ė œí•  눘 ė—†ėŠĩ니다.", "unable_to_delete_user": "ė‚ŦėšŠėžëĨŧ ė‚­ė œí•  눘 ė—†ėŠĩ니다.", + "unable_to_delete_workflow": "ė›ŒíŦ플로ëĨŧ ė‚­ė œí•  눘 ė—†ėŠĩ니다", "unable_to_download_files": "파ėŧė„ ë‹¤ėš´ëĄœë“œí•  눘 ė—†ėŠĩ니다.", "unable_to_edit_exclusion_pattern": "ė œė™¸ ęˇœėš™ė„ ėˆ˜ė •í•  눘 ė—†ėŠĩ니다.", "unable_to_empty_trash": "íœ´ė§€í†ĩė„ ëš„ėš¸ 눘 ė—†ėŠĩ니다.", @@ -1058,6 +1144,7 @@ "unable_to_update_settings": "ė„¤ė •ė„ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_update_timeline_display_status": "íƒ€ėž„ëŧė¸ í‘œė‹œ ėƒíƒœëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŠĩ니다.", "unable_to_update_user": "ė‚ŦėšŠėžëĨŧ ė—…ë°ė´íŠ¸í•  눘 ė—†ėŠĩ니다.", + "unable_to_update_workflow": "ė›ŒíŦ플로ëĨŧ ė—…ë°ė´íŠ¸í•  눘 ė—†ėŠĩ니다", "unable_to_upload_file": "파ėŧė„ ė—…ëĄœë“œí•  눘 ė—†ėŠĩ니다." }, "exclusion_pattern": "ė œė™¸ ęˇœėš™", @@ -1104,14 +1191,15 @@ "features": "기ëŠĨ", "features_in_development": "개발 ė¤‘ė¸ 기ëŠĨ", "features_setting_description": "ė‚Ŧė§„ 및 ë™ė˜ėƒ 관ëĻŦ 기ëŠĨė„ ė„¤ė •í•Šë‹ˆë‹¤.", - "file_name": "파ėŧ ė´ëĻ„", "file_name_or_extension": "파ėŧëĒ… 또는 확ėžĨėž", "file_size": "파ėŧ íŦ기", "filename": "파ėŧëĒ…", "filetype": "파ėŧ í˜•ė‹", "filter": "필터", + "filter_description": "ëŒ€ėƒ ėžė‚°ė„ 필터링하기 ėœ„í•œ ėĄ°ęą´", "filter_people": "ė¸ëŦŧ 필터", "filter_places": "ėžĨė†Œ 필터", + "filters": "필터", "find_them_fast": "ė´ëĻ„ėœŧ로 ę˛€ėƒ‰í•˜ė—Ŧ ëš ëĨ´ę˛Œ ė°žę¸°", "first": "ė˛Ģ ë˛ˆė§¸", "fix_incorrect_match": "ėž˜ëĒģ된 ëļ„ëĨ˜ ėˆ˜ė •", @@ -1121,12 +1209,16 @@ "folders_feature_description": "파ėŧ ė‹œėŠ¤í…œė˜ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ 폴더 ëŗ´ę¸°ëĄœ íƒėƒ‰í•Šë‹ˆë‹¤.", "forgot_pin_code_question": "PIN 번호ëĨŧ ėžŠė–´ë˛„ë ¸ë‚˜ėš”?", "forward": "ė•žėœŧ로", + "free_up_space": "ė €ėžĨ ęŗĩ간 í™•ëŗ´", + "free_up_space_description": "ë°ąė—…ëœ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ę¸°ę¸°ė˜ íœ´ė§€í†ĩėœŧ로 ė´ë™í•˜ė—Ŧ ė €ėžĨ ęŗĩę°„ė„ í™•ëŗ´í•˜ė„¸ėš”. ė›ëŗ¸ 파ėŧė€ ė„œë˛„ė— ė•ˆė „í•˜ę˛Œ ëŗ´ę´€ëŠë‹ˆë‹¤.", + "free_up_space_settings_subtitle": "ę¸°ę¸°ė˜ ė €ėžĨ ęŗĩę°„ė„ í™•ëŗ´í•Šë‹ˆë‹¤.", "full_path": "렄랴 ę˛Ŋ로: {path}", "gcast_enabled": "ęĩŦ글 ėēėŠ¤íŠ¸", "gcast_enabled_description": "ė´ 기ëŠĨė€ Googleė˜ 뙏ëļ€ ëĻŦė†ŒėŠ¤ëĨŧ ė‚ŦėšŠí•Šë‹ˆë‹¤.", "general": "ėŧ반", "geolocation_instruction_location": "GPS ėĸŒí‘œę°€ íŦ함된 항ëĒŠė„ 클ëĻ­í•´ ėœ„ėš˜ëĨŧ ė‚ŦėšŠí•˜ęą°ë‚˜, ė§€ë„ė—ė„œ 링렑 ėœ„ėš˜ëĨŧ ė„ íƒí•˜ė„¸ėš”.", "get_help": "ë„ė›€ ė–ģ기", + "get_people_error": "ė‚ŦëžŒë“¤ė„ ëļˆëŸŦė˜¤ëŠ” 데 똤ëĨ˜ę°€ ë°œėƒí–ˆėŠĩ니다", "get_wifiname_error": "Wi-Fi ė´ëĻ„ė„ 氀렏ė˜Ŧ 눘 ė—†ėŠĩ니다. í•„ėˆ˜ ęļŒí•œė´ ëļ€ė—Ŧë˜ė—ˆëŠ”ė§€, Wi-Fi ë„¤íŠ¸ė›ŒíŦ뗐 ė—°ę˛°ë˜ė–´ ėžˆëŠ”ė§€ í™•ė¸í•˜ė„¸ėš”.", "getting_started": "ė‹œėž‘í•˜ę¸°", "go_back": "뒤로", @@ -1159,6 +1251,8 @@ "hide_named_person": "ė¸ëŦŧ {name} 눍揰揰", "hide_password": "비밀번호 눍揰揰", "hide_person": "ė¸ëŦŧ 눍揰揰", + "hide_schema": "ėŠ¤í‚¤ë§ˆ 눍揰揰", + "hide_text_recognition": "í…ėŠ¤íŠ¸ ė¸ė‹ 눍揰揰", "hide_unnamed_people": "ė´ëĻ„ ė—†ëŠ” ė¸ëŦŧ 눍揰揰", "home_page_add_to_album_conflicts": "{album} ė•¨ë˛”ė— 항ëĒŠ {added}개가 ėļ”ę°€ë˜ė—ˆėŠĩ니다. 항ëĒŠ {failed}개는 ė•¨ë˛”ė— ė´ë¯¸ ėĄ´ėžŦ합니다.", "home_page_add_to_album_err_local": "로ėģŦ 항ëĒŠė€ ė•¨ë˛”ė— ėļ”가할 눘 뗆떴 건너뜁니다.", @@ -1205,6 +1299,7 @@ "in_albums": "íŦ함된 ė•¨ë˛” {count, plural, one {#氜} other {#氜}}", "in_archive": "ëŗ´ę´€ëœ 항ëĒŠ", "in_year": "{year}년도", + "in_year_selector": "ė•ˆė—", "include_archived": "ëŗ´ę´€ëœ 항ëĒŠ íŦ함", "include_shared_albums": "ęŗĩ뜠 ė•¨ë˛” íŦ함", "include_shared_partner_assets": "파트너가 ęŗĩėœ í•œ 항ëĒŠ íŦ함", @@ -1229,9 +1324,18 @@ "ios_debug_info_processing_ran_at": "{dateTime}뗐 래ëĻŦ됨", "items_count": "{count, plural, one {#氜} other {#氜}} 항ëĒŠ", "jobs": "ėž‘ė—…", + "json_editor": "JSON íŽ¸ė§‘ę¸°", + "json_error": "JSON 똤ëĨ˜", "keep": "ėœ ė§€", + "keep_albums": "ė•¨ë˛” ėœ ė§€", + "keep_albums_count": "{count}ę°œė˜ {count, plural, one {ė•¨ë˛”} other {ė•¨ë˛”}} ėœ ė§€", "keep_all": "ëĒ¨ë‘ ėœ ė§€", + "keep_description": "ė €ėžĨ ęŗĩ간 í™•ëŗ´ė‹œė— ėœ ė§€í•  항ëĒŠė„ ė„ íƒí•˜ė„¸ėš”.", + "keep_favorites": "ėĻę˛¨ė°žę¸° ėœ ė§€", + "keep_on_device": "揰揰뗐 ėœ ė§€", + "keep_on_device_hint": "ė´ 揰揰뗐 ėœ ė§€í•  항ëĒŠė„ ė„ íƒí•Šë‹ˆë‹¤", "keep_this_delete_others": "ė´ 항ëĒŠė€ ėœ ė§€í•˜ęŗ  ë‚˜ë¨¸ė§€ëŠ” ė‚­ė œ", + "keeping": "ėœ ė§€: {items}", "kept_this_deleted_others": "ė´ 항ëĒŠė„ ėœ ė§€í•˜ęŗ  {count, plural, one {#ę°œė˜ 항ëĒŠ} other {#ę°œė˜ 항ëĒŠ}}ė„ ė‚­ė œí•¨", "keyboard_shortcuts": "í‚¤ëŗ´ë“œ 단ėļ•키", "language": "떏떴", @@ -1273,6 +1377,7 @@ "local": "로ėģŦ", "local_asset_cast_failed": "ė„œë˛„ė— ė—…ëĄœë“œë˜ė§€ ė•Šė€ 항ëĒŠė„ ėēėŠ¤íŒ…í•  눘 ė—†ėŒ", "local_assets": "로ėģŦ 항ëĒŠ", + "local_id": "로ėģŦ ID", "local_media_summary": "로ėģŦ ë¯¸ë””ė–´ ėš”ė•Ŋ", "local_network": "로ėģŦ ë„¤íŠ¸ė›ŒíŦ", "local_network_sheet_info": "ė§€ė •ëœ Wi-FiëĨŧ ė‚ŦėšŠí•  때 ė•ąė´ ė•„ëž˜ URL로 ė„œë˛„ė— ė—°ę˛°í•Šë‹ˆë‹¤.", @@ -1324,8 +1429,17 @@ "loop_videos_description": "ėƒė„¸ ëŗ´ę¸°ė—ė„œ ė˜ėƒė„ 반ëŗĩ ėžŦėƒí•Šë‹ˆë‹¤.", "main_branch_warning": "개발 ë˛„ė „ė„ ė‚ŦėšŠ ė¤‘ėž…ë‹ˆë‹¤. ė •ė‹ ëĻ´ëĻŦ늤 ë˛„ė „ ė‚ŦėšŠė„ ęļŒėžĨ합니다!", "main_menu": "ëŠ”ė¸ 메뉴", + "maintenance_description": "Immich가 ėœ ė§€ę´€ëĻŦ ëĒ¨ë“œëĄœ ė „í™˜ë˜ė—ˆėŠĩ니다.", + "maintenance_end": "ėœ ė§€ 관ëĻŦ ëĒ¨ë“œ ėĸ…ëŖŒ", + "maintenance_end_error": "ėœ ė§€ę´€ëĻŦ ëĒ¨ë“œëĨŧ ėĸ…ëŖŒí•˜ëŠ” 데 ė‹¤íŒ¨í–ˆėŠĩ니다.", + "maintenance_logged_in_as": "현ėžŦ {user} 님ėœŧ로 ëĄœęˇ¸ė¸ë˜ė–´ ėžˆėŠĩ니다", + "maintenance_title": "ėŧė‹œė ėœŧ로 ė´ėšŠí•  눘 ė—†ėŠĩ니다", "make": "ė œėĄ°ė‚Ŧ", "manage_geolocation": "ėœ„ėš˜ ė •ëŗ´ 관ëĻŦ", + "manage_media_access_rationale": "ė´ ęļŒí•œė€ ėžė‚°ė„ íœ´ė§€í†ĩėœŧ로 ė´ë™í•˜ęŗ  íœ´ė§€í†ĩė—ė„œ ëŗĩė›í•˜ëŠ” ėž‘ė—…ė„ ė˜Ŧ바ëĨ´ę˛Œ 래ëĻŦ하는 데 í•„ėš”í•Šë‹ˆë‹¤.", + "manage_media_access_settings": "네렕 뗴揰", + "manage_media_access_subtitle": "Immich ė•ąė´ ë¯¸ë””ė–´ 파ėŧė„ 관ëĻŦí•˜ęŗ  ė´ë™í•  눘 ėžˆë„ëĄ í—ˆėšŠí•˜ė‹­ė‹œė˜¤.", + "manage_media_access_title": "ë¯¸ë””ė–´ 관ëĻŦ ė•Ąė„¸ėŠ¤", "manage_shared_links": "ęŗĩ뜠 링íŦ 관ëĻŦ", "manage_sharing_with_partners": "ęŗĩėœ í•  파트너ëĨŧ ė´ˆëŒ€í•˜ęą°ë‚˜ ė œęą°í•Šë‹ˆë‹¤.", "manage_the_app_settings": "ė•ą ë™ėž‘ 및 í‘œė‹œ 환ę˛Ŋė„ ė‚ŦėšŠėž ė •ė˜í•Šë‹ˆë‹¤.", @@ -1380,6 +1494,8 @@ "minimize": "ėĩœė†Œí™”", "minute": "ëļ„", "minutes": "ëļ„", + "mirror_horizontal": "ėˆ˜í‰", + "mirror_vertical": "눘링", "missing": "누ëŊ", "mobile_app": "ëĒ¨ë°”ėŧ ė•ą", "mobile_app_download_onboarding_note": "ë‹¤ėŒ ė˜ĩė…˜ 뤑 하나ëĨŧ ė‚ŦėšŠí•´ ëĒ¨ë°”ėŧ ė•ąė„ ë‹¤ėš´ëĄœë“œí•˜ė„¸ėš”.", @@ -1388,11 +1504,14 @@ "monthly_title_text_date_format": "yyyy년 Mė›”", "more": "ë”ëŗ´ę¸°", "move": "ė´ë™", + "move_down": "ė•„ëž˜ëĄœ ė´ë™", "move_off_locked_folder": "ėž ę¸ˆ í´ë”ė—ė„œ í•´ė œ", "move_to": "ë‹¤ėŒėœŧ로 ė´ë™", + "move_to_device_trash": "ę¸°ę¸°ė˜ íœ´ė§€í†ĩėœŧ로 ė´ë™", "move_to_lock_folder_action_prompt": "ėž ę¸ˆ 폴더로 항ëĒŠ {count}氜 ė´ë™ë¨", "move_to_locked_folder": "ėž ę¸ˆ 폴더로 ė´ë™", "move_to_locked_folder_confirmation": "ė„ íƒí•œ ė‚Ŧė§„ 또는 ë™ė˜ėƒė´ ëĒ¨ë“  ė•¨ë˛”ė—ė„œ ė œęą°ë˜ëŠ°, ėž ę¸ˆ í´ë”ė—ė„œë§Œ ëŗŧ 눘 ėžˆėŠĩ니다.", + "move_up": "ėœ„ëĄœ ė´ë™", "moved_to_archive": "ëŗ´ę´€í•¨ėœŧ로 항ëĒŠ {count, plural, one {#氜} other {#氜}} ė´ë™ë¨", "moved_to_library": "ëŧė´ë¸ŒëŸŦëĻŦ로 항ëĒŠ {count, plural, one {#氜} other {#氜}} ė´ë™ë¨", "moved_to_trash": "íœ´ė§€í†ĩėœŧ로 ė´ë™ë˜ė—ˆėŠĩ니다.", @@ -1402,6 +1521,7 @@ "my_albums": "내 ė•¨ë˛”", "name": "ė´ëĻ„", "name_or_nickname": "ė´ëĻ„ 또는 ë‹‰ë„¤ėž„", + "name_required": "ė´ëĻ„ė€ í•„ėˆ˜ ėž…ë Ĩ ė‚Ŧí•­ėž…ë‹ˆë‹¤", "navigate": "íƒėƒ‰", "navigate_to_time": "ė‹œę°„ėœŧ로 íƒėƒ‰", "network_requirement_photos_upload": "ė‚Ŧė§„ ë°ąė—…ė— ëĒ¨ë°”ėŧ ë°ė´í„° ė‚ŦėšŠ", @@ -1419,12 +1539,15 @@ "new_pin_code": "냈 PIN ėŊ”드", "new_pin_code_subtitle": "ėž ę¸ˆ í´ë”ė— ė˛˜ėŒ ė ‘ęˇŧí•˜ė…¨ėŠĩ니다. ė´ęŗŗė— ė•ˆė „í•˜ę˛Œ ė ‘ęˇŧ하기 ėœ„í•œ PIN ėŊ”드ëĨŧ ė„¤ė •í•˜ė„¸ėš”.", "new_timeline": "냈 íƒ€ėž„ëŧė¸", + "new_update": "ėƒˆëĄœėš´ ė—…ë°ė´íŠ¸", "new_user_created": "ė‚ŦėšŠėž ęŗ„ė •ė´ ėƒė„ąë˜ė—ˆėŠĩ니다.", "new_version_available": "냈 ë˛„ė „ ė‚ŦėšŠ 가ëŠĨ", "newest_first": "ėĩœė‹ ėˆœ", "next": "ë‹¤ėŒ", "next_memory": "ë‹¤ėŒ ėļ”ė–ĩ", "no": "ė•„ë‹ˆėš”", + "no_actions_added": "땄링 ėļ”ę°€ëœ ėž‘ė—…ė´ ė—†ėŠĩ니다", + "no_albums_found": "ė•¨ë˛”ė´ ė—†ėŠĩ니다.", "no_albums_message": "ė•¨ë˛”ė„ ėƒė„ąí•˜ė—Ŧ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ė •ëĻŦ하기", "no_albums_with_name_yet": "땄링 해당하는 ė´ëĻ„ė˜ ė•¨ë˛”ė´ ė—†ëŠ” 것 같ėŠĩ니다.", "no_albums_yet": "땄링 ė•¨ë˛”ė´ ė—†ëŠ” 것 같ėŠĩ니다.", @@ -1434,12 +1557,16 @@ "no_cast_devices_found": "ėēėŠ¤íŠ¸ 기기 ė—†ėŒ", "no_checksum_local": "랴íŦė„Ŧė´ ė—†ėŠĩ니다. 로ėģŦ 항ëĒŠė„ ëļˆëŸŦė˜Ŧ 눘 ė—†ėŠĩ니다.", "no_checksum_remote": "랴íŦė„Ŧė´ ė—†ėŠĩ니다. ė›ę˛Š 항ëĒŠė„ ëļˆëŸŦė˜Ŧ 눘 ė—†ėŠĩ니다.", + "no_configuration_needed": "ëŗ„ë„ė˜ ė„¤ė •ė´ í•„ėš”í•˜ė§€ ė•ŠėŠĩ니다", + "no_devices": "ėŠšė¸ë˜ė§€ ė•Šė€ 기기", "no_duplicates_found": "ëš„ėŠˇí•œ 항ëĒŠė´ ė—†ėŠĩ니다.", "no_exif_info_available": "EXIF ė •ëŗ´ ė—†ėŒ", "no_explore_results_message": "더 ë§Žė€ ė‚Ŧė§„ė„ ė—…ëĄœë“œí•˜ė—Ŧ íƒėƒ‰ 기ëŠĨė„ ė‚ŦėšŠí•˜ė„¸ėš”.", "no_favorites_message": "ėĻę˛¨ė°žę¸°ė—ė„œ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ëš ëĨ´ę˛Œ ė°žę¸°", + "no_filters_added": "땄링 ėļ”ę°€ëœ 필터 ė—†ėŒ", "no_libraries_message": "뙏ëļ€ ëŧė´ë¸ŒëŸŦëĻŦ로 다ëĨ¸ ę˛ŊëĄœė˜ ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ í™•ė¸í•˜ė„¸ėš”.", "no_local_assets_found": "랴íŦė„Ŧęŗŧ ėŧėš˜í•˜ëŠ” 로ėģŦ 항ëĒŠė„ ė°žė„ 눘 ė—†ėŠĩ니다.", + "no_location_set": "ėœ„ėš˜ę°€ ė„¤ė •ë˜ė§€ ė•Šė•˜ėŠĩ니다", "no_locked_photos_message": "ėž ę¸ˆ í´ë”ė˜ ė‚Ŧė§„ 및 ë™ė˜ėƒė€ ėˆ¨ę˛¨ė§€ëŠ° ëŧė´ë¸ŒëŸŦëĻŦëĨŧ íƒėƒ‰í•  때 í‘œė‹œë˜ė§€ ė•ŠėŠĩ니다.", "no_name": "ė´ëĻ„ ė—†ėŒ", "no_notifications": "ė•ŒëĻŧ ė—†ėŒ", @@ -1454,7 +1581,6 @@ "not_available": "ė—†ėŒ", "not_in_any_album": "ė•¨ë˛”ė— ė—†ėŒ", "not_selected": "ė„ íƒë˜ė§€ ė•ŠėŒ", - "note_apply_storage_label_to_previously_uploaded assets": "및溠: ė´ė „ė— ė—…ëĄœë“œí•œ 항ëĒŠė—ë„ ėŠ¤í† ëĻŦė§€ ë ˆė´ë¸”ė„ ė ėšŠí•˜ë ¤ëŠ´ ë‹¤ėŒė„ ė‹¤í–‰í•Šë‹ˆë‹¤,", "notes": "및溠", "nothing_here_yet": "땄링 ė•„ëŦ´ę˛ƒë„ ė—†ėŒ", "notification_permission_dialog_content": "ė•ŒëĻŧė„ í™œė„ąí™”í•˜ë ¤ëŠ´ ė„¤ė •ė—ė„œ ė•ŒëĻŧ ęļŒí•œė„ í—ˆėšŠí•˜ė„¸ėš”.", @@ -1467,6 +1593,7 @@ "oauth": "OAuth", "obtainium_configurator": "Obtainium ęĩŦė„ą", "obtainium_configurator_instructions": "Obtainiumėœŧ로 Immich GitHub ëĻ´ëĻŦėŠ¤ė—ė„œ 링렑 ė•ˆë“œëĄœė´ë“œ ė•ąė„ ė„¤ėš˜í•˜ęŗ  ė—…ë°ė´íŠ¸í•˜ė„¸ėš”. API 키ëĨŧ ėƒė„ąí•˜ęŗ  ëŗ€í˜•ė„ ė„ íƒí•´ Obtanium 네렕 링íŦëĨŧ ėƒė„ąí•˜ė„¸ėš”.", + "ocr": "OCR", "official_immich_resources": "Immich ęŗĩė‹ ëĻŦė†ŒėŠ¤", "offline": "ė˜¤í”„ëŧė¸", "offset": "ė˜¤í”„ė…‹", @@ -1498,6 +1625,7 @@ "other_variables": "기타 ëŗ€ėˆ˜", "owned": "ė†Œėœ í•¨", "owner": "ė†Œėœ ėž", + "page": "íŽ˜ė´ė§€", "partner": "파트너", "partner_can_access": "{partner}ë‹˜ė´ ė ‘ęˇŧ할 눘 ėžˆëŠ” 항ëĒŠ", "partner_can_access_assets": "ëŗ´ę´€ë˜ęą°ë‚˜ ė‚­ė œëœ 항ëĒŠė„ ė œė™¸í•œ ëĒ¨ë“  ė‚Ŧė§„ 및 ë™ė˜ėƒ", @@ -1530,11 +1658,12 @@ "people": "ė¸ëŦŧ", "people_edits_count": "ė¸ëŦŧ {count, plural, one {#ëĒ…} other {#ëĒ…}}ė´ ėˆ˜ė •ë˜ė—ˆėŠĩ니다.", "people_feature_description": "ė‚Ŧė§„ęŗŧ ë™ė˜ėƒė„ ė¸ëŦŧ ęˇ¸ëŖšëŗ„ëĄœ íƒėƒ‰", + "people_selected": "ė¸ëŦŧ {count, plural, one {#ëĒ…} other {#ëĒ…}} ė„ íƒë¨", "people_sidebar_description": "ė‚Ŧė´ë“œë°”ė— ė¸ëŦŧ 링íŦ í‘œė‹œ", "permanent_deletion_warning": "똁ęĩŦ ė‚­ė œ ę˛Ŋęŗ ", "permanent_deletion_warning_setting_description": "항ëĒŠė„ ė™„ė „ížˆ ė‚­ė œí•˜ę¸° ė „ ę˛Ŋęŗ  ëŠ”ė‹œė§€ëĨŧ í‘œė‹œí•Šë‹ˆë‹¤.", "permanently_delete": "똁ęĩŦ ė‚­ė œ", - "permanently_delete_assets_count": "{count, plural, one {항ëĒŠ} other {항ëĒŠ}} 똁ęĩŦ ė‚­ė œ", + "permanently_delete_assets_count": "{count, plural, one {asset} other {assets}}ëĨŧ 똁ęĩŦė‚­ė œ", "permanently_delete_assets_prompt": "{count, plural, one {ė´ 항ëĒŠė„} other {항ëĒŠ #氜ëĨŧ}} 똁ęĩŦ렁ėœŧ로 ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까? {count, plural, one {항ëĒŠė´} other {항ëĒŠė´}} ė•¨ë˛”ė— íŦ함된 ę˛Ŋ뚰 ė•¨ë˛”ė—ė„œ ė œęą°ëŠë‹ˆë‹¤.", "permanently_deleted_asset": "항ëĒŠė´ 똁ęĩŦ렁ėœŧ로 ė‚­ė œë˜ė—ˆėŠĩ니다.", "permanently_deleted_assets_count": "{count, plural, one {#氜} other {#氜}} 항ëĒŠė´ 똁ęĩŦ렁ėœŧ로 ė‚­ė œë¨", @@ -1554,6 +1683,8 @@ "person_age_years": "{years, plural, other {#넏}}", "person_birthdate": "{date} ėļœėƒ", "person_hidden": "{name}{hidden, select, true { (ėˆ¨ęš€)} other {}}", + "person_recognized": "ė‹ ė›ė´ í™•ė¸ëœ ė‚Ŧ람", + "person_selected": "ė„ íƒëœ ė‚Ŧ람", "photo_shared_all_users": "ė´ë¯¸ ëĒ¨ë“  ė‚ŦėšŠėžė™€ ė‚Ŧė§„ė„ ęŗĩ뜠 ė¤‘ė´ęą°ë‚˜ 다ëĨ¸ ė‚ŦėšŠėžę°€ ė—†ëŠ” 것 같ėŠĩ니다.", "photos": "ė‚Ŧė§„", "photos_and_videos": "ė‚Ŧė§„ 및 ë™ė˜ėƒ", @@ -1648,7 +1779,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {항ëĒŠ #氜} other {항ëĒŠ #氜}}ëĨŧ 냈 ė¸ëŦŧė—ę˛Œ ėžŦė§€ė •í–ˆėŠĩ니다.", "reassing_hint": "ę¸°ėĄ´ ė¸ëŦŧ뗐 ė„ íƒí•œ 항ëĒŠ 할당", "recent": "ėĩœęˇŧ", - "recent-albums": "ėĩœęˇŧ ė•¨ë˛”", + "recent_albums": "ėĩœęˇŧ ė•¨ë˛”", "recent_searches": "ėĩœęˇŧ ę˛€ėƒ‰", "recently_added": "ėĩœęˇŧ ėļ”ę°€", "recently_added_page_title": "ėĩœęˇŧ ėļ”ę°€", @@ -1712,6 +1843,7 @@ "reset_sqlite_confirmation": "SQLite ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ė´ˆę¸°í™”í•˜ė‹œę˛ ėŠĩ니까? ë°ė´í„°ëĨŧ ėžŦ동기화하려면 ëĄœęˇ¸ė•„ė›ƒ 후 ë‹¤ė‹œ ëĄœęˇ¸ė¸í•´ė•ŧ 합니다.", "reset_sqlite_success": "SQLite ë°ė´í„°ë˛ ė´ėŠ¤ëĨŧ ė´ˆę¸°í™”í–ˆėŠĩ니다.", "reset_to_default": "ę¸°ëŗ¸ę°’ėœŧ로 ëŗĩ뛐", + "resolution": "í•´ėƒë„", "resolve_duplicates": "ëš„ėŠˇí•œ 항ëĒŠ í™•ė¸", "resolved_all_duplicates": "ëš„ėŠˇí•œ 항ëĒŠė„ ëĒ¨ë‘ 래ëĻŦ했ėŠĩ니다.", "restore": "ëŗĩ뛐", @@ -1736,6 +1868,7 @@ "saved_settings": "ė„¤ė •ė´ ė €ėžĨë˜ė—ˆėŠĩ니다.", "say_something": "ëŒ“ę¸€ė„ ėž…ë Ĩí•˜ė„¸ėš”", "scaffold_body_error_occurred": "똤ëĨ˜ę°€ ë°œėƒí–ˆėŠĩ니다.", + "scan": "늤ėē”", "scan_all_libraries": "ëĒ¨ë“  ëŧė´ë¸ŒëŸŦëĻŦ 늤ėē”", "scan_library": "늤ėē”", "scan_settings": "늤ėē” ė„¤ė •", @@ -1747,6 +1880,7 @@ "search_by_description_example": "ë™í•´ė•ˆė—ė„œ ë§žė´í•œ ėƒˆí•´ ėŧėļœ", "search_by_filename": "파ėŧëĒ… 또는 확ėžĨėžëĄœ ę˛€ėƒ‰", "search_by_filename_example": "똈: IMG_1234.JPG 또는 PNG", + "search_by_ocr": "OCR로 ę˛€ėƒ‰", "search_camera_lens_model": "렌ėψ ëĒ¨ë¸ ę˛€ėƒ‰...", "search_camera_make": "ėš´ëŠ”ëŧ ė œėĄ°ė‚Ŧ ę˛€ėƒ‰...", "search_camera_model": "ėš´ëŠ”ëŧ ëĒ¨ë¸ëĒ… ę˛€ėƒ‰...", @@ -1800,7 +1934,9 @@ "second": "봈", "see_all_people": "ëĒ¨ë“  ė¸ëŦŧ ëŗ´ę¸°", "select": "ė„ íƒ", + "select_album": "ė•¨ë˛” ė„ íƒ", "select_album_cover": "ė•¨ë˛” ėģ¤ë˛„ ė„ íƒ", + "select_albums": "ė•¨ë˛” ė„ íƒ", "select_all": "ëĒ¨ë‘ ė„ íƒ", "select_all_duplicates": "ëš„ėŠˇí•œ 항ëĒŠ ëĒ¨ë‘ ė„ íƒ", "select_all_in": "{group}ė˜ ëĒ¨ë“  항ëĒŠ ė„ íƒ", @@ -1811,6 +1947,8 @@ "select_keep_all": "ëĒ¨ë‘ ėœ ė§€", "select_library_owner": "ëŧė´ë¸ŒëŸŦëĻŦ ė†Œėœ ėž ė„ íƒ", "select_new_face": "냈 ė–ŧęĩ´ ė„ íƒ", + "select_people": "ė‚Ŧ람 ė„ íƒ", + "select_person": "ė‚Ŧ람 ė„ íƒ", "select_person_to_tag": "태그할 ė¸ëŦŧė„ ė„ íƒí•˜ė„¸ėš”.", "select_photos": "ė‚Ŧė§„ ė„ íƒ", "select_trash_all": "ëĒ¨ë‘ ė‚­ė œ", @@ -1826,6 +1964,8 @@ "server_offline": "ė˜¤í”„ëŧė¸", "server_online": "똍ëŧė¸", "server_privacy": "ę°œė¸ė •ëŗ´", + "server_restarting_description": "ė´ íŽ˜ė´ė§€ëŠ” ėž ė‹œ 후 ėƒˆëĄœ ęŗ ėŗė§‘ë‹ˆë‹¤.", + "server_restarting_title": "ė„œë˛„ę°€ ėžŦė‹œėž‘ ė¤‘ėž…ë‹ˆë‹¤", "server_stats": "ė„œë˛„ í†ĩęŗ„", "server_update_available": "ė„œë˛„ ė—…ë°ė´íŠ¸ 가ëŠĨ", "server_version": "ė„œë˛„ ë˛„ė „", @@ -1944,11 +2084,13 @@ "show_password": "비밀번호 í‘œė‹œ", "show_person_options": "ė¸ëŦŧ ė˜ĩė…˜ í‘œė‹œ", "show_progress_bar": "ė§„í–‰ í‘œė‹œė¤„ í‘œė‹œ", + "show_schema": "ėŠ¤í‚¤ë§ˆ í‘œė‹œ", "show_search_options": "ę˛€ėƒ‰ ė˜ĩė…˜ í‘œė‹œ", "show_shared_links": "ęŗĩ뜠 링íŦ í‘œė‹œ", "show_slideshow_transition": "ėŠŦëŧė´ë“œ ė „í™˜ í‘œė‹œ", "show_supporter_badge": "ė„œíŦ터 ë°°ė§€", "show_supporter_badge_description": "ė„œíŦ터 ë°°ė§€ í‘œė‹œ", + "show_text_recognition": "í…ėŠ¤íŠ¸ ė¸ė‹ í‘œė‹œ", "show_text_search_menu": "í…ėŠ¤íŠ¸ ę˛€ėƒ‰ 메뉴 í‘œė‹œ", "shuffle": "ė…”í”Œ", "sidebar": "ė‚Ŧė´ë“œë°”", @@ -1960,6 +2102,8 @@ "skip_to_folders": "폴더로 건너뛰기", "skip_to_tags": "태그로 건너뛰기", "slideshow": "ėŠŦëŧė´ë“œ ė‡ŧ", + "slideshow_repeat": "ėŠŦëŧė´ë“œ ė‡ŧ 반ëŗĩ", + "slideshow_repeat_description": "ėŠŦëŧė´ë“œ ė‡ŧ가 끝나면 ė˛˜ėŒėœŧ로 ë˜ëŒė•„ę°‘ë‹ˆë‹¤", "slideshow_settings": "ėŠŦëŧė´ë“œ ė‡ŧ 네렕", "sort_albums_by": "ë‹¤ėŒėœŧ로 ė•¨ë˛” ė •ë Ŧ...", "sort_created": "ėƒė„ąëœ ë‚ ė§œ", @@ -2019,6 +2163,7 @@ "tags": "태그", "tap_to_run_job": "탭하ė—Ŧ ėž‘ė—… ė‹¤í–‰", "template": "템플ëĻŋ", + "text_recognition": "í…ėŠ¤íŠ¸ ė¸ė‹", "theme": "테마", "theme_selection": "테마 ė„ íƒ", "theme_selection_description": "ė‹œėŠ¤í…œė˜ 다íŦ ëĒ¨ë“œ 네렕뗐 따ëŧ 테마ëĨŧ ėžë™ėœŧ로 ė ėšŠí•Šë‹ˆë‹¤.", @@ -2037,7 +2182,9 @@ "theme_setting_three_stage_loading_title": "3ë‹¨ęŗ„ 로드 í™œė„ąí™”", "they_will_be_merged_together": "ė„ íƒí•œ ė¸ëŦŧë“¤ė„ 한 ė¸ëŦŧ로 í•ŠėšŠë‹ˆë‹¤.", "third_party_resources": "ė„œë“œ 파티 ëĻŦė†ŒėŠ¤", + "time": "ė‹œę°„", "time_based_memories": "ė‹œę°„ 揰뤀 ėļ”ė–ĩ", + "time_based_memories_duration": "각 ė´ë¯¸ė§€ëĨŧ í‘œė‹œí•˜ëŠ” 데 깸ëĻŦ는 ė‹œę°„(봈).", "timeline": "íƒ€ėž„ëŧė¸", "timezone": "ė‹œę°„ëŒ€", "to_archive": "ëŗ´ę´€í•¨ėœŧ로 ė´ë™", @@ -2049,6 +2196,7 @@ "to_select": "ė„ íƒ", "to_trash": "ė‚­ė œ", "toggle_settings": "네렕 ëŗ€ę˛Ŋ", + "toggle_theme_description": "테마 ė „í™˜", "total": "렄랴", "total_usage": "ė´ ė‚ŦėšŠëŸ‰", "trash": "íœ´ė§€í†ĩ", @@ -2066,6 +2214,13 @@ "trash_page_select_assets_btn": "항ëĒŠ ė„ íƒ", "trash_page_title": "íœ´ė§€í†ĩ ({count})", "trashed_items_will_be_permanently_deleted_after": "íœ´ė§€í†ĩėœŧ로 ė´ë™ëœ 항ëĒŠė€ {days, plural, one {#ėŧ} other {#ėŧ}} 후 똁ęĩŦ렁ėœŧ로 ė‚­ė œëŠë‹ˆë‹¤.", + "trigger": "트ëĻŦęą°", + "trigger_asset_uploaded": "ėžė‚° ė—…ëĄœë“œë¨", + "trigger_asset_uploaded_description": "ėƒˆëĄœėš´ ė—ė…‹ė´ ė—…ëĄœë“œë  때 트ëĻŦ거됩니다", + "trigger_description": "ė›ŒíŦí”ŒëĄœėš°ëĨŧ ė‹œėž‘í•˜ëŠ” ė´ë˛¤íŠ¸", + "trigger_person_recognized": "닠뛐 í™•ė¸ë¨", + "trigger_person_recognized_description": "ė‚ŦëžŒė´ ę°ė§€ë˜ëŠ´ ėž‘ë™í•Šë‹ˆë‹¤", + "trigger_type": "트ëĻŦęą° ėœ í˜•", "troubleshoot": "ëŦ¸ė œ 해결", "type": "í˜•ė‹", "unable_to_change_pin_code": "PIN ėŊ”드ëĨŧ ëŗ€ę˛Ŋ할 눘 ė—†ėŒ", @@ -2080,6 +2235,7 @@ "unhide_person": "ė¸ëŦŧ ėˆ¨ęš€ í•´ė œ", "unknown": "ė•Œ 눘 ė—†ėŒ", "unknown_country": "ė•Œ 눘 ė—†ëŠ” 맀뗭", + "unknown_date": "ė•Œ 눘 ė—†ëŠ” ë‚ ė§œ", "unknown_year": "ė•Œ 눘 ė—†ëŠ” ė—°ë„", "unlimited": "ëŦ´ė œí•œ", "unlink_motion_video": "ëĒ¨ė…˜ ëš„ë””ė˜¤ 링íŦ í•´ė œ", @@ -2096,17 +2252,19 @@ "unstack": "ėŠ¤íƒ 풀기", "unstack_action_prompt": "항ëĒŠ {count}氜 ėŠ¤íƒ 풀ëĻŧ", "unstacked_assets_count": "항ëĒŠ {count, plural, one {#氜} other {#氜}}ė˜ ėŠ¤íƒė„ í’€ė—ˆėŠĩ니다.", + "unsupported_field_type": "ė§€ė›ë˜ė§€ ė•ŠëŠ” 필드 ėœ í˜•", "untagged": "태그 í•´ė œë¨", + "untitled_workflow": "렜ëĒŠ ė—†ëŠ” ė›ŒíŦ플로", "up_next": "ë‹¤ėŒ", "update_location_action_prompt": "ė„ íƒí•œ {count}氜 항ëĒŠ ėœ„ėš˜ ė—…ë°ė´íŠ¸:", "updated_at": "ė—…ë°ė´íŠ¸ë¨", "updated_password": "비밀번호가 ëŗ€ę˛Ŋë˜ė—ˆėŠĩ니다.", "upload": "ė—…ëĄœë“œ", - "upload_action_prompt": "{count}氜 항ëĒŠ ė—…ëĄœë“œ 대기 뤑", "upload_concurrency": "ė—…ëĄœë“œ ë™ė‹œė„ą", "upload_details": "ė—…ëĄœë“œ ėƒė„¸", "upload_dialog_info": "ė„ íƒí•œ 항ëĒŠė„ ė„œë˛„ė— ë°ąė—…í•˜ė‹œę˛ ėŠĩ니까?", "upload_dialog_title": "항ëĒŠ ė—…ëĄœë“œ", + "upload_error_with_count": "{count, plural, one {#氜} other {#氜}} 항ëĒŠ ė—…ëĄœë“œ ė‹¤íŒ¨", "upload_errors": "ė—…ëĄœë“œę°€ ė™„ëŖŒë˜ė—ˆėŠĩ니다. 항ëĒŠ {count, plural, one {#氜} other {#氜}}ëĨŧ ė—…ëĄœë“œí•˜ė§€ ëĒģ했ėŠĩ니다. ė—…ëĄœë“œëœ 항ëĒŠė„ ëŗ´ë ¤ëŠ´ íŽ˜ė´ė§€ëĨŧ ėƒˆëĄœęŗ ėš¨í•˜ė„¸ėš”.", "upload_finished": "ė—…ëĄœë“œ ė™„ëŖŒ", "upload_progress": "렄랴 {total, number}氜 뤑 {processed, number}氜 ė™„ëŖŒ, {remaining, number}氜 대기 뤑", @@ -2142,6 +2300,7 @@ "utilities": "도ęĩŦ", "validate": "검ėĻ", "validate_endpoint_error": "ėœ íš¨í•œ URLė„ ėž…ë Ĩí•˜ė„¸ėš”.", + "validation_error": "ėœ íš¨ė„ą 검ė‚Ŧ 똤ëĨ˜", "variables": "ëŗ€ėˆ˜", "version": "ë˛„ė „", "version_announcement_closing": "ë‹šė‹ ė˜ ėšœęĩŦ, Alex가", @@ -2157,6 +2316,7 @@ "view_album": "ė•¨ë˛” ëŗ´ę¸°", "view_all": "ëĒ¨ë‘ ëŗ´ę¸°", "view_all_users": "ëĒ¨ë“  ė‚ŦėšŠėž ëŗ´ę¸°", + "view_asset_owners": "ėžė‚° ė†Œėœ ėž ëŗ´ę¸°", "view_details": "ėƒė„¸ ëŗ´ę¸°", "view_in_timeline": "íƒ€ėž„ëŧė¸ė—ė„œ ëŗ´ę¸°", "view_link": "링íŦ ëŗ´ę¸°", @@ -2172,6 +2332,8 @@ "viewer_stack_use_as_main_asset": "대표 항ëĒŠėœŧ로 네렕", "viewer_unstack": "ėŠ¤íƒ 풀기", "visibility_changed": "ė¸ëŦŧ {count, plural, one {#ëĒ…} other {#ëĒ…}}ė˜ í‘œė‹œ ė—Ŧëļ€ę°€ ëŗ€ę˛Ŋ됨", + "visual": "비ėŖŧė–ŧ", + "visual_builder": "비ėŖŧė–ŧ 빌더", "waiting": "대기 뤑", "waiting_count": "대기: {count}", "warning": "ę˛Ŋęŗ ", @@ -2180,6 +2342,19 @@ "welcome_to_immich": "í™˜ė˜í•Šë‹ˆë‹¤", "width": "너비", "wifi_name": "W-Fi ė´ëĻ„", + "workflow_delete_prompt": "ė´ ė›ŒíŦ플로ëĨŧ ė •ë§ëĄœ ė‚­ė œí•˜ė‹œę˛ ėŠĩ니까?", + "workflow_deleted": "ė›ŒíŦ플로가 ė‚­ė œë˜ė—ˆėŠĩ니다", + "workflow_description": "ė›ŒíŦ플로 네ëĒ…", + "workflow_info": "ė›ŒíŦí”ŒëĄœėš° ė •ëŗ´", + "workflow_json": "ė›ŒíŦí”ŒëĄœėš° JSON", + "workflow_json_help": "ė›ŒíŦ플로 ęĩŦė„ąė„ JSON í˜•ė‹ėœŧ로 íŽ¸ė§‘í•˜ė„¸ėš”. ëŗ€ę˛Ŋ ė‚Ŧí•­ė€ 비ėŖŧė–ŧ ëšŒë”ė— 동기화됩니다.", + "workflow_name": "ė›ŒíŦ플로 ė´ëĻ„", + "workflow_navigation_prompt": "ëŗ€ę˛Ŋ ė‚Ŧí•­ė„ ė €ėžĨí•˜ė§€ ė•Šęŗ  ė´ë™í•˜ė‹œę˛ ėŠĩ니까?", + "workflow_summary": "ė›ŒíŦí”ŒëĄœėš° ėš”ė•Ŋ", + "workflow_update_success": "ė›ŒíŦ플로가 ė„ąęŗĩ렁ėœŧ로 ė—…ë°ė´íŠ¸ë˜ė—ˆėŠĩ니다", + "workflow_updated": "ė›ŒíŦ플로가 ė—…ë°ė´íŠ¸ë˜ė—ˆėŠĩ니다", + "workflows": "ė›ŒíŦ플로", + "workflows_help_text": "ė›ŒíŦ플로는 트ëĻŦęą°ė™€ 필터ëĨŧ 기반ėœŧ로 ėžė‚°ė— 대한 ėž‘ė—…ė„ ėžë™í™”í•Šë‹ˆë‹¤", "wrong_pin_code": "ėž˜ëĒģ된 PIN ėŊ”드", "year": "년", "years_ago": "{years, plural, one {#년} other {#년}} ė „", diff --git a/i18n/lt.json b/i18n/lt.json index 5e02311666..fec5905957 100644 --- a/i18n/lt.json +++ b/i18n/lt.json @@ -5,8 +5,10 @@ "acknowledge": "Patvirtinti", "action": "Veiksmas", "action_common_update": "Naujinti", + "action_description": "Veiksmai, kurie atliekami filtruotiems elementams", "actions": "Veiksmai", "active": "Vykdoma", + "active_count": "Vykdoma: {count}", "activity": "Veikla", "activity_changed": "Veikla yra {enabled, select, true {įjungta} other {iÅĄjungta}}", "add": "Pridėti", @@ -14,9 +16,14 @@ "add_a_location": "Pridėti vietovę", "add_a_name": "Pridėti vardą", "add_a_title": "Pridėti pavadinimą", + "add_action": "Pridėti veiksmą", + "add_action_description": "Spustelėkite, kad pridėtumėte veiksmą atlikimui", + "add_assets": "Pridėti", "add_birthday": "Pridėti gimimo diena", "add_endpoint": "Pridėti galutinį taÅĄką", "add_exclusion_pattern": "Pridėti iÅĄimčiÅŗ ÅĄabloną", + "add_filter": "Pritaikyti filtrą", + "add_filter_description": "Spustelėkite, kad pridėtumėte filtro sąlygą", "add_location": "Pridėti vietovę", "add_more_users": "Pridėti daugiau naudotojÅŗ", "add_partner": "Pridėti partnerį", @@ -26,13 +33,16 @@ "add_to": "Pridėti įâ€Ļ", "add_to_album": "Pridėti į albumą", "add_to_album_bottom_sheet_added": "Pridėta į {album}", - "add_to_album_bottom_sheet_already_exists": "Jau yra albume {album}", + "add_to_album_bottom_sheet_already_exists": "Jau yra {album}", "add_to_album_bottom_sheet_some_local_assets": "Dalis vietiniÅŗ elementÅŗ negalėjo bÅĢti pridėti į albumą", - "add_to_album_toggle": "Perjungti paÅžymėjimus albumui {album}", + "add_to_album_toggle": "Perjungti pasirinkimus ÅĄiam {album}", "add_to_albums": "Pridėti į albumus", "add_to_albums_count": "Pridėti į albumus ({count})", + "add_to_bottom_bar": "Pridėti prie", "add_to_shared_album": "Pridėti į bendrinamą albumą", + "add_upload_to_stack": "Pridėti įkėlimą į krÅĢvą", "add_url": "Pridėti URL", + "add_workflow_step": "Pridėti darbÅŗ eigos Åžingsnį", "added_to_archive": "Pridėta į archyvą", "added_to_favorites": "Pridėta prie mėgstamiausiÅŗ", "added_to_favorites_count": "{count, plural, one {# pridėtas} few {# pridėti} other {# pridėta}} prie mėgstamiausiÅŗ", @@ -65,6 +75,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ą?", + "copy_config_to_clipboard_description": "Kopijuokite dabartinę sistemos konfigÅĢraciją kaip JSON objektą į iÅĄkarpinę", "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", @@ -72,6 +83,8 @@ "disable_login": "IÅĄjungti prisijungimą", "duplicate_detection_job_description": "Vykdyti maÅĄininį mokymąsi panaÅĄiÅŗ vaizdÅŗ aptikimui. Priklauso nuo iÅĄmaniosios paieÅĄkos", "exclusion_pattern_description": "IÅĄimčiÅŗ ÅĄablonai leidÅžia nepaisyti failÅŗ ir aplankÅŗ skenuojant jÅĢsÅŗ biblioteką. Tai yra naudinga, jei turite aplankÅŗ su failais, kuriÅŗ nenorite importuoti, pavyzdÅžiui, RAW failai.", + "export_config_as_json_description": "AtsisiÅŗskite dabartinę sistemos konfigÅĢraciją kaip JSON failą", + "external_libraries_page_description": "Administratoriaus iÅĄorinės bibliotekos puslapis", "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.", @@ -91,6 +104,8 @@ "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", + "image_progressive": "Progresyvus", + "image_progressive_description": "JPEG vaizdus koduokite progresyviai, kad vaizdai krautÅŗsi laipsniÅĄkai. Tai neturi jokios įtakos WebP nuotraukoms.", "image_quality": "Kokybė", "image_resolution": "Rezoliucija", "image_resolution_description": "Didesnės rezoliucijos gali iÅĄsaugoti daugiau detaliÅŗ, bet ilgiau uÅžtrunka uÅžkoduoti, failai yra didesni ir programos reagavimo laikas gali sumaŞėti.", @@ -99,6 +114,7 @@ "image_thumbnail_description": "MaÅža miniatiÅĢra su iÅĄvalytais metaduomenimis, naudojama kai ÅžiÅĢrimos nuotraukÅŗ grupės, kaip ir pagrindinėje laiko juostoje", "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", + "import_config_from_json_description": "Importuokite sistemos konfigÅĢraciją, įkeliant JSON konfigÅĢracijos failą", "job_concurrency": "{job} lygiagretumas", "job_created": "UÅžduotis sukurta", "job_not_concurrency_safe": "Å i uÅžduotis nėra saugi apdoroti lygiagrečiai.", @@ -106,16 +122,22 @@ "job_settings_description": "Keisti uÅžduočiÅŗ lygiagretumą", "jobs_delayed": "{jobCount, plural, one {# atidėtas} few {# atidėti} other {# atidėtÅŗ}}", "jobs_failed": "{jobCount, plural, other {# nepavyko}}", + "jobs_over_time": "UÅžduotys per laiką", "library_created": "Sukurta biblioteka: {library}", "library_deleted": "Biblioteka iÅĄtrinta", + "library_details": "Bibliotekos savybės", + "library_folder_description": "Nurodykite importuotiną aplanką. Å is aplankas, įskaitant poaplankius, bus nuskaitytas ieÅĄkant vaizdÅŗ ir vaizdo įraÅĄÅŗ.", + "library_remove_exclusion_pattern_prompt": "Ar tikrai norite paÅĄalinti ÅĄią iÅĄimtį?", + "library_remove_folder_prompt": "Ar tikrai norite paÅĄalinti ÅĄÄ¯ importo aplanką?", "library_scanning": "Periodinis skenavimas", "library_scanning_description": "KonfigÅĢruoti periodinį bibliotekos skanavimą", "library_scanning_enable_description": "ÄŽgalinti periodinį bibliotekos skenavimą", "library_settings": "IÅĄorinė biblioteka", "library_settings_description": "Tvarkyti iÅĄorinės bibliotekos parametrus", "library_tasks_description": "Skenuoti iÅĄorines bibliotekas, ieÅĄkant naujÅŗ arba pakeistÅŗ iÅĄtekliÅŗ", + "library_updated": "Atnaujinta biblioteka", "library_watching_enable_description": "Stebėti iÅĄorines bibliotekas dėl failÅŗ pakeitimÅŗ", - "library_watching_settings": "BibliotekÅŗ stebėjimas (EKSPERIMENTINIS)", + "library_watching_settings": "BibliotekÅŗ stebėjimas [EKSPERIMENTINIS]", "library_watching_settings_description": "AutomatiÅĄkai stebėti dėl pakeistÅŗ failÅŗ", "logging_enable_description": "ÄŽjungti Åžurnalo vedimą", "logging_level_description": "ÄŽjungus, kokį Åžurnalo vedimo lygį naudot.", @@ -149,8 +171,18 @@ "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_ocr": "OCR", "machine_learning_ocr_description": "Naudoti maÅĄininį mokymąsį, teksto atpaÅžinimui nuotraukose", + "machine_learning_ocr_enabled": "ÄŽjungti OCR", + "machine_learning_ocr_enabled_description": "Jei ÅĄis parametras iÅĄjungtas, vaizdams nebus pritaikytas teksto atpaÅžinimas.", "machine_learning_ocr_max_resolution": "Maksimali skiriamoji geba", + "machine_learning_ocr_max_resolution_description": "PerÅžiÅĢros, kuriÅŗ skiriamoji geba yra didesnė nei ÅĄi, bus pakeistos iÅĄlaikant proporcijas. Didesnės vertės yra tikslesnės, tačiau jÅŗ apdorojimas trunka ilgiau ir sunaudoja daugiau atminties.", + "machine_learning_ocr_min_detection_score": "Minimalus atpaÅžinimo balas", + "machine_learning_ocr_min_detection_score_description": "Minimalus pasitikėjimo balas, reikalingas tekstui aptikti, yra nuo 0 iki 1. MaÅžesnės vertės aptiks daugiau teksto, bet gali sukelti klaidingÅŗ teigiamÅŗ rezultatÅŗ.", + "machine_learning_ocr_min_recognition_score": "Minimalus atpaÅžinimo balas", + "machine_learning_ocr_min_score_recognition_description": "Minimalus pasitikėjimo balas, kad aptiktas tekstas bÅĢtÅŗ atpaÅžintas nuo 0 iki 1. MaÅžesnės vertės atpaÅžins daugiau teksto, bet gali sukelti klaidingus teigiamus rezultatus.", + "machine_learning_ocr_model": "OCR modelis", + "machine_learning_ocr_model_description": "ServeriÅŗ modeliai yra tikslesni nei mobilieji modeliai, tačiau jÅŗ apdorojimas trunka ilgiau ir jie naudoja daugiau atminties.", "machine_learning_settings": "MaÅĄininio mokymosi nustatymai", "machine_learning_settings_description": "Tvarkyti maÅĄininio mokymosi funkcijas ir nustatymus", "machine_learning_smart_search": "IÅĄmanioji paieÅĄka", @@ -158,7 +190,23 @@ "machine_learning_smart_search_enabled": "ÄŽjungti iÅĄmaniąją paieÅĄką", "machine_learning_smart_search_enabled_description": "Jei iÅĄjungta, vaizdai nebus uÅžkoduoti iÅĄmaniajai paieÅĄkai.", "machine_learning_url_description": "MaÅĄininio mokymosi serverio URL. Jei pateikta daugiau nei vienas URL, serveriai bus bandomi eilės tvarka nuo pirmo iki paskutinio tol, kol bus rastas vienas veikiantis serveris.", + "maintenance_delete_backup": "IÅĄtrinti atsarginę kopiją", + "maintenance_delete_backup_description": "Å is failas bus negrįŞtamai iÅĄtrintas.", + "maintenance_delete_error": "Nepavyko iÅĄtrinti atsarginės kopijos.", + "maintenance_restore_backup": "Atstatyti atsarginę kopiją", + "maintenance_restore_backup_description": "Immich bus iÅĄtrintas ir atkurtas iÅĄ pasirinktos atsarginės kopijos. PrieÅĄ tęsiant bus sukurta atsarginė kopija.", + "maintenance_restore_backup_different_version": "Å i atsarginė kopija buvo sukurta su skirtinga Immich versija!", + "maintenance_restore_backup_unknown_version": "Nepavyko nustatyti atsarginės kopijos versijos.", + "maintenance_restore_database_backup": "Atstatyti duomenÅŗ bazę", + "maintenance_restore_database_backup_description": "GrÄ…Åžinti į ankstesnę duomenÅŗ bazės bÅĢseną naudojant atsarginę kopiją", + "maintenance_settings": "Aptarnavimas", + "maintenance_settings_description": "Perjungti „Immich“ į aptarnavimo reÅžimą.", + "maintenance_start": "Perjungti į aptarnavimo reÅžimą", + "maintenance_start_error": "Nepavyko paleisti aptarnavimo reÅžimo.", + "maintenance_upload_backup": "IÅĄsiÅŗsti atsarginę duomenÅŗ bazės kopiją", + "maintenance_upload_backup_error": "Nepavyko iÅĄsiÅŗsti atsarginės kopijos, ar tai .sql/.sql.gz failas?", "manage_concurrency": "Tvarkyti lygiagretumą", + "manage_concurrency_description": "Eikite į darbÅŗ puslapį, kad galėtumėte valdyti darbÅŗ lygiagretumą", "manage_log_settings": "Valdyti Åžurnalo nuostatas", "map_dark_style": "Tamsioji tema", "map_enable_description": "ÄŽgalinti Åžemėlapio funkcijas", @@ -208,6 +256,8 @@ "notification_email_ignore_certificate_errors_description": "Nepaisyti TLS sertifikato patvirtinimo klaidÅŗ (nerekomenduojama)", "notification_email_password_description": "SlaptaÅžodis, naudojant autentikacijai su elektroninio paÅĄto serveriu", "notification_email_port_description": "El. paÅĄto serverio prievadas (pvz. 25, 465 arba 587)", + "notification_email_secure": "SMTPS", + "notification_email_secure_description": "Naudoti SMTPS (SMTP per TLS)", "notification_email_sent_test_email_button": "SiÅŗsti bandomąjį el. laiÅĄką ir iÅĄsaugoti", "notification_email_setting_description": "El. paÅĄto praneÅĄimÅŗ siuntimo nustatymai", "notification_email_test_email": "IÅĄsiÅŗsti bandomąjį el. laiÅĄką", @@ -222,7 +272,7 @@ "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_client_secret_description": "Privalomas konfidencialaus kliento, arba jei vieÅĄasis klientas nepalaiko PKCE (Proof Key for Code Exchange).", "oauth_enable_description": "Prisijungti su OAuth", "oauth_mobile_redirect_uri": "Mobiliojo peradresavimo URI", "oauth_mobile_redirect_uri_override": "Mobiliojo peradresavimo URI pakeitimas", @@ -246,10 +296,14 @@ "password_settings_description": "Tvarkyti prisijungimo slaptaÅžodÅžiu nustatymus", "paths_validated_successfully": "Visi keliai patvirtinti sėkmingai", "person_cleanup_job": "IÅĄvalyti asmenis", + "queue_details": "IÅĄsami informacija apie eilę", + "queues": "DarbÅŗ eilės", + "queues_page_description": "Administratoriaus darbÅŗ eilės puslapis", "quota_size_gib": "Kvotos dydis (GiB)", "refreshing_all_libraries": "Perkraunamos visos bibliotekos", "registration": "Administratoriaus registracija", "registration_description": "Kadangi esate pirmasis ÅĄio sistemos naudotojas, jums bus priskirta administratoriaus rolė, ir bÅĢsite atsakingas uÅž administracines uÅžduotis ir papildomÅŗ naudotojÅŗ kÅĢrimą.", + "remove_failed_jobs": "PaÅĄalinti nepavykusius darbus", "require_password_change_on_login": "Reikalauti, kad naudotojas pasikeistÅŗ slaptaÅžodį po pirmojo prisijungimo", "reset_settings_to_default": "Atstatyti nustatymus į numatytuosius", "reset_settings_to_recent_saved": "NustatymÅŗ atstatymas į neseniai iÅĄsaugotus nustatymus", @@ -262,8 +316,10 @@ "server_public_users_description": "Pridedant naudotoją į bendrinamus albumus, rodomas visÅŗ naudotojÅŗ sąraÅĄas (vardas ir el. paÅĄtas). Jei iÅĄjungta, naudotojÅŗ sąraÅĄas bus prieinamas tik administratoriÅŗ paskyroms.", "server_settings": "Serverio nustatymai", "server_settings_description": "Tvarkyti serverio nustatymus", + "server_stats_page_description": "Administratoriaus serverio statistikos puslapis", "server_welcome_message": "Sveikinimo praneÅĄimas", "server_welcome_message_description": "ÅŊinutė, rodoma prisijungimo puslapyje.", + "settings_page_description": "Administratoriaus nustatymÅŗ puslapis", "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", @@ -331,7 +387,7 @@ "transcoding_max_b_frames": "Maksimaliai B-kadrÅŗ", "transcoding_max_b_frames_description": "Didesnės reikÅĄmės pagerina suspaudimo efektyvumą, bet sulėtina uÅžkodavimą. Senesniuose prietaisuose gali bÅĢti nepalaikomas aparatinis spartinimas. 0 iÅĄjungia B-kadrus, o -1 nustato reikÅĄmę automatiÅĄkai.", "transcoding_max_bitrate": "Maksimalus bitÅŗ srautas", - "transcoding_max_bitrate_description": "Pasirenkant max bitrate galima pasiekti labiau nuspėjamą failÅŗ dydį su minimaliais kokybės praradimais. Prie 720p, tipinės reikÅĄmės yra 2600 kbits/s jei BP9 ar HVEC, arba 4500 kbits/s jei H.264. Neveiksnus jei pasirenkamas 0.", + "transcoding_max_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. Kai vienetai nenurodyti, priimama k (kaip kbits/s); taigi 5000, 5000k, ir 5M (kaip Mbits/s) yra atitikmenys.", "transcoding_max_keyframe_interval": "Maksimalus raktinio kadro intervalas", "transcoding_max_keyframe_interval_description": "Nustato maksimalÅŗ kadro atstumą tarp raktiniÅŗ kadrÅŗ. ÅŊemesnės reikÅĄmės pablogina suspaudimo efektyvumą, bet pagerina prasukimo laiką ir gali pagerinti greito veiksmo scenÅŗ kokybę. 0 - nustato ÅĄią reikÅĄmę automatiÅĄkai.", "transcoding_optimal_description": "Vaizdo įraÅĄai aukÅĄtesne nei tikslinė rezoliucija arba nepalaikomu formatu", @@ -349,7 +405,7 @@ "transcoding_target_resolution": "Skiriamoji geba", "transcoding_target_resolution_description": "Didesnės skiriamosios gebos gali iÅĄsaugoti daugiau detaliÅŗ, tačiau jas koduoti uÅžtrunka ilgiau, failÅŗ dydÅžiai yra didesni ir gali sumaŞėti programos jautrumas.", "transcoding_temporal_aq": "Laikinas adaptyvus kvantavimas", - "transcoding_temporal_aq_description": "Galioja tik NVENC. Pagerina detaliÅŗ, maÅžo judesio scenÅŗ kokybę. Gali bÅĢti nepalaikoma senesniÅŗ įrenginiÅŗ.", + "transcoding_temporal_aq_description": "Galioja tik NVENC. Temporal Adaptive Quantization pagerina kokybę didesnės raiÅĄkos, maÅžo judesio scenÅŗ kokybę. Gali bÅĢti nepalaikoma senesniÅŗ įrenginiÅŗ.", "transcoding_threads": "Gijos", "transcoding_threads_description": "Didesnės reikÅĄmės pagreitina kodavimą, bet kol aktyvus palieka maÅžiau serverio resursÅŗ kitoms uÅžduotims. Å i reikÅĄmė negali bÅĢti didesnė uÅž procesoriaus branduoliÅŗ kiekį. Jei reikÅĄmė 0, tai iÅĄnaudoja maksimaliai.", "transcoding_tone_mapping": "TonÅŗ atvaizdavimas", @@ -382,6 +438,8 @@ "user_restore_scheduled_removal": "Atkurti naudotoją - suplanuotas paÅĄalinimas {date, date, long}", "user_settings": "Naudotojo nustatymai", "user_settings_description": "Valdyti naudotojo nustatymus", + "user_successfully_removed": "Naudotojas {email} sėkmingai paÅĄalintas.", + "users_page_description": "AdministratoriÅŗ vartotojÅŗ puslapis", "version_check_enabled_description": "ÄŽgalinti versijÅŗ tikrinimą", "version_check_implications": "VersijÅŗ tikrinimas reikalauja periodiÅĄkos komunikacijos su github.com", "version_check_settings": "Versijos tikrinimas", @@ -393,17 +451,20 @@ "admin_password": "Administratoriaus slaptaÅžodis", "administration": "Administravimas", "advanced": "Sudėtingesnis", + "advanced_settings_clear_image_cache": "IÅĄvalyti vaizdo talpyklą", + "advanced_settings_clear_image_cache_error": "Nepavyko iÅĄvalyti vaizdo taupyklos", + "advanced_settings_clear_image_cache_success": "Sėkmingai iÅĄvalyta {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Naudokite ÅĄÄ¯ nustatymą medijos filtravimui sinchronizuojant remiantis alternatyviais kriterijais. Naudokite tik jei programa turi problemÅŗ su visÅŗ albumÅŗ aptikimu.", "advanced_settings_enable_alternate_media_filter_title": "[EKSPERIMENTINIS] Naudokite alternatyvÅŗ įrenginio albumÅŗ sinchronizavimo filtrą", "advanced_settings_log_level_title": "ÅŊurnalo įraÅĄÅŗ lygis: {level}", "advanced_settings_prefer_remote_subtitle": "Kai kurie įrenginiai labai lėtai įkelia miniatiÅĢras iÅĄ vietiniÅŗ elementÅŗ. Aktyvuokite ÅĄÄ¯ nustatymą, kad vietoje to uÅžkrautumėte nuotolines nuotraukas.", "advanced_settings_prefer_remote_title": "Teikti pirmenybę nuotolinėms nuotraukoms", "advanced_settings_proxy_headers_subtitle": "Nustatykite tarpinio serverio antraÅĄtes kurias Immich siÅŗs su kiekvienu uÅžklausimu", - "advanced_settings_proxy_headers_title": "Tarpinio serverio antraÅĄtės", + "advanced_settings_proxy_headers_title": "Custom proxy headeriai [Experimentinis]", "advanced_settings_readonly_mode_subtitle": "ÄŽgalina tik skaitymo reÅžimą kai nuotraukas galima tik ÅžiÅĢrėti, draudÅžiama paÅžymėti kelias, dalintis, transliuoti ar iÅĄtrinti. ÄŽgalinkit/uÅždrauskit tik skaitymą per naudotojo avatar'ą iÅĄ pagrindinio lango", - "advanced_settings_readonly_mode_title": "Tik skaitymo reÅžimas", + "advanced_settings_readonly_mode_title": "Tik skaitymo rÄ—Åžimas", "advanced_settings_self_signed_ssl_subtitle": "PraleidÅžia SSL sertifikato tikrinimą serverio galutiniam taÅĄkui. Privaloma pačiÅŗ pasiraÅĄytiems sertifikatams.", - "advanced_settings_self_signed_ssl_title": "Leisti pačiÅŗ pasiraÅĄytus SSL sertifikatus", + "advanced_settings_self_signed_ssl_title": "Leisti self-signed SSL sertifikatus [Experimentinis]", "advanced_settings_sync_remote_deletions_subtitle": "AutomatiÅĄkai iÅĄtrinti ar atkurti elementus įrenginyje, kai tie veiksmai atliekami narÅĄyklėje", "advanced_settings_sync_remote_deletions_title": "Sinchronizuoti nuotolinius iÅĄtrynimus [EKSPERIMENTINIS]", "advanced_settings_tile_subtitle": "PaÅžangesni naudotojÅŗ nustatymai", @@ -412,6 +473,7 @@ "age_months": "AmÅžius {months, plural, one {# mėnesis} few {# mėnesiai} other {# mėnesiÅŗ}}", "age_year_months": "AmÅžius 1 metai, {months, plural, one {# mėnesis} few {# mėnesiai} other {# mėnesiÅŗ}}", "age_years": "{years, plural, other {AmÅžius #}}", + "album": "Albumas", "album_added": "Albumas pridėtas", "album_added_notification_setting_description": "Gauti el. paÅĄto praneÅĄimą, kai bÅĢsite pridėtas prie bendrinamo albumo", "album_cover_updated": "Albumo virÅĄelis atnaujintas", @@ -428,10 +490,12 @@ "album_remove_user": "PaÅĄalinti naudotoją?", "album_remove_user_confirmation": "Ar tikrai norite paÅĄalinti naudotoją {user}?", "album_search_not_found": "Pagal jÅĢsÅŗ paieÅĄką albumÅŗ nerasta", + "album_selected": "Albumas pasirinktas", "album_share_no_users": "Atrodo, kad bendrinate ÅĄÄ¯ albumą su visais naudotojais, arba neturite naudotojÅŗ, su kuriais galėtumėte bendrinti.", "album_summary": "Albumo santrauka", "album_updated": "Albumas atnaujintas", "album_updated_setting_description": "Gauti praneÅĄimą el. paÅĄtu, kai bendrinamas albumas turi naujÅŗ elementÅŗ", + "album_upload_assets": "ÄŽkelkite elementus iÅĄ savo kompiuterio ir pridėkite prie albumo", "album_user_left": "Paliko {album}", "album_user_removed": "PaÅĄalintas {user}", "album_viewer_appbar_delete_confirm": "Ar tikrai norite iÅĄtrinti ÅĄÄ¯ albumą iÅĄ savo paskyros?", @@ -449,24 +513,34 @@ "albums_default_sort_order_description": "Pradinė elementÅŗ rÅĢÅĄiavimo tvarka kai kuriamas naujas albumas.", "albums_feature_description": "ElementÅŗ rinkinys kuriuo galima dalintis su kitais naudotojais.", "albums_on_device_count": "AlbumÅŗ įrenginyje ({count})", + "albums_selected": "{count, plural, one {# pasirinktas albumas} other {# pasirinkti albumai}}", "all": "Visi", "all_albums": "Visi albumai", "all_people": "Visi Åžmonės", + "all_photos": "Visos nuotraukos", "all_videos": "Visi video", "allow_dark_mode": "Leisti tamsÅŗjį reÅžimą", "allow_edits": "Leisti redagavimus", "allow_public_user_to_download": "Leisti vieÅĄam naudotojui atsisiÅŗsti", "allow_public_user_to_upload": "Leisti vieÅĄam naudotojui įkelti", + "allowed": "LeidÅžiama", "alt_text_qr_code": "QR kodo paveiksliukas", - "anti_clockwise": "PrieÅĄ laikrodÅžio rodykles", + "always_keep": "Visada laikyti", + "always_keep_photos_hint": "Atlaisvinti Vietos iÅĄsaugos visas foto ÅĄiame įrenginyje.", + "always_keep_videos_hint": "Atlaisvinti Vietos iÅĄsaugos visas video ÅĄiame įrenginyje.", + "anti_clockwise": "PrieÅĄ laikrodÅžio rodyklę", "api_key": "API raktas", "api_key_description": "Å i reikÅĄmė bus parodyta tik vieną kartą. PraÅĄome nusikopijuoti prieÅĄ uÅždarant ÅĄÄ¯ langą.", "api_key_empty": "JÅĢsÅŗ API rakto pavadinimas netÅĢrėtÅŗ bÅĢti tuÅĄÄias", "api_keys": "API raktai", + "app_architecture_variant": "Variantas (architektÅĢra)", "app_bar_signout_dialog_content": "Ar tikrai norite atsijungti?", "app_bar_signout_dialog_ok": "Taip", "app_bar_signout_dialog_title": "Atsijungti", + "app_download_links": "Programėlės atsisiuntimo nuorodos", "app_settings": "Programos nustatymai", + "app_stores": "ProgramėliÅŗ parduotuvės", + "app_update_available": "Prieinamas programėlės atnaujinimas", "appears_in": "Susiję", "apply_count": "Taikyti ({count, number})", "archive": "Archyvas", @@ -480,10 +554,12 @@ "archived_count": "{count, plural, other {# suarchyvuota}}", "are_these_the_same_person": "Ar tai tas pats asmuo?", "are_you_sure_to_do_this": "Ar tikrai norite tai daryti?", + "array_field_not_fully_supported": "MasyvÅŗ laukams reikia rankinio JSON redagavimo", "asset_action_delete_err_read_only": "Negalima iÅĄtrinti tik skaitom(o, Åŗ) element(o, Åŗ), praleidÅžiama", "asset_action_share_err_offline": "Negalima uÅžkrauti neprisijungusiÅŗ elementÅŗ, praleidÅžiama", "asset_added_to_album": "Pridėta į albumą", "asset_adding_to_album": "Pridedama į albumąâ€Ļ", + "asset_created": "Elementas sukurtas", "asset_description_updated": "Elemento apraÅĄymas buvo atnaujintas", "asset_filename_is_offline": "Elementas {filename} nepasiekiamas", "asset_has_unassigned_faces": "Elementas turi nepriskirtÅŗ veidÅŗ", @@ -496,6 +572,9 @@ "asset_list_layout_sub_title": "IÅĄdėstymas", "asset_list_settings_subtitle": "NuotraukÅŗ tinklelio iÅĄdėstymo nustatymai", "asset_list_settings_title": "NuotraukÅŗ tinklelis", + "asset_not_found_on_device_android": "ÄŽrenginyje elementas nerastas", + "asset_not_found_on_device_ios": "ÄŽrenginyje elementas nerastas. Jei naudojate iCloud, elementas gali bÅĢti nepasiekiamas dėl blogo failo, saugomo iCloud", + "asset_not_found_on_icloud": "Elementas nerastas iCloud. Elementas gali bÅĢti nepasiekiamas dėl blogo failo, saugomo iCloud", "asset_offline": "Elementas nepasiekiamas", "asset_offline_description": "Å is iÅĄorinis elementas neberandamas diske. Dėl pagalbos susisiekite su savo Immich administratoriumi.", "asset_restored_successfully": "Elementas atkurtas sėkmingai", @@ -514,20 +593,20 @@ "assets_cannot_be_added_to_album_count": "{count, plural, one {Elementas negali bÅĢti pridėtas} few {Elementai negali bÅĢti pridėti} other {ElementÅŗ negali bÅĢti pridėta}} į albumą", "assets_cannot_be_added_to_albums": "{count, plural, one {Elementas negali bÅĢti pridėtas} few {Elementai negali bÅĢti pridėti} other {ElementÅŗ negali bÅĢti pridėta}} į nei vieną albumą", "assets_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", - "assets_deleted_permanently": "{count} elementÅŗ iÅĄtrinta galutinai", - "assets_deleted_permanently_from_server": "{count} elementÅŗ iÅĄtrinta galutinai iÅĄ Immich serverio", + "assets_deleted_permanently": "{count} {count, plural, one {elementas iÅĄtrintas} few {elementai iÅĄtrinti} other {elementÅŗ iÅĄtrinta}} galutinai", + "assets_deleted_permanently_from_server": "{count} {count, plural, one {elementas iÅĄtrintas} few {elementai iÅĄtrinti} other {elementÅŗ iÅĄtrinta}} galutinai iÅĄ Immich serverio", "assets_downloaded_failed": "{count, plural, one {AtsisiÅŗstas # failas - {error} failas nepavyko} few {AtsisiÅŗsti # failai - {error} failai nepavyko} other {AtsisiÅŗsta # failÅŗ - {error} failÅŗ nepavyko}}", "assets_downloaded_successfully": "{count, plural, one {AtsisiÅŗstas # failas sėkmingai} few {AtsisiÅŗsti # failai sėkmingai} other {AtsisiÅŗsta # failÅŗ sėkmingai}}", "assets_moved_to_trash_count": "{count, plural, one {# elementas perkeltas} few {# elementai perkelti} other {# elementÅŗ perkelta}} į ÅĄiukÅĄliadėŞę", "assets_permanently_deleted_count": "{count, plural, one {# elementas iÅĄtrintas} few {# elementai iÅĄtrinti} other {# elementÅŗ iÅĄtrinta}} visam laikui", "assets_removed_count": "{count, plural, one {PaÅĄalintas # elementas} few {PaÅĄalinti # elementai} other {PaÅĄalinta # elementÅŗ}}", - "assets_removed_permanently_from_device": "{count} elementÅŗ paÅĄalinta galutinai iÅĄ jÅĢsÅŗ įrenginio", + "assets_removed_permanently_from_device": "{count} {count, plural, one {elementas paÅĄalintas} few {elementai paÅĄalinti} other {elementÅŗ paÅĄalinta}} galutinai iÅĄ jÅĢsÅŗ įrenginio", "assets_restore_confirmation": "Ar tikrai norite atkurti visus ÅĄiukÅĄliadėŞėje esančius perkeltus elementus? Å io veiksmo atÅĄaukti negalėsite! Pastaba: nepasiekiami elementai tokiu bÅĢdu atkurti nebus.", "assets_restored_count": "{count, plural, one {Atkurtas # elementas} few {Atkurti # elementai} other {Atkurta # elementÅŗ}}", - "assets_restored_successfully": "{count} element(as, ai, Åŗ) atkurta sėkmingai", - "assets_trashed": "{count} element(ai,Åŗ,as) perkelta į ÅĄiukÅĄliadėŞę", + "assets_restored_successfully": "{count} {count, plural, one {elementas atkurtas} few {elementai atkurti} other {elementÅŗ atkurta}} sėkmingai", + "assets_trashed": "{count} {count, plural, one {elementas perkeltas} few {elementai perkelti} other {elementÅŗ perkelta}} į ÅĄiukÅĄliadėŞę", "assets_trashed_count": "Perkelta į ÅĄiukÅĄliadėŞę {count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", - "assets_trashed_from_server": "{count} element(as, ai, Åŗ) perkelta į ÅĄiukÅĄliadėŞę iÅĄ Immich serverio", + "assets_trashed_from_server": "{count} {count, plural, one {elementas perkeltas} few {elementai perkelti} other {elementÅŗ perkelta}} į ÅĄiukÅĄliadėŞę iÅĄ Immich serverio", "assets_were_part_of_album_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}} jau prieÅĄ tai buvo albume", "assets_were_part_of_albums_count": "{count, plural, one {Elementas } few {Elementai} other {ElementÅŗ}} jau buvo albumuose", "authorized_devices": "Autorizuoti įrenginiai", @@ -550,6 +629,7 @@ "backup_albums_sync": "Atsarginio kopijavimo albumÅŗ sinchronizacija", "backup_all": "Visi", "backup_background_service_backup_failed_message": "Nepavyko sukurti atsarginiÅŗ kopijÅŗ. Bandoma dar kartąâ€Ļ", + "backup_background_service_complete_notification": "ElementÅŗ atsarginės kopijos kÅĢrimas baigtas", "backup_background_service_connection_failed_message": "Nepavyko prisijungti prie serverio. Bandoma dar kartąâ€Ļ", "backup_background_service_current_upload_notification": "ÄŽkeliamas {filename}", "backup_background_service_default_notification": "IeÅĄkoma naujÅŗ elementÅŗâ€Ļ", @@ -557,6 +637,7 @@ "backup_background_service_in_progress_notification": "Kuriama elementÅŗ atsarginė kopijaâ€Ļ", "backup_background_service_upload_failure_notification": "Nepavyko įkelti {filename}", "backup_controller_page_albums": "Atsarginės kopijos albumai", + "backup_controller_page_background_app_refresh_disabled_content": "Norėdami naudoti foninį atsarginį kopijavimą, įjunkite foninį programÅŗ atnaujinimą meniu „Nustatymai“ > „Bendrieji“ > „Foninis programÅŗ atnaujinimas“.", "backup_controller_page_background_app_refresh_disabled_title": "Foninis programos atnaujinimas iÅĄjungtas", "backup_controller_page_background_app_refresh_enable_button_text": "Eiti į nustatymus", "backup_controller_page_background_battery_info_link": "Parodyk man kaip", @@ -606,6 +687,7 @@ "backup_options_page_title": "Atsarginio kopijavimo nustatymai", "backup_setting_subtitle": "Tvarkyti foninio ir priekinio plano įkėlimo nustatymus", "backup_settings_subtitle": "Tvarkyti įkėlimo nustatymus", + "backup_upload_details_page_more_details": "Bakstelėkite detalesnei informacijai", "backward": "Atgalinis", "biometric_auth_enabled": "Biometrinis autentifikavimas įgalintas", "biometric_locked_out": "JÅĢs esate uÅžblokuotas biometrinio autentifikavimo funkcijai", @@ -615,6 +697,8 @@ "birthdate_set_description": "Gimimo data naudojama apskaičiuoti asmens amÅžiÅŗ nuotraukos darymo metu.", "blurred_background": "NeryÅĄkus fonas", "bugs_and_feature_requests": "KlaidÅŗ ir funkcijÅŗ uÅžklausos", + "build": "Versija", + "build_image": "Programos Paketas", "bulk_delete_duplicates_confirmation": "Ar tikrai norite iÅĄtrinti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančiÅŗ elementÅŗ}}? Bus paliktas didÅžiausias kiekvienos grupės elementas ir negrįŞtamai iÅĄtrinti kiti besidubliuojantys elementai. Å io veiksmo atÅĄaukti negalėsite!", "bulk_keep_duplicates_confirmation": "Ar tikrai norite palikti visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančiÅŗ elementÅŗ}}? Tokiu bÅĢdu nieko netrinant bus sutvarkytos visos dublikatÅŗ grupės.", "bulk_trash_duplicates_confirmation": "Ar tikrai norite perkelti į ÅĄiukÅĄliadėŞę visus {count, plural, one {# besidubliuojantį elementą} few {# besidubliuojančius elementus} other {# besidubliuojančiÅŗ elementÅŗ}}? Bus paliktas didÅžiausias kiekvienos grupės elementas ir į ÅĄiukÅĄliadėŞę perkelti kiti besidubliuojantys elementai.", @@ -656,10 +740,14 @@ "change_password_description": "Tai arba pirmas kartas, kai jungiatės prie sistemos, arba buvo pateikta uÅžklausa pakeisti jÅĢsÅŗ slaptaÅžodį. PraÅĄome įvesti naują slaptaÅžodį Åžemiau.", "change_password_form_confirm_password": "Patvirtinti slaptaÅžodį", "change_password_form_description": "Labas {name},\n\nTai yra pirmas kartas kai tu prisijungei prie sistemos arba buvo praÅĄymas pakeisti slaptaÅžodį. PraÅĄome įvesti naują slaptaÅžodį Åžemiau.", + "change_password_form_log_out": "Atjungti visus kitus įrenginius", + "change_password_form_log_out_description": "Rekomenduojama atsijungti nuo visÅŗ kitÅŗ įrenginiÅŗ", "change_password_form_new_password": "Naujas slaptaÅžodis", "change_password_form_password_mismatch": "SlaptaÅžodÅžiai nesutampa", "change_password_form_reenter_new_password": "Pakartotinai įveskite naują slaptaÅžodį", "change_pin_code": "Pakeisti PIN kodą", + "change_trigger": "Pakeisti vykdymo sąlygą", + "change_trigger_prompt": "Ar tikrai norite vykdymo sąlygą? Tai paÅĄalins visas esamas veiksmÅŗ sekas ir filtrus.", "change_your_password": "Pakeisti slaptaÅžodį", "changed_visibility_successfully": "Matomumas pakeistas sėkmingai", "charging": "Kraunasi", @@ -668,8 +756,18 @@ "check_corrupt_asset_backup_button": "Atlikti patikrinimą", "check_corrupt_asset_backup_description": "Paleiskite ÅĄÄ¯ patikrinimą tik per Wi-Fi ir tik kai visi elementai buvo perkopijuoti. Å i procedÅĢra uÅžtruks kelias minutes.", "check_logs": "Tikrinti Åžurnalus", + "checksum": "„Checksum“", "choose_matching_people_to_merge": "Pasirinkite atitinkančius Åžmones sujungimui", "city": "Miestas", + "cleanup_confirm_description": "Immich rado {count} {count, plural, one {elementą (sukurtą iki {date}), kurio atsarginė kopija jau iÅĄsaugota serveryje. PaÅĄalinti vietinę kopiją iÅĄ ÅĄio įrenginio} few {elementai (sukurti iki {date}), kuriÅŗ atsarginės kopijos jau iÅĄsaugotos serveryje. PaÅĄalinti vietines kopijas iÅĄ ÅĄio įrenginio} other {elementÅŗ (sukurtÅŗ iki {date}), kuriÅŗ atsarginės kopijos jau iÅĄsaugotos serveryje. PaÅĄalinti vietines kopijas iÅĄ ÅĄio įrenginio}}?", + "cleanup_confirm_prompt_title": "IÅĄtrinti iÅĄ ÅĄio įrenginio?", + "cleanup_deleted_assets": "IÅĄmesti {count} {count, plural, one {elementą} few {elementus} other {elementÅŗ}} į ÅĄiukÅĄlinę", + "cleanup_deleting": "Metama į ÅĄiukÅĄlinę...", + "cleanup_found_assets": "Rasta {count} {count, plural, one {iÅĄsaugotas elementas} few {iÅĄsaugoti elementai} other {iÅĄsaugotÅŗ elementÅŗ}}", + "cleanup_found_assets_with_size": "Rasta {count} {count, plural, one {iÅĄsaugotas elementas} few {iÅĄsaugoti elementai} other {iÅĄsaugotÅŗ elementÅŗ}} ({size})", + "cleanup_no_assets_found": "Nerasta elementÅŗ, atitinkančiÅŗ aukÅĄÄiau pateiktus kriterijus. Atlaisvinti Vietos gali paÅĄalinti tik tuos iÅĄteklius, kuriÅŗ atsarginės kopijos yra serveryje", + "cleanup_preview_title": "ElementÅŗ iÅĄtrinti ({count})", + "cleanup_step4_summary": "{count} {count, plural, one {elementas (sukurtas iki {date}), kurį reikia paÅĄalinti iÅĄ vietinio įrenginio. Nuotrauka liks pasiekiama Immich galerijoje} few {elementai (sukurti iki {date}), kuriuos reikia paÅĄalinti iÅĄ vietinio įrenginio. Nuotraukos liks pasiekiamos Immich galerijoje} other {elementÅŗ (sukurtÅŗ iki {date}), kuriuos reikia paÅĄalinti iÅĄ vietinio įrenginio. Nuotraukos liks pasiekiamos Immich galerijoje}}.", "clear": "IÅĄvalyti", "clear_all": "IÅĄvalyti viską", "clear_all_recent_searches": "IÅĄvalyti visas naujausias paieÅĄkas", @@ -682,14 +780,15 @@ "client_cert_import_success_msg": "Kliento sertifikatas yra importuotas", "client_cert_invalid_msg": "Netinkamas sertifikato failas arba neteisingas slaptaÅžodis", "client_cert_remove_msg": "Kliento sertifikatas yra paÅĄalintas", - "client_cert_subtitle": "Palaikomi tik PKCS12 (.p12, .pfx) formatai. Sertifikato importavimas/paÅĄalinimas galimas tik prieÅĄ prisijungimą", - "client_cert_title": "SSL kliento sertifikatas", + "client_cert_subtitle": "Palaikomi tik PKCS12 (.p12, .pfx) formatai. Sertifikato importavimas/ paÅĄalinimas galimas tik prieÅĄ prisijungimą", + "client_cert_title": "SSL kliento sertifikatas [Experimentinis]", "clockwise": "Pagal laikrodÅžio rodykles", "close": "UÅždaryti", "collapse": "Suskleisti", "collapse_all": "Suskleisti viską", "color": "Spalva", "color_theme": "Temos spalva", + "command": "Komanda", "comment_deleted": "Komentaras iÅĄtrintas", "comment_options": "KomentarÅŗ parinktys", "comments_and_likes": "Komentarai ir patiktukai", @@ -733,6 +832,8 @@ "create": "Sukurti", "create_album": "Sukurti albumą", "create_album_page_untitled": "Be pavadinimo", + "create_api_key": "Sukurti API raktą", + "create_first_workflow": "Sukurti pirmą darbÅŗ eigą", "create_library": "Sukurti biblioteką", "create_link": "Sukurti nuorodą", "create_link_to_share": "Sukurti bendrinimo nuorodą", @@ -747,21 +848,30 @@ "create_tag": "Sukurti Åžymą", "create_tag_description": "Sukurti naują Åžymą. ÄŽdėtinėms Åžymoms įveskite pilną kelią, įskaitant pasviruosius brÅĢkÅĄnius.", "create_user": "Sukurti naudotoją", + "create_workflow": "Sukurti darbÅŗ eigą", "created": "Sukurta", "created_at": "Sukurta", "creating_linked_albums": "Kuriami susieti albumai...", "crop": "Apkirpti", + "crop_aspect_ratio_fixed": "UÅžfiksuota", + "crop_aspect_ratio_free": "Nefiksuota", + "crop_aspect_ratio_original": "Originalus", "curated_object_page_title": "Daiktai", "current_device": "Dabartinis įrenginys", "current_pin_code": "Dabartinis PIN kodas", "current_server_address": "Dabartinis serverio adresas", + "custom_date": "Pasirinktinė data", "custom_locale": "Pasirinktinė vietovė", "custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną", "custom_url": "Pasirinktinis URL", + "cutoff_date_description": "PaÅĄalinkite senesnes nuotraukas ir vaizdo įraÅĄus nei", + "cutoff_day": "{count, plural, one {diena} other {dienos}}", + "cutoff_year": "{count, plural, one {metai} other {metai}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Tamsi", "dark_theme": "Perjungti tamsią temą", + "date": "Data", "date_after": "Data po", "date_and_time": "Data ir laikas", "date_before": "Data prieÅĄ", @@ -793,7 +903,7 @@ "delete_key": "IÅĄtrinti raktą", "delete_library": "IÅĄtrinti biblioteką", "delete_link": "IÅĄtrinti nuorodą", - "delete_local_action_prompt": "{count} iÅĄtrinti vietiniame įrenginyje", + "delete_local_action_prompt": "{count} iÅĄtrinta vietiniame įrenginyje", "delete_local_dialog_ok_backed_up_only": "IÅĄtrinti tik turinčius atsarginę kopiją", "delete_local_dialog_ok_force": "Vis tiek iÅĄtrinti", "delete_others": "IÅĄtrinti kitus", @@ -812,6 +922,7 @@ "deselect_all": "AtÅžymėti visus", "details": "Detalės", "direction": "Kryptis", + "disable": "IÅĄjungti", "disabled": "IÅĄjungta", "disallow_edits": "Neleisti redaguoti", "discord": "Discord", @@ -827,7 +938,7 @@ "documentation": "Dokumentacija", "done": "Atlikta", "download": "AtsisiÅŗsti", - "download_action_prompt": "Atsisiunčiami {count} elementai", + "download_action_prompt": "Atsisiunčiama {count} {count, plural, one {elementas} few {elementai} other {elementÅŗ}}", "download_canceled": "Atsisiuntimas atÅĄauktas", "download_complete": "Atsisiuntimas pabaigtas", "download_enqueue": "Atsisiuntimai įtraukti į eilę", @@ -837,6 +948,7 @@ "download_include_embedded_motion_videos": "ÄŽterpti vaizdo įraÅĄai", "download_include_embedded_motion_videos_description": "Pridėti prie judesio nuotraukÅŗ įterptus video kaip atskirą failą", "download_notfound": "Atsisiuntimas nerastas", + "download_original": "AtsisiÅŗsti originalą", "download_paused": "Atsisiuntimas pristabdytas", "download_settings": "AtsisiÅŗsti", "download_settings_description": "Tvarkyti elementÅŗ atsisiuntimo nustatymus", @@ -846,6 +958,7 @@ "download_waiting_to_retry": "Laukiama bandymo iÅĄ naujo", "downloading": "Siunčiama", "downloading_asset_filename": "Parsisiunčiamas resursas {filename}", + "downloading_from_icloud": "AtsisiÅŗsti iÅĄ iCloud", "downloading_media": "Atsisiunčiama medija", "drop_files_to_upload": "UÅžkelkite failus bet kurioje vietoje kad įkeltumėte", "duplicates": "Dublikatai", @@ -857,7 +970,7 @@ "edit_birthday": "Redaguoti gimtadienį", "edit_date": "Redaguoti datą", "edit_date_and_time": "Redaguoti datą ir laiką", - "edit_date_and_time_action_prompt": "{count} data ir laikas redaguotas", + "edit_date_and_time_action_prompt": "{count} {count, plural, one {data ir laikas redaguotas} few {datos ir laikai redaguoti} other {datÅŗ ir laikÅŗ redaguota}}", "edit_date_and_time_by_offset": "Keisti datą pagal poslinkį", "edit_date_and_time_by_offset_interval": "Naujas datos intervalas: {from} - {to}", "edit_description": "Redaguoti apraÅĄymą", @@ -867,18 +980,24 @@ "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", - "edit_location_action_prompt": "{count} vietovės pakeistos", + "edit_location_action_prompt": "{count} {count, plural, one {vietovė pakeista} few {vietovės pakeistos} other {vietoviÅŗ pakeista}}", "edit_location_dialog_title": "Vietovė", "edit_name": "Redaguoti vardą", "edit_people": "Redaguoti Åžmones", "edit_tag": "Redaguoti Åžymą", "edit_title": "Redaguoti antraÅĄtę", "edit_user": "Redaguoti naudotoją", + "edit_workflow": "Redaguoti darbÅŗ eigą", "editor": "Redaktorius", "editor_close_without_save_prompt": "Pakeitimai nebus iÅĄsaugoti", "editor_close_without_save_title": "UÅždaryti redaktoriÅŗ?", - "editor_crop_tool_h2_aspect_ratios": "Vaizdo santykis", - "editor_crop_tool_h2_rotation": "Pasukimas", + "editor_confirm_reset_all_changes": "Ar tikrai norite atstatyti visus pakeitimus?", + "editor_flip_horizontal": "Apversti horizontaliai", + "editor_flip_vertical": "Apversti vertikaliai", + "editor_orientation": "Orientacija", + "editor_reset_all_changes": "AtÅĄaukti pakeitimus", + "editor_rotate_left": "Pasukti 90° prieÅĄ laikrodÅžio rodyklę", + "editor_rotate_right": "Pasukti 90° pagal laikrodÅžio rodyklę", "email": "El. paÅĄtas", "email_notifications": "El. paÅĄto praneÅĄimai", "empty_folder": "Å is katalogas yra tuÅĄÄias", @@ -910,7 +1029,7 @@ "cant_change_asset_favorite": "Elementui negalima pakeisti mėgstamiausio", "cant_change_metadata_assets_count": "Negalima pakeisti {count, plural, one {# elemento} other {# elementÅŗ}} metadata", "cant_get_faces": "Nepavyko gauti veidus", - "cant_get_number_of_comments": "Nepavyko gauti komentarÅŗ skaičiaus", + "cant_get_number_of_comments": "KomentarÅŗ skaičiaus gauti negalima", "cant_search_people": "Negalima ieÅĄkoti ÅžmoniÅŗ", "cant_search_places": "Negalima ieÅĄkoti vietoviÅŗ", "error_adding_assets_to_album": "Klaida pridedant elementus į albumą", @@ -936,6 +1055,7 @@ "failed_to_unstack_assets": "Nepavyko iÅĄgrupuoti elementÅŗ", "failed_to_update_notification_status": "Nepavyko atnaujinti praneÅĄimo statuso", "incorrect_email_or_password": "Neteisingas el. paÅĄto adresas arba slaptaÅžodis", + "library_folder_already_exists": "Å ita importavimo vieta jau egzistuoja.", "paths_validation_failed": "Nepavyko {paths, plural, one {# kelio} other {# keliÅŗ}} patvirtinimas", "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomÅŗ pikseliÅŗ. PraÅĄome priartinti ir/arba perkelkite nuotrauką.", "quota_higher_than_disk_size": "Nustatyta kvota, virÅĄija disko dydį", @@ -958,6 +1078,7 @@ "unable_to_complete_oauth_login": "Nepavyko prisijungti su OAuth", "unable_to_connect": "Nepavyko prisijungti", "unable_to_copy_to_clipboard": "Negalima kopijuoti į iÅĄkarpinę, įsitikinkite, kad prie puslapio prieinate per https", + "unable_to_create": "Nepavyko sukurti darbÅŗ eigos", "unable_to_create_admin_account": "Nepavyko sukurti administratoriaus paskyros", "unable_to_create_api_key": "Nepavyko sukurti naujo API rakto", "unable_to_create_library": "Nepavyko sukurti bibliotekos", @@ -968,12 +1089,13 @@ "unable_to_delete_exclusion_pattern": "Nepavyksta iÅĄtrinti iÅĄimčiÅŗ ÅĄablono", "unable_to_delete_shared_link": "Nepavyko iÅĄtrinti bendrinimo nuorodos", "unable_to_delete_user": "Nepavyksta iÅĄtrinti naudotojo", + "unable_to_delete_workflow": "Nepavyko iÅĄtrinti darbÅŗ eigos", "unable_to_download_files": "Nepavyksta atsisiÅŗsti failÅŗ", "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti iÅĄimčiÅŗ ÅĄablono", "unable_to_empty_trash": "Nepavyko iÅĄtrinti ÅĄiukÅĄliadėŞės", "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano reÅžimą", "unable_to_exit_fullscreen": "Nepavyksta iÅĄeiti iÅĄ viso ekrano reÅžimo", - "unable_to_get_comments_number": "Nepavyko gauti komentarÅŗ skaičiaus", + "unable_to_get_comments_number": "KomentarÅŗ skaičiaus gauti nepavyko", "unable_to_get_shared_link": "Nepavyko gauti bendrinimo nuorodos", "unable_to_hide_person": "Nepavyksta paslėpti Åžmogaus", "unable_to_link_motion_video": "Nepavyko susieti judesio video", @@ -1007,7 +1129,8 @@ "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_set_rating": "Nepavyko nustatyti įvertinimo", + "unable_to_submit_job": "Nepavyko sukurti uÅžduoties", "unable_to_trash_asset": "Nepavyko perkelti į ÅĄiukÅĄliadėŞę", "unable_to_unlink_account": "Nepavyko atsieti paskyrÅŗ", "unable_to_unlink_motion_video": "Nepavyko atsieti judesio video", @@ -1018,8 +1141,11 @@ "unable_to_update_settings": "Nepavyko atnaujinti nustatymÅŗ", "unable_to_update_timeline_display_status": "Nepavyko atnaujinti laiko juostos rodymo statuso", "unable_to_update_user": "Nepavyko atnaujinti naudotoją", + "unable_to_update_workflow": "Nepvyko atnaujinti darbÅŗ eigos", "unable_to_upload_file": "Nepavyksta įkelti failo" }, + "errors_text": "Klaidos", + "exclusion_pattern": "Atskyrimo ÅĄablonas", "exif": "Exif", "exif_bottom_sheet_description": "Pridėti apraÅĄymą...", "exif_bottom_sheet_description_error": "Klaida atnaujinant apraÅĄymą", @@ -1050,11 +1176,12 @@ "external_network_sheet_info": "Kai neprisijungta prie pageidaujamo Wi-Fi tinklo, programa jungsis prie serverio per pirmą URL nuorodą, kurią galės pasiekti, pradedant nuo virÅĄaus į apačią", "face_unassigned": "Nepriskirta", "failed": "ÄŽvyko klaida", + "failed_count": "Nepavykę: {count}", "failed_to_authenticate": "Nepavyko autentifikuoti", "failed_to_load_assets": "Nepavyko įkelti elementÅŗ", "failed_to_load_folder": "Nepavyko įkelti katalogą", "favorite": "Mėgstamiausias", - "favorite_action_prompt": "{count} pridėta prie mėgstamiausiÅŗ", + "favorite_action_prompt": "{count} {count, plural, one {pridėtas} few {pridėti} other {pridėta}} prie mėgstamiausiÅŗ", "favorite_or_unfavorite_photo": "ÄŽtraukti prie arba paÅĄalinti iÅĄ mėgstamiausiÅŗ", "favorites": "Mėgstamiausi", "favorites_page_no_favorites": "Nerasta mėgstamiausiÅŗ elementÅŗ", @@ -1062,7 +1189,6 @@ "features": "Funkcijos", "features_in_development": "KÅĢrimo funkcijos", "features_setting_description": "Valdyti aplikacijos funkcijas", - "file_name": "Failo pavadinimas", "file_name_or_extension": "Failo pavadinimas arba plėtinys", "file_size": "Failo dydis", "filename": "Failopavadinimas", @@ -1070,6 +1196,7 @@ "filter": "Filtras", "filter_people": "Filtruoti Åžmones", "filter_places": "Filtruoti vietoves", + "filters": "Filtrai", "find_them_fast": "Raskite greitai paieÅĄkoje pagal vardą", "first": "Pirmas", "fix_incorrect_match": "Pataisyti neteisingą porą", @@ -1079,11 +1206,16 @@ "folders_feature_description": "PerÅžiÅĢrėkite failÅŗ sistemoje esančias nuotraukas ir vaizdo įraÅĄus aplankÅŗ rodinyje", "forgot_pin_code_question": "PamirÅĄote savo PIN?", "forward": "Pirmyn", + "free_up_space": "Atlaisvinti Vietos", + "free_up_space_description": "Perkelkite atsargines nuotraukÅŗ ir vaizdo įraÅĄÅŗ kopijas į įrenginio ÅĄiukÅĄliadėŞę, kad atlaisvintumėte vietos. JÅĢsÅŗ kopijos serveryje lieka saugios.", + "free_up_space_settings_subtitle": "Atlaisvinkite vietos įrenginyje", + "full_path": "Pilnas kelias: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Kad veiktÅŗ, ÅĄi funkcija įkelia iÅĄorinius „Google“ iÅĄteklius.", "general": "Bendri", "geolocation_instruction_location": "Paspauskite ant elemento su GPS koordinatėmis norint naudoti tą vietovę arba pasirinkite vietovę tiesiogiai Åžemėlapyje", "get_help": "Gauti pagalbos", + "get_people_error": "Klaida gaunant Åžmones", "get_wifiname_error": "Nepavyko gauti Wi-Fi pavadinimo. ÄŽsitikinkite, kad suteikti bÅĢtini leidimai ir esate prisijungę prie Wi-Fi tinklo", "getting_started": "Pradedama", "go_back": "Eiti atgal", @@ -1104,17 +1236,20 @@ "hash_asset": "Kurti bylos paraÅĄÄ… elementui", "hashed_assets": "Elementai su bylÅŗ paraÅĄais", "hashing": "BylÅŗ paraÅĄo kÅĢrimas", - "header_settings_add_header_tip": "Pridėti antraÅĄtę", + "header_settings_add_header_tip": "Pridėti headerį", "header_settings_field_validator_msg": "ReikÅĄmė negali bÅĢti tuÅĄÄia", "header_settings_header_name_input": "AntraÅĄtės pavadinimas", "header_settings_header_value_input": "AntraÅĄtės reikÅĄmė", "headers_settings_tile_title": "Pasirinktinės tarpinio serverio antraÅĄtės", + "height": "AukÅĄtis", "hi_user": "Labas {name} ({email})", "hide_all_people": "Slėpti visus asmenis", "hide_gallery": "Slėpti galeriją", "hide_named_person": "Slėpti asmenį {name}", "hide_password": "Slėpti slaptaÅžodį", "hide_person": "Slėpti asmenį", + "hide_schema": "Slėpti schemą", + "hide_text_recognition": "Slėpti teksto atpaÅžinimą", "hide_unnamed_people": "Slėpti neįvardintus asmenis", "home_page_add_to_album_conflicts": "Pridėta {added} elementÅŗ į albumą {album}. {failed} elementai jau yra albume.", "home_page_add_to_album_err_local": "Kol kas negalima pridėti vietiniÅŗ elementÅŗ į albumus, praleidÅžiama", @@ -1160,6 +1295,8 @@ "import_path": "Importavimo kelias", "in_albums": "{count, plural, one {# Albume} few {#Albumuose} other {# AlbumÅŗ}}", "in_archive": "Archyve", + "in_year": "{year} metais", + "in_year_selector": " ", "include_archived": "ÄŽtraukti archyvuotus", "include_shared_albums": "ÄŽtraukti bendrinamus albumus", "include_shared_partner_assets": "ÄŽtraukti partnerio pasidalintus elementus", @@ -1184,9 +1321,18 @@ "ios_debug_info_processing_ran_at": "Apdorojimas vyko {dateTime}", "items_count": "{count, plural, one {# elementas} few {# elementai} other {# elementÅŗ}}", "jobs": "UÅžduotys", + "json_editor": "JSON redagavimas", + "json_error": "JSON klaida", "keep": "Palikti", + "keep_albums": "Palikti albumus", + "keep_albums_count": "{count, plural, one {Paliekamas {count} albumas} few {Paliekami {count} albumai} other {Paliekama {count} albumÅŗ}}", "keep_all": "Palikti visus", + "keep_description": "Pasirinkite, kas lieka jÅĢsÅŗ įrenginyje atlaisvinus vietos.", + "keep_favorites": "Palikti mėgstamiausius", + "keep_on_device": "Palikti įrenginyje", + "keep_on_device_hint": "Pasirinkite elementus, kuriuos norite palikti ÅĄiame įrenginyje", "keep_this_delete_others": "IÅĄsaugoti ÅĄÄ¯, kitus iÅĄtrinti", + "keeping": "Paliekama: {items}", "kept_this_deleted_others": "IÅĄsaugotas ÅĄis elementas ir {count, plural, one {iÅĄtrintas # elementas} few {iÅĄtrinti # elementai} other {iÅĄtrinta # elementÅŗ}}", "keyboard_shortcuts": "Spartieji klaviatÅĢros klaviÅĄai", "language": "Kalba", @@ -1196,6 +1342,7 @@ "language_setting_description": "Pasirinkti pageidaujamą kalbą", "large_files": "Dideli failai", "last": "Paskutinis", + "last_months": "{count, plural, one {Paskutinis mėnuo} other {Paskutiniai # mėnesiai}}", "last_seen": "Paskutinį kartą matytas", "latest_version": "Naujausia versija", "latitude": "Platuma", @@ -1205,6 +1352,8 @@ "let_others_respond": "Leisti kitiems reaguoti", "level": "Lygis", "library": "Biblioteka", + "library_add_folder": "Pridėti aplanką", + "library_edit_folder": "Redaguoti aplanką", "library_options": "Bibliotekos pasirinktys", "library_page_device_albums": "Albumai įrenginyje", "library_page_new_album": "Naujas albumas", @@ -1225,9 +1374,11 @@ "local": "Vietinis", "local_asset_cast_failed": "Negalima transliuoti elemento kuris neįkeltas į serverį", "local_assets": "Vietiniai elementai", + "local_id": "Vietinis ID", "local_media_summary": "Vietinės medijos santrauka", "local_network": "Vietinis tinklas", "local_network_sheet_info": "Programa jungsis prie serverio per ÅĄÄ¯ URL kai naudos pasirinktą Wi-Fi tinklą", + "location": "Vietovė", "location_permission": "Vietovės leidimai", "location_permission_content": "Norint naudoti automatinio persijungimo opciją, Immich reikia tikslios vietovės leidimo, kad galėtÅŗ nuskaityti Wi-Fi tinklo pavadinimą", "location_picker_choose_on_map": "Pasirinkite Åžemėlapyje", @@ -1275,8 +1426,18 @@ "loop_videos_description": "ÄŽgalinti automatinį vaizdo įraÅĄo rodymą iÅĄ naujo detaliÅŗ perÅžiÅĢroje.", "main_branch_warning": "JÅĢs naudojate kÅĢrėjo versiją, mes stipriai rekomenduojame naudoti galutinę versiją!", "main_menu": "Pagrindinis meniu", + "maintenance_description": "ÄŽjungtas Immich techninės prieÅžiÅĢros reÅžimas.", + "maintenance_end": "Baigti techninę prieÅžiÅĢrą", + "maintenance_end_error": "Nepavyko iÅĄjungti techninės prieÅžiÅĢros reÅžimo.", + "maintenance_logged_in_as": "Å iuo metu prisijungę kaip {user}", + "maintenance_restore_library_folder_has_files": "{folder} turi {count} {count, plural, one {aplanką} few {aplankus} other {aplankÅŗ}}", + "maintenance_title": "Laikinai Neprieinamas", "make": "Gamintojas", "manage_geolocation": "Tvarkyti vietovę", + "manage_media_access_rationale": "Å is leidimas reikalingas norint tinkamai perkelti elementus į ÅĄiukÅĄliadėŞę ir atkurti juos iÅĄ jos.", + "manage_media_access_settings": "Atidaryti nustatymus", + "manage_media_access_subtitle": "Leisti Immich tvarkyti ir perkelti medijos failus.", + "manage_media_access_title": "Medijos Valdymo Prieiga", "manage_shared_links": "Bendrinimo nuorodÅŗ tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", "manage_the_app_settings": "Valdyti programos nustatymus", @@ -1331,17 +1492,24 @@ "minimize": "SumaÅžinti", "minute": "Minutė", "minutes": "Minutės", + "mirror_horizontal": "Horizontaliai", + "mirror_vertical": "Vertikaliai", "missing": "TrÅĢkstami", - "mobile_app": "Mobili aplikacija", + "mobile_app": "Mobili programa", + "mobile_app_download_onboarding_note": "AtsisiÅŗskite mobiliąją programėlę naudodami ÅĄias parinktis", "model": "Modelis", "month": "Mėnesis", "monthly_title_text_date_format": "MMMM y", "more": "Daugiau", "move": "Perkelti", + "move_down": "ÅŊemyn", "move_off_locked_folder": "IÅĄtraukti iÅĄ uÅžrakinto aplanko", + "move_to": "Perkelti į", + "move_to_device_trash": "Perkelti į įrenginio ÅĄiukÅĄliadėŞę", "move_to_lock_folder_action_prompt": "{count} įkelta į uÅžrakintą aplanką", "move_to_locked_folder": "ÄŽtraukti į uÅžrakintą aplanką", "move_to_locked_folder_confirmation": "Å ios nuotraukos ir vaizdo įraÅĄai bus paÅĄalinti iÅĄ visÅŗ albumÅŗ ir bus matomi tik uÅžrakintame aplanke", + "move_up": "AukÅĄtyn", "moved_to_archive": "{count, plural, one {# Elementas perkeltas} few {# Elementai perkelti} other {# ElementÅŗ perkelta}} į archyvą", "moved_to_library": "{count, plural, one {# Elementas perkeltas} few {# Elementai perkelti} other {# ElementÅŗ perkelta}} į biblioteką", "moved_to_trash": "Perkelta į ÅĄiukÅĄliadėŞę", @@ -1351,6 +1519,9 @@ "my_albums": "Mano albumai", "name": "Vardas", "name_or_nickname": "Vardas arba slapyvardis", + "name_required": "Vardas yra privalomas", + "navigate": "Naviguoti", + "navigate_to_time": "Naviguoti pagal Laiką", "network_requirement_photos_upload": "Naudoti mobilÅŗ internetą atsarginėms nuotraukÅŗ kopijoms", "network_requirement_videos_upload": "Naudoti mobilÅŗ internetą atsarginėms vaizdo įraÅĄÅŗ kopijoms", "network_requirements": "Tinklo reikalavimai", @@ -1360,32 +1531,39 @@ "never": "Niekada", "new_album": "Naujas albumas", "new_api_key": "Naujas API raktas", + "new_date_range": "Naujas datos intervalas", "new_password": "Naujas slaptaÅžodis", "new_person": "Naujas asmuo", "new_pin_code": "Naujas PIN kodas", "new_pin_code_subtitle": "Tai pirmas kartas, kai naudojate uÅžrakinto aplanko funkciją. Nustatykite PIN kodą savo uÅžrakintam aplankui", "new_timeline": "Nauja laiko juosta", + "new_update": "Nauja versija", "new_user_created": "Naujas naudotojas sukurtas", "new_version_available": "PRIEINAMA NAUJA VERSIJA", "newest_first": "Pirmiausia naujausi", "next": "Sekantis", "next_memory": "Sekantis atsiminimas", "no": "Ne", + "no_actions_added": "JokiÅŗ veiksmÅŗ dar nepridėta", "no_albums_message": "Sukurkite albumą nuotraukoms ir vaizdo įraÅĄams tvarkyti", "no_albums_with_name_yet": "Atrodo, kad dar neturite albumÅŗ su ÅĄiuo pavadinimu.", "no_albums_yet": "Atrodo, kad dar neturite albumÅŗ.", "no_archived_assets_message": "Suarchyvuokite nuotraukas ir vaizdo įraÅĄus, kad jie nebÅĢtÅŗ rodomi nuotraukÅŗ rodinyje", - "no_assets_message": "SPUSTELĖKITE NORĖDAMI ÄŽKELTI PIRMĄJĄ NUOTRAUKĄ", + "no_assets_message": "SPUSTELĖKITE NORĖDAMI ÄŽKELTI SAVO PIRMĄJĄ NUOTRAUKĄ", "no_assets_to_show": "Nėra rodomÅŗ elementÅŗ", "no_cast_devices_found": "Nerasta transliavimo įrenginiÅŗ", "no_checksum_local": "Kontrolinė suma nepasiekiama – negalima gauti vietiniÅŗ elementÅŗ", "no_checksum_remote": "Kontrolinė suma nepasiekiama – negalima gauti nuotoliniÅŗ elementÅŗ", + "no_configuration_needed": "KonfigÅĢracija nereikalinga", + "no_devices": "Nėra autorizuotÅŗ įrenginiÅŗ", "no_duplicates_found": "DublikatÅŗ nerasta.", "no_exif_info_available": "Nėra Exif informacijos", "no_explore_results_message": "ÄŽkelkite daugiau nuotraukÅŗ ir tyrinėkite savo kolekciją.", "no_favorites_message": "Pridėti į mėgstamiausius, kad greitai rastum geriausias nuotraukas ir vaizdo įraÅĄus", + "no_filters_added": "FiltrÅŗ dar nepridėta", "no_libraries_message": "Sukurkite iÅĄorinę biblioteką nuotraukoms ir vaizdo įraÅĄams perÅžiÅĢrėti", "no_local_assets_found": "Nerasta jokiÅŗ vietiniÅŗ elementÅŗ su ÅĄia kontroline suma", + "no_location_set": "Nenustatyta vietovė", "no_locked_photos_message": "UÅžrakintame aplanke esančios nuotraukos ir vaizdo įraÅĄai yra paslėpti ir nematomi narÅĄant ir ieÅĄkant.", "no_name": "Be vardo", "no_notifications": "PraneÅĄimÅŗ nėra", @@ -1396,24 +1574,28 @@ "no_results_description": "Pabandykite sinonimą arba bendresnį raktaÅžodį", "no_shared_albums_message": "Sukurkite nuotraukÅŗ ar vaizdo įraÅĄÅŗ albumą dalinimuisi su Åžmonėmis jÅĢsÅŗ tinkle", "no_uploads_in_progress": "Nėra vykstančiÅŗ įkėlimÅŗ", + "none": "Niekas", + "not_allowed": "NeleidÅžiama", "not_available": "Nepasiekiamas", "not_in_any_album": "Nė viename albume", "not_selected": "Nepasirinkta", - "note_apply_storage_label_to_previously_uploaded assets": "Pastaba: Priskirti Saugyklos ÅŊymą prie anksčiau įkeltÅŗ iÅĄtekliu, paleiskite ÅĄÄ¯", "notes": "Pastabos", "nothing_here_yet": "Kol kas tuÅĄÄia", "notification_permission_dialog_content": "PraneÅĄimÅŗ įgalinimui eikite į Nustatymus ir pasirinkite Leisti.", "notification_permission_list_tile_content": "Suteikti leidimą praneÅĄimÅŗ įgalinimui.", - "notification_permission_list_tile_enable_button": "ÄŽgalinti praneÅĄimus", + "notification_permission_list_tile_enable_button": "ÄŽjungti praneÅĄimus", "notification_permission_list_tile_title": "PraneÅĄimÅŗ leidimai", "notification_toggle_setting_description": "ÄŽjungti el. paÅĄto praneÅĄimus", "notifications": "PraneÅĄimai", "notifications_setting_description": "Tvarkyti praneÅĄimus", "oauth": "OAuth", + "obtainium_configurator": "Obtainium KonfigÅĢratorius", + "obtainium_configurator_instructions": "Naudokite Obtainium, jei norite įdiegti ir atnaujinti Android programėlę tiesiai iÅĄ Immich GitHub. Sukurkite API raktą ir pasirinkite variantą, Obtainium konfigÅĢracijos nuorodos sukÅĢrimui", + "ocr": "OCR", "official_immich_resources": "OficialÅĢs Immich iÅĄtekliai", "offline": "Neprisijungęs", "offset": "Ofsetas", - "ok": "Ok", + "ok": "Gerai", "oldest_first": "Seniausias pirmas", "on_this_device": "Å iame įrenginyje", "onboarding": "ÄŽdarbinimas", @@ -1441,6 +1623,7 @@ "other_variables": "Kiti kintamieji", "owned": "Nuosavi", "owner": "Savininkas", + "page": "Puslapis", "partner": "Partneris", "partner_can_access": "{partner} gali naudotis", "partner_can_access_assets": "Visos jÅĢsÅŗ nuotraukos ir vaizdo įraÅĄai, iÅĄskyrus archyvuotus ir iÅĄtrintus", @@ -1473,12 +1656,13 @@ "people": "Asmenys", "people_edits_count": "{count, plural, one {Redaguotas # asmuo} few {Redaguoti # asmenys} other {Redaguota # asmenÅŗ}}", "people_feature_description": "PerÅžiÅĢrėkite nuotraukas ir vaizdo įraÅĄus sugrupuotus pagal asmenis", + "people_selected": "{count, plural, one {# asmuo pasirinktas} other {# asmenÅŗ pasirinkta}}", "people_sidebar_description": "Rodyti asmenÅŗ rodinio nuorodą ÅĄoninėje juostoje", "permanent_deletion_warning": "IÅĄtrynimo visam laikui perspėjimas", "permanent_deletion_warning_setting_description": "Rodyti perspėjimą kai elementas iÅĄtrinamas visam laikui", "permanently_delete": "IÅĄtrinti visam laikui", "permanently_delete_assets_count": "Visam laikui iÅĄtrinti {count, plural, one {# elementą} few {# elementus} other {# elementÅŗ}}", - "permanently_delete_assets_prompt": "Ar tikrai norite visam laikui iÅĄtrinti {count, plural, one {ÅĄitą elementą?} few {ÅĄituos # elementus?} other {ÅĄitÅŗ # elementÅŗ?}} Tuo pačiu {count, plural, one {jis bus paÅĄalintas} other {jie bus paÅĄalinti}} iÅĄ albumo.", + "permanently_delete_assets_prompt": "Ar tikrai norite visam laikui iÅĄtrinti {count, plural, one {ÅĄÄ¯ elementą?} other {ÅĄiuos # elementus?}} Tai bus tuo pačiu paÅĄalinta {count, plural, one {iÅĄ} other {iÅĄ jÅŗ}} albumo(Åŗ).", "permanently_deleted_asset": "VisiÅĄkai iÅĄtrinti elementai", "permanently_deleted_assets_count": "Visam laikui {count, plural, one {iÅĄtrintas # elementas} few {iÅĄtrinti # elementai} other {iÅĄtrinta # elementÅŗ}}", "permission": "Leidimas", @@ -1497,12 +1681,17 @@ "person_age_years": "{years, plural, other {# metÅŗ}} amÅžiaus", "person_birthdate": "Gimė {date}", "person_hidden": "{name}{hidden, select, true { (paslėptas)} other {}}", + "person_recognized": "Asmuo atpaÅžintas", + "person_selected": "Asmuo pasirinktas", "photo_shared_all_users": "PanaÅĄu, kad savo nuotraukomis pasidalijote su visais naudotojais arba neturite naudotojÅŗ, su kuriais galėtumėte jomis pasidalyti.", "photos": "Nuotraukos", "photos_and_videos": "Nuotraukos ir vaizdo įraÅĄai", "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukÅŗ}}", "photos_from_previous_years": "AnkstesniÅŗ metÅŗ nuotraukos", - "pick_a_location": "IÅĄsirinkite vietovę", + "photos_only": "Tik nuotraukos", + "pick_a_location": "Pasirinkite vietovę", + "pick_custom_range": "Pasirinktinis diapazonas", + "pick_date_range": "Pasirinkti datos diapazoną", "pin_code_changed_successfully": "PIN kodas pakeistas sėkmingai", "pin_code_reset_successfully": "PIN kodas sėkmingai atstatytas", "pin_code_setup_successfully": "PIN kodas sėkmingai nustatytas", @@ -1514,6 +1703,9 @@ "play_memories": "Leisti atsiminimus", "play_motion_photo": "Rodyti judančias nuotraukas", "play_or_pause_video": "Rodyti arba sustabdyti vaizdo įraÅĄÄ…", + "play_original_video": "Rodyti originalÅŗ vaizdo įraÅĄÄ…", + "play_original_video_setting_description": "Rodyti originalius vaizdo įraÅĄus vietoje konvertuotÅŗ vaizdo įraÅĄÅŗ. Jei originalus įraÅĄas yra nesuderinamas, jis gali groti neteisingai.", + "play_transcoded_video": "Rodyti konvertuotą vaizdo įraÅĄÄ…", "please_auth_to_access": "PraÅĄome patvirtinti prisijungimą", "port": "Portas", "preferences_settings_subtitle": "Tvarkyti programos nuostatas", @@ -1537,7 +1729,7 @@ "profile_image_of_user": "{user} profilio nuotrauka", "profile_picture_set": "Profilio nuotrauka nustatyta.", "public_album": "VieÅĄas albumas", - "public_share": "VieÅĄas dilinimasis", + "public_share": "VieÅĄas dalinimasis", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", "purchase_activated_time": "Suaktyvinta {date}", @@ -1547,17 +1739,17 @@ "purchase_button_buy_immich": "Pirkti Immich", "purchase_button_never_show_again": "Niekada daugiau nerodyti", "purchase_button_reminder": "Priminti man po 30 dienÅŗ", - "purchase_button_remove_key": "PaÅĄalinti produkto rakta", + "purchase_button_remove_key": "PaÅĄalinti produkto raktą", "purchase_button_select": "Pasirinkti", "purchase_failed_activation": "Nepavyko suaktyvinti! Patikrinkite el. paÅĄtą, ar turite teisingo produkto koda!", "purchase_individual_description_1": "Asmeniui", "purchase_individual_description_2": "Rėmėjo statusas", "purchase_individual_title": "Asmeninis", "purchase_input_suggestion": "Turite produkto raktą? ÄŽveskite jį Åžemiau", - "purchase_license_subtitle": "ÄŽsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą", + "purchase_license_subtitle": "ÄŽsigykite Immich, kad palaikytumėte tolesnį paslaugos vystymą", "purchase_lifetime_description": "Pirkimas visam gyvenimui", "purchase_option_title": "PIRKIMO PASIRINKIMAS", - "purchase_panel_info_1": "„Immich“ kÅĢrimas uÅžima daug laiko ir pastangÅŗ, o visą darbo dieną dirba inÅžinieriai, kad jis bÅĢtÅŗ kuo geresnis. MÅĢsÅŗ misija yra, kad atvirojo kodo programinė įranga ir etiÅĄka verslo praktika taptÅŗ tvariu kÅĢrėjÅŗ pajamÅŗ ÅĄaltiniu ir sukurtÅŗ privatumą gerbiančią ekosistemą su realiomis alternatyvomis iÅĄnaudojamoms debesijos paslaugoms.", + "purchase_panel_info_1": "Immich kÅĢrimas uÅžima daug laiko ir pastangÅŗ, o visą darbo dieną dirba inÅžinieriai, kad jis bÅĢtÅŗ kuo geresnis. MÅĢsÅŗ misija yra, kad atvirojo kodo programinė įranga ir etiÅĄka verslo praktika taptÅŗ tvariu kÅĢrėjÅŗ pajamÅŗ ÅĄaltiniu ir sukurtÅŗ privatumą gerbiančią ekosistemą su realiomis alternatyvomis iÅĄnaudojamoms debesijos paslaugoms.", "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamÅŗ sienÅŗ, ÅĄis pirkinys nesuteiks jums jokiÅŗ papildomÅŗ Immich funkcijÅŗ. Mes tikime, kad tokie naudotojai kaip jÅĢs palaikys nuolatinį Immich vystymąsi.", "purchase_panel_title": "Palaikykite projektą", "purchase_per_server": "Vienam serveriui", @@ -1570,12 +1762,18 @@ "purchase_server_description_2": "Rėmėjo statusas", "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", + "query_asset_id": "UÅžklausti Elemento ID", + "queue_status": "Eilėje {count}/{total}", + "rate_asset": "ÄŽvertinti Elementą", "rating": "ÄŽvertinimas ÅžvaigÅždutėmis", + "rating_clear": "PaÅĄalinti įvertinimą", "rating_count": "{count, plural, one {# įvertinimas} few {# įvertinimai} other {# įvertinimÅŗ}}", "rating_description": "Rodyti EXIF įvertinimus informacijos skydelyje", + "reaction_options": "ReakcijÅŗ parinktys", "read_changelog": "Skaityti pakeitimÅŗ sąraÅĄÄ…", "ready_for_upload": "ParuoÅĄta įkėlimui", - "recent-albums": "Naujausi albumai", + "recent": "Naujausi", + "recent_albums": "Naujausi albumai", "recent_searches": "Naujausios paieÅĄkos", "recently_added": "Neseniai pridėta", "recently_added_page_title": "Neseniai pridėta", @@ -1591,18 +1789,26 @@ "refreshing_encoded_video": "Perkraunamas apdorotas vaizdo įraÅĄas", "refreshing_faces": "Perkraunami veidai", "refreshing_metadata": "Perkraunami metaduomenys", + "regenerating_thumbnails": "PerkÅĢriama miniatiÅĢra", + "remote": "Nuotolinis", + "remote_assets": "Nuotoliniai Elementai", + "remote_media_summary": "Nuotolinės Medijos Santrauka", "remove": "PaÅĄalinti", + "remove_assets_album_confirmation": "Ar tikrai norite paÅĄalinti {count, plural, one {# elementą} few {# elementus} other {# elementÅŗ}} iÅĄ albumo?", "remove_assets_shared_link_confirmation": "Ar tikrai norite paÅĄalinti {count, plural, one {# elementą} few {# elementus} other {# elementÅŗ}} iÅĄ ÅĄios bendrinimo nuorodos?", "remove_assets_title": "PaÅĄalinti elementus?", "remove_deleted_assets": "PaÅĄalinti IÅĄtrintus Elemenuts", "remove_from_album": "PaÅĄalinti iÅĄ albumo", "remove_from_album_action_prompt": "{count} paÅĄalinta iÅĄ albumo", "remove_from_favorites": "PaÅĄalinti iÅĄ mėgstamiausiÅŗ", - "remove_from_lock_folder_action_prompt": "{count} iÅĄtraukta iÅĄ uÅžrakinto aplanko", + "remove_from_lock_folder_action_prompt": "{count} paÅĄalinta iÅĄ uÅžrakinto aplanko", "remove_from_locked_folder": "IÅĄimti iÅĄ uÅžrakinto aplanko", "remove_from_locked_folder_confirmation": "Ar tikrai norite perkelti ÅĄias nuotraukas ir vaizdo įraÅĄus iÅĄ uÅžrakinto aplanko? Jie taps matomi jÅĢsÅŗ galerijoje.", "remove_from_shared_link": "PaÅĄalinti iÅĄ bendrinimo nuorodos", + "remove_memory": "PaÅĄalinti atsiminimus", + "remove_photo_from_memory": "PaÅĄalinti nuotrauką iÅĄ atsiminimÅŗ", "remove_tag": "PaÅĄalinti Åžymę", + "remove_url": "PaÅĄalinti URL", "remove_user": "PaÅĄalinti naudotoją", "removed_api_key": "PaÅĄalintas API Raktas: {name}", "removed_from_archive": "PaÅĄalinta iÅĄ archyvo", @@ -1617,119 +1823,211 @@ "replace_with_upload": "Pakeisti naujai įkeltu failu", "repository": "Repozitoriumas", "require_password": "Reikalauti slaptaÅžodÅžio", + "require_user_to_change_password_on_first_login": "Reikalauti, kad vartotojas pakeistÅŗ slaptaÅžodį pirmą kartą prisijungdamas", "rescan": "Perskenuoti", "reset": "Atstatyti", - "reset_password": "Atstayti slaptaÅžodį", + "reset_password": "Atstatyti slaptaÅžodį", + "reset_people_visibility": "Atstatyti ÅžmoniÅŗ matomumą", "reset_pin_code": "Atsatyti PIN kodą", "reset_pin_code_description": "Jei pamirÅĄote PIN kodą, galite susisiekti su serverio administratoriumi, kad jis jį atstatytÅŗ", + "reset_pin_code_success": "Sėkmingai atstatytas PIN kodas", "reset_pin_code_with_password": "PIN kodą visada galite atkurti naudodami savo slaptaÅžodį", + "reset_sqlite": "Atstatyti SQLite duomenÅŗ bazę", + "reset_sqlite_confirmation": "Ar tikrai norite atstatyti SQLite duomenÅŗ bazę? Turėsite atsijungti ir vėl prisijungti, kad iÅĄ naujo sinchronizuotumėte duomenis", + "reset_sqlite_success": "Sėkmingai atstatyta SQLite duomenÅŗ bazė", "reset_to_default": "Atkurti numatytuosius", + "resolution": "Rezoliucija", "resolve_duplicates": "Sutvarkyti dublikatus", "resolved_all_duplicates": "Sutvarkyti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", + "restore_trash_action_prompt": "{count} atstatyta iÅĄ ÅĄiukÅĄliadėŞės", "restore_user": "Atkurti naudotoją", "restored_asset": "Atkurti elementą", + "resume": "Tęsti", + "resume_paused_jobs": "Tęsti {count, plural, one {# pristabdytą darbą} other {# pristabdytus darbus}}", + "retry_upload": "Bandyti iÅĄsiÅŗsti dar kartą", "review_duplicates": "PerÅžiÅĢrėti dublikatus", + "review_large_files": "PerÅžiÅĢrėti didelius failus", + "role": "Rolė", + "role_editor": "Redaktorius", + "role_viewer": "Stebėtojas", + "running": "Vykdoma", "save": "IÅĄsaugoti", "save_to_gallery": "IÅĄsaugoti galerijoje", + "saved": "IÅĄsaugota", "saved_api_key": "IÅĄsaugotas API raktas", "saved_profile": "IÅĄsaugotas profilis", "saved_settings": "IÅĄsaugoti nustatymai", "say_something": "Ką nors pasakykite", "scaffold_body_error_occurred": "ÄŽvyko klaida", + "scan": "Skenuoti", "scan_all_libraries": "Skenuoti visas bibliotekas", "scan_library": "Skenuoti", "scan_settings": "Skenavimo nustatymai", + "scanning": "Skenuojama", "scanning_for_album": "Skenuojama albumÅŗ...", "search": "IeÅĄkoti", "search_albums": "IeÅĄkoti albumÅŗ", "search_by_context": "IeÅĄkoti pagal kontekstą", "search_by_description": "IeÅĄkoti pagal apraÅĄymą", - "search_by_description_example": "ÅŊygio diena Sapoje", + "search_by_description_example": "Ilga kelionė per kopas", "search_by_filename": "IeÅĄkoti pagal failo pavadinimą arba plėtinį", "search_by_filename_example": "pvz. IMG_1234.JPG arba PNG", + "search_by_ocr": "IeÅĄkoti pagal OCR", + "search_by_ocr_example": "Latte", + "search_camera_lens_model": "IeÅĄkoti objektyvo modelio...", "search_camera_make": "IeÅĄkoti pagal kameros gamintoją...", "search_camera_model": "IeÅĄkoti kameros modelį...", "search_city": "IeÅĄkoti miesto...", "search_country": "IeÅĄkoti ÅĄalies...", + "search_filter_apply": "Filtruoti", + "search_filter_camera_title": "Pasirinkti kameros tipą", "search_filter_date": "Data", + "search_filter_date_interval": "{start} iki {end}", + "search_filter_date_title": "Pasirinkti datos diapazoną", "search_filter_display_option_not_in_album": "Ne albume", "search_filter_display_options": "Rodymo Nustatymai", "search_filter_filename": "IeÅĄkoti pagal failo pavadinimą", "search_filter_location": "Vietovė", "search_filter_location_title": "Pasirinkti vietovę", - "search_filter_media_type": "Medijos timas", + "search_filter_media_type": "Medijos tipas", "search_filter_media_type_title": "Pasirinkti medijos tipą", + "search_filter_ocr": "IeÅĄkoti pagal OCR", + "search_filter_people_title": "Pasirinkti asmenis", + "search_filter_star_rating": "ÄŽvertinimas", + "search_for": "IeÅĄkoti ko", + "search_for_existing_person": "IeÅĄkoti įvardinto asmens", "search_no_more_result": "Nėra daugiau rezultatÅŗ", + "search_no_people": "Be asmenÅŗ", "search_no_people_named": "Nėra ÅžmoniÅŗ vardu „{name}“", + "search_no_result": "RezultatÅŗ nerasta, pabandykite kitą paieÅĄkos terminą ar derinį", + "search_options": "PaieÅĄkos parinktys", + "search_page_categories": "Kategorijos", + "search_page_motion_photos": "Judanti Foto", + "search_page_no_objects": "Objekto info nepasiekiama", + "search_page_no_places": "Vietovės info nepasiekiama", "search_page_screenshots": "Ekrano nuotraukos", "search_page_search_photos_videos": "IeÅĄkokite nuotraukÅŗ ir vaizdo įraÅĄÅŗ", "search_page_selfies": "Asmenukės", "search_page_things": "Dalykai", "search_page_view_all_button": "PerÅžiÅĢrėti visus", + "search_page_your_activity": "JÅĢsÅŗ veikla", + "search_page_your_map": "JÅĢsÅŗ Åžemėlapis", "search_people": "IeÅĄkoti ÅžmoniÅŗ", "search_places": "IeÅĄkoti vietÅŗ", "search_rating": "IeÅĄkoti pagal įvertinimą...", + "search_result_page_new_search_hint": "Nauja PaieÅĄka", "search_settings": "IeÅĄkoti nustatymÅŗ", + "search_state": "IeÅĄkoti valstijos/apskrities...", + "search_suggestion_list_smart_search_hint_1": "IÅĄmanioji paieÅĄka įjungta pagal numatytuosius nustatymus, metaduomenÅŗ paieÅĄkai naudokite sintaksę ", + "search_suggestion_list_smart_search_hint_2": "PaieÅĄka", "search_tags": "IeÅĄkoti ÅžymÅŗ...", "search_timezone": "IeÅĄkoti laiko zonos...", "search_type": "PaieÅĄkos tipas", "search_your_photos": "IeÅĄkoti nuotraukÅŗ", + "searching_locales": "IeÅĄkoma vietoviÅŗ...", + "second": "Sekundė", + "see_all_people": "Pamatyti visus asmenis", + "select": "Pasirinkti", + "select_album": "Rinktis albumą", + "select_album_cover": "Rinktis albumo virÅĄelį", + "select_albums": "Rinktis albumus", + "select_all": "Pasirinkti visus", "select_all_duplicates": "Pasirinkti visus dublikatus", "select_all_in": "PaÅžymėti visus esančius {group}", "select_avatar_color": "Pasirinkti avataro spalvą", + "select_count": "{count, plural, one {Pasirinkti #} other {Pasirinkti #}}", + "select_cutoff_date": "Pasirinkite galutinę datą", "select_face": "Pasirinkti veidą", "select_featured_photo": "Pasirinkti rodomą nuotrauką", "select_from_computer": "Pasirinkti iÅĄ kompiuterio", "select_keep_all": "Visus paÅžymėti \"Palikti\"", "select_library_owner": "Pasirinkti bibliotekos savininką", "select_new_face": "Pasirinkti naują veidą", + "select_people": "Pasirinkti asmenis", + "select_person": "Pasirinkti asmenį", + "select_person_to_tag": "Pasirinkti asmenį Åžymai", "select_photos": "Pasirinkti nuotraukas", "select_trash_all": "Visus paÅžymėti \"IÅĄmesti\"", + "select_user_for_sharing_page_err_album": "Nepavyko sukurti albumo", "selected": "Pasirinkta", "selected_count": "{count, plural, one {# pasirinktas} few {# pasirinkti} other {# pasirinktÅŗ}}", + "selected_gps_coordinates": "Pasirinkti GPS Koordinates", "send_message": "SiÅŗsti Åžinutę", "send_welcome_email": "SiÅŗsti sveikinimo el. laiÅĄką", - "server_info_box_app_version": "Programėlės versija", + "server_endpoint": "Serverio Galinis TaÅĄkas", + "server_info_box_app_version": "Programos versija", "server_info_box_server_url": "Serverio URL", "server_offline": "Serveris nepasiekiamas", "server_online": "Serveris pasiekiamas", "server_privacy": "Serverio Privatumas", + "server_restarting_description": "Å is puslapis atsinaujins neuÅžilgo.", + "server_restarting_title": "Serveris restartuoja", "server_stats": "Serverio statistika", + "server_update_available": "Yra Serverio atnaujinimas", "server_version": "Serverio versija", "set": "Nustatyti", + "set_as_album_cover": "Naudoti kaip albumo virÅĄelį", + "set_as_featured_photo": "Naudoti foto asmens profiliui", "set_as_profile_picture": "Nustatyti kaip profilio nuotrauką", "set_date_of_birth": "Nustatyti gimimo datą", "set_profile_picture": "Nustatyti profilio nuotrauką", "set_slideshow_to_fullscreen": "Nustatyti skaidriÅŗ perÅžiÅĢrą per visą ekraną", "set_stack_primary_asset": "Nustatyti kaip pagrindinį elementą", + "setting_image_viewer_help": "Detali perÅžiÅĢra pirmiausia įkelia maŞą miniatiÅĢrą, tada įkelia vidutinio dydÅžio versiją (jei įjungta) ir galiausiai įkelia originalą (jei įjungta).", + "setting_image_viewer_original_subtitle": "ÄŽjunkite, kad įkeltumėte originalÅŗ pilnos raiÅĄkos vaizdą (didelį!). IÅĄjunkite, kad sumaÅžintumėte duomenÅŗ naudojimą (tiek tinkle, tiek įrenginio talpykloje).", "setting_image_viewer_original_title": "UÅžkrauti originalią nuotrauką", + "setting_image_viewer_preview_subtitle": "ÄŽjunkite, jei norite įkelti vidutinės raiÅĄkos vaizdą. IÅĄjunkite, jei norite tiesiogiai įkelti originalą ar naudoti tik miniatiÅĢrą.", "setting_image_viewer_preview_title": "UÅžkrauti perÅžiÅĢros nuotrauką", "setting_image_viewer_title": "Nuotraukos", "setting_languages_apply": "Pritaikyti", + "setting_languages_subtitle": "Pakeisti programos kalbą", "setting_notifications_notify_failures_grace_period": "Informuoti apie foninio atsarginio kopijavimo nesėkmes: {duration}", "setting_notifications_notify_hours": "{count} valandÅŗ", + "setting_notifications_notify_immediately": "nedelsiant", "setting_notifications_notify_minutes": "{count} minučiÅŗ", "setting_notifications_notify_never": "niekada", "setting_notifications_notify_seconds": "{count} sekundÅžiÅŗ", "setting_notifications_single_progress_subtitle": "Detali įkėlimo progreso informacija kiekvienam elementui", + "setting_notifications_single_progress_title": "Rodyti foninio atsarginio kopijavimo eigą", + "setting_notifications_subtitle": "Koreguoti praneÅĄimÅŗ nuostatas", + "setting_notifications_total_progress_subtitle": "Bendra įkėlimo eiga (atlikta/viso elementÅŗ)", + "setting_notifications_total_progress_title": "Rodyti visą foninio atsarginio kopijavimo eigą", + "setting_video_viewer_auto_play_subtitle": "AutomatiÅĄkai pradėti leisti vaizdo įraÅĄus juos atidarius", + "setting_video_viewer_auto_play_title": "Groti video automatiÅĄkai", + "setting_video_viewer_looping_title": "Kartotinai", + "setting_video_viewer_original_video_subtitle": "Transliuojant vaizdo įraÅĄÄ… iÅĄ serverio, leisti originalą, net jei įraÅĄas yra konvertuotas. ÄŽraÅĄas gali strigti. Vietoje pasiekiami vaizdo įraÅĄai leidÅžiami originalia kokybe, nepaisant ÅĄio nustatymo.", + "setting_video_viewer_original_video_title": "Priversti originalÅŗ vaizdo įraÅĄÄ…", "settings": "Nustatymai", "settings_require_restart": "PraÅĄome perkrauti Immich, siekiant pritaikyti ÅĄÄ¯ nustatymą", "settings_saved": "Nustatymai iÅĄsaugoti", "setup_pin_code": "Nustatyti PIN kodą", "share": "Dalintis", + "share_action_prompt": "{count} dalinamasi", "share_add_photos": "ÄŽtraukti nuotraukÅŗ", "share_assets_selected": "{count} paÅžymėta", "share_dialog_preparing": "RuoÅĄiama...", "share_link": "Bendrinti nuorodą", "shared": "Bendrinami", + "shared_album_activities_input_disable": "Komentarai iÅĄjungti", + "shared_album_activity_remove_content": "Ar norite iÅĄtrinti ÅĄią veiklą?", + "shared_album_activity_remove_title": "IÅĄtrinti veiklą", + "shared_album_section_people_action_error": "Klaida iÅĄeinant/ÅĄalinant iÅĄ albumo", + "shared_album_section_people_action_leave": "PaÅĄalinti naudotoją iÅĄ albumo", + "shared_album_section_people_action_remove_user": "PaÅĄalinti naudotoją iÅĄ albumo", + "shared_album_section_people_title": "ASMENYS", + "shared_by": "Bendrina", "shared_by_user": "Bendrina {user}", "shared_by_you": "Bendrinama jÅĢsÅŗ", "shared_from_partner": "Nuotraukos iÅĄ {partner}", "shared_intent_upload_button_progress_text": "{current} / {total} ÄŽkelta", + "shared_link_app_bar_title": "Dalinimosi Nuorodos", "shared_link_clipboard_copied_massage": "Nukopijuota į iÅĄkarpinę", "shared_link_clipboard_text": "Nuoroda: {link}\nSlaptaÅžodis: {password}", + "shared_link_create_error": "Klaida kuriant bendrinimo nuorodą", + "shared_link_custom_url_description": "Pasiekite ÅĄią bendrinimo nuorodą naudodami tinkintą URL", + "shared_link_edit_description_hint": "ÄŽveskite bendrinimo apraÅĄymą", "shared_link_edit_expire_after_option_day": "1 diena", "shared_link_edit_expire_after_option_days": "{count} dienÅŗ", "shared_link_edit_expire_after_option_hour": "1 valanda", @@ -1738,26 +2036,37 @@ "shared_link_edit_expire_after_option_minutes": "{count} minučiÅŗ", "shared_link_edit_expire_after_option_months": "{count} mėnesiÅŗ", "shared_link_edit_expire_after_option_year": "{count} metÅŗ", + "shared_link_edit_password_hint": "ÄŽveskite bendrinimo slaptaÅžodį", + "shared_link_edit_submit_button": "Atnaujinti nuorodą", + "shared_link_error_server_url_fetch": "Nepavyksta gauti serverio url", "shared_link_expires_day": "Galiojimas baigsis uÅž {count} dienos", "shared_link_expires_days": "Galiojimas baigsis uÅž {count} dienÅŗ", "shared_link_expires_hour": "Galiojimas baigsis uÅž {count} valandos", "shared_link_expires_hours": "Galiojimas baigsis uÅž {count} valandÅŗ", "shared_link_expires_minute": "Galiojimas baigsis uÅž {count} minutės", "shared_link_expires_minutes": "Galiojimas baigsis uÅž {count} minučiÅŗ", + "shared_link_expires_never": "Galiojimas baigiasi ∞", "shared_link_expires_second": "Galiojimas baigsis uÅž {count} sekundės", "shared_link_expires_seconds": "Galiojimas baigsis uÅž {count} sekundÅžiÅŗ", + "shared_link_individual_shared": "Asmuo pasidalintas", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_manage_links": "Valdyti Bendrinimo nuorodas", "shared_link_options": "Bendrinimo nuorodos parametrai", + "shared_link_password_description": "Bendrinimo nuorodos prieigai reikalingas slaptaÅžodis", "shared_links": "Bendrinimo nuorodos", + "shared_links_description": "Dalintis foto ir video su nuoroda", "shared_photos_and_videos_count": "{assetCount, plural, one {# bendrinama nuotrauka ir vaizdo įraÅĄas} few {# bendrinamos nuotraukos ir vaizdo įraÅĄai} other {# bendrinamÅŗ nuotraukÅŗ ir vaizdo įraÅĄÅŗ}}", "shared_with_me": "Bendrinama su manimi", "shared_with_partner": "Pasidalinta su {partner}", "sharing": "Dalijimasis", "sharing_enter_password": "Norėdami perÅžiÅĢrėti ÅĄÄ¯ puslapį, įveskite slaptaÅžodį.", "sharing_page_album": "Bendrinami albumai", + "sharing_page_description": "Kurkite bendrinamus albumus, kad galėtumėte dalintis foto ir video su Åžmonėmis savo tinkle.", "sharing_page_empty_list": "TUŠČIAS SĄRAÅ AS", "sharing_sidebar_description": "Rodyti bendrinimo rodinio nuorodą ÅĄoninėje juostoje", "sharing_silver_appbar_create_shared_album": "Naujas bendrinamas albumas", "sharing_silver_appbar_share_partner": "Bendrinti su partneriu", + "shift_to_permanent_delete": "spauskite ⇧, kad visam laikui iÅĄtrintumėte elementą", "show_album_options": "Rodyti albumo parinktis", "show_albums": "Rodyti albumus", "show_all_people": "Rodyti visus asmenis", @@ -1771,11 +2080,17 @@ "show_metadata": "Rodyti metaduomenis", "show_or_hide_info": "Rodyti arba slėpti informaciją", "show_password": "Rodyti slaptaÅžodį", + "show_person_options": "Rodyti asmens parinktis", "show_progress_bar": "Rodyti progreso juostą", + "show_schema": "Rodyti schemą", "show_search_options": "Rodyti paieÅĄkos parinktis", + "show_shared_links": "Rodyti bendrinamas nuorodas", "show_slideshow_transition": "Rodyti perėjimą tarp skaidriÅŗ", "show_supporter_badge": "Rėmėjo Åženklelis", "show_supporter_badge_description": "Rodyti rėmėjo Åženklelį", + "show_text_recognition": "Rodyti teksto atpaÅžinimą", + "show_text_search_menu": "Rodyti teksto paieÅĄkos meniu", + "shuffle": "IÅĄmaiÅĄyti", "sidebar": "Å oninė juosta", "sidebar_display_description": "Rodyti rodinio nuorodą ÅĄoninėje juostoje", "sign_out": "Atsijungti", @@ -1785,6 +2100,8 @@ "skip_to_folders": "Praleisti iki aplankÅŗ", "skip_to_tags": "Praleisti iki ÅžymiÅŗ", "slideshow": "SkaidriÅŗ perÅžiÅĢra", + "slideshow_repeat": "Kartoti skaidres", + "slideshow_repeat_description": "Pradėti iÅĄ pradÅžiÅŗ, kai skaidrės baigiasi", "slideshow_settings": "SkaidriÅŗ perÅžiÅĢros nustatymai", "sort_albums_by": "Rikiuoti albumus pagal...", "sort_created": "SukÅĢrimo data", @@ -1797,57 +2114,95 @@ "sort_title": "Pavadinimas", "source": "Å altinis", "stack": "Grupuoti", + "stack_action_prompt": "{count} sugrupuota", "stack_duplicates": "Grupuoti dublikatus", "stack_select_one_photo": "Pasirinkti pagrindinę grupės nuotrauką", "stack_selected_photos": "Grupuoti pasirinktas nuotraukas", "stacked_assets_count": "{count, plural, one {Sugrupuotas # elementas} few {Sugrupuoti # elementai} other {Sugrupuota # elementÅŗ}}", + "stacktrace": "Stacktrace", "start": "Pradėti", "start_date": "PradÅžios data", "start_date_before_end_date": "PradÅžios data turi bÅĢti ankstesnė uÅž pabaigos datą", + "state": "Valstija/Apskritis", "status": "Statusas", "stop_casting": "Nutraukti transliavimą", + "stop_motion_photo": "Sustabdyti Judančią Foto", "stop_photo_sharing": "Nustoti dalytis savo nuotraukomis?", + "stop_photo_sharing_description": "{partner} nebeturės prieigos prie jÅĢsÅŗ nuotraukÅŗ.", "stop_sharing_photos_with_user": "Nustoti dalintis savo nuotraukomis su ÅĄiuo vartotoju", "storage": "Saugykla", "storage_label": "Saugyklos ÅŊyma", + "storage_quota": "Saugyklos Kvota", "storage_usage": "Naudojama {used} iÅĄ {available}", "submit": "Pateikti", + "success": "Sėkmė", "suggestions": "PasiÅĢlymai", "sunrise_on_the_beach": "Saulėtekis paplÅĢdimyje", "support": "Pagalba", "support_and_feedback": "Palaikymas ir atsiliepimai", + "support_third_party_description": "JÅĢsÅŗ Immich paketas yra sukurtas trečios ÅĄalies. Problemos, su kuriomis susiduriate, gali bÅĢti susijusios su ÅĄiuo paketu, todėl pirmiausia praneÅĄkite apie problemas jiems, naudodami toliau pateiktas nuorodas.", + "swap_merge_direction": "Keisti sujungimo kryptį", "sync": "Sinchronizuoti", "sync_albums": "Sinchronizuoti albumus", "sync_albums_manual_subtitle": "Sinchronizuoti visus įkeltus vaizdo įraÅĄus ir nuotraukas su pasirinktomis atsarginėmis kopijomis", + "sync_local": "Sinchronizuoti vietinį", + "sync_remote": "Sinchronizuoti nuotolinį", + "sync_status": "Sinchronizacijos bÅĢklė", + "sync_status_subtitle": "ÅŊiÅĢrėti ir valdyti sinchronizacijos systemą", "sync_upload_album_setting_subtitle": "Sukurti ir įkelti jÅĢsÅŗ nuotraukas ir vaizdo įraÅĄus į pasirinktus Immich albumus", "tag": "ÅŊyma", + "tag_assets": "PaÅžymėti", "tag_created": "Sukurta Åžyma: {tag}", "tag_feature_description": "PerÅžiÅĢrėkite nuotraukas ir vaizdo įraÅĄus sugrupuotus pagal suÅžymėtas temas", "tag_not_found_question": "Nerandate Åžymos? Sukurti naują Åžymą.", + "tag_people": "PaÅžymėti ÅŊmones", "tag_updated": "Atnaujinta Åžyma: {tag}", "tagged_assets": "ÅŊyma pridėta prie {count, plural, one {# elemento} other {# elementÅŗ}}", "tags": "ÅŊymos", + "tap_to_run_job": "Paspauskite, kad pradėti darbą", "template": "Å ablonas", + "text_recognition": "Teksto atpaÅžinimas", "theme": "Tema", "theme_selection": "Temos pasirinkimas", "theme_selection_description": "AutomatiÅĄkai nustatykite ÅĄviesią arba tamsią temą pagal narÅĄyklės sistemos nustatymus", + "theme_setting_asset_list_storage_indicator_title": "Rodyti saugyklos indikatoriÅŗ elementÅŗ plytelėse", "theme_setting_asset_list_tiles_per_row_title": "ElementÅŗ per eilutę ({count})", + "theme_setting_colorful_interface_subtitle": "Fono pavirÅĄiams uÅžtepkite pagrindinę spalvą.", + "theme_setting_colorful_interface_title": "Spalvinga sąsaja", + "theme_setting_image_viewer_quality_subtitle": "Koreguoti detaliÅŗ vaizdÅŗ perÅžiÅĢros kokybę", + "theme_setting_image_viewer_quality_title": "Vaizdo perÅžiÅĢros priemonės kokybė", + "theme_setting_primary_color_subtitle": "Pasirinkite spalvą pagrindiniams veiksmams ir akcentams.", "theme_setting_primary_color_title": "Pagrindinė spalva", "theme_setting_system_primary_color_title": "Naudoti sistemos spalvą", "theme_setting_system_theme_switch": "Automatinė (Naudoti sistemos nustatymus)", + "theme_setting_theme_subtitle": "Pasirinkite programos temos nustatymą", "theme_setting_three_stage_loading_subtitle": "TrijÅŗ etapÅŗ įkėlimas gali padidinti įkėlimo naÅĄumą, tačiau sukelia Åžymiai didesnę tinklo apkrovą", + "theme_setting_three_stage_loading_title": "ÄŽjungti trijÅŗ etapÅŗ įkėlimą", + "then": "Tada", + "they_will_be_merged_together": "Jie bus sujungti kartu", + "third_party_resources": "Trečios Å alies IÅĄtekliai", + "time": "Laikas", "time_based_memories": "Atsiminimai pagal laiką", + "time_based_memories_duration": "Kiekvieno vaizdo rodymo laikas sekundėmis.", "timeline": "Laiko skalė", "timezone": "Laiko juosta", "to_archive": "Archyvuoti", "to_change_password": "Pakeisti slaptaÅžodį", "to_favorite": "ÄŽtraukti prie mėgstamiausiÅŗ", "to_login": "Prisijungti", + "to_multi_select": "pasirinkti kelis elementus", + "to_parent": "Persikelti į virÅĄÅŗ", + "to_select": "į pasirinkimą", "to_trash": "IÅĄmesti", + "toggle_settings": "ÄŽjungti nustatymus", + "toggle_theme_description": "ÄŽjungti temą", "total": "Viso", + "total_usage": "Viso naudojama", "trash": "Å iukÅĄliadėŞė", + "trash_action_prompt": "{count} iÅĄmesta į ÅĄiukÅĄliadėŞę", "trash_all": "Perkelti visus į ÅĄiukÅĄliadėŞę", "trash_count": "Perkelti {count, number} į ÅĄiukÅĄliadėŞę", + "trash_delete_asset": "IÅĄmesti/IÅĄtrinti elementą", "trash_emptied": "IÅĄvalytos ÅĄiukÅĄlės", "trash_no_results_message": "ÄŽ ÅĄiukÅĄliadėŞę perkeltos nuotraukos ir vaizdo įraÅĄai bus rodomi čia.", "trash_page_delete_all": "IÅĄtrinti Visus", @@ -1855,15 +2210,24 @@ "trash_page_info": "Å iukÅĄliadėŞės elementai bus galutinai iÅĄtrinti uÅž {days} dienÅŗ", "trash_page_no_assets": "Nėra iÅĄmestÅŗ elementÅŗ", "trash_page_restore_all": "Atkurti Visus", + "trash_page_select_assets_btn": "Pasirinkti elementus", "trash_page_title": "Å iukÅĄliÅŗ ({count})", "trashed_items_will_be_permanently_deleted_after": "ÄŽ ÅĄiukÅĄliadėŞę perkelti elementai bus visam laikui iÅĄtrinti po {days, plural, one {# dienos} other {# dienÅŗ}}.", + "trigger_asset_uploaded": "Elementas IÅĄsiÅŗstas", + "trigger_person_recognized": "Asmuo AtpaÅžintas", + "troubleshoot": "Å alinti triktis", "type": "Tipas", "unable_to_change_pin_code": "Negalima pakeisti PIN kodo", + "unable_to_check_version": "Nepavyko patvirtinti programos/serverio versijos", "unarchive": "IÅĄarchyvuoti", + "unarchive_action_prompt": "{count} paÅĄalinta iÅĄ Archyvo", "unarchived_count": "{count, plural, other {# iÅĄarchyvuota}}", "unfavorite": "PaÅĄalinti iÅĄ mėgstamiausiÅŗ", + "unfavorite_action_prompt": "{count} paÅĄalinta iÅĄ MėgstamiausiÅŗ", "unhide_person": "Nebeslėpti Åžmogaus", + "unknown": "NeÅžinoma", "unknown_country": "NeÅžinoma Å alis", + "unknown_date": "NeÅžinoma data", "unknown_year": "NeÅžinomi metai", "unlimited": "Neribota", "unlink_oauth": "Atsieti OAuth", @@ -1874,13 +2238,18 @@ "unsaved_change": "NeiÅĄsaugoti pakeitimai", "unselect_all": "AtÅĄaukti visÅŗ pasirinkimą", "unselect_all_duplicates": "AtÅžymėti visus dublikatus", + "unselect_all_in": "AtÅžymėti viską {group}", "unstack": "IÅĄgrupuoti", + "unstack_action_prompt": "{count} iÅĄgrupuota", "unstacked_assets_count": "{count, plural, one {IÅĄgrupuotas # elementas} few {IÅĄgrupuoti # elementai} other {IÅĄgrupuota # elementÅŗ}}", + "untagged": "NepaÅžymėta", "up_next": "Seknatis", + "update_location_action_prompt": "Atnaujinti {count} {count, plural, one {pasirinkto elemento} few {pasirinktÅŗ elementÅŗ} other {pasirinktÅŗ elementÅŗ}} vietovę naudojant:", "updated_at": "Atnaujintas", "updated_password": "SlaptaÅžodis atnaujintas", "upload": "ÄŽkelti", "upload_concurrency": "ÄŽkėlimo lygiagretumas", + "upload_details": "ÄŽkėlimo Detalės", "upload_dialog_info": "Ar norite sukurti pasirinkto(-Åŗ) turinio(-Åŗ) atsarginę kopiją serveryje?", "upload_dialog_title": "ÄŽkelti turinį", "upload_errors": "ÄŽkėlimas įvyko su {count, plural, one {# klaida} few {# klaidomis} other {# klaidÅŗ}}, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", @@ -1891,10 +2260,11 @@ "upload_success": "ÄŽkėlimas pavyko, norėdami pamatyti naujai įkeltus elementus perkraukite puslapį.", "upload_to_immich": "ÄŽkelti į Immich ({count})", "uploading": "ÄŽkeliama", + "uploading_media": "ÄŽkeliama medija", "url": "URL", "usage": "Naudojimas", "use_biometric": "Naudoti biometriją", - "use_current_connection": "naudoti dabartinį ryÅĄÄ¯", + "use_current_connection": "Naudoti dabartinį ryÅĄÄ¯", "user": "Naudotojas", "user_has_been_deleted": "Å is naudotojas buvo iÅĄtrintas.", "user_id": "Naudotojo ID", @@ -1903,14 +2273,17 @@ "user_pin_code_settings_description": "Tvarkykite savo PIN kodą", "user_privacy": "Vartotojo Privatumas", "user_purchase_settings": "ÄŽsigyti", + "user_purchase_settings_description": "Tvarkyti savo pirkinį", "user_role_set": "Nustatyti {user}, kaip {role}", "user_usage_stats": "Paskyros naudojimo statistika", "user_usage_stats_description": "ÅŊiÅĢrėti paskyros naudojimo statistiką", "username": "Naudotojo vardas", "users": "Naudotojai", + "users_added_to_album_count": "Pridėta {count, plural, one {# naudotojas} few {# naudotojai} other {# naudotojÅŗ}} į albumą", "utilities": "ÄŽrankiai", "validate": "Validuoti", "validate_endpoint_error": "PraÅĄome įvesti galiojantį URL", + "validation_error": "Patvirtinimo klaida", "variables": "Kintamieji", "version": "Versija", "version_announcement_closing": "Tavo draugas, Alex", @@ -1922,25 +2295,45 @@ "video_hover_setting_description": "Atkurti vaizdo įraÅĄo miniatiÅĢrą, kai pelė uÅžvedama ant elemento. Net ir iÅĄjungus, atkÅĢrimą galima pradėti uÅžvedus pelės Åžymeklį ant atkÅĢrimo piktogramos.", "videos": "Video", "videos_count": "{count, plural, one {# vaizdo įraÅĄas} few {# vaizdo įraÅĄai} other {# vaizdo įraÅĄÅŗ}}", + "videos_only": "Tik Video", "view": "ÅŊiÅĢrėti", "view_album": "ÅŊiÅĢrėti albumą", "view_all": "PerÅžiÅĢrėti viską", "view_all_users": "PerÅžiÅĢrėti visus naudotojus", "view_in_timeline": "ÅŊiÅĢrėti laiko skalėje", + "view_link": "ÅŊiÅĢrėti nuorodą", "view_links": "ÅŊiÅĢrėti nuorodas", + "view_name": "ÅŊiÅĢrėti", + "view_next_asset": "ÅŊiÅĢrėti sekantį elementą", "view_qr_code": "ÅŊiÅĢrėti QR kodą", + "view_similar_photos": "ÅŊiÅĢrėti panaÅĄias foto", "view_stack": "PerÅžiÅĢrėti grupę", "waiting": "Laukiama", + "waiting_count": "Laukiama: {count}", "warning": "ÄŽspėjimas", "week": "Savaitė", "welcome": "Sveiki atvykę", "welcome_to_immich": "Sveiki atvykę į Immich", + "width": "Plotis", "wifi_name": "Wi-Fi Pavadinimas", + "workflow_delete_prompt": "Ar tikrai norite iÅĄtrinti ÅĄią darbÅŗ eigą?", + "workflow_deleted": "DarbÅŗ eiga iÅĄtrinta", + "workflow_description": "DarbÅŗ eigos apraÅĄymas", + "workflow_info": "DarbÅŗ eigos informacija", + "workflow_json": "DarbÅŗ eigos JSON", + "workflow_json_help": "Redaguoti darbÅŗ eigos konfigÅĢraciją JSON formatu. Pakeitimai bus sinchronizuoti su vizualiuoju kÅĢrėju.", + "workflow_name": "DarbÅŗ eigos pavadinimas", + "workflow_navigation_prompt": "Ar norite iÅĄeiti neiÅĄsaugoję pakeitimÅŗ?", + "workflow_summary": "DarbÅŗ eigos santrauka", + "workflow_update_success": "DarbÅŗ eiga sėkmingai atnaujinta", + "workflow_updated": "DarbÅŗ eiga atnaujinta", + "workflows": "DarbÅŗ eigos", "wrong_pin_code": "Neteisingas PIN kodas", "year": "Metai", "years_ago": "PrieÅĄ {years, plural, one {# metus} other {# metÅŗ}}", "yes": "Taip", "you_dont_have_any_shared_links": "Bendrinimo nuorodÅŗ neturite", "your_wifi_name": "JÅĢsÅŗ Wi-Fi pavadinimas", - "zoom_image": "Priartinti vaizdą" + "zoom_image": "Priartinti vaizdą", + "zoom_to_bounds": "Priartinti iki kraÅĄtÅŗ" } diff --git a/i18n/lv.json b/i18n/lv.json index bfdfac3bc9..f5c96d8bcb 100644 --- a/i18n/lv.json +++ b/i18n/lv.json @@ -5,8 +5,10 @@ "acknowledge": "Pieņemt", "action": "DarbÄĢba", "action_common_update": "Atjaunināt", + "action_description": "DarbÄĢbu kopums, ko veikt ar filtrētajiem failiem", "actions": "DarbÄĢbas", "active": "AktÄĢvs", + "active_count": "AktÄĢvi: {count}", "activity": "Aktivitāte", "activity_changed": "Aktivitāte ir {enabled, select, true {iespējota} other {atspējota}}", "add": "Pievienot", @@ -14,9 +16,14 @@ "add_a_location": "Pievienot atraÅĄanās vietu", "add_a_name": "Pievienot vārdu", "add_a_title": "Pievienot virsrakstu", + "add_action": "Pievienot darbÄĢbu", + "add_action_description": "KlikÅĄÄˇini, lai pievienotu veicamo darbÄĢbu", + "add_assets": "Pievienot failus", "add_birthday": "Pievienot dzimÅĄanas dienu", "add_endpoint": "Pievienot galapunktu", "add_exclusion_pattern": "Pievienot izslēgÅĄanas ÅĄablonu", + "add_filter": "Pievienot filtru", + "add_filter_description": "KlikÅĄÄˇini, lai pievienotu filtra nosacÄĢjumu", "add_location": "Pievienot lokāciju", "add_more_users": "Pievienot vēl lietotājus", "add_partner": "Pievienot partneri", @@ -35,6 +42,7 @@ "add_to_shared_album": "Pievienot koplietotam albumam", "add_upload_to_stack": "Pievienot augÅĄupielādi kaudzei", "add_url": "Pievienot URL", + "add_workflow_step": "Pievienot darba plÅĢsmas soli", "added_to_archive": "Pievienots arhÄĢvam", "added_to_favorites": "Pievienots izlasei", "added_to_favorites_count": "{count, number} pievienoti izlasei", @@ -67,6 +75,7 @@ "confirm_reprocess_all_faces": "Vai tieÅĄÄm vēlies atkārtoti apstrādāt visas sejas? Tas arÄĢ atiestatÄĢs personas ar vārdiem.", "confirm_user_password_reset": "Vai tieÅĄÄm vēlaties atiestatÄĢt lietotāja {user} paroli?", "confirm_user_pin_code_reset": "Vai tieÅĄÄm vēlaties atiestatÄĢt {user} PIN kodu?", + "copy_config_to_clipboard_description": "Kopēt paÅĄreizējo sistēmas konfigurāciju kā JSON objektu starpliktuvē", "create_job": "Izveidot uzdevumu", "cron_expression": "Cron izteiksme", "cron_expression_description": "Iestatiet skenÄ“ÅĄanas intervālu, izmantojot cron formātu. Papildu informācijai skatiet, piemēram, Crontab Guru", @@ -74,6 +83,7 @@ "disable_login": "Atspējot pieteikÅĄanos", "duplicate_detection_job_description": "Analizēt failus ar maÅĄÄĢnmācÄĢÅĄanos, lai noteiktu lÄĢdzÄĢgus attēlus. Å ÄĢ funkcija izmanto viedo meklÄ“ÅĄanu", "exclusion_pattern_description": "IzslēgÅĄanas ÅĄabloni Äŧauj ignorēt failus un mapes, skenējot bibliotēku. Tas ir noderÄĢgi, ja jums ir mapes, kas satur failus, kurus nevēlaties importēt, piemēram, RAW failus.", + "export_config_as_json_description": "Lejupielādēt paÅĄreizējo sistēmas konfigurāciju kā JSON failu", "face_detection": "Seju noteikÅĄana", "face_detection_description": "AtpazÄĢt attēlos sejas, izmantojot maÅĄÄĢnmācÄĢÅĄanos. Video gadÄĢjumā tiek ņemta vērā tikai sÄĢktēls. \"Atsvaidzināt\" atkārtoti apstrādā visus attēlus. \"AtiestatÄĢt\" izdzÄ“ÅĄ visus paÅĄreizējos seju datus. \"TrÅĢkstoÅĄie\" ierindo attēlus, kas vēl nav apstrādāti. Pēc seju noteikÅĄanas pabeigÅĄanas atrastās sejas tiek ierindotas seju atpazÄĢÅĄanai, grupējot tās pēc esoÅĄas vai jauns personas.", "facial_recognition_job_description": "Grupēt atpazÄĢtās sejas pēc cilvēkiem. Å is solis tiek veikts pēc seju noteikÅĄanas pabeigÅĄanas. \"AtiestatÄĢt\" atkārtoti sagrupē visas sejas. \"TrÅĢkstoÅĄie\" ierindo sejas, kurām nav pieÅĄÄˇirta persona.", @@ -93,6 +103,8 @@ "image_preview_description": "Vidēja izmēra attēls ar noņemtiem metadatiem, ko izmanto, skatot vienu failu un maÅĄÄĢnmācÄĢÅĄanās apmācÄĢbai", "image_preview_quality_description": "PriekÅĄskatÄĢjuma kvalitāte no 1 lÄĢdz 100. Augstāka kvalitāte ir labāka, bet veido lielākus failus un var samazināt lietotnes reaÄŖÄ“ÅĄanas ātrumu. Zemas vērtÄĢbas iestatÄĢÅĄana var ietekmēt maÅĄÄĢnmācÄĢÅĄanās kvalitāti.", "image_preview_title": "PriekÅĄskatÄĢjuma iestatÄĢjumi", + "image_progressive": "ProgresÄĢvi", + "image_progressive_description": "JPEG attēlus iekodēt progresÄĢvi, lai tie ielādētos pakāpeniski. Tas neietekmē WebP attēlus.", "image_quality": "Kvalitāte", "image_resolution": "IzÅĄÄˇirtspēja", "image_resolution_description": "Augstāka izÅĄÄˇirtspēja Äŧauj saglabāt vairāk detaÄŧu, taču kodÄ“ÅĄana aizņem vairāk laika, failu izmērs ir lielāks un var samazināties lietotnes reaÄŖÄ“ÅĄanas ātrums.", @@ -101,11 +113,13 @@ "image_thumbnail_description": "Neliels sÄĢktēls bez metadatiem, ko izmanto, lai apskatÄĢtu vairākus fotoattēlus, piemēram, galvenajā laika skalā", "image_thumbnail_quality_description": "SÄĢktēlu kvalitāte no 1 lÄĢdz 100. Augstāka kvalitāte ir labāka, bet veido lielākus failus un var samazināt lietotnes reaÄŖÄ“ÅĄanas ātrumu.", "image_thumbnail_title": "SÄĢktēlu iestatÄĢjumi", + "import_config_from_json_description": "Importēt sistēmas konfigurāciju, augÅĄupielādējot JSON konfigurācijas failu", "job_concurrency": "{job} vienlaicÄĢgi", "job_created": "Uzdevums izveidots", "job_not_concurrency_safe": "Å is uzdevums nav droÅĄs vienlaicÄĢgai izpildei.", "job_settings": "Uzdevumu iestatÄĢjumi", "job_settings_description": "Uzdevumu izpildes vienlaicÄĢguma pārvaldÄĢba", + "jobs_over_time": "Uzdevumi laika gaitā", "library_created": "Izveidoja bibliotēku: {library}", "library_deleted": "Bibliotēka dzēsta", "library_details": "Bibliotēkas dati", @@ -168,7 +182,21 @@ "machine_learning_smart_search_enabled": "Iespējot viedo meklÄ“ÅĄanu", "machine_learning_smart_search_enabled_description": "Ja funkcija ir atspējota, attēli netiks kodēti viedai meklÄ“ÅĄanai.", "machine_learning_url_description": "MaÅĄÄĢnmācÄĢÅĄanās servera URL. Ja ir norādÄĢts vairāk nekā viens URL, katrs serveris, sākot no pirmā lÄĢdz pēdējam, tiks pārbaudÄĢts pa vienam, lÄĢdz kāds no tiem atbildēs veiksmÄĢgi. Serveri, kas neatbild, tiks ÄĢslaicÄĢgi ignorēti, lÄĢdz tie atkal bÅĢs pieejami tieÅĄsaistē.", + "maintenance_delete_backup": "Dzēst rezerves kopiju", + "maintenance_delete_backup_description": "Å is fails tiks neatgriezeniski dzēsts.", + "maintenance_delete_error": "Neizdevās dzēst rezerves kopiju.", + "maintenance_restore_backup": "Atjaunot no rezerves kopijas", + "maintenance_restore_backup_different_version": "Å ÄĢ rezerves kopija tika izveidota ar citu Immich versiju!", + "maintenance_restore_backup_unknown_version": "Nevarēja noteikt rezerves kopijas versiju.", + "maintenance_restore_database_backup_description": "Atgrizties pie iepriekÅĄÄ“jā datubāzes stāvokÄŧa, izmantojot rezerves kopijas failu", + "maintenance_settings": "Apkope", + "maintenance_settings_description": "Pārslēgt Immich apkopes reÅžÄĢmā.", + "maintenance_start": "Sākt apkopes reÅžÄĢmu", + "maintenance_start_error": "Neizdevās uzsākt apkopes reÅžÄĢmu.", + "maintenance_upload_backup": "AugÅĄupielādēt datubāzes rezerves kopijas failu", + "maintenance_upload_backup_error": "Nevarēja augÅĄupielādēt rezerves kopiju, vai tas ir .sql/.sql.gz fails?", "manage_concurrency": "VienlaicÄĢgas darbÄĢbas pārvaldÄĢba", + "manage_concurrency_description": "Pāriet uz uzdevumu lapu, lai pārvaldÄĢtu uzdevumu vienlaicÄĢgu darbÄĢbu", "manage_log_settings": "ÅŊurnāla iestatÄĢjumu pārvaldÄĢba", "map_dark_style": "TumÅĄais stils", "map_enable_description": "Iespējot kartes funkcijas", @@ -183,6 +211,7 @@ "map_settings": "Karte", "map_settings_description": "Kartes iestatÄĢjumu pārvaldÄĢba", "map_style_description": "URL uz style.json kartes tēmu", + "memory_cleanup_job": "Atmiņu tÄĢrÄĢÅĄana", "memory_generate_job": "Atmiņu ÄŖenerÄ“ÅĄana", "metadata_extraction_job": "Metadatu iegÅĢÅĄana", "metadata_extraction_job_description": "IegÅĢt metadatu informāciju no katra faila, piemēram, GPS, sejas un izÅĄÄˇirtspēju", @@ -235,6 +264,9 @@ "oauth_button_text": "Pogas teksts", "oauth_client_secret_description": "NepiecieÅĄams, ja OAuth pakalpojuma sniedzējs neatbalsta PKCE (Proof Key for Code Exchange)", "oauth_enable_description": "Pieslēgties ar OAuth", + "oauth_mobile_redirect_uri": "Mobilās pāradresÄ“ÅĄanas URI", + "oauth_mobile_redirect_uri_override": "Mobilās pāradresÄ“ÅĄanas URI pārrakstÄĢÅĄana", + "oauth_mobile_redirect_uri_override_description": "Jāiespējo, ja OAuth pakalpojuma sniedzējs nepieÄŧauj mobilo URI, piemēram, \"{callback}\"", "oauth_role_claim": "Lomas pieteikums", "oauth_role_claim_description": "Automātiski pieÅĄÄˇirt administratora piekÄŧuvi, pamatojoties uz ÅĄÄĢs prasÄĢbas klātbÅĢtni. PrasÄĢba var bÅĢt vai nu \"user\", vai \"admin\".", "oauth_settings": "OAuth", @@ -245,11 +277,14 @@ "oauth_storage_quota_default": "Noklusējuma krātuves kvota (GiB)", "oauth_timeout": "PieprasÄĢjuma noildze", "oauth_timeout_description": "PieprasÄĢjumu laika limits milisekundēs", + "ocr_job_description": "Izmantot maÅĄÄĢnmācÄĢÅĄanos, lai atpazÄĢtu tekstu attēlos", "password_enable_description": "PieteikÅĄanās ar e-pasta adresi un paroli", "password_settings": "PieteikÅĄanās ar paroli", "password_settings_description": "PieteikÅĄanās ar paroli iestatÄĢjumu pārvaldÄĢba", "paths_validated_successfully": "Visi ceÄŧi veiksmÄĢgi pārbaudÄĢti", "person_cleanup_job": "Personu tÄĢrÄĢÅĄana", + "queue_details": "Vaicājuma dati", + "queues": "Uzdevumu rindas", "quota_size_gib": "Kvotas izmērs (GiB)", "refreshing_all_libraries": "Atsvaidzina visas bibliotēkas", "registration": "Administratora reÄŖistrācija", @@ -339,6 +374,9 @@ "admin_password": "Administratora parole", "administration": "AdministrÄ“ÅĄana", "advanced": "Papildu", + "advanced_settings_clear_image_cache": "NotÄĢrÄĢt attēlu keÅĄatmiņu", + "advanced_settings_clear_image_cache_error": "Neizdevās notÄĢrÄĢt attēlu keÅĄatmiņu", + "advanced_settings_clear_image_cache_success": "VeiksmÄĢgi notÄĢrÄĢti {size}", "advanced_settings_log_level_title": "ÅŊurnalÄ“ÅĄanas lÄĢmenis: {level}", "advanced_settings_prefer_remote_subtitle": "DaŞās ierÄĢcēs sÄĢktēli no ierÄĢces atmiņas ielādējas Äŧoti lēni. Aktivizējiet ÅĄo iestatÄĢjumu, lai tā vietā ielādētu attālus attēlus.", "advanced_settings_prefer_remote_title": "Dot priekÅĄroku attāliem attēliem", @@ -351,6 +389,7 @@ "age_months": "Vecums {months, plural, zero {# mēneÅĄu} one {# mēnesis} other {# mēneÅĄi}}", "age_year_months": "Vecums 1 gads, {months, plural, zero {# mēneÅĄu} one {# mēnesis} other {# mēneÅĄi}}", "age_years": "{years, plural, zero {# gadu} one {# gads} other {# gadi}}", + "album": "Albums", "album_added": "Albums pievienots", "album_added_notification_setting_description": "Saņemt e-pasta paziņojumu, kad tevi pievieno kopÄĢgam albumam", "album_cover_updated": "Albuma vāciÅ†ÅĄ atjaunināts", @@ -362,8 +401,10 @@ "album_leave": "Pamest albumu?", "album_name": "Albuma nosaukums", "album_remove_user": "Noņemt lietotāju?", + "album_selected": "Albums izvēlēts", "album_summary": "Albuma kopsavilkums", "album_updated": "Albums atjaunināts", + "album_upload_assets": "AugÅĄupielādē failus no sava datora un pievieno tos albumam", "album_user_left": "Pameta {album}", "album_user_removed": "Noņēma {user}", "album_viewer_appbar_delete_confirm": "Vai tieÅĄÄm vēlaties dzēst ÅĄo albumu no sava konta?", @@ -382,12 +423,16 @@ "all": "Visi", "all_albums": "Visi albumi", "all_people": "Visas personas", + "all_photos": "Visas fotogrāfijas", "all_videos": "Visi video", "allow_dark_mode": "AtÄŧaut tumÅĄo reÅžÄĢmu", "allow_edits": "AtÄŧaut laboÅĄanu", "allow_public_user_to_download": "AtÄŧaut lejupielādēt publiskiem lietotājiem", "allow_public_user_to_upload": "AtÄŧaut augÅĄupielādēt publiskiem lietotājiem", "alt_text_qr_code": "QR koda attēls", + "always_keep": "Vienmēr paturēt", + "always_keep_photos_hint": "Vietas atbrÄĢvoÅĄanas funkcija paturēs visas fotogrāfijas ÅĄajā ierÄĢcē.", + "always_keep_videos_hint": "Vietas atbrÄĢvoÅĄanas funkcija paturēs visus video ÅĄajā ierÄĢcē.", "anti_clockwise": "Pretēji pulksteņrādÄĢtāja virzienam", "api_key": "API atslēga", "api_key_description": "Å ÄĢ vērtÄĢba tiks parādÄĢta tikai vienu reizi. Nokopējiet to pirms loga aizvērÅĄanas.", @@ -408,6 +453,7 @@ "archive_size": "ArhÄĢva izmērs", "archived": "Arhivēts", "are_these_the_same_person": "Vai ÅĄÄĢ ir tā pati persona?", + "array_field_not_fully_supported": "MasÄĢva lauki prasa manuālu JSON rediÄŖÄ“ÅĄanu", "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", @@ -562,8 +608,16 @@ "charging": "Lādē", "charging_requirement_mobile_backup": "Fona dublÄ“ÅĄanai nepiecieÅĄams, lai ierÄĢce tiktu lādēta", "check_corrupt_asset_backup_button": "Veikt pārbaudi", + "checksum": "Kontrolsumma", "choose_matching_people_to_merge": "Izvēlies atbilstoÅĄas personas apvienoÅĄanai", "city": "Pilsēta", + "cleanup_confirm_prompt_title": "Dzēst no ÅĄÄĢs ierÄĢces?", + "cleanup_deleted_assets": "Pārvietoja {count} failus uz ierÄĢces atkritni", + "cleanup_deleting": "Pārvieto uz atkritni...", + "cleanup_found_assets_with_size": "Atrada {count} dublētus failus ({size})", + "cleanup_icloud_shared_albums_excluded": "SkenÄ“ÅĄanā netiek iekÄŧauti iCloud kopÄĢgotie albumi", + "cleanup_preview_title": "DzÄ“ÅĄamie faili ({count})", + "cleanup_trash_hint": "Lai pilnÄĢbā atbrÄĢvotu uzglabÄÅĄanas vietu, atveriet sistēmas galerijas lietotni un iztukÅĄojiet atkritni", "clear": "NotÄĢrÄĢt", "clear_all": "NotÄĢrÄĢt visu", "clear_all_recent_searches": "NotÄĢrÄĢt visas pēdējās meklÄ“ÅĄanas", @@ -575,6 +629,8 @@ "client_cert_import": "Importēt", "client_cert_import_success_msg": "Klienta sertifikāts ir importēts", "client_cert_invalid_msg": "NederÄĢgs sertifikāta fails vai nepareiza parole", + "client_cert_password_message": "Ievadi ÅĄÄĢ sertifikāta paroli", + "client_cert_password_title": "Sertifikāta parole", "client_cert_remove_msg": "Klienta sertifikāts ir noņemts", "client_cert_subtitle": "Atbalsta tikai PKCS12 (.p12, .pfx) formātu. Sertifikātu importÄ“ÅĄana/noņemÅĄana ir pieejama tikai pirms pieslēgÅĄanās", "client_cert_title": "SSL klienta sertifikāts [EKSPERIMENTĀLS]", @@ -584,6 +640,7 @@ "collapse_all": "SakÄŧaut visu", "color": "Krāsa", "color_theme": "Krāsu tēma", + "command": "Komanda", "comment_deleted": "Komentārs dzēsts", "comment_options": "Komentāru iespējas", "comments_and_likes": "Komentāri un tÄĢkÅĄÄˇi", @@ -614,6 +671,7 @@ "create_album": "Izveidot albumu", "create_album_page_untitled": "Bez nosaukuma", "create_api_key": "Izveidot API atslēgu", + "create_first_workflow": "Izveidot pirmo darba plÅĢsmu", "create_library": "Izveidot bibliotēku", "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopÄĢgoÅĄanas saiti", @@ -624,14 +682,20 @@ "create_shared_album_page_share_add_assets": "PIEVIENOT AKTÄĒVUS", "create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle", "create_user": "Izveidot lietotāju", + "create_workflow": "Izveidot darba plÅĢsmu", "created_at": "Izveidots", "crop": "Apcirpt", + "crop_aspect_ratio_original": "OriÄŖināls", "curated_object_page_title": "Lietas", "current_pin_code": "EsoÅĄais PIN kods", "current_server_address": "PaÅĄreizējā servera adrese", + "custom_date": "Pielāgots datums", "custom_locale": "Pielāgota lokalizācija", "custom_locale_description": "Formatēt datumus un skaitÄŧus atbilstoÅĄi valodai un reÄŖionam", "custom_url": "Pielāgots URL", + "cutoff_date_description": "Paturēt fotoattēlus no pēdējāâ€Ļ", + "cutoff_day": "{count, plural, one {dienas} other {dienām}}", + "cutoff_year": "{count, plural, one {gada} other {gadiem}}", "daily_title_text_date_year": "E, MMM dd, gggg", "dark_theme": "Pārslēgt tumÅĄo tēmu", "date_after": "Datums pēc", @@ -693,6 +757,7 @@ "download_include_embedded_motion_videos": "Iegultie videoklipi", "download_include_embedded_motion_videos_description": "IekÄŧaut video, kas iebÅĢvēti kustÄĢgos fotoattēlos, kā atseviÅĄÄˇu failu", "download_notfound": "Lejupielāde nav atrasta", + "download_original": "Lejupielādēt oriÄŖinālu", "download_paused": "Lejupielāde nopauzēta", "download_settings": "Lejupielāde", "download_settings_description": "Ar failu lejupielādi saistÄĢto iestatÄĢjumu pārvaldÄĢba", @@ -702,6 +767,7 @@ "download_waiting_to_retry": "Gaida, lai mēĪinātu atkārtoti", "downloading": "Lejupielādē", "downloading_asset_filename": "Lejupielādē failu {filename}", + "downloading_from_icloud": "Lejupielādē no iCloud", "downloading_media": "Lejupielādē failu", "duplicates": "Dublikāti", "duplicates_description": "Atrisini katru grupu, norādot, kuri no tiem ir dublikāti", @@ -725,10 +791,18 @@ "edit_people": "Labot profilu", "edit_title": "Labot nosaukumu", "edit_user": "Labot lietotāju", + "edit_workflow": "Labot darba plÅĢsmu", "editor": "Redaktors", "editor_close_without_save_prompt": "Izmaiņas netiks saglabātas", "editor_close_without_save_title": "Aizvērt redaktoru?", - "editor_crop_tool_h2_rotation": "Rotācija", + "editor_discard_edits_confirm": "Atmest labojumus", + "editor_discard_edits_title": "Atmest labojumus?", + "editor_flip_horizontal": "Apvērst horizontāli", + "editor_flip_vertical": "Apvērst vertikāli", + "editor_orientation": "Orientācija", + "editor_reset_all_changes": "Atcelt izmaiņas", + "editor_rotate_left": "Pagriezt par 90° pretēji pulksteņrādÄĢtāja virzienam", + "editor_rotate_right": "Pagriezt par 90° pulksteņrādÄĢtāja virzienā", "email": "E-pasts", "email_notifications": "E-pasta paziņojumi", "empty_folder": "Å ÄĢ mape ir tukÅĄa", @@ -742,10 +816,13 @@ "enter_your_pin_code_subtitle": "Ievadi savu PIN kodu, lai piekÄŧÅĢtu slēgtajai mapei", "error": "KÄŧÅĢda", "error_change_sort_album": "Neizdevās nomainÄĢt albuma kārtoÅĄanas secÄĢbu", + "error_loading_albums": "KÄŧÅĢda, ielādējot albumus", "error_loading_image": "KÄŧÅĢda, ielādējot attēlu", "error_loading_partners": "KÄŧÅĢda, ielādējot partnerus: {error}", + "error_retrieving_asset_information": "KÄŧÅĢda, iegÅĢstot informāciju par resursu", "error_saving_image": "KÄŧÅĢda: {error}", "error_title": "KÄŧÅĢda - kaut kas nogāja greizi", + "error_while_navigating": "KÄŧÅĢda, navigējot uz resursu", "errors": { "cannot_navigate_next_asset": "Nevar pāriet uz nākamo resursu", "cannot_navigate_previous_asset": "Nevar pāriet uz iepriekÅĄÄ“jo resursu", @@ -792,6 +869,7 @@ "unable_to_trash_asset": "Neizdevās pārvietot failu uz atkritni", "unable_to_update_album_cover": "Nevar atjaunināt albuma vāciņu" }, + "errors_text": "KÄŧÅĢdas", "exif": "Exif", "exif_bottom_sheet_description": "Pievienot Aprakstu...", "exif_bottom_sheet_details": "INFORMĀCIJA", @@ -818,6 +896,7 @@ "external_network_sheet_info": "Kad nav pieejams izvēlētais Wi-Fi tÄĢkls, aplikācija pieslēgsies serverim lietojot pirmo strādājoÅĄo URL no saraksta, sākot ar augÅĄÄ“jo", "face_unassigned": "NepieÅĄÄˇirts", "failed": "Neizdevās", + "failed_count": "Neizdevās: {count}", "failed_to_authenticate": "Neizdevās autentificēties", "failed_to_load_assets": "Neizdevās ielādēt failus", "failed_to_load_folder": "Neizdevās ielādēt mapi", @@ -826,19 +905,25 @@ "favorites_page_no_favorites": "Nav atrasti iecienÄĢtākie faili", "features_in_development": "Izstrādes stadijā esoÅĄas funkcijas", "features_setting_description": "Lietotnes funkciju pārvaldÄĢba", - "file_name": "Faila nosaukums", "file_name_or_extension": "Faila nosaukums vai paplaÅĄinājums", + "file_name_text": "Faila nosaukums", + "file_name_with_value": "Faila nosaukums: {file_name}", "filename": "Faila nosaukums", "filetype": "Faila tips", "filter": "Filtrēt", "filter_people": "Filtrēt personas", "filter_places": "Filtrēt vietas", + "filters": "Filtri", "first": "Pirmais", "folder": "Mape", "folder_not_found": "Mape nav atrasta", "folders": "Mapes", "forgot_pin_code_question": "Aizmirsi savu PIN?", "forward": "Uz priekÅĄu", + "free_up_space": "AtbrÄĢvot vietu", + "free_up_space_description": "Pārvietot dublētās fotogrāfijas un videoklipus uz ierÄĢces atkritni, lai atbrÄĢvotu vietu. Failu kopijas serverÄĢ paliks droÅĄÄĢbā.", + "free_up_space_settings_subtitle": "AtbrÄĢvot ierÄĢces atmiņu", + "full_path": "Pilnais ceÄŧÅĄ: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Å ÄĢ funkcija darbojas, lejupielādējot ārējos resursus no Google.", "get_help": "Saņemt palÄĢdzÄĢbu", @@ -866,11 +951,14 @@ "header_settings_header_name_input": "Galvenes lauks", "header_settings_header_value_input": "Galvenes vērtÄĢba", "headers_settings_tile_title": "Pielāgotas starpniekservera galvenes", + "height": "Augstums", "hide_all_people": "Paslēpt visas personas", "hide_gallery": "Paslēpt galeriju", "hide_named_person": "Paslēpt personu {name}", "hide_password": "Paslēpt paroli", "hide_person": "Paslēpt personu", + "hide_schema": "Paslēpt shēmu", + "hide_text_recognition": "Slēpt teksta atpazÄĢÅĄanu", "hide_unnamed_people": "Paslēpt nenosauktas personas", "home_page_add_to_album_conflicts": "Pievienoja {added} failus albumam {album}. {failed} faili jau ir albumā.", "home_page_add_to_album_err_local": "Albumiem vēl nevar pievienot lokālos failus, izlaiÅž", @@ -924,9 +1012,17 @@ "ios_debug_info_processing_ran_at": "Apstrāde notika {dateTime}", "items_count": "{count, plural, one {# vienums} other {# vienumi}}", "jobs": "Uzdevumi", + "json_editor": "JSON redaktors", + "json_error": "JSON kÄŧÅĢda", "keep": "Paturēt", + "keep_albums": "Paturēt albumus", + "keep_albums_count": "Patur {count} {count, plural, one {albumu} other {albumus}}", "keep_all": "Paturēt visus", + "keep_description": "Izvēlies, kas paliks tavā ierÄĢcē, atbrÄĢvojot vietu.", + "keep_on_device": "Paturēt ierÄĢcē", + "keep_on_device_hint": "Izvēlies failus, kurus paturēt ÅĄajā ierÄĢcē", "keep_this_delete_others": "Paturēt ÅĄo, dzēst citus", + "keeping": "Patur: {items}", "keyboard_shortcuts": "TastatÅĢras saÄĢsnes", "language": "Valoda", "language_no_results_subtitle": "MēĪini pielāgot meklÄ“ÅĄanas terminu", @@ -944,6 +1040,8 @@ "let_others_respond": "Äģaut citiem atbildēt", "level": "LÄĢmenis", "library": "Bibliotēka", + "library_add_folder": "Pievienot mapi", + "library_edit_folder": "Labot mapi", "library_options": "Bibliotēkas opcijas", "library_page_device_albums": "Albumi ierÄĢcē", "library_page_new_album": "Jauns albums", @@ -999,6 +1097,23 @@ "longitude": "Äĸeogrāfiskais garums", "look": "Izskats", "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaÄŧu skatÄĢtājā.", + "maintenance_action_restore": "Atjauno datubāzi", + "maintenance_description": "Immich ir pārslēgts apkopes reÅžÄĢmā.", + "maintenance_restore_from_backup": "Atjaunot no rezerves kopijas", + "maintenance_restore_library": "Atjaunot tavu bibliotēku", + "maintenance_restore_library_confirm": "Ja tas izskatās pareizi, turpini rezerves kopijas atjaunoÅĄanu!", + "maintenance_restore_library_description": "Atjauno datubāzi", + "maintenance_restore_library_folder_has_files": "{folder} satur {count} mapes", + "maintenance_restore_library_folder_no_files": "{folder} trÅĢkst faili!", + "maintenance_restore_library_folder_pass": "lasāms un rakstāms", + "maintenance_restore_library_folder_read_fail": "nav nolasāms", + "maintenance_restore_library_folder_write_fail": "nav rakstāms", + "maintenance_restore_library_loading": "Ielādē integritātes pārbaudes un heiristikuâ€Ļ", + "maintenance_task_backup": "Veido esoÅĄÄs datubāzes rezerves kopijuâ€Ļ", + "maintenance_task_migrations": "Veic datubāzes migrācijuâ€Ļ", + "maintenance_task_restore": "Atjauno izvēlēto rezerves kopijuâ€Ļ", + "maintenance_task_rollback": "AtjaunoÅĄana neizdevās, atgrieÅžas pie atjaunoÅĄanas punktaâ€Ļ", + "maintenance_title": "ÄĒslaicÄĢgi nav pieejams", "make": "RaÅžotājs", "manage_geolocation": "PārvaldÄĢt atraÅĄanās vietu", "manage_shared_links": "KopÄĢgoto saiÅĄu pārvaldÄĢba", @@ -1031,6 +1146,8 @@ "map_settings_only_show_favorites": "RādÄĢt tikai izlasi", "map_settings_theme_settings": "Kartes Dizains", "map_zoom_to_see_photos": "Attāliniet, lai redzētu fotoattēlus", + "mark_all_as_read": "AtzÄĢmēt visus kā lasÄĢtus", + "marked_all_as_read": "Visi atzÄĢmēti kā lasÄĢti", "matches": "AtbilstÄĢbas", "media_type": "Faila veids", "memories": "Atmiņas", @@ -1048,6 +1165,8 @@ "minimize": "Minimizēt", "minute": "MinÅĢte", "minutes": "MinÅĢtes", + "mirror_horizontal": "Horizontāli", + "mirror_vertical": "Vertikāli", "missing": "TrÅĢkstoÅĄie", "mobile_app": "Mobilā lietotne", "mobile_app_download_onboarding_note": "Lejupielādē papildinoÅĄo mobilo lietotni, izmantojot ÅĄÄdas izvēles iespējas", @@ -1056,9 +1175,13 @@ "monthly_title_text_date_format": "MMMM g", "more": "Vairāk", "move": "Pārvietot", + "move_down": "Pārvietot lejup", "move_off_locked_folder": "Izņemt no slēgtās mapes", + "move_to": "Pārvietot uz", + "move_to_device_trash": "Pārvietot uz ierÄĢces atkritni", "move_to_locked_folder": "Pārvietot uz slēgto mapi", "move_to_locked_folder_confirmation": "Å ÄĢs fotogrāfijas un video tiks izņemti no visiem albumiem un bÅĢs apskatāmi tikai no slēgtās mapes", + "move_up": "Pārvietot augÅĄup", "moved_to_archive": "Pārvietoja {count, plural, one {# failu} other {# failus}} uz arhÄĢvu", "moved_to_library": "Pārvietoja {count, plural, one {# failu} other {# failus}} uz bibliotēku", "moved_to_trash": "Pārvietots uz atkritni", @@ -1082,11 +1205,13 @@ "new_person": "Jauna persona", "new_pin_code": "Jaunais PIN kods", "new_timeline": "Jaunā laikjosla", + "new_update": "Pieejams atjauninājums", "new_user_created": "Izveidots jauns lietotājs", "new_version_available": "PIEEJAMA JAUNA VERSIJA", "next": "Nākamais", "next_memory": "Nākamā atmiņa", "no": "Nē", + "no_albums_found": "Nav atrasts neviens albums", "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.", @@ -1096,6 +1221,7 @@ "no_cast_devices_found": "Nav atrasta neviena pārraides ierÄĢce", "no_checksum_local": "Nav pieejama kontrolsumma - nevar iegÅĢt lokālos failus", "no_checksum_remote": "Nav pieejama kontrolsumma - nevar iegÅĢt attālo failu", + "no_configuration_needed": "Konfigurācija nav nepiecieÅĄama", "no_duplicates_found": "Dublikāti netika atrasti.", "no_exif_info_available": "Nav pieejama exif informācija", "no_explore_results_message": "AugÅĄupielādē vairāk fotogrāfiju, lai iepazÄĢtu savu kolekciju.", @@ -1105,10 +1231,10 @@ "no_places": "Nav atraÅĄanās vietu", "no_results": "Nav rezultātu", "no_results_description": "IzmēĪiniet sinonÄĢmu vai vispārÄĢgāku atslēgvārdu", + "not_allowed": "Nav atÄŧauts", "not_available": "Nav pieejams", "not_in_any_album": "Nav nevienā albumā", "not_selected": "Nav izvēlēts", - "note_apply_storage_label_to_previously_uploaded assets": "PiezÄĢme: Lai piemērotu glabātuves nosaukumu iepriekÅĄ augÅĄupielādētiem failiem, izpildiet", "notes": "PiezÄĢmes", "nothing_here_yet": "Å eit vēl nekā nav", "notification_permission_dialog_content": "Lai iespējotu paziņojumus, atveriet IestatÄĢjumi un atlasiet AtÄŧaut.", @@ -1147,6 +1273,7 @@ "other_variables": "Citi mainÄĢgie", "owned": "ÄĒpaÅĄumā", "owner": "ÄĒpaÅĄnieks", + "page": "Lapa", "partner": "Partneris", "partner_can_access": "{partner} var piekÄŧÅĢt", "partner_can_access_location": "Fotogrāfiju uzņemÅĄanas vieta", @@ -1180,10 +1307,15 @@ "permission_onboarding_permission_limited": "AtÄŧauja ierobeÅžota. Lai atÄŧautu Immich dublÄ“ÅĄanu un varētu pārvaldÄĢt visu galeriju kolekciju, sadaÄŧā IestatÄĢjumi pieÅĄÄˇiriet fotoattēlu un video atÄŧaujas.", "permission_onboarding_request": "Immich nepiecieÅĄama atÄŧauja skatÄĢt jÅĢsu fotoattēlus un videoklipus.", "person": "Persona", + "person_recognized": "Persona atpazÄĢta", + "person_selected": "Persona izvēlēta", "photos": "Fotoattēli", "photos_and_videos": "Fotogrāfijas un video", "photos_from_previous_years": "Fotogrāfijas no iepriekÅĄÄ“jiem gadiem", + "photos_only": "Tikai fotogrāfijas", "pick_a_location": "Izvēlies atraÅĄanās vietu", + "pick_custom_range": "Pielāgots intervāls", + "pick_date_range": "Izvēlies datumu intervālu", "pin_verification": "PIN koda pārbaude", "place": "AtraÅĄanās vieta", "places": "Vietas", @@ -1240,6 +1372,7 @@ "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Servera produkta atslēgu pārvalda administrators", "queue_status": "Ierindo {count}/{total}", + "rate_asset": "Novērtēt failu", "rating_clear": "Noņemt vērtējumu", "rating_description": "RādÄĢt EXIF vērtējumu informācijas panelÄĢ", "reaction_options": "Reakcijas iespējas", @@ -1307,9 +1440,11 @@ "saved_settings": "IestatÄĢjumi saglabāti", "say_something": "Teikt kaut ko", "scaffold_body_error_occurred": "Radās kÄŧÅĢda", + "scan": "Skenēt", "scan_all_libraries": "Skenēt visas bibliotēkas", "scan_library": "Skenēt", "scan_settings": "SkenÄ“ÅĄanas iestatÄĢjumi", + "scanning": "Skenē", "scanning_for_album": "Skenē albumu...", "search": "Meklēt", "search_albums": "Meklēt albumus", @@ -1331,6 +1466,7 @@ "search_filter_location_title": "Izvēlies atraÅĄanās vietu", "search_filter_media_type": "Multivides veids", "search_filter_media_type_title": "Izvēlies multivides veidu", + "search_filter_star_rating": "ZvaigznÄĢÅĄu vērtējums", "search_for_existing_person": "Meklēt esoÅĄu personu", "search_no_people": "Nav personu", "search_no_people_named": "Nav personas ar vārdu \"{name}\"", @@ -1357,14 +1493,18 @@ "searching_locales": "Meklē lokalizācijas...", "second": "Sekunde", "see_all_people": "SkatÄĢt visas personas", + "select_album": "Izvēlies albumu", "select_album_cover": "Izvēlieties albuma vāciņu", - "select_all_duplicates": "AtlasÄĢt visus dublikātus", + "select_albums": "Izvēlies albumus", + "select_all_duplicates": "AtlasÄĢt visus paturÄ“ÅĄanai", "select_avatar_color": "Izvēlies avatāra krāsu", "select_face": "Izvēlies seju", "select_from_computer": "Izvēlēties no datora", "select_keep_all": "AtzÄĢmēt visus paturÄ“ÅĄanai", "select_library_owner": "Izvēlies bibliotēkas ÄĢpaÅĄnieku", "select_new_face": "Izvēlies jaunu seju", + "select_people": "Izvēlies personas", + "select_person": "Izvēlies personu", "select_photos": "Fotoattēlu Izvēle", "select_trash_all": "AtzÄĢmēt visus dzÄ“ÅĄanai", "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", @@ -1374,6 +1514,8 @@ "server_info_box_server_url": "Servera URL", "server_online": "Serveris tieÅĄsaistē", "server_privacy": "Servera privātums", + "server_restarting_description": "Å ÄĢ lapa pēc brÄĢÅža tiks atjaunināta.", + "server_restarting_title": "Serveris tiek pārstartēts", "server_stats": "Servera statistika", "server_update_available": "Pieejams servera atjauninājums", "server_version": "Servera versija", @@ -1472,11 +1614,13 @@ "show_password": "ParādÄĢt paroli", "show_person_options": "RādÄĢt personas opcijas", "show_progress_bar": "RādÄĢt progresa joslu", + "show_schema": "RādÄĢt shēmu", "show_search_options": "RādÄĢt meklÄ“ÅĄanas opcijas", "show_shared_links": "RādÄĢt kopÄĢgotās saites", "show_slideshow_transition": "RādÄĢt slÄĢdrādes pāreju", "show_supporter_badge": "AtbalstÄĢtāja nozÄĢmÄĢte", "show_supporter_badge_description": "RādÄĢt atbalstÄĢtāja nozÄĢmÄĢti", + "show_text_recognition": "RādÄĢt teksta atpazÄĢÅĄanu", "show_text_search_menu": "RādÄĢt teksta meklÄ“ÅĄanas izvēlni", "shuffle": "Jaukta", "sidebar": "Sānu josla", @@ -1487,6 +1631,8 @@ "skip_to_content": "Pāriet uz saturu", "skip_to_folders": "Pāriet uz mapēm", "slideshow": "SlÄĢdrāde", + "slideshow_repeat": "Atkārtot slÄĢdrādi", + "slideshow_repeat_description": "Beidzoties slÄĢdrādei, atgriezties pie tās sākuma", "slideshow_settings": "SlÄĢdrādes iestatÄĢjumi", "sort_albums_by": "Kārtot albumus pēc...", "sort_created": "Izveides datums", @@ -1499,6 +1645,7 @@ "sort_title": "Nosaukums", "source": "Pirmkods", "stack": "Apvienot kaudzē", + "stack_duplicates": "Apvienot dublikātus kaudzē", "start": "Sākt", "start_date": "Sākuma datums", "start_date_before_end_date": "Sākuma datumam jābÅĢt pirms beigu datuma", @@ -1520,6 +1667,7 @@ "sync_local": "Sinhronizēt lokāli", "sync_status": "Sinhronizācijas statuss", "sync_status_subtitle": "SkatÄĢt un pārvaldÄĢt sinhronizācijas sistēmu", + "text_recognition": "Teksta atpazÄĢÅĄana", "theme": "Dizains", "theme_setting_asset_list_storage_indicator_title": "RādÄĢt krātuves indikatoru uz attēliem reÅžga skatā", "theme_setting_asset_list_tiles_per_row_title": "Failu skaits rindā ({count})", @@ -1534,6 +1682,7 @@ "theme_setting_theme_subtitle": "Izvēlieties programmas dizaina iestatÄĢjumu", "theme_setting_three_stage_loading_subtitle": "TrÄĢspakāpju ielāde var palielināt ielādÄ“ÅĄanas veiktspēju, bet izraisa ievērojami lielāku tÄĢkla noslodzi", "theme_setting_three_stage_loading_title": "Iespējot trÄĢspakāpju ielādi", + "then": "Tad", "they_will_be_merged_together": "Tās tiks apvienotas", "third_party_resources": "TreÅĄo puÅĄu resursi", "timeline": "Laika skala", @@ -1543,6 +1692,7 @@ "to_favorite": "Pievienot izlasei", "to_trash": "Pārvietot uz atkritni", "toggle_settings": "Pārslēgt iestatÄĢjumus", + "toggle_theme_description": "Pārslēgt motÄĢvu", "total": "Kopā", "total_usage": "Kopējais lietojums", "trash": "Atkritne", @@ -1560,6 +1710,9 @@ "trash_page_select_assets_btn": "AtlasÄĢt aktÄĢvus", "trash_page_title": "Atkritne ({count})", "trashed_items_will_be_permanently_deleted_after": "Faili no atkritnes tiks neatgriezeniski dzēsti pēc {days, plural, one {# dienas} other {# dienām}}.", + "trigger_asset_uploaded": "Fails augÅĄupielādēts", + "trigger_description": "Notikums, kas uzsāk darba plÅĢsmu", + "trigger_person_recognized": "Persona atpazÄĢta", "troubleshoot": "Problēmu novērÅĄana", "type": "Veids", "unable_to_change_pin_code": "Neizdevās nomainÄĢt PIN kodu", @@ -1569,17 +1722,20 @@ "unhide_person": "Atcelt personas slēpÅĄanu", "unknown": "Nezināms", "unknown_country": "Nezināma Valsts", + "unknown_date": "Nezināms datums", "unknown_year": "Nezināms gads", "unlimited": "NeierobeÅžots", "unnamed_album": "Albums bez nosaukuma", "unsaved_change": "Nesaglabāta izmaiņa", "unselect_all": "Atcelt visu atlasi", + "unselect_all_duplicates": "AtlasÄĢt visus dzÄ“ÅĄanai", "unstack": "At-Stekot", + "unsupported_field_type": "NesatbalstÄĢts lauka tips", + "untitled_workflow": "Nenosaukta darba plÅĢsma", "update_location_action_prompt": "NorādÄĢt {count} izvēlēto failu atraÅĄanās vietu kā:", "updated_at": "Atjaunināts", "updated_password": "Parole ir atjaunināta", "upload": "AugÅĄupielādēt", - "upload_action_prompt": "{count} ierindoti augÅĄupielādei", "upload_dialog_info": "Vai vēlaties veikt izvēlētā(-o) aktÄĢva(-u) dublējumu uz servera?", "upload_dialog_title": "AugÅĄupielādēt AktÄĢvu", "upload_finished": "AugÅĄupielāde pabeigta", @@ -1608,6 +1764,7 @@ "users": "Lietotāji", "utilities": "RÄĢki", "validate": "PārbaudÄĢt", + "validation_error": "Pārbaudes kÄŧÅĢda", "variables": "MainÄĢgie", "version": "Versija", "version_announcement_closing": "Tavs draugs, Alekss", @@ -1617,10 +1774,12 @@ "video": "Videoklips", "video_hover_setting_description": "Atskaņot video sÄĢktēlu, kad peles kursors atrodas virs objekta. Pat ja funkcija ir atspējota, atskaņoÅĄanu var sākt, uzvirzot kursoru uz atskaņoÅĄanas ikonas.", "videos": "Videoklipi", + "videos_only": "Tikai video", "view": "ApskatÄĢt", "view_album": "SkatÄĢt Albumu", "view_all": "ApskatÄĢt visu", "view_all_users": "SkatÄĢt visus lietotājus", + "view_asset_owners": "SkatÄĢt failu ÄĢpaÅĄniekus", "view_details": "ApskatÄĢt informāciju", "view_in_timeline": "SkatÄĢt laika skalā", "view_link": "SkatÄĢt saiti", @@ -1635,16 +1794,31 @@ "viewer_remove_from_stack": "Noņemt no Steka", "viewer_stack_use_as_main_asset": "Izmantot kā Galveno AktÄĢvu", "viewer_unstack": "At-Stekot", + "visual": "Vizuāli", + "visual_builder": "Vizuālais veidotājs", "waiting": "Gaida", + "waiting_count": "Gaida: {count}", "warning": "BrÄĢdinājums", "week": "NedēÄŧa", "welcome": "Laipni lÅĢgti", "welcome_to_immich": "Laipni lÅĢgti Immich", + "width": "Platums", "wifi_name": "Wi-Fi nosaukums", + "workflow_deleted": "Darba plÅĢsma dzēsta", + "workflow_description": "Darba plÅĢsmas apraksts", + "workflow_info": "Darba plÅĢsmas informācija", + "workflow_json": "Darba plÅĢsmas JSON", + "workflow_json_help": "Labot darba plÅĢsmas konfigurāciju JSON formātā. Izmaiņas tiks sinhronizētas ar vizuālo veidotāju.", + "workflow_name": "Darba plÅĢsmas nosaukums", + "workflow_summary": "Darba plÅĢsmas kopsavilkums", + "workflow_update_success": "Darba plÅĢsma veiksmÄĢgi izmainÄĢta", + "workflow_updated": "Darba plÅĢsma izmainÄĢta", + "workflows": "Darba plÅĢsmas", "wrong_pin_code": "Nepareizs PIN kods", "year": "Gads", "years_ago": "Pirms {years, plural, one {# gada} other {# gadiem}}", "yes": "Jā", "your_wifi_name": "Tava Wi-Fi nosaukums", + "zero_to_clear_rating": "nospied 0, lai notÄĢrÄĢtu faila vērtējumu", "zoom_image": "Pietuvināt attēlu" } diff --git a/i18n/mk.json b/i18n/mk.json index 507beb15b2..b12fe7ca15 100644 --- a/i18n/mk.json +++ b/i18n/mk.json @@ -195,7 +195,6 @@ "edit_people": "ĐŖŅ€Đĩди ĐģŅƒŅ“Đĩ", "edit_user": "ĐŖŅ€Đĩди ĐēĐžŅ€Đ¸ŅĐŊиĐē", "editor": "ĐŖŅ€ĐĩĐ´ŅƒĐ˛Đ°Ņ‡", - "editor_crop_tool_h2_rotation": "Đ ĐžŅ‚Đ°Ņ†Đ¸Ņ˜Đ°", "email": "Е-ĐŋĐžŅˆŅ‚Đ°", "empty_trash": "Đ˜ŅĐŋŅ€Đ°ĐˇĐŊи ĐŗĐž Ņ“ŅƒĐąŅ€ĐĩŅ‚Đž", "enable": "ОвозĐŧĐžĐļи", @@ -217,7 +216,6 @@ "favorite": "ОĐŧиĐģĐĩĐŊĐž", "favorites": "ОĐŧиĐģĐĩĐŊи", "features": "Đ¤ŅƒĐŊĐēии", - "file_name": "ИĐŧĐĩ ĐŊа Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа", "filename": "ИĐŧĐĩ ĐŊа Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа", "filetype": "ĐĸиĐŋ ĐŊа Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа", "filter_people": "ФиĐģŅ‚Ņ€Đ¸Ņ€Đ°Ņ˜ ĐģŅƒŅ“Đĩ", diff --git a/i18n/ml.json b/i18n/ml.json index 09877f7f53..9e93ce9fc6 100644 --- a/i18n/ml.json +++ b/i18n/ml.json @@ -1,12 +1,14 @@ { - "about": "ā´•āĩā´ąā´ŋⴚāĩā´šāĩ", + "about": "ⴈ ā´†ā´Ēāĩā´Ēā´ŋā´¨āĩ† ā´•āĩā´ąā´ŋⴚāĩā´šāĩ", "account": "ā´…ā´•āĩā´•āĩ—ā´Ŗāĩā´Ÿāĩ", "account_settings": "ā´…ā´•āĩā´•āĩ—ā´Ŗāĩā´Ÿāĩ ā´•āĩā´°ā´Žāĩ€ā´•ā´°ā´Ŗā´™āĩā´™āĩž", "acknowledge": "ā´…ā´‚ā´—āĩ€ā´•ā´°ā´ŋā´•āĩā´•āĩā´•", "action": "ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚", "action_common_update": "ā´…ā´Ēāĩā´Ąāĩ‡ā´ąāĩā´ąāĩ ⴚāĩ†ā´¯āĩā´¯āĩā´•", + "action_description": "ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´¤āĩā´¤ ā´ĩā´¸āĩā´¤āĩā´•āĩā´•ā´ŗā´ŋāĩŊ ⴍⴟā´Ēāĩā´Ēā´ŋā´˛ā´žā´•āĩā´•āĩ‡ā´Ŗāĩā´Ÿ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´™āĩā´™āĩž", "actions": "ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´ŋā´•āĩž", "active": "ⴏⴜāĩ€ā´ĩā´‚", + "active_count": "ⴏⴜāĩ€ā´ĩā´Žā´žā´¯ā´¤āĩ: {count}", "activity": "ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚", "activity_changed": "ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚ {enabled, select, true {ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´•āĩā´ˇā´Žā´Žā´žā´•āĩā´•ā´ŋ} other {ā´¨ā´ŋāĩŧⴜāĩā´œāĩ€ā´ĩā´Žā´žā´•āĩā´•ā´ŋ}}", "add": "ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", @@ -14,9 +16,14 @@ "add_a_location": "ā´¸āĩā´Ĩā´žā´¨ā´‚ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_a_name": "ā´Ēāĩ‡ā´°āĩ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_a_title": "ā´ļāĩ€āĩŧⴎⴕⴂ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", + "add_action": "ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", + "add_action_description": "ⴍⴟā´Ēāĩā´Ēā´ŋā´˛ā´žā´•āĩā´•āĩ‡ā´Ŗāĩā´Ÿ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´‚ ⴚāĩ‡āĩŧā´•āĩā´•ā´žāĩģ ā´‡ā´ĩā´ŋⴟāĩ† ā´•āĩā´˛ā´ŋā´•āĩā´•āĩ ⴚāĩ†ā´¯āĩā´¯āĩā´•", + "add_assets": "ā´ĩā´¸āĩā´¤āĩā´•āĩā´•āĩž ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_birthday": "ⴜⴍāĩā´Žā´Ļā´ŋⴍⴂ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_endpoint": "ā´Žāĩģā´Ąāĩâ€Œā´Ēāĩ‹ā´¯ā´ŋā´¨āĩā´ąāĩ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_exclusion_pattern": "ā´’ā´´ā´ŋā´ĩā´žā´•āĩā´•āĩŊ ā´Ēā´žā´ąāĩā´ąāĩ‡āĩē ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", + "add_filter": "ā´Ģā´ŋāĩŊā´ąāĩā´ąāĩŧ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", + "add_filter_description": "ā´’ā´°āĩ ā´Ģā´ŋāĩŊⴟāĩā´Ÿāĩŧ ⴚāĩ‡āĩŧā´•āĩā´•ā´žāĩģ ā´•āĩā´˛ā´ŋā´•āĩā´•āĩ ⴚāĩ†ā´¯āĩā´¯āĩā´•", "add_location": "ā´¸āĩā´Ĩā´žā´¨ā´‚ ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_more_users": "ā´•āĩ‚ā´Ÿāĩā´¤āĩŊ ā´‰ā´Ēā´¯āĩ‹ā´•āĩā´¤ā´žā´•āĩā´•ā´ŗāĩ† ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", "add_partner": "ā´Ēā´™āĩā´•ā´žā´ŗā´ŋā´¯āĩ† ⴚāĩ‡āĩŧā´•āĩā´•āĩā´•", @@ -914,8 +921,6 @@ "editor": "ā´Žā´Ąā´ŋā´ąāĩā´ąāĩŧ", "editor_close_without_save_prompt": "ā´Žā´žā´ąāĩā´ąā´™āĩā´™āĩž ā´¸āĩ‡ā´ĩāĩ ⴚāĩ†ā´¯āĩā´¯ā´ŋā´˛āĩā´˛", "editor_close_without_save_title": "ā´Žā´Ąā´ŋā´ąāĩā´ąāĩŧ ā´…ā´Ÿā´¯āĩā´•āĩā´•ā´Ŗāĩ‹?", - "editor_crop_tool_h2_aspect_ratios": "ā´ĩāĩ€ā´•āĩā´ˇā´Ŗā´žā´¨āĩā´Ēā´žā´¤ā´‚", - "editor_crop_tool_h2_rotation": "ā´ąāĩŠā´Ÿāĩā´Ÿāĩ‡ā´ˇāĩģ", "email": "ā´‡ā´Žāĩ†ā´¯ā´ŋāĩŊ", "email_notifications": "ā´‡ā´Žāĩ†ā´¯ā´ŋāĩŊ ā´…ā´ąā´ŋā´¯ā´ŋā´Ēāĩā´Ēāĩā´•āĩž", "empty_folder": "ⴈ ā´Ģāĩ‹āĩžā´Ąāĩŧ ā´ļāĩ‚ā´¨āĩā´¯ā´Žā´žā´Ŗāĩ", @@ -1101,7 +1106,6 @@ "features": "ā´Ģāĩ€ā´šāĩā´šā´ąāĩā´•āĩž", "features_in_development": "ā´ĩā´ŋā´•ā´¸ā´ŋā´Ēāĩā´Ēā´ŋⴚāĩā´šāĩā´•āĩŠā´Ŗāĩā´Ÿā´ŋā´°ā´ŋā´•āĩā´•āĩā´¨āĩā´¨ ā´Ģāĩ€ā´šāĩā´šā´ąāĩā´•āĩž", "features_setting_description": "ā´†ā´Ēāĩā´Ēāĩ ā´Ģāĩ€ā´šāĩā´šā´ąāĩā´•āĩž ā´•āĩˆā´•ā´žā´°āĩā´¯ā´‚ ⴚāĩ†ā´¯āĩā´¯āĩā´•", - "file_name": "ā´Ģⴝⴞā´ŋā´¨āĩā´ąāĩ† ā´Ēāĩ‡ā´°āĩ", "file_name_or_extension": "ā´Ģⴝⴞā´ŋā´¨āĩā´ąāĩ† ā´Ēāĩ‡ā´°āĩ ā´…ā´˛āĩā´˛āĩ†ā´™āĩā´•ā´ŋāĩŊ ā´Žā´•āĩā´¸āĩā´ąāĩā´ąāĩģā´ˇāĩģ", "file_size": "ā´Ģā´¯āĩŊ ā´ĩā´˛ā´ŋā´Ēāĩā´Ēā´‚", "filename": "ā´Ģā´¯āĩŊā´¨ā´žā´Žā´‚", @@ -1464,7 +1468,6 @@ "not_available": "ⴞⴭāĩā´¯ā´Žā´˛āĩā´˛", "not_in_any_album": "ā´’ā´°āĩ ā´†āĩŊā´Ŧā´¤āĩā´¤ā´ŋā´˛āĩā´Žā´ŋā´˛āĩā´˛", "not_selected": "ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´¤āĩā´¤ā´ŋⴟāĩā´Ÿā´ŋā´˛āĩā´˛", - "note_apply_storage_label_to_previously_uploaded assets": "ā´•āĩā´ąā´ŋā´Ēāĩā´Ēāĩ: ā´Žāĩā´Žāĩā´Ēāĩ ā´…ā´Ēāĩâ€Œā´˛āĩ‹ā´Ąāĩ ⴚāĩ†ā´¯āĩā´¤ ā´…ā´¸ā´ąāĩā´ąāĩā´•ā´ŗā´ŋāĩŊ ā´¸āĩā´ąāĩā´ąāĩ‹ā´ąāĩ‡ā´œāĩ ā´˛āĩ‡ā´ŦāĩŊ ā´Ēāĩā´°ā´¯āĩ‹ā´—ā´ŋā´•āĩā´•ā´žāĩģ, ⴇⴤāĩ ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´ŋā´Ēāĩā´Ēā´ŋā´•āĩā´•āĩā´•", "notes": "ā´•āĩā´ąā´ŋā´Ēāĩā´Ēāĩā´•āĩž", "nothing_here_yet": "ā´‡ā´ĩā´ŋⴟāĩ† ⴇⴤāĩā´ĩā´°āĩ† ā´’ā´¨āĩā´¨āĩā´Žā´ŋā´˛āĩā´˛", "notification_permission_dialog_content": "ā´…ā´ąā´ŋā´¯ā´ŋā´Ēāĩā´Ēāĩā´•āĩž ā´Ēāĩā´°ā´ĩāĩŧā´¤āĩā´¤ā´¨ā´•āĩā´ˇā´Žā´Žā´žā´•āĩā´•ā´žāĩģ, ā´•āĩā´°ā´Žāĩ€ā´•ā´°ā´Ŗā´™āĩā´™ā´ŗā´ŋā´˛āĩ‡ā´•āĩā´•āĩ ā´Ēāĩ‹ā´¯ā´ŋ 'ā´…ā´¨āĩā´ĩā´Ļā´ŋā´•āĩā´•āĩā´•' ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´•āĩā´•āĩā´•.", @@ -1659,7 +1662,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# ā´…ā´¸ā´ąāĩā´ąāĩ} other {# ā´…ā´¸ā´ąāĩā´ąāĩā´•āĩž}} ā´’ā´°āĩ ā´Ēāĩā´¤ā´ŋā´¯ ā´ĩāĩā´¯ā´•āĩā´¤ā´ŋā´•āĩā´•āĩ ā´ĩāĩ€ā´Ŗāĩā´Ÿāĩā´‚ ā´¨āĩŊā´•ā´ŋ", "reassing_hint": "ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´¤āĩā´¤ ā´…ā´¸ā´ąāĩā´ąāĩā´•āĩž ā´¨ā´ŋā´˛ā´ĩā´ŋā´˛āĩā´ŗāĩā´ŗ ā´’ā´°āĩ ā´ĩāĩā´¯ā´•āĩā´¤ā´ŋā´•āĩā´•āĩ ā´¨āĩŊā´•āĩā´•", "recent": "ā´¸ā´Žāĩ€ā´Ēā´•ā´žā´˛ā´‚", - "recent-albums": "ā´¸ā´Žāĩ€ā´Ēā´•ā´žā´˛ ā´†āĩŊā´Ŧā´™āĩā´™āĩž", + "recent_albums": "ā´¸ā´Žāĩ€ā´Ēā´•ā´žā´˛ ā´†āĩŊā´Ŧā´™āĩā´™āĩž", "recent_searches": "ā´¸ā´Žāĩ€ā´Ēā´•ā´žā´˛ ā´¤ā´ŋⴰⴝⴞāĩā´•āĩž", "recently_added": "ā´…ā´Ÿāĩā´¤āĩā´¤ā´ŋⴟāĩ† ⴚāĩ‡āĩŧā´¤āĩā´¤ā´¤āĩ", "recently_added_page_title": "ā´…ā´Ÿāĩā´¤āĩā´¤ā´ŋⴟāĩ† ⴚāĩ‡āĩŧā´¤āĩā´¤ā´¤āĩ", @@ -2122,7 +2125,6 @@ "updated_at": "ā´…ā´Ēāĩā´Ąāĩ‡ā´ąāĩā´ąāĩ ⴚāĩ†ā´¯āĩā´¤ā´¤āĩ", "updated_password": "ā´Ēā´žā´¸āĩâ€Œā´ĩāĩ‡ā´Ąāĩ ā´…ā´Ēāĩā´Ąāĩ‡ā´ąāĩā´ąāĩ ⴚāĩ†ā´¯āĩā´¤āĩ", "upload": "ā´…ā´Ēāĩâ€Œā´˛āĩ‹ā´Ąāĩ", - "upload_action_prompt": "{count} ā´Žā´Ŗāĩā´Ŗā´‚ ā´…ā´Ēāĩâ€Œā´˛āĩ‹ā´Ąā´ŋā´¨ā´žā´¯ā´ŋ ā´•āĩā´¯āĩ‚ā´ĩā´ŋāĩŊ ⴚāĩ‡āĩŧā´¤āĩā´¤āĩ", "upload_concurrency": "ā´…ā´Ēāĩâ€Œā´˛āĩ‹ā´Ąāĩ ā´•āĩ‹āĩēā´•ā´ąāĩģā´¸ā´ŋ", "upload_details": "ā´…ā´Ēāĩâ€Œā´˛āĩ‹ā´Ąāĩ ā´ĩā´ŋā´ļā´Ļā´žā´‚ā´ļā´™āĩā´™āĩž", "upload_dialog_info": "ā´¤ā´ŋā´°ā´žāĩā´žāĩ†ā´Ÿāĩā´¤āĩā´¤ ā´…ā´¸ā´ąāĩā´ąāĩ(ā´•āĩž) ā´¸āĩ†āĩŧā´ĩā´ąā´ŋā´˛āĩ‡ā´•āĩā´•āĩ ā´Ŧā´žā´•āĩā´•ā´Ēāĩā´Ēāĩ ⴚāĩ†ā´¯āĩā´¯ā´Ŗāĩ‹?", @@ -2198,7 +2200,6 @@ "welcome": "ā´¸āĩā´ĩā´žā´—ā´¤ā´‚", "welcome_to_immich": "Immich-ā´˛āĩ‡ā´•āĩā´•āĩ ā´¸āĩā´ĩā´žā´—ā´¤ā´‚", "wifi_name": "ā´ĩāĩˆ-ā´Ģāĩˆā´¯āĩā´Ÿāĩ† ā´Ēāĩ‡ā´°āĩ", - "workflow": "ā´ĩāĩŧā´•āĩā´•āĩā´Ģāĩā´˛āĩ‹ (Workflow)", "wrong_pin_code": "ā´¤āĩ†ā´ąāĩā´ąā´žā´¯ ā´Ēā´ŋāĩģ ā´•āĩ‹ā´Ąāĩ", "year": "ā´ĩāĩŧⴎⴂ", "years_ago": "{years, plural, one {# ā´ĩāĩŧⴎⴂ} other {# ā´ĩāĩŧⴎⴙāĩā´™āĩž}} ā´Žāĩā´Žāĩā´Ēāĩ", diff --git a/i18n/mr.json b/i18n/mr.json index 4b143e2488..f31b080e37 100644 --- a/i18n/mr.json +++ b/i18n/mr.json @@ -7,6 +7,7 @@ "action_common_update": "⤅ā¤ĻāĨā¤¯ā¤¯ā¤žā¤ĩ⤤", "actions": "⤕āĨƒā¤¤āĨā¤¯āĨ‡", "active": "⤏⤕āĨā¤°ā¤ŋ⤝", + "active_count": "⤕āĨƒā¤¤āĨ€: {count}", "activity": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ", "activity_changed": "⤗⤤ā¤ŋā¤ĩā¤ŋ⤧ā¤ŋ {enabled, select, true {enabled} other {disabled}}", "add": "⤜āĨ‹ā¤Ąā¤ž", @@ -14,6 +15,7 @@ "add_a_location": "ā¤ā¤• ⤏āĨā¤Ĩ⤺ ā¤Ÿā¤žā¤•ā¤ž", "add_a_name": "ā¤¨ā¤žā¤ĩ ā¤Ÿā¤žā¤•ā¤ž", "add_a_title": "ā¤ļāĨ€ā¤°āĨā¤ˇā¤• ā¤Ÿā¤žā¤•ā¤ž", + "add_action": "⤕āĨƒā¤¤āĨ€ ⤜āĨ‹ā¤Ąā¤ž", "add_birthday": "⤜⤍āĨā¤Žā¤Ļā¤ŋā¤ĩ⤏ ⤍āĨ‹ā¤‚ā¤Ļā¤ĩā¤ž", "add_endpoint": "ā¤ā¤‚ā¤Ąā¤ĒāĨ‰ā¤‡ā¤‚ā¤Ÿ ⤜āĨ‹ā¤Ąā¤ž", "add_exclusion_pattern": "⤅ā¤Ēā¤ĩā¤žā¤Ļ ā¤¨ā¤ŽāĨā¤¨ā¤ž ⤜āĨ‹ā¤Ąā¤ž", @@ -914,8 +916,6 @@ "editor": "ā¤ā¤Ąā¤ŋ⤟⤰", "editor_close_without_save_prompt": "ā¤Ŧā¤Ļ⤞ ⤜⤤⤍ ā¤šāĨ‹ā¤Ŗā¤žā¤° ā¤¨ā¤žā¤šāĨ€", "editor_close_without_save_title": "ā¤ā¤Ąā¤ŋ⤟⤰ ā¤Ŧ⤂ā¤Ļ ā¤•ā¤°ā¤žā¤¯ā¤šā¤ž ā¤•ā¤ž?", - "editor_crop_tool_h2_aspect_ratios": "⤅⤍āĨā¤Ēā¤žā¤¤ ā¤•ā¤°ā¤ž", - "editor_crop_tool_h2_rotation": "ā¤Ģā¤ŋ⤰ā¤ĩā¤ž", "email": "ā¤ˆā¤ŽāĨ‡ā¤˛", "email_notifications": "ā¤ˆā¤ŽāĨ‡ā¤˛ ⤏āĨ‚ā¤šā¤¨ā¤ž", "empty_folder": "ā¤šā¤ž ā¤ĢāĨ‹ā¤˛āĨā¤Ąā¤° ⤰ā¤ŋā¤•ā¤žā¤Žā¤ž ā¤†ā¤šāĨ‡", @@ -1101,7 +1101,6 @@ "features": "ā¤ĩāĨˆā¤ļā¤ŋ⤎āĨā¤ŸāĨā¤¯āĨ‡", "features_in_development": "ā¤ĩā¤ŋā¤•ā¤žā¤¸ā¤žā¤§āĨ€ā¤¨ ā¤ĩāĨˆā¤ļā¤ŋ⤎āĨā¤ŸāĨā¤¯āĨ‡", "features_setting_description": "⤅āĨ…ā¤Ē⤚āĨ€ ā¤ĩāĨˆā¤ļā¤ŋ⤎āĨā¤ŸāĨā¤¯āĨ‡ ā¤ĩāĨā¤¯ā¤ĩ⤏āĨā¤Ĩā¤žā¤Ēā¤ŋ⤤ ā¤•ā¤°ā¤ž", - "file_name": "ā¤Ģā¤žā¤ˆā¤˛ ā¤¨ā¤žā¤ĩ", "file_name_or_extension": "ā¤Ģā¤žā¤ˆā¤˛ ā¤¨ā¤žā¤ĩ ⤕ā¤ŋ⤂ā¤ĩā¤ž ā¤ā¤•āĨā¤¸āĨā¤ŸāĨ‡ā¤‚ā¤ļ⤍", "file_size": "ā¤Ģā¤žā¤‡ā¤˛ ā¤¸ā¤žā¤‡ā¤œā¤ŧ", "filename": "ā¤Ģā¤žā¤‡ā¤˛ā¤¨ā¤žā¤ĩ", @@ -1464,7 +1463,6 @@ "not_available": "⤉ā¤Ē⤞ā¤ŦāĨā¤§ ā¤¨ā¤žā¤šāĨ€", "not_in_any_album": "⤕āĨ‹ā¤Ŗā¤¤āĨā¤¯ā¤žā¤šāĨ€ ⤅⤞āĨā¤Ŧā¤Žā¤Žā¤§āĨā¤¯āĨ‡ ā¤¨ā¤žā¤šāĨ€", "not_selected": "⤍ā¤ŋā¤ĩā¤Ąā¤˛āĨ‡ā¤˛āĨ‡ ā¤¨ā¤žā¤šāĨ€", - "note_apply_storage_label_to_previously_uploaded assets": "⤍āĨ‹ā¤Ÿ: ⤆⤧āĨ€ ⤅ā¤Ē⤞āĨ‹ā¤Ą ⤕āĨ‡ā¤˛āĨ‡ā¤˛āĨā¤¯ā¤ž ⤅āĨ…⤏āĨ‡ā¤ŸāĨā¤¸ā¤ĩ⤰ ⤏āĨā¤ŸāĨ‹ā¤°āĨ‡ā¤œ ⤞āĨ‡ā¤Ŧ⤞ ā¤˛ā¤žā¤—āĨ‚ ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ ā¤šā¤ž ⤆ā¤ĻāĨ‡ā¤ļ ā¤šā¤žā¤˛ā¤ĩā¤ž", "notes": "⤍āĨ‹ā¤ŸāĨā¤¸", "nothing_here_yet": "⤇ā¤ĨāĨ‡ ā¤…ā¤œāĨ‚⤍ ā¤•ā¤žā¤šāĨ€ ā¤¨ā¤žā¤šāĨ€", "notification_permission_dialog_content": "⤏āĨ‚ā¤šā¤¨ā¤ž ⤏⤕āĨā¤ˇā¤Ž ⤕⤰⤪āĨā¤¯ā¤žā¤¸ā¤žā¤ āĨ€ ⤏āĨ‡ā¤Ÿā¤ŋ⤂⤗āĨā¤œā¤Žā¤§āĨā¤¯āĨ‡ ā¤œā¤ž ⤆⤪ā¤ŋ ⤅⤍āĨā¤Žā¤¤āĨ€ ā¤ĻāĨā¤¯ā¤ž.", @@ -1659,7 +1657,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# ā¤†ā¤¯ā¤Ÿā¤Ž} other {# ā¤†ā¤¯ā¤Ÿā¤Ž}} ⤍ā¤ĩāĨā¤¯ā¤ž ā¤ĩāĨā¤¯ā¤•āĨā¤¤āĨ€ā¤•ā¤ĄāĨ‡ ā¤ĒāĨā¤¨āĨā¤šā¤ž ⤍ā¤ŋ⤝āĨā¤•āĨā¤¤ ⤕āĨ‡ā¤˛āĨ‡", "reassing_hint": "⤍ā¤ŋā¤ĩā¤Ąā¤˛āĨ‡ā¤˛āĨ‡ ā¤†ā¤¯ā¤Ÿā¤Ž ā¤ĩā¤ŋā¤ĻāĨā¤¯ā¤Žā¤žā¤¨ ā¤ĩāĨā¤¯ā¤•āĨā¤¤āĨ€ā¤•ā¤ĄāĨ‡ ⤍ā¤ŋ⤝āĨā¤•āĨā¤¤ ā¤•ā¤°ā¤ž", "recent": "⤅⤞āĨ€ā¤•ā¤ĄāĨ€ā¤˛", - "recent-albums": "⤅⤞āĨ€ā¤•ā¤ĄāĨ€ā¤˛ ⤅⤞āĨā¤Ŧā¤Ž", + "recent_albums": "⤅⤞āĨ€ā¤•ā¤ĄāĨ€ā¤˛ ⤅⤞āĨā¤Ŧā¤Ž", "recent_searches": "⤅⤞āĨ€ā¤•ā¤ĄāĨ€ā¤˛ ā¤ļāĨ‹ā¤§", "recently_added": "⤍āĨā¤•⤤āĨ‡ā¤š ⤜āĨ‹ā¤Ąā¤˛āĨ‡ā¤˛āĨ‡", "recently_added_page_title": "⤍āĨā¤•⤤āĨ‡ā¤š ⤜āĨ‹ā¤Ąā¤˛āĨ‡ā¤˛āĨ‡", @@ -2122,7 +2120,6 @@ "updated_at": "⤅ā¤ĻāĨā¤¯ā¤¯ā¤žā¤ĩ⤤ ⤕āĨ‡ā¤˛āĨ‡", "updated_password": "ā¤Ē⤰ā¤ĩ⤞āĨ€ā¤šā¤ž ā¤ļā¤ŦāĨā¤Ļ ⤅ā¤ĻāĨā¤¯ā¤¯ā¤žā¤ĩ⤤ ⤕āĨ‡ā¤˛ā¤ž", "upload": "⤅ā¤Ē⤞āĨ‹ā¤Ą", - "upload_action_prompt": "⤅ā¤Ē⤞āĨ‹ā¤Ąā¤¸ā¤žā¤ āĨ€ {count} ā¤°ā¤žā¤‚ā¤—āĨ‡ā¤¤", "upload_concurrency": "⤅ā¤Ē⤞āĨ‹ā¤Ą ā¤¸ā¤Žā¤žā¤‚ā¤¤ā¤°ā¤¤ā¤ž", "upload_details": "⤅ā¤Ē⤞āĨ‹ā¤Ą ⤤ā¤Ēā¤ļāĨ€ā¤˛", "upload_dialog_info": "⤍ā¤ŋā¤ĩā¤Ąā¤˛āĨ‡ā¤˛āĨ‡ ā¤†ā¤¯ā¤Ÿā¤Ž ⤏⤰āĨā¤ĩāĨā¤šā¤°ā¤ĩ⤰ ā¤ŦāĨ…⤕⤅ā¤Ē ā¤•ā¤°ā¤žā¤¯ā¤šāĨ‡ ā¤•ā¤ž?", @@ -2198,7 +2195,6 @@ "welcome": "⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤†ā¤šāĨ‡", "welcome_to_immich": "Immich ā¤Žā¤§āĨā¤¯āĨ‡ ⤆ā¤Ē⤞āĨ‡ ⤏āĨā¤ĩā¤žā¤—ā¤¤ ā¤†ā¤šāĨ‡", "wifi_name": "ā¤ĩā¤žā¤¯-ā¤Ģā¤žā¤¯ā¤šāĨ‡ ā¤¨ā¤žā¤ĩ", - "workflow": "ā¤•ā¤žā¤°āĨā¤¯ā¤ĒāĨā¤°ā¤ĩā¤žā¤š", "wrong_pin_code": "⤅ā¤ĩāĨˆā¤§ ā¤Ēā¤ŋ⤍ ⤕āĨ‹ā¤Ą", "year": "ā¤ĩ⤰āĨā¤ˇ", "years_ago": "{years, plural, one {# ā¤ĩ⤰āĨā¤ˇā¤žā¤ĒāĨ‚⤰āĨā¤ĩāĨ€} other {# ā¤ĩ⤰āĨā¤ˇā¤žā¤‚ā¤ĒāĨ‚⤰āĨā¤ĩāĨ€}}", diff --git a/i18n/ms.json b/i18n/ms.json index 8af92f9c69..cbec851018 100644 --- a/i18n/ms.json +++ b/i18n/ms.json @@ -7,6 +7,7 @@ "action_common_update": "Kemaskini", "actions": "Tindakan", "active": "Aktif", + "active_count": "Aktif: {count}", "activity": "Aktiviti", "activity_changed": "Aktiviti {enabled, select, true {enabled} other {disabled}}", "add": "Tambah", @@ -50,9 +51,13 @@ "backup_database": "Buat Salinan Pangkalan Data", "backup_database_enable_description": "Dayakan salinan pangkalan data", "backup_keep_last_amount": "Jumlah salinan pangkalan data sebelumnya untuk disimpan", - "backup_onboarding_1_description": "salinan luar tapak di awan atau di lokasi fizikal lain", + "backup_onboarding_1_description": "salinan luar tapak di awan atau di lokasi fizikal lain.", "backup_onboarding_2_description": "salinan tempatan pada peranti yang berbeza. Ini termasuk fail utama dan sandaran fail tersebut secara setempat.", "backup_onboarding_3_description": "jumlah salinan data anda, termasuk fail asal. Ini termasuk 1 salinan luar tapak dan 2 salinan tempatan.", + "backup_onboarding_description": "Strategi sandaran 3-2-1 disarankan untuk melindungi data anda. Anda perlu menyimpan salinan foto/video yang dimuat naik serta pangkalan data Immich bagi memastikan penyelesaian sandaran yang menyeluruh.", + "backup_onboarding_footer": "Untuk maklumat lanjut tentang membuat sandaran Immich, sila rujuk dokumentasi.", + "backup_onboarding_parts_title": "Sandaran 3-2-1 merangkumi:", + "backup_onboarding_title": "Sandaran", "backup_settings": "Tetapan Salinan Pangkalan Data", "backup_settings_description": "Urus tetapan salinan pangkalan data.", "cleared_jobs": "Kerja telah dibersihkan untuk: {job}", @@ -63,6 +68,7 @@ "confirm_reprocess_all_faces": "Adakah anda pasti mahu memproses semula semua wajah? Ini juga akan membersihkan orang bernama.", "confirm_user_password_reset": "Adakah anda pasti mahu menetapkan semula kata laluan {user}?", "confirm_user_pin_code_reset": "Adakah anda pasti untuk mengubah kod PIN {user}'s ?", + "copy_config_to_clipboard_description": "Salin konfigurasi sistem semasa sebagai objek JSON ke papan klip", "create_job": "Cipta tugas", "cron_expression": "Ungkapan cron", "cron_expression_description": "Tetapkan selang imbasan menggunakan format cron. Untuk maklumat lanjut, sila rujuk ke sebagai contoh Crontab Guru", @@ -70,6 +76,8 @@ "disable_login": "Lumpuhkan fungsi log masuk", "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mengesan imej yang serupa. Bergantung pada Carian Pintar", "exclusion_pattern_description": "Corak pengecualian membolehkan anda mengabaikan fail dan folder semasa mengimbas pustaka anda. Ini berguna jika anda mempunyai folder yang mengandungi fail yang anda tidak mahu import, seperti fail RAW.", + "export_config_as_json_description": "Muat turun konfigurasi sistem semasa sebagai fail JSON", + "external_libraries_page_description": "Halaman pustaka luaran admin", "face_detection": "Pengesanan wajah", "face_detection_description": "Kesan wajah dalam aset menggunakan pembelajaran mesin. Untuk video, hanya lakaran kecil dipertimbangkan. \"Segar Semula\" memproses semula semua aset. \"Tetapkan Semula\" juga mengosongkan semua data wajah semasa. \"Hilang\" baris gilir aset yang belum diproses lagi. Wajah yang dikesan akan beratur untuk Pengecaman Wajah selepas Pengesanan Wajah selesai, menghimpunkannya kepada orang sedia ada atau baharu.", "facial_recognition_job_description": "Kumpulan wajah yang dikesan ke dalam orang. Langkah ini dijalankan selepas Pengesanan Wajah selesai. \"Tetapkan semula\" mengelompokkan semula semua wajah. \"Hilang\" jalankan proses pada wajah yang tidak mempunyai orang yang ditetapkan.", @@ -97,6 +105,7 @@ "image_thumbnail_description": "Lakaran kecil dengan metadata yang dilucutkan, digunakan semasa melihat kumpulan foto seperti garis masa utama", "image_thumbnail_quality_description": "Kualiti lakaran kenit daripada 1-100. Lebih tinggi adalah lebih baik, tetapi menghasilkan fail yang lebih besar dan boleh mengurangkan responsif apl.", "image_thumbnail_title": "Tetapan Lakaran Kenit", + "import_config_from_json_description": "Import konfigurasi sistem melalui muat naik fail JSON", "job_concurrency": "Konkurensi {job}", "job_created": "Tugas yang dicipta", "job_not_concurrency_safe": "Konkurensi tugas ini tidak selamat.", @@ -104,16 +113,22 @@ "job_settings_description": "Urus konkurensi tugas", "jobs_delayed": "{jobCount, plural, other {# tertangguh}}", "jobs_failed": "{jobCount, plural, other {# gagal}}", + "jobs_over_time": "Tugas berjadual dari semasa ke semasa", "library_created": "Pustaka dicipta: {library}", "library_deleted": "Pustaka dipadamkan", + "library_details": "Butiran pustaka", + "library_folder_description": "Tentukan folder untuk diimport. Folder ini, termasuk subfolder, akan diimbas untuk imej dan video.", + "library_remove_exclusion_pattern_prompt": "Adakah anda pasti mahu membuang corak pengecualian ini?", + "library_remove_folder_prompt": "Adakah anda pasti mahu membuang folder import ini?", "library_scanning": "Pengimbasan Berkala", "library_scanning_description": "Konfigurasikan pengimbasan perpustakaan berkala", "library_scanning_enable_description": "Dayakan pengimbasan perpustakaan berkala", "library_settings": "Perpustakaan Luaran", "library_settings_description": "Urus tetapan perpustakaan luaran", "library_tasks_description": "Imbas pustaka luaran untuk aset yang baru dan/atau telah diubah", + "library_updated": "Pustaka dikemas kini", "library_watching_enable_description": "Perhatikan perpustakaan luaran untuk perubahan fail", - "library_watching_settings": "Perhati perpustakaan (EKSPERIMEN)", + "library_watching_settings": "Perhati perpustakaan [EKSPERIMEN]", "library_watching_settings_description": "Perhati fail yang diubah secara automatik", "logging_enable_description": "Dayakan pengelogan", "logging_level_description": "Apabila didayakan, tahap log yang hendak digunakan.", @@ -140,6 +155,11 @@ "machine_learning_min_detection_score_description": "Skor keyakinan minimum untuk wajah dikesan dari 0-1. Nilai yang lebih rendah akan mengesan lebih banyak muka tetapi mungkin menghasilkan positif palsu.", "machine_learning_min_recognized_faces": "Minimum mengenali wajah", "machine_learning_min_recognized_faces_description": "Bilangan minima wajah yang dikenali untuk seseorang dicipta. Peningkatan ini menjadikan Pengecaman Wajah lebih tepat atas kos meningkatkan peluang wajah tidak diberikan kepada seseorang.", + "machine_learning_ocr_enabled": "Dayakan OCR", + "machine_learning_ocr_enabled_description": "Jika dinyahdayakan, imej tidak akan melalui pengecaman teks.", + "machine_learning_ocr_max_resolution": "Resolusi Maksimum", + "machine_learning_ocr_max_resolution_description": "Pratonton yang melebihi resolusi ini akan diubah saiz sambil mengekalkan nisbah aspek. Nilai yang lebih tinggi adalah lebih tepat, tetapi mengambil masa pemprosesan yang lebih lama dan menggunakan lebih banyak memori.", + "machine_learning_ocr_model": "Model OCR", "machine_learning_settings": "Tetapan Pembelajaran Mesin", "machine_learning_settings_description": "Urus ciri dan tetapan pembelajaran mesin", "machine_learning_smart_search": "Carian Pintar", @@ -147,6 +167,10 @@ "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 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.", + "maintenance_settings": "Penyelenggaraan", + "maintenance_settings_description": "Letak Immich ke dalam mod penyelenggaraan", + "maintenance_start": "Mulakan mod penyelenggaraan", + "maintenance_start_error": "Gagal mulakan mod penyelenggaraan.", "manage_concurrency": "Urus Concurrency", "manage_log_settings": "Urus tetapan log", "map_dark_style": "Tema gelap", @@ -319,7 +343,7 @@ "transcoding_max_b_frames": "Bingkai-B maksimum", "transcoding_max_b_frames_description": "Nilai yang lebih tinggi meningkatkan kecekapan mampatan, tetapi memperlahankan pengekodan. Mungkin tidak serasi dengan pecutan perkakasan pada peranti lama. 0 melumpuhkan bingkai B, manakala -1 menetapkan nilai ini secara automatik.", "transcoding_max_bitrate": "Kadar bit maksimum", - "transcoding_max_bitrate_description": "Menetapkan kadar bit maksima boleh menjadikan saiz fail lebih boleh diramal dengan kekurangan yang kecil kepada kualiti. Pada 720p, nilai biasa ialah 2600 kbit/s untuk VP9 atau HEVC, atau 4500 kbit/s untuk H.264. Dilumpuhkan jika ditetapkan kepada 0.", + "transcoding_max_bitrate_description": "Menetapkan bitrate maksimum boleh menjadikan saiz fail lebih mudah diramal dengan sedikit pengorbanan kualiti. Pada 720p, nilai biasa ialah 2600 kbit/s untuk VP9 atau HEVC, atau 4500 kbit/s untuk H.264. Dimatikan jika ditetapkan kepada 0. Apabila tiada unit dinyatakan, k (untuk kbit/s) diandaikan; oleh itu 5000, 5000k dan 5M (untuk Mbit/s) adalah setara.", "transcoding_max_keyframe_interval": "Selangan keyframe maksimum", "transcoding_max_keyframe_interval_description": "Menetapkan jarak bingkai maksimum antara keyframes. Nilai yang lebih rendah memburukkan kecekapan mampatan, tetapi menambah baik masa carian dan mungkin meningkatkan kualiti dalam adegan dengan pergerakan pantas. 0 menetapkan nilai ini secara automatik.", "transcoding_optimal_description": "Video yang lebih tinggi daripada resolusi sasaran atau tidak dalam format yang diterima", @@ -337,7 +361,7 @@ "transcoding_target_resolution": "Resolusi sasaran", "transcoding_target_resolution_description": "Peleraian yang lebih tinggi boleh mengekalkan lebih banyak butiran tetapi mengambil masa lebih lama untuk mengekod, mempunyai saiz fail yang lebih besar dan boleh mengurangkan responsif app.", "transcoding_temporal_aq": "AQ sementara", - "transcoding_temporal_aq_description": "Terpakai hanya untuk NVEC. Meningkatkan kualiti adegan yang berperinci tinggi dan berpunya rendah gerakan. Mungkin tidak serasi dengan peranti lama.", + "transcoding_temporal_aq_description": "Terpakai hanya untuk NVEC. Temporal Adaptive Quantization meningkatkan kualiti adegan yang berperinci tinggi dan berpunya rendah gerakan. Mungkin tidak serasi dengan peranti lama.", "transcoding_threads": "Benang", "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", @@ -384,9 +408,9 @@ "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_proxy_headers_title": "Pengepala Proksi khusus [EKSPERIMEN]", "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_self_signed_ssl_title": "Benarkan sijil SSL self-signed [EKSPERIMEN]", "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", @@ -394,6 +418,20 @@ "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}}", + "album_added": "Album telah ditambah", + "album_added_notification_setting_description": "Terima pemberitahuan e-mel apabila anda ditambah ke album perkongsian", + "album_cover_updated": "Album dikemas kini", + "album_leave": "Tinggalkan album?", + "album_leave_confirmation": "Adakah anda pasti mahu meninggalkan {album} ini?", + "album_name": "Nama Album", + "album_remove_user": "Buang pengguna?", + "album_remove_user_confirmation": "Adakah anda pasti mahu membuang {user}?", + "album_share_no_users": "Nampaknya anda telah berkongsi album ini dengan semua pengguna atau anda tidak mempunyai mana-mana pengguna untuk dikongsi.", + "album_updated": "Album dikemas kini", + "album_updated_setting_description": "Terima pemberitahuan e-mel apabila album perkongsian mempunyai aset baharu", + "album_user_left": "Kiri {album}", + "album_user_removed": "{user} telah dibuang", + "album_with_link_access": "Benarkan sesiapa yang mempunyai pautan melihat foto dan individu dalam album ini.", "deduplication_criteria_1": "Saiz imej dalam bait", "deduplication_criteria_2": "Kiraan data EXIF", "deduplication_info": "Maklumat Pendeduplikasian", @@ -444,9 +482,14 @@ "total": "Jumlah", "user_usage_stats": "Statistik penggunaan akaun", "user_usage_stats_description": "Papar statistik penggunaan akaun", + "width": "Lebar", + "wifi_name": "Nama Wi-Fi", + "wrong_pin_code": "Kod PIN salah", "year": "Tahun", + "years_ago": "{years, plural, other {# tahun lalu}}", "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" + "zoom_image": "Zum Gambar", + "zoom_to_bounds": "Zum ke sempadan" } diff --git a/i18n/nb_NO.json b/i18n/nb_NO.json index 0c566fbfa7..564c3c0de9 100644 --- a/i18n/nb_NO.json +++ b/i18n/nb_NO.json @@ -5,6 +5,7 @@ "acknowledge": "Bekreft", "action": "Handling", "action_common_update": "Oppdater", + "action_description": "Ett sett med handlinger som skal utføres pÃĨ de filtrerede objekter", "actions": "Handlinger", "active": "Aktiv", "active_count": "Aktiv: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Legg til sted", "add_a_name": "Legg til navn", "add_a_title": "Legg til tittel", + "add_action": "Legg til hendelse", + "add_action_description": "Trykk for ÃĨ legge til en hendelse ÃĨ utføre", + "add_assets": "Legg til objekter", "add_birthday": "Legg til bursdag", - "add_endpoint": "API endepunkt", + "add_endpoint": "Legg til endepunkt", "add_exclusion_pattern": "Legg til ekskluderingsmønster", + "add_filter": "Legg til filter", + "add_filter_description": "Trykk for ÃĨ legge til filter begrensning", "add_location": "Legg til sted", "add_more_users": "Legg til flere brukere", "add_partner": "Legg til partner", @@ -36,6 +42,7 @@ "add_to_shared_album": "Legg til delt album", "add_upload_to_stack": "Legg til opplasting i stakken", "add_url": "Legg til URL", + "add_workflow_step": "Trykk for ÃĨ legge til oppgave i arbeidsflyten", "added_to_archive": "Lagt til i arkivet", "added_to_favorites": "Lagt til favoritter", "added_to_favorites_count": "Lagt til {count, number} i favoritter", @@ -43,7 +50,7 @@ "add_exclusion_pattern_description": "Legg til ekskluderingsmønstre. Globbing med *, ** og ? støttes. For ÃĨ ignorere alle filer i en hvilken som helst mappe som heter \"Raw\", bruk \"**/Raw/**\". For ÃĨ ignorere alle filer som slutter pÃĨ \".tif\", bruk \"**/*.tif\". For ÃĨ ignorere en absolutt filplassering, bruk \"/filsti/til/ignorer/**\".", "admin_user": "Administrasjonsbruker", "asset_offline_description": "Dette eksterne bibliotekselementet finnes ikke lenger pÃĨ disk og har blitt flyttet til papirkurven. Hvis filen ble flyttet innad i biblioteket, se etter det tilsvarende elementet i tidslinjen din. For ÃĨ gjenopprette elementet, vennligst sørg for at filstien under er tilgjengelig for Immich og skann biblioteket.", - "authentication_settings": "Godkjenningsinnstillinger", + "authentication_settings": "Godkjenninger", "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.", "authentication_settings_reenable": "For ÃĨ aktivere pÃĨ nytt, bruk en Server Command.", @@ -58,7 +65,7 @@ "backup_onboarding_footer": "For mer informasjon om sikkerhetskopiering av Immich, se dokumentasjonen.", "backup_onboarding_parts_title": "En 3-2-1 sikkerhetskopi inkluderer:", "backup_onboarding_title": "Sikkerhetskopier", - "backup_settings": "Database-dump instillinger", + "backup_settings": "Database-dump", "backup_settings_description": "HÃĨndter innstillinger for database-dump.", "cleared_jobs": "Ryddet opp jobber for: {job}", "config_set_by_file": "Konfigurasjonen er for øyeblikket satt av en konfigurasjonsfil", @@ -79,8 +86,8 @@ "export_config_as_json_description": "Last ned nÃĨvÃĻrende systemkonfigurasjon som en JSON fil", "external_libraries_page_description": "Administrering for eksterne bibliotek", "face_detection": "Ansiktsgjenkjennelse", - "face_detection_description": "Finn ansikter i bilder ved hjelp av maskinlÃĻring. For videoer brukes bare miniatyrbildet. \"Alle\" gÃĨr gjennom alle bilder (igjen). \"Tilbakestill\" fjerner all gjeldende ansiktsdata. \"Manglende\" legger til filer som ikke har blitt behandlet enda i køen. Oppdagede ansikter vil blir sendt til ansiktsgjenkjenning, og koblet til eksisterende eller nye personer.", - "facial_recognition_job_description": "Kobler oppdagede ansikt til personer. Dette utføres etter at ansiktssøk er fullført. \"Tilbakestill\" (om-)grupperer alle ansikt pÃĨ nytt. \"Missing\" stiller opp ansikt som ikke har blitt tilordnet en person ennÃĨ.", + "face_detection_description": "Finn ansikter i bilder ved hjelp av maskinlÃĻring. For videoer brukes bare miniatyrbildet. \"Alle\" gÃĨr gjennom alle bilder (igjen). \"Tilbakestill\" fjerner all gjeldende ansiktsdata. \"Mangler\" legger til filer som ikke har blitt behandlet enda i køen. Oppdagede ansikter vil blir sendt til ansiktsgjenkjenning, og koblet til eksisterende eller nye personer.", + "facial_recognition_job_description": "Kobler oppdagede ansikt til personer. Dette utføres etter at ansiktssøk er fullført. \"Tilbakestill\" (om-)grupperer alle ansikt pÃĨ nytt. \"Mangler\" stiller opp ansikt som ikke har blitt tilordnet en person ennÃĨ.", "failed_job_command": "Kommandoen {command} feilet for jobb: {job}", "force_delete_user_warning": "ADVARSEL: Dette vil umiddelbart fjerne brukeren og alle data. Dette kan ikke angres, og filene kan ikke gjenopprettes.", "image_format": "Format", @@ -97,6 +104,8 @@ "image_preview_description": "Mellomstort bilde med strippet metadata, brukt nÃĨr du ser pÃĨ en enkelt ressurs og for maskinlÃĻring", "image_preview_quality_description": "Kvalitet pÃĨ forhÃĨndsvisning fra 1-100. Høyere er bedre, men genererer større filer og kan redusere hastigheten pÃĨ systemet. Ved for lav verdi kan det pÃĨvirke kvaliteten pÃĨ maskinlÃĻringen.", "image_preview_title": "ForhÃĨndsvisningsinnstillinger", + "image_progressive": "Progressiv", + "image_progressive_description": "Kod JPEG-bilder progressivt for gradvis lasting av visning. Dette har ingen effekt pÃĨ WebP-bilder.", "image_quality": "Kvalitet", "image_resolution": "Oppløsning", "image_resolution_description": "Høyere oppløsninger kan bevare flere detaljer, men det tar lengre tid ÃĨ kode, har større filstørrelser og kan redusere appresponsen.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Aktiver smart søk", "machine_learning_smart_search_enabled_description": "Hvis deaktivert, vil bilder ikke bli enkodet for smart søk.", "machine_learning_url_description": "URL til maskinlÃĻrings-serveren. Hvis mer enn en URL er lagt inn, hver server vill bli forsøkt en om gangen frem til en svarer suksessfullt, i rekkefølge fra først til sist. Servere som ikke svarer vil midlertidig bli oversett frem til dem svarer igjen.", + "maintenance_delete_backup": "Slett sikkerhetskopi", + "maintenance_delete_backup_description": "Denne filen vil bli permanent slettet.", + "maintenance_delete_error": "Feilet ved sletting av sikkerhetskopi.", + "maintenance_restore_backup": "Gjenopprett Sikkerhetskopi", + "maintenance_restore_backup_description": "Immich vil bli sletter og gjenopprettet fra en valgt sikkerhetskopi. En sikkerhetskopi vil utføres før handlingen fortsetter.", + "maintenance_restore_backup_different_version": "Denne sikkerhetskopien ble laget med en annen versjon av Immich!", + "maintenance_restore_backup_unknown_version": "Kunne ikke fastslÃĨ versjon for sikkerhetskopi.", + "maintenance_restore_database_backup": "Gjenopprett sikkerhetskopi av database", + "maintenance_restore_database_backup_description": "Rull tilbake til en tidligere database ved ÃĨ bruke en sikkerhetskopi", "maintenance_settings": "Vedlikehold", "maintenance_settings_description": "Sett Immich i vedlikeholdsmodus.", - "maintenance_start": "Start vedlikeholdsmodus", + "maintenance_start": "Bytt til vedlikeholdsmodus", "maintenance_start_error": "Kunne ikke starte vedlikeholdsmodus.", + "maintenance_upload_backup": "Last opp sikkerhetskopi av databasen", + "maintenance_upload_backup_error": "Klarte ikke ÃĨ laste opp sikkerhetskopi, er det en .sql/.sql.gz fil?", "manage_concurrency": "Administrer samtidighet", "manage_concurrency_description": "Naviger til jobb-siden for ÃĨ justere samtidige jobber", "manage_log_settings": "Administrer logginnstillinger", @@ -198,7 +218,7 @@ "map_reverse_geocoding": "Omvendt geokoding", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokoding", "map_reverse_geocoding_settings": "Innstillinger for omvendt geokoding", - "map_settings": "Innstillinger for kart og GPS", + "map_settings": "Kart", "map_settings_description": "Administrer kartinnstillinger", "map_style_description": "URL til et style.json-karttema", "memory_cleanup_job": "Minneopprydding", @@ -245,14 +265,14 @@ "notification_email_test_email_sent": "En test-e-post er sendt til {email}. Vennligst sjekk innboksen din.", "notification_email_username_description": "Brukernavn som skal brukes ved autentisering med e-posts serveren", "notification_enable_email_notifications": "Aktiver e-postvarsler", - "notification_settings": "Innstillinger for varsler", + "notification_settings": "Varselinnstillinger", "notification_settings_description": "Administrer varselinnstillinger, inkludert e-post", "oauth_auto_launch": "Automatisk oppstart", "oauth_auto_launch_description": "Start OAuth-innloggingsflyten automatisk nÃĨr du navigerer til innloggingssiden", "oauth_auto_register": "Automatisk registrering", "oauth_auto_register_description": "Registrer automatisk nye brukere etter innlogging med OAuth", "oauth_button_text": "Knappetekst", - "oauth_client_secret_description": "Kreves hvis PKCE (Proof Key for Code Exchange) ikke støttes av OAuth-leverandøren", + "oauth_client_secret_description": "Kreves for konfidensiell klient, eller hvis PKCE (Proof Key for Code Exchange) ikke støttes for offentlig klient.", "oauth_enable_description": "Logg inn med OAuth", "oauth_mobile_redirect_uri": "Mobil omdirigerings-URI", "oauth_mobile_redirect_uri_override": "Mobil omdirigerings-URI overstyring", @@ -358,12 +378,12 @@ "transcoding_constant_rate_factor": "Konstant ratefaktor (-crf)", "transcoding_constant_rate_factor_description": "NivÃĨet pÃĨ videokvaliteten. Typiske verdier er 23 for H.264, 28 for HEVC, 31 for VP9 og 35 for AV1. Lavere verdier gir bedre kvalitet, men større filstørrelser.", "transcoding_disabled_description": "Ikke transkoder noen videoer; dette kan føre til avspillingsproblemer pÃĨ visse klienter", - "transcoding_encoding_options": "Kodek Alternativer", + "transcoding_encoding_options": "Kodek-alternativer", "transcoding_encoding_options_description": "Sett kodeks, oppløsning, kvalitet og andre valg for koding av videoer", "transcoding_hardware_acceleration": "Maskinvareakselerasjon", "transcoding_hardware_acceleration_description": "Eksperimentell: raskere transkoding, men kan ha lavere kvalitet ved samme bithastighet", "transcoding_hardware_decoding": "Maskinvaredekoding", - "transcoding_hardware_decoding_setting_description": "Gjelder bare for NVENC,QSV og RKMPP. Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", + "transcoding_hardware_decoding_setting_description": "Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", "transcoding_max_b_frames": "Maksimalt antall B-frames", "transcoding_max_b_frames_description": "Høyere verdier forbedrer komprimeringseffektiviteten, men senker ned kodingen. Kan vÃĻre inkompatibelt med maskinvareakselerasjon pÃĨ eldre enheter. 0 deaktiverer B-rammer, mens -1 setter verdien automatisk.", "transcoding_max_bitrate": "Maksimal bithastighet", @@ -431,6 +451,9 @@ "admin_password": "Administratorpassord", "administration": "Administrasjon", "advanced": "Avansert", + "advanced_settings_clear_image_cache": "Tøm Bildecache", + "advanced_settings_clear_image_cache_error": "Feiled ved tømming av bildecache", + "advanced_settings_clear_image_cache_success": "Vellykket tømt {size}", "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}", @@ -467,10 +490,12 @@ "album_remove_user": "Fjerne bruker?", "album_remove_user_confirmation": "Vil du virkelig fjerne {user}?", "album_search_not_found": "Ingen album ble funnet som traff ditt søk", + "album_selected": "Album valgt", "album_share_no_users": "Dette albumet er allerede delt med du har delt dette albumet med alle brukere, eller du ikke har noen brukere ÃĨ dele det med.", "album_summary": "Oppsummering av album", "album_updated": "Album oppdatert", "album_updated_setting_description": "Motta e-postvarsling nÃĨr et delt album fÃĨr nye filer", + "album_upload_assets": "Last opp medier fra datamaskinen og legg til i album", "album_user_left": "Forlot {album}", "album_user_removed": "Fjernet {user}", "album_viewer_appbar_delete_confirm": "Vil du virkelig slette dette albumet fra kontoen din?", @@ -488,9 +513,11 @@ "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": "Album pÃĨ enheten {count}", + "albums_selected": "{count, plural, one {# valgt album} other {# albumer valgt}}", "all": "Alle", "all_albums": "Alle album", "all_people": "Alle personer", + "all_photos": "Alle bilder", "all_videos": "Alle videoer", "allow_dark_mode": "Tillat mørk modus", "allow_edits": "Tillat redigering", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Tillat uautentiserte brukere ÃĨ laste opp", "allowed": "Tillatt", "alt_text_qr_code": "QR-kodebilde", + "always_keep": "Alltid behold", + "always_keep_photos_hint": "Frigjør plass vil beholde alle bilder pÃĨ denne enheten.", + "always_keep_videos_hint": "Frigjør plass til beholde alle videoer pÃĨ denne enheten.", "anti_clockwise": "Mot klokken", "api_key": "API-nøkkel", "api_key_description": "Denne verdien vil vises kun Ên gang. Pass pÃĨ ÃĨ kopiere den før du lukker vinduet.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {Arkivert #}}", "are_these_the_same_person": "Er disse samme person?", "are_you_sure_to_do_this": "Vil du virkelig gjøre dette?", + "array_field_not_fully_supported": "Arrayfelter krever manuell JSON endring", "asset_action_delete_err_read_only": "Kunne ikke slette element(er) med kun lese-rettighet, hopper over", "asset_action_share_err_offline": "Kunne ikke hente offline element(er), hopper over", "asset_added_to_album": "Lagt til i album", "asset_adding_to_album": "Legger til i albumâ€Ļ", + "asset_created": "Objekt opprettet", "asset_description_updated": "Elementbeskrivelse har blitt oppdatert", "asset_filename_is_offline": "Element {filename} er offline", "asset_has_unassigned_faces": "Element har ikke-tilordnede ansikter", @@ -540,8 +572,11 @@ "asset_list_layout_sub_title": "Fordeling", "asset_list_settings_subtitle": "Innstillinger for layout av fotorutenett", "asset_list_settings_title": "Fotorutenett", + "asset_not_found_on_device_android": "Elementet ble ikke funnet pÃĨ enheten", + "asset_not_found_on_device_ios": "Elementet ble ikke funnet pÃĨ enheten. Hvis du bruker iCloud, kan elementet vÃĻre utilgjengelig pÃĨ grunn av en feilaktig fil er lagret i iCloud", + "asset_not_found_on_icloud": "Elementet ble ikke funnet pÃĨ iCloud. Elementet kan vÃĻre utilgjengelig fordi det ligger en feilaktig fil i iCloud", "asset_offline": "Fil utilgjengelig", - "asset_offline_description": "Dette elementet er offline. Immich kan ikke aksessere dets lokasjon. Vennligst pÃĨse at elementet er tilgjengelig og skann sÃĨ biblioteket pÃĨ nytt.", + "asset_offline_description": "Dette elementet er offline. Immich kan ikke finne dets possisjon. Vennligst pÃĨse at elementet er tilgjengelig og skann sÃĨ biblioteket pÃĨ nytt.", "asset_restored_successfully": "Objekt(er) gjenopprettet", "asset_skipped": "Hoppet over", "asset_skipped_in_trash": "I papirkurven", @@ -652,6 +687,7 @@ "backup_options_page_title": "Backupinnstillinger", "backup_setting_subtitle": "Administrer opplastingsinnstillinger for bakgrunn og forgrunn", "backup_settings_subtitle": "HÃĨndter opplastingsinnstillinger", + "backup_upload_details_page_more_details": "Trykk for flere detaljer", "backward": "Bakover", "biometric_auth_enabled": "Biometrisk autentisering aktivert", "biometric_locked_out": "Du er lÃĨst ute av biometrisk verifisering", @@ -710,6 +746,8 @@ "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", "change_pin_code": "Endre PIN-kode", + "change_trigger": "Endre utløser", + "change_trigger_prompt": "Er du sikker pÃĨ at du vil endre utløser? Dette vil fjerne alle eksisterende handlinger og filtre.", "change_your_password": "Endre passordet ditt", "changed_visibility_successfully": "Endret synlighet vellykket", "charging": "Lading", @@ -718,8 +756,21 @@ "check_corrupt_asset_backup_button": "Utfør sjekk", "check_corrupt_asset_backup_description": "Kjør denne sjekken kun over Wi-Fi og nÃĨr alle elementer har blitt lastet opp. Denne sjekken kan ta noen minutter.", "check_logs": "Sjekk Logger", + "checksum": "Sjekksum", "choose_matching_people_to_merge": "Velg personer som skal slÃĨs sammen", "city": "By", + "cleanup_confirm_description": "Immich fant {count} mediefiler (opprettet før {date}) som er lastet opp til serveren. Vil du fjerne disse lokale kopiene fra denne enheten?", + "cleanup_confirm_prompt_title": "Fjern fra denne enheten?", + "cleanup_deleted_assets": "Flyttet {count} mediefiler til enhetens søppelkasse", + "cleanup_deleting": "Flytter til søppelkasse...", + "cleanup_found_assets": "Fant {count} mediefiler som er sikkerhetskopiert", + "cleanup_found_assets_with_size": "Fant {count} sikkerhetskopierte objekter ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud delte albumer er ekskludert fra skanningen", + "cleanup_no_assets_found": "Ingen opplastede mediefiler funnet som treffer dine søkekriterier. Frigjør plass kan kun fjerne objekter som har blitt sikkerhetskopiert", + "cleanup_preview_title": "Mediefiler ÃĨ fjerne ({count})", + "cleanup_step3_description": "Skann etter bilder og videoer ved ÃĨ velge sluttdato og filter i søkeinnstillinger.", + "cleanup_step4_summary": "{count} mediefiler (opprettet før {date}= er plassert i kø for fjerning fra enheten. Bildene vil vÃĻre tilgjengelige fra Immich appen.", + "cleanup_trash_hint": "For ÃĨ frigjøre lagringsplassen helt, ÃĨpne systemgalleri-appen og tøm papirkurven", "clear": "Tøm", "clear_all": "Tøm alt", "clear_all_recent_searches": "Fjern alle nylige søk", @@ -785,6 +836,7 @@ "create_album": "Opprett album", "create_album_page_untitled": "Navnløst", "create_api_key": "Opprett API nøkkel", + "create_first_workflow": "Opprett første arbeidsfly", "create_library": "Opprett Bibliotek", "create_link": "Opprett lenke", "create_link_to_share": "Opprett delelink", @@ -799,17 +851,25 @@ "create_tag": "Lag merkelapp", "create_tag_description": "Lag en ny tag. For undertag, vennligst fullfør hele stien til taggen, inkludert forovervendt skrÃĨstrek.", "create_user": "Opprett Bruker", + "create_workflow": "Opprett arbeidsflyt", "created": "Opprettet", "created_at": "Laget", "creating_linked_albums": "Oppretter sammenkoblede album...", "crop": "BeskjÃĻr", + "crop_aspect_ratio_fixed": "Fikset", + "crop_aspect_ratio_free": "Lagret", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Ting", "current_device": "NÃĨvÃĻrende enhet", "current_pin_code": "NÃĨvÃĻrende PIN kode", "current_server_address": "NÃĨvÃĻrende serveradresse", + "custom_date": "Egendefinert dato", "custom_locale": "Tilpasset lokalisering", "custom_locale_description": "Formater datoer og tall basert pÃĨ sprÃĨk og region", "custom_url": "Tilpasset URL", + "cutoff_date_description": "Fjern bilder som er eldre ennâ€Ļ", + "cutoff_day": "{count, plural, one {dag} other {dager}}", + "cutoff_year": "{count, plural, one {ÃĨr} other {ÃĨr}}", "daily_title_text_date": "E MMM. dd", "daily_title_text_date_year": "E MMM. dddd, yyyy", "dark": "Mørk", @@ -865,6 +925,7 @@ "deselect_all": "Avmerk alle", "details": "Detaljer", "direction": "Retning", + "disable": "Deaktiver", "disabled": "Deaktivert", "disallow_edits": "Forby redigering", "discord": "Discord", @@ -890,6 +951,7 @@ "download_include_embedded_motion_videos": "Innebygde videoer", "download_include_embedded_motion_videos_description": "Inkluder innebygde videoer i levende bilder som en egen fil", "download_notfound": "Nedlasting ikke funnet", + "download_original": "Last ned original", "download_paused": "Nedlasting pauset", "download_settings": "Last ned", "download_settings_description": "Administrer innstillinger relatert til nedlasting av filer", @@ -899,6 +961,7 @@ "download_waiting_to_retry": "Venter pÃĨ nytt forsøk", "downloading": "Laster ned", "downloading_asset_filename": "Last ned {filename}", + "downloading_from_icloud": "Laster ned fra iCloud", "downloading_media": "Laster ned media", "drop_files_to_upload": "Slipp filer hvor som helst for ÃĨ laste opp", "duplicates": "Duplikater", @@ -927,11 +990,17 @@ "edit_tag": "Rediger etikett", "edit_title": "Rediger tittel", "edit_user": "Rediger bruker", + "edit_workflow": "Endre arbeidsflyt", "editor": "Redaktør", "editor_close_without_save_prompt": "Endringene vil ikke bli lagret", "editor_close_without_save_title": "Lukk redigering?", - "editor_crop_tool_h2_aspect_ratios": "Sideforhold", - "editor_crop_tool_h2_rotation": "Rotasjon", + "editor_confirm_reset_all_changes": "Er du sikker pÃĨ at du vil tilbakestille alle endringer?", + "editor_flip_horizontal": "Roter horisontalt", + "editor_flip_vertical": "Roter vertikalt", + "editor_orientation": "Orientering", + "editor_reset_all_changes": "Tilbakestill endringer", + "editor_rotate_left": "Roter 90° mot klokken", + "editor_rotate_right": "Roter 90° med klokken", "email": "E-postadresse", "email_notifications": "Epostvarsler", "empty_folder": "Denne mappen er tom", @@ -939,22 +1008,25 @@ "empty_trash_confirmation": "Vil du virkelig 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", + "enable_biometric_auth_description": "Skriv inn PIN-koden for ÃĨ aktivere biometrisk autentisering", "enabled": "Aktivert", - "end_date": "Slutt dato", + "end_date": "Sluttdato", "enqueued": "I kø", "enter_wifi_name": "Skriv inn Wi-Fi navn", - "enter_your_pin_code": "Skriv inn din PIN kode", - "enter_your_pin_code_subtitle": "Skriv inn din PIN kode for ÃĨ fÃĨ tilgang til lÃĨst mappe", + "enter_your_pin_code": "Skriv inn din PIN-kode", + "enter_your_pin_code_subtitle": "Skriv inn din PIN-kode for ÃĨ fÃĨ tilgang til lÃĨst mappe", "error": "Feil", "error_change_sort_album": "Mislyktes ved endring av sorteringsrekkefølge pÃĨ album", "error_delete_face": "Feil ved sletting av ansikt fra aktivia", "error_getting_places": "Feil ved henting av steder", + "error_loading_albums": "Feil ved lasting av albumer", "error_loading_image": "Feil ved lasting av bilde", "error_loading_partners": "Feil ved lasting av partnere: {error}", + "error_retrieving_asset_information": "Feil ved henting av objektinformasjon", "error_saving_image": "Feil: {error}", "error_tag_face_bounding_box": "Feil ved merking av ansikt - klarte ikke ÃĨ fÃĨ koordinatene pÃĨ omrisset", "error_title": "Feil - Noe gikk galt", + "error_while_navigating": "Feil ved navigering til objekt", "errors": { "cannot_navigate_next_asset": "Kunne ikke navigere til neste fil", "cannot_navigate_previous_asset": "Kunne ikke navigere til forrige fil", @@ -1011,7 +1083,8 @@ "unable_to_change_visibility": "Kunne ikke endre synlighet for {count, plural, one {# person} other {# people}}", "unable_to_complete_oauth_login": "Kunne ikke fullføre OAuth innlogging", "unable_to_connect": "Kunne ikke koble til", - "unable_to_copy_to_clipboard": "Kunne ikke kopiere til utklippstavlen, sørg for at du fÃĨr tilgang til siden via HTTPS", + "unable_to_copy_to_clipboard": "Kunne ikke kopiere til utklippstavlen, sørg for at du fÃĨr tilgang til siden via https", + "unable_to_create": "Klarte ikke ÃĨ opprette arbeidsflyt", "unable_to_create_admin_account": "Kunne ikke opprette administrator bruker", "unable_to_create_api_key": "Kunne ikke opprette en ny API-nøkkel", "unable_to_create_library": "Kunne ikke opprette bibliotek", @@ -1022,6 +1095,7 @@ "unable_to_delete_exclusion_pattern": "Kunne ikke slette eksklusjonsmønster", "unable_to_delete_shared_link": "Kunne ikke slette delt lenke", "unable_to_delete_user": "Kunne ikke slette bruker", + "unable_to_delete_workflow": "Klarte ikke ÃĨ slette arbeidsflyt", "unable_to_download_files": "Kunne ikke laste ned filer", "unable_to_edit_exclusion_pattern": "Kunne ikke redigere eksklusjonsmønster", "unable_to_empty_trash": "Kunne ikke Tømme papirkurven", @@ -1061,6 +1135,7 @@ "unable_to_scan_library": "Kunne ikke skanne bibliotek", "unable_to_set_feature_photo": "Kunne ikke sette funksjonsbilde", "unable_to_set_profile_picture": "Kunne ikke sette profilbilde", + "unable_to_set_rating": "Klarte ikke ÃĨ sette rating", "unable_to_submit_job": "Kunne ikke sende inn jobb", "unable_to_trash_asset": "Kunne ikke flytte filen til papirkurven", "unable_to_unlink_account": "Kunne ikke fjerne kobling til konto", @@ -1072,10 +1147,12 @@ "unable_to_update_settings": "Kunne ikke oppdatere innstillinger", "unable_to_update_timeline_display_status": "Kunne ikke oppdatere visningsstatus for tidslinje", "unable_to_update_user": "Kunne ikke oppdatere bruker", + "unable_to_update_workflow": "Klarte ikke ÃĨ oppdatere arbeidsflyt", "unable_to_upload_file": "Kunne ikke laste opp fil" }, + "errors_text": "Feil", "exclusion_pattern": "Ekskluderingsmønster", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "Legg til beskrivelse ...", "exif_bottom_sheet_description_error": "Feil ved oppdatering av beskrivelsen", "exif_bottom_sheet_details": "DETALJER", @@ -1117,15 +1194,16 @@ "feature_photo_updated": "Fremhevet bilde oppdatert", "features": "Funksjoner", "features_in_development": "Funksjoner under utvikling", - "features_setting_description": "Administrerer funksjoner for appen", - "file_name": "Filnavn", + "features_setting_description": "Administrer funksjoner for appen", "file_name_or_extension": "Filnavn eller filtype", "file_size": "Filstørrelse", "filename": "Filnavn", "filetype": "Filtype", "filter": "Filter", + "filter_description": "Betingelser for ÃĨ filtrere objekter", "filter_people": "Filtrer personer", "filter_places": "Filtrer steder", + "filters": "Filtre", "find_them_fast": "Finn dem raskt ved søking av navn", "first": "Første", "fix_incorrect_match": "Fiks feilaktig match", @@ -1135,12 +1213,16 @@ "folders_feature_description": "Utforsker mappe visning for bilder og videoer pÃĨ fil systemet", "forgot_pin_code_question": "Glemt PIN-koden?", "forward": "Fremover", + "free_up_space": "Rydd opp lagringsplass", + "free_up_space_description": "Flytt sikkerhetskopierte bilder og videoer til enhetens papirkurv for ÃĨ frigjøre plass. Kopiene dine pÃĨ serveren forblir trygge.", + "free_up_space_settings_subtitle": "Frigjør lagringsplass pÃĨ enheten", "full_path": "Full sti: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Denne funksjonen laster eksterne ressurser fra Google for ÃĨ fungere.", "general": "Generelt", "geolocation_instruction_location": "Klikk pÃĨ et element med GPS-koordinater for ÃĨ bruke posisjonen, eller velg en posisjon direkte fra kartet", "get_help": "FÃĨ Hjelp", + "get_people_error": "Feilet ved henting av mennesker", "get_wifiname_error": "Kunne ikke hente Wi-Fi-navnet. Sørg for at du har gitt de nødvendige tillatelsene og er koblet til et Wi-Fi-nettverk", "getting_started": "Kom i gang", "go_back": "GÃĨ tilbake", @@ -1166,12 +1248,14 @@ "header_settings_header_name_input": "Header navn", "header_settings_header_value_input": "Header verdi", "headers_settings_tile_title": "Egendefinerte proxy headere", + "height": "Høyde", "hi_user": "Hei {name} ({email})", "hide_all_people": "Skjul alle mennesker", "hide_gallery": "Skjul galleri", "hide_named_person": "Skjul {name}", "hide_password": "Skjul passord", "hide_person": "Skjul person", + "hide_schema": "Skjul skjema", "hide_text_recognition": "Skjul tekstgjenkjenning", "hide_unnamed_people": "Skjul mennesker uten navn", "home_page_add_to_album_conflicts": "Lagt til {added} elementer til album {album}. {failed} elementer er allerede i albumet.", @@ -1244,9 +1328,18 @@ "ios_debug_info_processing_ran_at": "Behandlingen ble kjørt {dateTime}", "items_count": "{count, plural, one {# gjenstand} other {# gjenstander}}", "jobs": "Oppgaver", + "json_editor": "JSON endrer", + "json_error": "JSON feil", "keep": "Behold", + "keep_albums": "Behold albumer", + "keep_albums_count": "Beholder {count} {count, plural, one {album} other {albumer}}", "keep_all": "Behold alle", + "keep_description": "Velg hva som skal forbli pÃĨ enheten din etter at plassen har blitt frigjort.", + "keep_favorites": "Behold favoritter", + "keep_on_device": "Behold pÃĨ enheten", + "keep_on_device_hint": "Velg objekter ÃĨ beholde pÃĨ denne enheten", "keep_this_delete_others": "Behold denne, slett de andre", + "keeping": "Beholder: {items}", "kept_this_deleted_others": "Behold denne filen og slett {count, plural, one {# element} other {# elementer}}", "keyboard_shortcuts": "Tastatursnarveier", "language": "SprÃĨk", @@ -1288,6 +1381,7 @@ "local": "Lokal", "local_asset_cast_failed": "Kunne ikke caste et bilde som ikke er lastet opp til serveren", "local_assets": "Lokale elementer", + "local_id": "Lokal ID", "local_media_summary": "Oppsummering av lokale media", "local_network": "Lokalt nettverk", "local_network_sheet_info": "Appen vil koble til serveren via denne URL-en nÃĨr du bruker det angitte Wi-Fi-nettverket", @@ -1335,14 +1429,32 @@ "logs": "Logger", "longitude": "Lengdegrad", "look": "Se", - "loop_videos": "Gjenta Videoer", + "loop_videos": "Gjenta videoer", "loop_videos_description": "Aktiver for ÃĨ automatisk loope en video i detaljeviseren.", "main_branch_warning": "Du bruker en utviklingsversjon; vi anbefaler pÃĨ det sterkeste og bruke en utgitt versjon!", "main_menu": "Hovedmeny", + "maintenance_action_restore": "Gjenoppretter database", "maintenance_description": "Immich er i Vedlikeholdsmodus.", "maintenance_end": "Avslutt vedlikeholdsmodus", "maintenance_end_error": "Kunne ikke avslutte vedlikeholdsmodus.", "maintenance_logged_in_as": "Logged inn som {user}", + "maintenance_restore_from_backup": "Gjenopprett fra sikkerhetskopi", + "maintenance_restore_library": "Gjenopprett biblioteket", + "maintenance_restore_library_confirm": "Hvis dette ser korrekt ut, fortsett for ÃĨ gjenopprette en sikkerhetskopi!", + "maintenance_restore_library_description": "Gjenoppretter database", + "maintenance_restore_library_folder_has_files": "{folder} har {count} mappe(r)", + "maintenance_restore_library_folder_no_files": "{folder} mangler filer!", + "maintenance_restore_library_folder_pass": "lesbar og skrivbar", + "maintenance_restore_library_folder_read_fail": "ikke lesbar", + "maintenance_restore_library_folder_write_fail": "ikke skrivbar", + "maintenance_restore_library_hint_missing_files": "Det kan hende du mangler viktige filer", + "maintenance_restore_library_hint_regenerate_later": "Du kan regenerere disse senere i innstillinger", + "maintenance_restore_library_hint_storage_template_missing_files": "Bruker du lagringstemplaten? Du kan mangle filer", + "maintenance_restore_library_loading": "Laster inn integritetskontroller og heuristikker â€Ļ", + "maintenance_task_backup": "Oppretter en sikkerhetskopi av eksisterende databaseâ€Ļ", + "maintenance_task_migrations": "Kjører databasemigreringerâ€Ļ", + "maintenance_task_restore": "Gjenoppretter valgte sikkerhetskopiâ€Ļ", + "maintenance_task_rollback": "Gjenoppretting feilet, ruller tilbake til gjenopprettingspunktâ€Ļ", "maintenance_title": "Midlertidig utilgjengelig", "make": "Merke", "manage_geolocation": "Administrer plassering", @@ -1359,9 +1471,9 @@ "manage_your_oauth_connection": "Administrer tilkoblingen din med OAuth", "map": "Kart", "map_assets_in_bounds": "{count, plural, =0 {Ingen bilder i dette omrÃĨdet} one {# photo} other {# photos}}", - "map_cannot_get_user_location": "Kunne ikke hente brukerlokasjon", + "map_cannot_get_user_location": "Kan ikke hente brukerens plassering", "map_location_dialog_yes": "Ja", - "map_location_picker_page_use_location": "Bruk denne lokasjonen", + "map_location_picker_page_use_location": "Bruk dette stedet", "map_location_service_disabled_content": "Lokasjonstjeneste mÃĨ vÃĻre aktivert for ÃĨ vise elementer fra din nÃĨvÃĻrende lokasjon. Vil du aktivere det nÃĨ?", "map_location_service_disabled_title": "Lokasjonstjeneste deaktivert", "map_marker_for_images": "Kart makeringer for bilder tatt i {city}, {country}", @@ -1404,6 +1516,8 @@ "minimize": "Minimer", "minute": "Minutt", "minutes": "Minutter", + "mirror_horizontal": "Horisontal", + "mirror_vertical": "Vertikal", "missing": "Mangler", "mobile_app": "Mobilapp", "mobile_app_download_onboarding_note": "Last ned den tilhørende mobilappen ved ÃĨ bruke følgende alternativer", @@ -1412,11 +1526,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mer", "move": "Flytt", + "move_down": "Flytt ned", "move_off_locked_folder": "Flytt ut av lÃĨst mappe", "move_to": "Flytt til", + "move_to_device_trash": "Flytt til enhetens søppelkasse", "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 album, og kun tilgjengelige via den lÃĨste mappen", + "move_up": "Flytt opp", "moved_to_archive": "Flyttet {count, plural, one {# element} other {# elementer}} til arkivet", "moved_to_library": "Flyttet {count, plural, one {# element} other {# elementer}} til biblioteket", "moved_to_trash": "Flyttet til papirkurven", @@ -1426,6 +1543,7 @@ "my_albums": "Mine album", "name": "Navn", "name_or_nickname": "Navn eller kallenavn", + "name_required": "Navn er pÃĨkrevd", "navigate": "Naviger", "navigate_to_time": "Naviger til tid", "network_requirement_photos_upload": "Bruk mobildata for backup av bilder", @@ -1450,20 +1568,24 @@ "next": "Neste", "next_memory": "Neste minne", "no": "Nei", + "no_actions_added": "Ingen hendelser lagt til enda", + "no_albums_found": "Ingen albumer funnet", "no_albums_message": "Opprett et album for ÃĨ organisere bildene og videoene dine", "no_albums_with_name_yet": "Det ser ut som om det ikke finnes noen album med dette navnet enda.", "no_albums_yet": "Det ser ut som om du ikke har noen album enda.", "no_archived_assets_message": "Arkiver bilder og videoer for ÃĨ skjule dem fra visningen av bildene dine", - "no_assets_message": "KLIKK FOR Å LASTE OPP DITT FØRSTE BILDE", + "no_assets_message": "Trykk her for ÃĨ laste opp ditt første bilde", "no_assets_to_show": "Ingen elementer ÃĨ vise", "no_cast_devices_found": "Ingen caste-enheter oppdaget", "no_checksum_local": "Ingen sjekksum tilgjengelig - Kunne ikke hente lokale elementer", "no_checksum_remote": "Ingen sjekksum tilgjengelig - Kunne ikke hente eksterne elementer", + "no_configuration_needed": "Ingen konfigurasjon nødvendig", "no_devices": "Ingen autoriserte enheter", "no_duplicates_found": "Ingen duplikater ble funnet.", - "no_exif_info_available": "Ingen EXIF-informasjon tilgjengelig", + "no_exif_info_available": "Ingen Exif-informasjon tilgjengelig", "no_explore_results_message": "Last opp flere bilder for ÃĨ utforske samlingen din.", "no_favorites_message": "Legg til favoritter for ÃĨ finne dine beste bilder og videoer raskt", + "no_filters_added": "Ingen filtre lagt til enda", "no_libraries_message": "Opprett et eksternt bibliotek for ÃĨ se bildene og videoene dine", "no_local_assets_found": "Ingen lokale elementer funnet med denne sjekksummen", "no_location_set": "Ingen lokasjon satt", @@ -1477,11 +1599,11 @@ "no_results_description": "Prøv et synonym eller mer generelt søkeord", "no_shared_albums_message": "Opprett et album for ÃĨ dele bilder og videoer med personer i nettverket ditt", "no_uploads_in_progress": "Ingen opplasting pÃĨgÃĨr", + "none": "Ingen", "not_allowed": "Ikke tillatt", "not_available": "Ikke tilgjengelig", "not_in_any_album": "Ikke i noe album", "not_selected": "Ikke valgt", - "note_apply_storage_label_to_previously_uploaded assets": "Merk: For ÃĨ bruke lagringsetiketten pÃĨ tidligere opplastede filer, kjør", "notes": "Notater", "nothing_here_yet": "Ingenting her enda", "notification_permission_dialog_content": "For ÃĨ aktivere notifikasjoner, gÃĨ til Innstillinger og velg tillat.", @@ -1559,6 +1681,7 @@ "people": "Personer", "people_edits_count": "Endret {count, plural, one {# person} other {# people}}", "people_feature_description": "Utforsk bilder og videoer gruppert etter mennesker", + "people_selected": "{count, plural, one {# person valgt} other {# personer valgt}}", "people_sidebar_description": "Vis en lenke til Personer i sidepanelet", "permanent_deletion_warning": "Advarsel om permanent sletting", "permanent_deletion_warning_setting_description": "Vis en advarsel ved permanent sletting av filer", @@ -1583,11 +1706,14 @@ "person_age_years": "{years, plural, other {# years}} gammel", "person_birthdate": "Født den {date}", "person_hidden": "{name}{hidden, select, true { (skjult)} other {}}", + "person_recognized": "Person gjenkjent", + "person_selected": "Person valgt", "photo_shared_all_users": "Det ser ut som om du deler bildene med alle brukere eller det er ingen brukere ÃĨ dele med.", "photos": "Bilder", "photos_and_videos": "Bilder & Videoer", "photos_count": "{count, plural, one {{count, number} Bilde} other {{count, number} Bilder}}", "photos_from_previous_years": "Bilder fra tidliger ÃĨr", + "photos_only": "Kun bilder", "pick_a_location": "Velg et sted", "pick_custom_range": "Tilpasset omrÃĨde", "pick_date_range": "Velg ett datoomrÃĨde", @@ -1596,7 +1722,7 @@ "pin_code_setup_successfully": "Vellykket oppsett av PIN kode", "pin_verification": "PINkode verifikasjon", "place": "Sted", - "places": "Plasseringer", + "places": "Steder", "places_count": "{count, plural, one {{count, number} Sted} other {{count, number} Steder}}", "play": "Spill av", "play_memories": "Spill av minner", @@ -1653,7 +1779,7 @@ "purchase_panel_title": "Hjelp prosjektet", "purchase_per_server": "For hver server", "purchase_per_user": "For hver bruker", - "purchase_remove_product_key": "Ta bor Produktnøkkel", + "purchase_remove_product_key": "Fjern produktnøkkel", "purchase_remove_product_key_prompt": "Vil du virkelig ta bort produktnøkkelen?", "purchase_remove_server_product_key": "Ta bort Server Produktnøkkel", "purchase_remove_server_product_key_prompt": "Vil du virkelig ta bort Server Produktnøkkelen?", @@ -1663,10 +1789,12 @@ "purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren", "query_asset_id": "Forespør elementID", "queue_status": "Kø {count}/{total}", + "rate_asset": "Vurder objekt", "rating": "Stjernevurdering", "rating_clear": "Slett vurdering", "rating_count": "{count, plural, one {# sjerne} other {# stjerner}}", - "rating_description": "Hvis EXIF vurdering i informasjons panelet", + "rating_description": "Vis EXIF vurdering i informasjonspanel", + "rating_set": "Vurdering satt til {rating, plural, one {# stjerne} other {# stjerner}}", "reaction_options": "Reaksjonsalternativer", "read_changelog": "Les endringslogg", "readonly_mode_disabled": "Skrivebeskyttet modus deaktivert", @@ -1677,7 +1805,7 @@ "reassigned_assets_to_new_person": "Flyttet {count, plural, one {# element} other {# elementer}} til en ny person", "reassing_hint": "Tilordne valgte eiendeler til en eksisterende person", "recent": "Nylig", - "recent-albums": "Nylige album", + "recent_albums": "Nylige album", "recent_searches": "Nylige søk", "recently_added": "Nylig lagt til", "recently_added_page_title": "Nylig oppført", @@ -1692,7 +1820,7 @@ "refreshes_every_file": "Oppdaterer alle filer", "refreshing_encoded_video": "Oppdaterer kodete video", "refreshing_faces": "Oppdaterer ansikter", - "refreshing_metadata": "Oppdaterer matadata", + "refreshing_metadata": "Oppdaterer metadata", "regenerating_thumbnails": "Regenererer miniatyrbilder", "remote": "Eksternt", "remote_assets": "Eksterne elementer", @@ -1766,9 +1894,11 @@ "saved_settings": "Lagret instillinger", "say_something": "Si noe", "scaffold_body_error_occurred": "Feil oppstÃĨtt", + "scan": "Skann", "scan_all_libraries": "Skann alle biblioteker", "scan_library": "Skann", "scan_settings": "Skanneinnstillinger", + "scanning": "Skanner", "scanning_for_album": "Skanner etter album...", "search": "Søk", "search_albums": "Søk i album", @@ -1781,7 +1911,7 @@ "search_by_ocr_example": "Latte", "search_camera_lens_model": "Søk etter objektivmodell...", "search_camera_make": "Søk etter kameramerke...", - "search_camera_model": "Søk etter kamera modell...", + "search_camera_model": "Søk etter kameramodell...", "search_city": "Søk etter by...", "search_country": "Søk etter land...", "search_filter_apply": "Aktiver filter", @@ -1798,6 +1928,7 @@ "search_filter_media_type_title": "Velg medietype", "search_filter_ocr": "Søk etter tekst i bilde", "search_filter_people_title": "Velg mennesker", + "search_filter_star_rating": "Stjernerating", "search_for": "Søk etter", "search_for_existing_person": "Søk etter eksisterende person", "search_no_more_result": "Ingen flere resultater", @@ -1821,7 +1952,7 @@ "search_rating": "Søk etter vurdering...", "search_result_page_new_search_hint": "Nytt søk", "search_settings": "Søke instillinger", - "search_state": "Søk etter stat...", + "search_state": "Søk etter fylke...", "search_suggestion_list_smart_search_hint_1": "Smartsøk er aktivert som standard, for ÃĨ søke etter metadata bruk syntaksen ", "search_suggestion_list_smart_search_hint_2": "m:ditt-søkeord", "search_tags": "Søk tags...", @@ -1832,17 +1963,23 @@ "second": "Sekund", "see_all_people": "Vis alle mennesker", "select": "Velg", + "select_album": "Velg album", "select_album_cover": "Velg albumomslag", + "select_albums": "Velg albumer", "select_all": "Velg alle", "select_all_duplicates": "Velg alle duplikater", "select_all_in": "Velg alt i {group}", "select_avatar_color": "Velg avatarfarge", + "select_count": "{count, plural, one {Velg #} other {Valgt #}}", + "select_cutoff_date": "Velg frist", "select_face": "Velg ansikt", "select_featured_photo": "Velg fremhevet bilde", "select_from_computer": "Velg fra datamaskin", "select_keep_all": "Velg beholde alle", "select_library_owner": "Velg bibliotekseier", "select_new_face": "Velg nytt ansikt", + "select_people": "Velg mennesker", + "select_person": "Valgt person", "select_person_to_tag": "Velg en person ÃĨ tagge", "select_photos": "Velg bilder", "select_trash_all": "Velg ÃĨ flytte alt til papirkurven", @@ -1978,6 +2115,7 @@ "show_password": "Vis passord", "show_person_options": "Vis personalternativer", "show_progress_bar": "Vis fremdriftslinje", + "show_schema": "Vis skjema", "show_search_options": "Vis søkealternativer", "show_shared_links": "Vis delte lenker", "show_slideshow_transition": "Vis overgang til lysbildefremvisning", @@ -1995,6 +2133,8 @@ "skip_to_folders": "Hopp til mapper", "skip_to_tags": "Hopp til tagger", "slideshow": "Lysbildefremvisning", + "slideshow_repeat": "Gjenta lysbildefremvisning", + "slideshow_repeat_description": "GÃĨ tilbake til begynnelsen nÃĨr lysbildeserien er slutt", "slideshow_settings": "Lysbildefremvisning innstillinger", "sort_albums_by": "Sorter album etter...", "sort_created": "Dato opprettet", @@ -2032,7 +2172,7 @@ "suggestions": "Forslag", "sunrise_on_the_beach": "Soloppgang pÃĨ stranden", "support": "Støtte", - "support_and_feedback": "Støtte og Tilbakemelding", + "support_and_feedback": "Støtte og tilbakemelding", "support_third_party_description": "Immich-installasjonen din ble pakket av en tredjepart. Problemer du opplever kan vÃĻre forÃĨrsaket av den pakken, sÃĨ vennligst ta opp problemer med dem i første omgang ved ÃĨ bruke koblingene nedenfor.", "swap_merge_direction": "Bytt retning pÃĨ sammenslÃĨingen", "sync": "Synkroniser", @@ -2071,6 +2211,7 @@ "theme_setting_theme_subtitle": "Velg app-ens temainnstilling", "theme_setting_three_stage_loading_subtitle": "Tre-trinns innlasting kan øke lasteytelsen, men forÃĨrsaker betydelig høyere nettverksbelastning", "theme_setting_three_stage_loading_title": "Aktiver tre-trinns innlasting", + "then": "Da", "they_will_be_merged_together": "De vil bli slÃĨtt sammen", "third_party_resources": "Tredjeparts Ressurser", "time": "Tid", @@ -2105,6 +2246,13 @@ "trash_page_select_assets_btn": "Velg elementer", "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}}.", + "trigger": "Utløser", + "trigger_asset_uploaded": "Objekt lastet opp", + "trigger_asset_uploaded_description": "Utløser nÃĨr ett nytt objekt er lastet opp", + "trigger_description": "En hendelse som utløser arbeidsflyten", + "trigger_person_recognized": "Person gjenkjent", + "trigger_person_recognized_description": "Utløses nÃĨr en person blir gjenkjent", + "trigger_type": "Utløsertype", "troubleshoot": "Feilsøk", "type": "Type", "unable_to_change_pin_code": "Klarte ikke ÃĨ endre PIN-kode", @@ -2119,6 +2267,7 @@ "unhide_person": "Vis person", "unknown": "Ukjent", "unknown_country": "Ukjent Land", + "unknown_date": "Ukjent dato", "unknown_year": "Ukjent ÃĨr", "unlimited": "Ubegrenset", "unlink_motion_video": "Koble fra bevegelsesvideo", @@ -2135,17 +2284,19 @@ "unstack": "avstable", "unstack_action_prompt": "{count} ustakket", "unstacked_assets_count": "Ikke stablet {count, plural, one {# element} other {# elementer}}", + "unsupported_field_type": "Ustøttede felttyper", "untagged": "Umerket", + "untitled_workflow": "Arbeidsflyt uten navn", "up_next": "Neste", "update_location_action_prompt": "Oppdater plasseringen til {count} valgte elementer med:", "updated_at": "Oppdatert", "updated_password": "Passord oppdatert", "upload": "Last opp", - "upload_action_prompt": "{count} i kø for opplasting", "upload_concurrency": "Samtidig opplastning", "upload_details": "Opplastingsdetaljer", "upload_dialog_info": "Vil du utføre backup av valgte element(er) til serveren?", "upload_dialog_title": "Last opp element", + "upload_error_with_count": "Opplastningsfeil for {count, plural, one {# element} other {# elementer}}", "upload_errors": "Opplasting fullført med {count, plural, one {# error} other {# errors}}, oppdater siden for ÃĨ se nye opplastingsressurser.", "upload_finished": "Opplasting fullført", "upload_progress": "GjenstÃĨende {remaining, number} – behandlet {processed, number}/{total, number}", @@ -2160,7 +2311,7 @@ "url": "URL", "usage": "Bruk", "use_biometric": "Bruk biometri", - "use_current_connection": "bruk nÃĨvÃĻrende tilkobling", + "use_current_connection": "Bruk nÃĨvÃĻrende tilkobling", "use_custom_date_range": "Bruk egendefinert datoperiode i stedet", "user": "Bruker", "user_has_been_deleted": "Denne brukeren har blitt slettet.", @@ -2172,7 +2323,7 @@ "user_purchase_settings": "Kjøpe", "user_purchase_settings_description": "Administrer dine kjøp", "user_role_set": "Sett {user} som {role}", - "user_usage_detail": "Detaljer av brukers forbruk", + "user_usage_detail": "Detaljer av brukernes forbruk", "user_usage_stats": "Kontobruksstatistikk", "user_usage_stats_description": "Vis kontobruksstatistikk", "username": "Brukernavn", @@ -2181,6 +2332,7 @@ "utilities": "Verktøy", "validate": "Valider", "validate_endpoint_error": "Skriv inn en gyldig URL", + "validation_error": "valideringsfeil", "variables": "Variabler", "version": "Versjon", "version_announcement_closing": "Din venn, Alex", @@ -2192,6 +2344,7 @@ "video_hover_setting_description": "Spill av forhÃĨndsvisning mens en musepeker er over elementet. Selv nÃĨr den er deaktivert, kan avspilling startes ved ÃĨ holde musepekeren over avspillingsikonet.", "videos": "Videoer", "videos_count": "{count, plural, one {# Video} other {# Videoer}}", + "videos_only": "Kun videoer", "view": "Vis", "view_album": "Vis album", "view_all": "Vis alle", @@ -2212,20 +2365,36 @@ "viewer_stack_use_as_main_asset": "Bruk som hovedelement", "viewer_unstack": "avstable", "visibility_changed": "Synlighet endret for {count, plural, one {# person} other {# people}}", + "visual": "Visuell", + "visual_builder": "Visuell oppbygging", "waiting": "Venter", "waiting_count": "Ventende: {count}", "warning": "Advarsel", "week": "Uke", "welcome": "Velkommen", "welcome_to_immich": "Velkommen til Immich", + "width": "Bredde", "wifi_name": "Wi-Fi-navn", - "workflow": "Arbeidsflyt", + "workflow_delete_prompt": "Er du sikker pÃĨ at du vil slette denne arbeidsflyten?", + "workflow_deleted": "Arbeidsflyt slettet", + "workflow_description": "Beskrivelse av arbeidsflyt", + "workflow_info": "Informasjon om arbeidsflyt", + "workflow_json": "Arbeidsflyt JSON", + "workflow_json_help": "Endre arbeidsflytskonfigurasjon i JSON format. Endringer vil synkroniseres til den visuelle konfiguratoren.", + "workflow_name": "Navn pÃĨ arbeidsflyt", + "workflow_navigation_prompt": "Er du sikker pÃĨ at du vil forlate uten ÃĨ lagre endringene?", + "workflow_summary": "Oppsummering av arbeidsflyt", + "workflow_update_success": "Vellykket oppdatering av arbeidsflyt", + "workflow_updated": "Arbeidsflyt oppdatert", + "workflows": "Arbeidsflyter", + "workflows_help_text": "Arbeidsflyter automatiserer hendelser pÃĨ dine mediefiler basert pÃĨ dine utløsere og filtre", "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", + "zero_to_clear_rating": "Trykk 0 for ÃĨ fjerne vurdering", "zoom_image": "Zoom Bilde", "zoom_to_bounds": "Zoom til grensene" } diff --git a/i18n/nl.json b/i18n/nl.json index 48ad3ddbd2..24197a15b8 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -1,10 +1,11 @@ { "about": "Over", "account": "Account", - "account_settings": "Account­instellingen", - "acknowledge": "Begrepen", + "account_settings": "Accountinstellingen", + "acknowledge": "Erkennen", "action": "Actie", "action_common_update": "Bijwerken", + "action_description": "Een groep acties om uit te voeren op de gefilterde items", "actions": "Acties", "active": "Actief", "active_count": "Actief: {count}", @@ -12,12 +13,17 @@ "activity_changed": "Activiteit is {enabled, select, true {ingeschakeld} other {uitgeschakeld}}", "add": "Toevoegen", "add_a_description": "Beschrijving toevoegen", - "add_a_location": "Locatie toevoegen", + "add_a_location": "Een locatie toevoegen", "add_a_name": "Naam toevoegen", "add_a_title": "Titel toevoegen", + "add_action": "Actie toevoegen", + "add_action_description": "Klik om een uit te voeren actie toe te voegen", + "add_assets": "Items toevoegen", "add_birthday": "Verjaardag toevoegen", "add_endpoint": "Server toevoegen", "add_exclusion_pattern": "Uitsluitingspatroon toevoegen", + "add_filter": "Filter toevoegen", + "add_filter_description": "Klik om een filter voorwaarde toe te voegen", "add_location": "Locatie toevoegen", "add_more_users": "Meer gebruikers toevoegen", "add_partner": "Partner toevoegen", @@ -36,6 +42,7 @@ "add_to_shared_album": "Aan gedeeld album toevoegen", "add_upload_to_stack": "Voeg upload toe aan stack", "add_url": "URL toevoegen", + "add_workflow_step": "Stap aan workflow toevoegen", "added_to_archive": "Toegevoegd aan archief", "added_to_favorites": "Toegevoegd aan favorieten", "added_to_favorites_count": "{count, number} toegevoegd aan favorieten", @@ -97,6 +104,8 @@ "image_preview_description": "Middelgrote afbeelding met verwijderde metadata, gebruikt bij het bekijken van een enkele item en voor machine learning", "image_preview_quality_description": "Voorbeeldafbeelding kwaliteit van 1-100. Hoger is beter, maar produceert grotere bestanden en kan de app vertragen. Een lage waarde kan de kwaliteit van machine learning beïnvloeden.", "image_preview_title": "Voorbeeldafbeelding instellingen", + "image_progressive": "Progressief", + "image_progressive_description": "Codeer JPEG-afbeeldingen progressief voor een geleidelijke weergave. Dit heeft geen effect op WebP-afbeeldingen.", "image_quality": "Kwaliteit", "image_resolution": "Resolutie", "image_resolution_description": "Hogere resoluties behouden meer details, maar verhogen de coderingstijd, bestandsgrootte en kunnen de app vertragen.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Slim zoeken inschakelen", "machine_learning_smart_search_enabled_description": "Indien uitgeschakeld, worden afbeeldingen niet verwerkt voor slim zoeken.", "machine_learning_url_description": "De URL van de machine learning server. Als er meer dan ÊÊn URL is opgegeven, wordt elke server geprobeerd totdat er een succesvol reageert, op volgorde van eerste tot laatste. Servers die geen reactie geven zullen tijdelijk genegeerd worden tot zij terug online komen.", + "maintenance_delete_backup": "Backup verwijderen", + "maintenance_delete_backup_description": "Dit bestand wordt onomkeerbaar verwijderd.", + "maintenance_delete_error": "Backup verwijderen mislukt.", + "maintenance_restore_backup": "Backup herstellen", + "maintenance_restore_backup_description": "Immich wordt gereset en hersteld vanaf de gekozen backup. Er wordt een backup gemaakt voor deze actie uitgevoerd wordt.", + "maintenance_restore_backup_different_version": "Deze backup is gemaakt met een andere versie van Immich!", + "maintenance_restore_backup_unknown_version": "Kan versie van backup niet bepalen.", + "maintenance_restore_database_backup": "Database backup terugzetten", + "maintenance_restore_database_backup_description": "Een eerdere versie van de database terugzetten door middel van een backup bestand", "maintenance_settings": "Onderhoud", "maintenance_settings_description": "Zet Immich in onderhouds­modus.", - "maintenance_start": "Onderhouds­modus starten", + "maintenance_start": "Onderhouds­modus activeren", "maintenance_start_error": "Onderhouds­modus starten mislukt.", + "maintenance_upload_backup": "Upload database backup bestand", + "maintenance_upload_backup_error": "Kon backup niet uploaden, is het een .sql/.sql.gz bestand?", "manage_concurrency": "Beheer gelijktijdigheid", "manage_concurrency_description": "Navigeer naar de taken­pagina om de gelijk­tÄŗdigheid van taken te beheren", "manage_log_settings": "Beheer logboekinstellingen", @@ -252,7 +272,7 @@ "oauth_auto_register": "Automatisch registreren", "oauth_auto_register_description": "Nieuwe gebruikers automatisch registreren na inloggen met OAuth", "oauth_button_text": "Knoptekst", - "oauth_client_secret_description": "Vereist als PKCE (Proof Key for Code Exchange) niet wordt ondersteund door de OAuth aanbieder", + "oauth_client_secret_description": "Vereist voor een confidentiÃĢle client, of als PKCE (Proof Key for Code Exchange) niet wordt ondersteund door de publieke client.", "oauth_enable_description": "Inloggen met OAuth", "oauth_mobile_redirect_uri": "Omleidings-URI voor mobiel", "oauth_mobile_redirect_uri_override": "Omleidings-URI voor mobiele app overschrijven", @@ -291,7 +311,7 @@ "search_jobs": "Taak zoekenâ€Ļ", "send_welcome_email": "Stuur een welkomstmail", "server_external_domain_settings": "Extern domein", - "server_external_domain_settings_description": "Domein voor openbaar gedeelde links, inclusief http(s)://", + "server_external_domain_settings_description": "Domein voor externe links", "server_public_users": "Openbare gebruikerslijst", "server_public_users_description": "Alle gebruikers (met naam en e-mailadres) worden weergegeven wanneer een gebruiker wordt toegevoegd aan gedeelde albums. Wanneer uitgeschakeld, is de gebruikerslijst alleen beschikbaar voor beheerders.", "server_settings": "Serverinstellingen", @@ -431,6 +451,9 @@ "admin_password": "Beheerder wachtwoord", "administration": "Beheer", "advanced": "Geavanceerd", + "advanced_settings_clear_image_cache": "Wis afbeeldingscache", + "advanced_settings_clear_image_cache_error": "Het wissen van de afbeeldingscache is mislukt", + "advanced_settings_clear_image_cache_success": "{size} succesvol gewist", "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}", @@ -467,10 +490,12 @@ "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_selected": "Album geselecteerd", "album_share_no_users": "Het lijkt erop dat je dit album met alle gebruikers hebt gedeeld, of dat je geen gebruikers hebt om mee te delen.", "album_summary": "Album samenvatting", "album_updated": "Album bijgewerkt", "album_updated_setting_description": "Ontvang een e-mailmelding wanneer een gedeeld album nieuwe items heeft", + "album_upload_assets": "Items uploaden van je computer en aan album toevoegen", "album_user_left": "{album} verlaten", "album_user_removed": "{user} verwijderd", "album_viewer_appbar_delete_confirm": "Weet je zeker dat je dit album uit je account wilt verwijderen?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "InitiÃĢle sorteervolgorde bij het maken van nieuwe albums.", "albums_feature_description": "Collectie van items die je kan delen met andere gebruikers.", "albums_on_device_count": "Albums op apparaat ({count})", + "albums_selected": "{count, plural, one {# album geselecteerd} other {# albums geselecteerd}}", "all": "Alle", "all_albums": "Alle albums", "all_people": "Alle mensen", + "all_photos": "Alle foto's", "all_videos": "Alle video's", "allow_dark_mode": "Donkere modus toestaan", "allow_edits": "Bewerkingen toestaan", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Sta openbare gebruiker toe om te uploaden", "allowed": "Toegestaan", "alt_text_qr_code": "QR-codeafbeelding", + "always_keep": "Altijd bewaren", + "always_keep_photos_hint": "Met Free Up Space blijven alle foto's op dit apparaat bewaard.", + "always_keep_videos_hint": "Met Free Up Space worden alle video's op dit apparaat bewaard.", "anti_clockwise": "Linksom", "api_key": "API-sleutel", "api_key_description": "Deze waarde wordt slechts ÊÊn keer getoond. Zorg ervoor dat je deze kopieert voordat je het venster sluit.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {# gearchiveerd}}", "are_these_the_same_person": "Zijn dit dezelfde personen?", "are_you_sure_to_do_this": "Weet je zeker dat je dit wilt doen?", + "array_field_not_fully_supported": "Array velden vereisen handmatige JSON bewerking", "asset_action_delete_err_read_only": "Kan alleen-lezen item(s) niet verwijderen, overslaan", "asset_action_share_err_offline": "Kan offline item(s) niet ophalen, overslaan", "asset_added_to_album": "Toegevoegd aan album", "asset_adding_to_album": "Toevoegen aan albumâ€Ļ", + "asset_created": "Item aangemaakt", "asset_description_updated": "Item beschrijving is bijgewerkt", "asset_filename_is_offline": "Item {filename} is offline", "asset_has_unassigned_faces": "Item heeft niet-toegewezen gezichten", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Fotoraster layout instellingen", "asset_list_settings_title": "Fotoraster", + "asset_not_found_on_device_android": "Item niet gevonden op apparaat", + "asset_not_found_on_device_ios": "Item niet gevonden op apparaat. Wanneer je iCloud gebruikt, kan het item niet toegankelijk zijn door een slecht bestand in iCloud", + "asset_not_found_on_icloud": "Item niet gevonden in iCloud. Het item kan ontoegankelijk zijn door een slecht bestand op iCloud", "asset_offline": "Item offline", "asset_offline_description": "Dit externe item is niet meer op de schijf te vinden. Neem contact op met de Immich beheerder voor hulp.", "asset_restored_successfully": "Item succesvol hersteld", @@ -575,7 +610,7 @@ "assets_were_part_of_album_count": "{count, plural, one {Item was} other {Items waren}} al onderdeel van het album", "assets_were_part_of_albums_count": "{count, plural, one {Middel is} other {Middelen zijn}} al onderdeel van de albums", "authorized_devices": "Geautoriseerde apparaten", - "automatic_endpoint_switching_subtitle": "Maak een lokale verbinding bij het opgegeven WiFi-netwerk en gebruik in andere gevallen de externe URL", + "automatic_endpoint_switching_subtitle": "Maak indien beschikbaar lokaal verbinding via het aangewezen wifi-netwerk en gebruik elders alternatieve verbindingen", "automatic_endpoint_switching_title": "Automatische serverwissel", "autoplay_slideshow": "Diavoorstelling automatisch afspelen", "back": "Terug", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Selecteer albums", "backup_album_selection_page_selection_info": "Selectie info", "backup_album_selection_page_total_assets": "Totaal unieke items", - "backup_albums_sync": "Backup albums synchronisatie", + "backup_albums_sync": "Backup Albums Synchronisatie", "backup_all": "Alle", "backup_background_service_backup_failed_message": "Fout bij het back-uppen van de items. Opnieuw proberenâ€Ļ", "backup_background_service_complete_notification": "Backup voltooid", @@ -646,7 +681,7 @@ "backup_info_card_assets": "bestanden", "backup_manual_cancelled": "Geannuleerd", "backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje", - "backup_manual_success": "Succes", + "backup_manual_success": "Gelukt", "backup_manual_title": "Uploadstatus", "backup_options": "Backup opties", "backup_options_page_title": "Back-up instellingen", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen", "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in", "change_pin_code": "Wijzig pincode", + "change_trigger": "Wijzig trigger", + "change_trigger_prompt": "Weet u zeker dat u deze trigger wilt wijzigen? Dit verwijdert alle bestaande acties en filters.", "change_your_password": "Wijzig je wachtwoord", "changed_visibility_successfully": "Zichtbaarheid succesvol gewijzigd", "charging": "Opladen", @@ -722,6 +759,18 @@ "checksum": "Controlegetal", "choose_matching_people_to_merge": "Kies overeenkomende mensen om samen te voegen", "city": "Stad", + "cleanup_confirm_description": "Immich heeft {count} items (gemaakt voor {date}) opgeslagen op de server. Lokale kopieÃĢn van dit apparaat verwijderen?", + "cleanup_confirm_prompt_title": "Van dit apparaat verwijderen?", + "cleanup_deleted_assets": "{count} items verplaats naar prullenbak van apparaat", + "cleanup_deleting": "Naar prullenbak verplaatsen...", + "cleanup_found_assets": "Er zijn {count} backup bestanden gevonden", + "cleanup_found_assets_with_size": "Er zijn {count} back-upbestanden gevonden ({size})", + "cleanup_icloud_shared_albums_excluded": "Gedeelde albums van iCloud zijn uitgesloten van de scan", + "cleanup_no_assets_found": "Er zijn geen bestanden gevonden die aan bovenstaande criteria voldoen. Free Up Space kan alleen bestanden verwijderen die op de server zijn geback-upt", + "cleanup_preview_title": "Bestanden te verwijderen ({count})", + "cleanup_step3_description": "Scan naar back-upbestanden die overeenkomen met uw datum en behoud uw instellingen.", + "cleanup_step4_summary": "{count} bestanden (gemaakt vÃŗÃŗr {date}) die van uw lokale apparaat moeten worden verwijderd. Foto's blijven toegankelijk via de Immich-app.", + "cleanup_trash_hint": "Om de opslagruimte volledig vrij te maken, opent u de systeemgalerij-app en leegt u de prullenbak", "clear": "Wissen", "clear_all": "Alles wissen", "clear_all_recent_searches": "Wis alle recente zoekopdrachten", @@ -731,8 +780,10 @@ "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Voer wachtwoord in", "client_cert_import": "Importeren", - "client_cert_import_success_msg": "Clientcertificaat is geïmporteerd", + "client_cert_import_success_msg": "CliÃĢntcertificaat is geïmporteerd", "client_cert_invalid_msg": "Ongeldig certificaatbestand of verkeerd wachtwoord", + "client_cert_password_message": "Voer het wachtwoord voor dit certificaat in", + "client_cert_password_title": "Certificaat wachtwoord", "client_cert_remove_msg": "Clientcertificaat is verwijderd", "client_cert_subtitle": "Ondersteunt alleen PKCS12-formaat (.p12, .pfx). Het importeren/verwijderen van certificaten is alleen beschikbaar vÃŗÃŗr het inloggen", "client_cert_title": "SSL clientcertificaat [EXPERIMENTEEL]", @@ -742,7 +793,12 @@ "collapse_all": "Alles inklappen", "color": "Kleur", "color_theme": "Kleurenthema", - "command": "Opdracht", + "command": "Commando", + "command_palette_prompt": "Vind snel pagina's, acties of commando's", + "command_palette_to_close": "om te sluiten", + "command_palette_to_navigate": "om te navigeren", + "command_palette_to_select": "om te selecteren", + "command_palette_to_show_all": "om alles te tonen", "comment_deleted": "Opmerking verwijderd", "comment_options": "Opties voor opmerkingen", "comments_and_likes": "Opmerkingen & likes", @@ -787,6 +843,7 @@ "create_album": "Album aanmaken", "create_album_page_untitled": "Naamloos", "create_api_key": "API-sleutel maken", + "create_first_workflow": "Maak eerste werkstroom", "create_library": "Bibliotheek maken", "create_link": "Link maken", "create_link_to_share": "Gedeelde link maken", @@ -801,17 +858,25 @@ "create_tag": "Tag aanmaken", "create_tag_description": "Maak een nieuwe tag. Voor geneste tags, voer het volledige pad van de tag in, inclusief schuine strepen.", "create_user": "Gebruiker aanmaken", + "create_workflow": "Maak werkstroom", "created": "Aangemaakt", "created_at": "Aangemaakt", "creating_linked_albums": "Gekoppelde albums worden aangemaakt...", "crop": "Bijsnijden", + "crop_aspect_ratio_fixed": "Vast", + "crop_aspect_ratio_free": "Vrij", + "crop_aspect_ratio_original": "Origineel", "curated_object_page_title": "Dingen", "current_device": "Huidig apparaat", "current_pin_code": "Huidige pincode", "current_server_address": "Huidig serveradres", + "custom_date": "Aangepaste datum", "custom_locale": "Aangepaste landinstelling", "custom_locale_description": "Formatteer datums en getallen op basis van de taal en de regio", "custom_url": "Aangepaste URL", + "cutoff_date_description": "Bewaar foto's van de laatsteâ€Ļ", + "cutoff_day": "{count, plural, one {dag} other {dagen}}", + "cutoff_year": "{count, plural, one {jaar} other {jaar}}", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "dark": "Donker", @@ -867,6 +932,7 @@ "deselect_all": "Alles deselecteren", "details": "Details", "direction": "Richting", + "disable": "Uitschakelen", "disabled": "Uitgeschakeld", "disallow_edits": "Geen bewerkingen toestaan", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Ingesloten video's", "download_include_embedded_motion_videos_description": "Voeg video's die in bewegingsfoto's zijn ingebed toe als een apart bestand", "download_notfound": "Download niet gevonden", + "download_original": "Download origineel", "download_paused": "Download gepauseerd", "download_settings": "Downloaden", "download_settings_description": "Beheer instellingen voor het downloaden van items", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "Wachten om opnieuw te proberen", "downloading": "Downloaden", "downloading_asset_filename": "Downloaden asset {filename}", + "downloading_from_icloud": "Media aan het downloaden van iCloud", "downloading_media": "Media aan het downloaden", "drop_files_to_upload": "Zet bestanden ergens neer om ze te uploaden", "duplicates": "Duplicaten", @@ -929,11 +997,22 @@ "edit_tag": "Tag bewerken", "edit_title": "Titel bewerken", "edit_user": "Gebruiker bewerken", + "edit_workflow": "Werkstroom bewerken", "editor": "Bewerker", "editor_close_without_save_prompt": "De wijzigingen worden niet opgeslagen", "editor_close_without_save_title": "Editor sluiten?", - "editor_crop_tool_h2_aspect_ratios": "Beeldverhoudingen", - "editor_crop_tool_h2_rotation": "Rotatie", + "editor_confirm_reset_all_changes": "Weet u zeker dat u alle wijzigingen wilt resetten?", + "editor_discard_edits_confirm": "Wijzigingen verwijderen", + "editor_discard_edits_prompt": "U heeft wijzigingen aangebracht die nog niet zijn opgeslagen. Weet u zeker dat u deze wilt verwijderen?", + "editor_discard_edits_title": "Wijzigingen verwijderen?", + "editor_edits_applied_error": "Het toepassen van de wijzigingen is mislukt", + "editor_edits_applied_success": "De wijzigingen zijn succesvol toegepast", + "editor_flip_horizontal": "Horizontaal spiegelen", + "editor_flip_vertical": "Verticaal spiegelen", + "editor_orientation": "OriÃĢntatie", + "editor_reset_all_changes": "Reset wijzigingen", + "editor_rotate_left": "Draai 90° tegen de klok in", + "editor_rotate_right": "Draai 90° met de klok mee", "email": "E-mailadres", "email_notifications": "E-mailmeldingen", "empty_folder": "Deze map is leeg", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Sorteervolgorde van album wijzigen mislukt", "error_delete_face": "Fout bij verwijderen van gezicht uit het item", "error_getting_places": "Fout bij ophalen plaatsen", + "error_loading_albums": "Fout bij het laden van albums", "error_loading_image": "Fout bij laden afbeelding", "error_loading_partners": "Fout bij ophalen partners: {error}", + "error_retrieving_asset_information": "Fout bij ophalen item informatie", "error_saving_image": "Fout: {error}", "error_tag_face_bounding_box": "Fout bij taggen van gezicht - kan coÃļrdinaten van omvattend kader niet ophalen", "error_title": "Fout - Er is iets misgegaan", + "error_while_navigating": "Fout bij navigeren naar item", "errors": { "cannot_navigate_next_asset": "Kan niet naar het volgende item navigeren", "cannot_navigate_previous_asset": "Kan niet naar het vorige item navigeren", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "Kan inloggen met OAuth niet voltooie", "unable_to_connect": "Kan niet verbinden", "unable_to_copy_to_clipboard": "Kan niet naar klembord kopiÃĢren, zorg ervoor dat je de pagina via https opent", + "unable_to_create": "Kan werkstroom niet aanmaken", "unable_to_create_admin_account": "Kan beheerdersaccount niet aanmaken", "unable_to_create_api_key": "Kan geen nieuwe API-sleutel aanmaken", "unable_to_create_library": "Kan bibliotheek niet aanmaken", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Kan uitsluitingspatroon niet verwijderen", "unable_to_delete_shared_link": "Kan gedeelde link niet verwijderen", "unable_to_delete_user": "Kan gebruiker niet verwijderen", + "unable_to_delete_workflow": "Kan werkstroom niet verwijderen", "unable_to_download_files": "Kan bestanden niet downloaden", "unable_to_edit_exclusion_pattern": "Kan uitsluitingspatroon niet bewerken", "unable_to_empty_trash": "Kan prullenbak niet legen", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "Kan bibliotheek niet scannen", "unable_to_set_feature_photo": "Kan uitgelichte foto niet instellen", "unable_to_set_profile_picture": "Kan profielfoto niet instellen", + "unable_to_set_rating": "Kan waardering niet opslaan", "unable_to_submit_job": "Kan taak niet uitvoeren", "unable_to_trash_asset": "Kan item niet naar prullenbak verplaatsen", "unable_to_unlink_account": "Kan account niet ontkoppelen", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Kan instellingen niet bijwerken", "unable_to_update_timeline_display_status": "Kan de status van de tijdlijn niet bijwerken", "unable_to_update_user": "Kan gebruiker niet bijwerken", + "unable_to_update_workflow": "Kan werkstroom niet bijwerken", "unable_to_upload_file": "Kan bestand niet uploaden" }, + "errors_text": "Errors", "exclusion_pattern": "Uitsluitingspatroon", "exif": "Exif", "exif_bottom_sheet_description": "Beschrijving toevoegen...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "MENSEN", "exif_bottom_sheet_person_add_person": "Naam toevoegen", "exit_slideshow": "Diavoorstelling sluiten", + "expand": "Uitklappen", "expand_all": "Alles uitvouwen", "experimental_settings_new_asset_list_subtitle": "Werk in uitvoering", "experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen", @@ -1120,14 +1208,17 @@ "features": "Functies", "features_in_development": "Functies in ontwikkeling", "features_setting_description": "Beheer de app functies", - "file_name": "Bestandsnaam", "file_name_or_extension": "Bestandsnaam of extensie", + "file_name_text": "Bestandsnaam", + "file_name_with_value": "Bestandsnaam: {file_name}", "file_size": "Bestandsgrootte", "filename": "Bestandsnaam", "filetype": "Bestandstype", "filter": "Filter", + "filter_description": "Filtervoorwaarden voor doel items", "filter_people": "Filter op mensen", "filter_places": "Filter locaties", + "filters": "Filters", "find_them_fast": "Vind ze snel op naam door te zoeken", "first": "Eerste", "fix_incorrect_match": "Onjuiste overeenkomst corrigeren", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Bladeren door de mapweergave van de foto's en video's op het bestandssysteem", "forgot_pin_code_question": "Pincode vergeten?", "forward": "Vooruit", + "free_up_space": "Maak opslag vrij", + "free_up_space_description": "Verplaats back-ups van foto's en video's naar de prullenbak van uw apparaat om ruimte vrij te maken. Uw kopieÃĢn op de server blijven veilig.", + "free_up_space_settings_subtitle": "Maak opslagruimte vrij op uw apparaat", "full_path": "Volledig pad: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Deze functie gebruikt externe bronnen van Google om te kunnen werken.", "general": "Algemeen", "geolocation_instruction_location": "Klik op een item met gps-coÃļrdinaten om de locatie te gebruiken, of kies een locatie direct op de kaart", - "get_help": "Krijg hulp", + "get_help": "Hulp vragen", + "get_people_error": "Fout bij ophalen mensen", "get_wifiname_error": "Kon de WiFi-naam niet ophalen. Zorg ervoor dat je de benodigde machtigingen hebt verleend en verbonden bent met een WiFi-netwerk", "getting_started": "Aan de slag", "go_back": "Ga terug", @@ -1175,6 +1270,7 @@ "hide_named_person": "Verberg persoon {name}", "hide_password": "Verberg wachtwoord", "hide_person": "Verberg persoon", + "hide_schema": "Schema verbergen", "hide_text_recognition": "Tekst­herkenning verbergen", "hide_unnamed_people": "Verberg mensen zonder naam", "home_page_add_to_album_conflicts": "{added} items toegevoegd aan album {album}. {failed} items staan al in het album.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "Verwerking uitgevoerd op {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Taken", + "json_editor": "JSON bewerker", + "json_error": "JSON fout", "keep": "Behouden", + "keep_albums": "Houd albums bij", + "keep_albums_count": "Het behouden van {count} {count, plural, one {album} other {albums}}", "keep_all": "Behoud alle", + "keep_description": "Kies zelf welke gegevens op je apparaat blijven staan wanneer je ruimte vrijmaakt.", + "keep_favorites": "Bewaar favorieten", + "keep_on_device": "Blijf op het apparaat", + "keep_on_device_hint": "Selecteer items die u op dit apparaat wilt bewaren", "keep_this_delete_others": "Deze behouden, andere verwijderen", + "keeping": "Bewaren: {items}", "kept_this_deleted_others": "Dit item behouden en {count, plural, one {# ander item} other {# andere items}} verwijderd", "keyboard_shortcuts": "Sneltoetsen", "language": "Taal", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Inschakelen om video's automatisch te herhalen in de detailweergave.", "main_branch_warning": "Je gebruikt een ontwikkelingsversie. We raden je ten zeerste aan een releaseversie te gebruiken!", "main_menu": "Hoofdmenu", + "maintenance_action_restore": "Database herstellen", "maintenance_description": "Immich is in de onderhouds­modus gezet.", "maintenance_end": "Onderhouds­modus beÃĢindigen", "maintenance_end_error": "Onderhouds­modus beÃĢindigen mislukt.", "maintenance_logged_in_as": "Momenteel ingelogd als {user}", + "maintenance_restore_from_backup": "Herstellen vanaf backup", + "maintenance_restore_library": "Bibliotheek herstellen", + "maintenance_restore_library_confirm": "Als dit er goed uit ziet ga dan verder om de backup terug te zetten!", + "maintenance_restore_library_description": "Database herstellen", + "maintenance_restore_library_folder_has_files": "{folder} heeft {count} map(pen)", + "maintenance_restore_library_folder_no_files": "{folder} mist bestanden!", + "maintenance_restore_library_folder_pass": "leesbaar en schrijfbaar", + "maintenance_restore_library_folder_read_fail": "niet leesbaar", + "maintenance_restore_library_folder_write_fail": "niet schrijfbaar", + "maintenance_restore_library_hint_missing_files": "Er missen mogelijk belangrijke bestanden", + "maintenance_restore_library_hint_regenerate_later": "Deze kun je later opnieuw genereren in de instellingen", + "maintenance_restore_library_hint_storage_template_missing_files": "Gebruik je een opslagtemplate? Je mist misschien bestanden", + "maintenance_restore_library_loading": "Integriteitscontrole en heuristieken ladenâ€Ļ", + "maintenance_task_backup": "Backup van bestaande database makenâ€Ļ", + "maintenance_task_migrations": "Bezig met database migratiesâ€Ļ", + "maintenance_task_restore": "De gekozen backup terugzettenâ€Ļ", + "maintenance_task_rollback": "Terugzetten backup mislukt, herstelpunt terugzettenâ€Ļ", "maintenance_title": "TÄŗdelÄŗk niet beschikbaar", "make": "Merk", "manage_geolocation": "Beheer locatie", @@ -1408,6 +1531,8 @@ "minimize": "Minimaliseren", "minute": "Minuut", "minutes": "Minuten", + "mirror_horizontal": "Horizontaal", + "mirror_vertical": "Verticaal", "missing": "Missend", "mobile_app": "Mobiele app", "mobile_app_download_onboarding_note": "Download de mobiele app via de onderstaande opties", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Meer", "move": "Verplaats", + "move_down": "Naar beneden verplaatsen", "move_off_locked_folder": "Verplaats uit vergrendelde map", "move_to": "Verplaatsen naar", + "move_to_device_trash": "Naar prullenbak van apparaat", "move_to_lock_folder_action_prompt": "{count} item(s) toegevoegd aan de vergrendelde map", "move_to_locked_folder": "Verplaats naar vergrendelde map", "move_to_locked_folder_confirmation": "Deze foto’s en video’s worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map", + "move_up": "Naar boven verplaatsen", "moved_to_archive": "{count, plural, one {# item} other {# items}} verplaatst naar archief", "moved_to_library": "{count, plural, one {# item} other {# items}} verplaatst naar bibliotheek", "moved_to_trash": "Naar de prullenbak verplaatst", @@ -1430,6 +1558,7 @@ "my_albums": "Mijn albums", "name": "Naam", "name_or_nickname": "Naam of gebruikersnaam", + "name_required": "Naam is verplicht", "navigate": "Navigeer", "navigate_to_time": "Navigeer naar tijdstip", "network_requirement_photos_upload": "Gebruik mobiele data voor de backup van foto's", @@ -1437,7 +1566,7 @@ "network_requirements": "Netwerk vereisten", "network_requirements_updated": "Netwerkeisen zijn gewijzigd, back-upwachtrij wordt opnieuw ingesteld", "networking_settings": "Netwerk", - "networking_subtitle": "Beheer de instellingen voor de server-URL", + "networking_subtitle": "Beheer de server-eindpuntinstellingen", "never": "Nooit", "new_album": "Nieuw album", "new_api_key": "Nieuwe API-sleutel", @@ -1454,20 +1583,24 @@ "next": "Volgende", "next_memory": "Volgende herinnering", "no": "Nee", + "no_actions_added": "Geen acties toegevoegd", + "no_albums_found": "Geen albums gevonden", "no_albums_message": "Maak een album om je foto's en video's te organiseren", "no_albums_with_name_yet": "Het lijkt erop dat je nog geen albums met deze naam hebt.", "no_albums_yet": "Het lijkt erop dat je nog geen albums hebt.", "no_archived_assets_message": "Archiveer foto's en video's om ze te verbergen in je Foto's overzicht", - "no_assets_message": "KLIK HIER OM JE EERSTE FOTO TE UPLOADEN", + "no_assets_message": "Klik hier om je eerste foto te uploaden", "no_assets_to_show": "Geen foto's om te laten zien", "no_cast_devices_found": "Geen cast-apparaten gevonden", "no_checksum_local": "Geen checksum beschikbaar - kan lokale assets niet ophalen", "no_checksum_remote": "Geen checksum beschikbaar - kan online assets niet ophalen", + "no_configuration_needed": "Geen configuratie nodig", "no_devices": "Geen geautoriseerde apparaten", "no_duplicates_found": "Er zijn geen duplicaten gevonden.", "no_exif_info_available": "Geen exif info beschikbaar", "no_explore_results_message": "Upload meer foto's om je verzameling te verkennen.", "no_favorites_message": "Voeg favorieten toe om snel je beste foto's en video's te vinden", + "no_filters_added": "Geen filters toegevoegd", "no_libraries_message": "Maak een externe bibliotheek om je foto's en video's te bekijken", "no_local_assets_found": "Geen lokale assets gevonden met deze checksum", "no_location_set": "Geen locatie ingesteld", @@ -1481,11 +1614,11 @@ "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", + "none": "Geen", "not_allowed": "Niet toegestaan", "not_available": "n.v.t.", "not_in_any_album": "Niet in een album", "not_selected": "Niet geselecteerd", - "note_apply_storage_label_to_previously_uploaded assets": "Opmerking: om het opslaglabel toe te passen op eerder geÃŧploade items, voer de volgende taak uit", "notes": "Opmerkingen", "nothing_here_yet": "Hier staan nog geen items", "notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.", @@ -1515,6 +1648,7 @@ "online": "Online", "only_favorites": "Alleen favorieten", "open": "Openen", + "open_calendar": "Open kalender", "open_in_map_view": "Openen in kaartweergave", "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", @@ -1563,6 +1697,7 @@ "people": "Mensen", "people_edits_count": "{count, plural, one {# persoon} other {# mensen}} bijgewerkt", "people_feature_description": "Bladeren door foto's en video's gegroepeerd op personen", + "people_selected": "{count, plural, one {# persoon geselecteerd} other {# mensen geselecteerd}}", "people_sidebar_description": "Toon een link naar Mensen in de zijbalk", "permanent_deletion_warning": "Waarschuwing voor permanent verwijderen", "permanent_deletion_warning_setting_description": "Toon een waarschuwing bij het permanent verwijderen van items", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, other {# jaar}} oud", "person_birthdate": "Geboren op {date}", "person_hidden": "{name}{hidden, select, true { (verborgen)} other {}}", + "person_recognized": "Persoon herkend", + "person_selected": "Persoon geselecteerd", "photo_shared_all_users": "Het lijkt erop dat je foto's met alle gebruikers zijn gedeeld, of dat je geen gebruikers hebt om mee te delen.", "photos": "Foto's", "photos_and_videos": "Foto's & video's", "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} foto's}}", "photos_from_previous_years": "Foto's van voorgaande jaren", + "photos_only": "Enkel foto's", "pick_a_location": "Kies een locatie", "pick_custom_range": "Aangepast bereik", "pick_date_range": "Selecteer een datumbereik", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "De licentiesleutel van de server wordt beheerd door de beheerder", "query_asset_id": "Item-ID opvragen", "queue_status": "Wachtrij {count}/{total}", + "rate_asset": "Item waardering geven", "rating": "Sterwaardering", "rating_clear": "Waardering verwijderen", "rating_count": "{count, plural, one {# ster} other {# sterren}}", "rating_description": "De EXIF-waardering weergeven in het infopaneel", + "rating_set": "Item {rating, plural, one {# ster} other {# sterren}} gegeven", "reaction_options": "Reactie-opties", "read_changelog": "Lees wijzigingen", "readonly_mode_disabled": "Alleen-lezen modus uitgeschakeld", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# item} other {# items}} opnieuw toegewezen aan een nieuw persoon", "reassing_hint": "Geselecteerde items toewijzen aan een bestaand persoon", "recent": "Recent", - "recent-albums": "Recente albums", + "recent_albums": "Recente albums", "recent_searches": "Recente zoekopdrachten", "recently_added": "Onlangs toegevoegd", "recently_added_page_title": "Recent toegevoegd", @@ -1739,7 +1879,7 @@ "reset_people_visibility": "Zichtbaarheid mensen resetten", "reset_pin_code": "Reset pincode", "reset_pin_code_description": "Als je jouw pincode bent vergeten, neem dan contact op met de administrator van de server om deze te resetten", - "reset_pin_code_success": "Resetten van pincode gelukt", + "reset_pin_code_success": "Pincode succesvol gereset", "reset_pin_code_with_password": "Je kan je pincode altijd resetten met je wachtwoord", "reset_sqlite": "SQLite database resetten", "reset_sqlite_confirmation": "Ben je zeker dat je de SQLite database wilt resetten? Je zal moeten uitloggen om de data opnieuw te synchroniseren", @@ -1770,9 +1910,11 @@ "saved_settings": "Instellingen opgeslagen", "say_something": "Zeg iets", "scaffold_body_error_occurred": "Fout opgetreden", + "scan": "Scan", "scan_all_libraries": "Scan alle bibliotheken", - "scan_library": "Scannen", + "scan_library": "Scan", "scan_settings": "Scaninstellingen", + "scanning": "Scannen", "scanning_for_album": "Scannen voor album...", "search": "Zoeken", "search_albums": "Zoek albums", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Selecteer mediatype", "search_filter_ocr": "Zoeken op tekst herkend door OCR", "search_filter_people_title": "Selecteer mensen", + "search_filter_star_rating": "Sterbeoordeling", "search_for": "Zoeken naar", "search_for_existing_person": "Zoek naar bestaande persoon", "search_no_more_result": "Geen resultaten meer", @@ -1836,17 +1979,23 @@ "second": "Seconde", "see_all_people": "Bekijk alle mensen", "select": "Selecteer", + "select_album": "Selecteer album", "select_album_cover": "Selecteer albumomslag", + "select_albums": "Selecteer albums", "select_all": "Alles selecteren", "select_all_duplicates": "Selecteer alle duplicaten", "select_all_in": "Selecteer alles in {group}", "select_avatar_color": "Selecteer avatarkleur", + "select_count": "{count, plural, one {Selecteer #} other {Selecteer #}}", + "select_cutoff_date": "Selecteer einddatum", "select_face": "Selecteer gezicht", "select_featured_photo": "Selecteer uitgelichte foto", "select_from_computer": "Selecteer van computer", "select_keep_all": "Selecteer alles behouden", "select_library_owner": "Selecteer bibliotheekeigenaar", "select_new_face": "Selecteer nieuw gezicht", + "select_people": "Selecteer mensen", + "select_person": "Selecteer persoon", "select_person_to_tag": "Selecteer een persoon om te taggen", "select_photos": "Selecteer foto's", "select_trash_all": "Selecteer alles naar prullenbak verplaatsen", @@ -1856,7 +2005,7 @@ "selected_gps_coordinates": "Geselecteerde gps-coÃļrdinaten", "send_message": "Bericht versturen", "send_welcome_email": "Stuur welkomstmail", - "server_endpoint": "Server-URL", + "server_endpoint": "Server-eindpunt", "server_info_box_app_version": "Appversie", "server_info_box_server_url": "Server-URL", "server_offline": "Server offline", @@ -1982,6 +2131,7 @@ "show_password": "Toon wachtwoord", "show_person_options": "Toon persoonopties", "show_progress_bar": "Toon voortgangsbalk", + "show_schema": "Toon schema", "show_search_options": "Zoekopties weergeven", "show_shared_links": "Toon gedeelde links", "show_slideshow_transition": "Diavoorstellingsovergang tonen", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Doorgaan naar mappen", "skip_to_tags": "Doorgaan naar tags", "slideshow": "Diavoorstelling", + "slideshow_repeat": "Herhaal diavoorstelling", + "slideshow_repeat_description": "Keer terug naar het begin wanneer de diavoorstelling eindigt", "slideshow_settings": "Diavoorstelling instellingen", "sort_albums_by": "Sorteer albums op...", "sort_created": "Datum aangemaakt", @@ -2032,19 +2184,20 @@ "storage_quota": "Opslaglimiet", "storage_usage": "{used} van {available} gebruikt", "submit": "Verzenden", - "success": "Succes", + "success": "Gelukt", "suggestions": "Suggesties", "sunrise_on_the_beach": "Zonsopkomst op het strand", "support": "Ondersteuning", "support_and_feedback": "Ondersteuning & feedback", "support_third_party_description": "Je Immich installatie is door een derde partij samengesteld. Problemen die je ervaart, kunnen door dat pakket veroorzaakt zijn. Meld problemen in eerste instantie bij hen via de onderstaande links.", + "supporter": "Supporter", "swap_merge_direction": "Wissel richting voor samenvoegen om", "sync": "Synchroniseren", "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_status": "Sync Status", + "sync_status": "Synchronisatiestatus", "sync_status_subtitle": "Bekijk en beheer het synchronisatie systeem", "sync_upload_album_setting_subtitle": "Maak en upload je foto's en video's naar de geselecteerde albums op Immich", "tag": "Tag", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "De thema-instelling van de app kiezen", "theme_setting_three_stage_loading_subtitle": "Laden in drie fasen kan de laadprestaties verbeteren, maar veroorzaakt een aanzienlijk hogere netwerkbelasting", "theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen", + "then": "Dan", "they_will_be_merged_together": "Zij zullen worden samengevoegd", "third_party_resources": "Bronnen van derden", "time": "Tijd", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Selecteer items", "trash_page_title": "Prullenbak ({count})", "trashed_items_will_be_permanently_deleted_after": "Items in de prullenbak worden na {days, plural, one {# dag} other {# dagen}} permanent verwijderd.", + "trigger": "Trigger", + "trigger_asset_uploaded": "Item geÃŧpload", + "trigger_asset_uploaded_description": "Getriggerd wanneer een nieuw item geÃŧpload wordt", + "trigger_description": "Een gebeurtenis die het proces start", + "trigger_person_recognized": "Persoon herkend", + "trigger_person_recognized_description": "Getriggerd wanneer een persoon herkend is", + "trigger_type": "Trigger type", "troubleshoot": "Problemen oplossen", "type": "Type", "unable_to_change_pin_code": "Pincode kan niet gewijzigd worden", @@ -2123,6 +2284,7 @@ "unhide_person": "Persoon zichtbaar maken", "unknown": "Onbekend", "unknown_country": "Onbekend Land", + "unknown_date": "Onbekende datum", "unknown_year": "Onbekend jaar", "unlimited": "Onbeperkt", "unlink_motion_video": "Ontkoppel bewegende video", @@ -2139,17 +2301,19 @@ "unstack": "Ontstapelen", "unstack_action_prompt": "{count} item(s) ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", - "untagged": "Ongemarkeerd", + "unsupported_field_type": "Veldtype niet ondersteund", + "untagged": "Zonder tags", + "untitled_workflow": "Naamloze werkstroom", "up_next": "Volgende", "update_location_action_prompt": "Werk de locatie bij van {count} geselecteerde items met:", "updated_at": "GeÃŧpdatet", "updated_password": "Wachtwoord bijgewerkt", "upload": "Uploaden", - "upload_action_prompt": "{count} item(s) staan in de wachtrij voor uploaden", "upload_concurrency": "Aantal gelijktijdige uploads", "upload_details": "Uploaddetails", "upload_dialog_info": "Wil je een backup maken van de geselecteerde item(s) op de server?", "upload_dialog_title": "Item uploaden", + "upload_error_with_count": "Upload fout voor {count, plural, one {# item} other {# items}}", "upload_errors": "Upload voltooid met {count, plural, one {# fout} other {# fouten}}, vernieuw de pagina om de nieuwe items te zien.", "upload_finished": "Uploaden is voltooid", "upload_progress": "Resterend {remaining, number} - Verwerkt {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "Gebruik", "use_biometric": "Gebruik biometrische authenticatie", - "use_current_connection": "gebruik huidige verbinding", + "use_current_connection": "Gebruik huidige verbinding", "use_custom_date_range": "Gebruik in plaats daarvan een aangepast datumbereik", "user": "Gebruiker", "user_has_been_deleted": "Deze gebruiker is verwijderd.", @@ -2185,6 +2349,7 @@ "utilities": "Gereedschap", "validate": "Valideren", "validate_endpoint_error": "Vul een geldige URL in", + "validation_error": "Validatiefout", "variables": "Variabelen", "version": "Versie", "version_announcement_closing": "Je vriend, Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Speel videominiatuur af wanneer de muis over het item beweegt. Zelfs wanneer uitgeschakeld, kan het afspelen worden gestart door de muis over het afspeelpictogram te bewegen.", "videos": "Video's", "videos_count": "{count, plural, one {# video} other {# video's}}", + "videos_only": "Enkel video's", "view": "Bekijken", "view_album": "Bekijk album", "view_all": "Bekijk alle", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "Zet bovenaan de stapel", "viewer_unstack": "Ontstapel", "visibility_changed": "Zichtbaarheid gewijzigd voor {count, plural, one {# persoon} other {# mensen}}", + "visual": "Visueel", + "visual_builder": "Visuele bouwer", "waiting": "Wachtend", "waiting_count": "In de wacht: {count}", "warning": "Waarschuwing", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Welkom bij Immich", "width": "Breedte", "wifi_name": "WiFi-naam", - "workflow": "Workflow", + "workflow_delete_prompt": "Weet je zeker dat je deze werkstroom wilt verwijderen?", + "workflow_deleted": "Werkstroom verwijderd", + "workflow_description": "Werkstroom omschrijving", + "workflow_info": "Werkstroom info", + "workflow_json": "Werkstroom JSON", + "workflow_json_help": "Bewerk de werkstroom configuratie in JSON formaat. Wijzigingen worden gesynchroniseerd naar de visuele bouwer.", + "workflow_name": "Werkstroom naam", + "workflow_navigation_prompt": "Weet je zeker dat je weg wilt navigeren zonder je wijzigingen op te slaan?", + "workflow_summary": "Werkstroom samenvatting", + "workflow_update_success": "Werkstroom succesvol bijgewerkt", + "workflow_updated": "Werkstroom bijgewerkt", + "workflows": "Werkstromen", + "workflows_help_text": "Werkstromen automatiseren acties op je items gebaseerd op triggers en filters", "wrong_pin_code": "Onjuiste pincode", "year": "Jaar", "years_ago": "{years, plural, one {# jaar} other {# jaar}} geleden", "yes": "Ja", "you_dont_have_any_shared_links": "Je hebt geen gedeelde links", "your_wifi_name": "Je WiFi-naam", + "zero_to_clear_rating": "druk op 0 om de sterwaardering te verwijderen", "zoom_image": "Inzoomen", "zoom_to_bounds": "Zoom naar randen" } diff --git a/i18n/nn.json b/i18n/nn.json index 73a9d02c14..cbf81e4807 100644 --- a/i18n/nn.json +++ b/i18n/nn.json @@ -5,8 +5,10 @@ "acknowledge": "Merk som lese", "action": "Handling", "action_common_update": "Oppdater", + "action_description": "Eit sett med handlingar som skal utføras pÃĨ dei filtrerte ressursane", "actions": "Handlingar", "active": "Aktive", + "active_count": "Aktive: {count}", "activity": "Aktivitet", "activity_changed": "Aktivitet er {enabled, select, true {aktivert} other {deaktivert}}", "add": "Legg til", @@ -14,9 +16,14 @@ "add_a_location": "Legg til ein stad", "add_a_name": "Legg til eit namn", "add_a_title": "Legg til ein tittel", + "add_action": "Legg til handling", + "add_action_description": "Trykk for ÃĨ leggja til ei handling som skal utføras", + "add_assets": "Legg til ressursar", "add_birthday": "Legg til ein fødselsdag", "add_endpoint": "Legg til endepunkt", "add_exclusion_pattern": "Legg til unnlatingsmønster", + "add_filter": "Legg til filter", + "add_filter_description": "Trykk for ÃĨ leggja til eit filtervilkÃĨr", "add_location": "Legg til stad", "add_more_users": "Legg til fleire brukarar", "add_partner": "Legg til partnar", @@ -27,6 +34,7 @@ "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_album_bottom_sheet_some_local_assets": "Somme lokale eigedelar kunne ikkje leggjast til i album", "add_to_albums": "Legg til i album", "add_to_albums_count": "Legg til i album ({count})", "add_to_shared_album": "Legg til i delt album", diff --git a/i18n/package.json b/i18n/package.json new file mode 100644 index 0000000000..47748c28e8 --- /dev/null +++ b/i18n/package.json @@ -0,0 +1,13 @@ +{ + "name": "immich-i18n", + "version": "2.5.6", + "private": true, + "scripts": { + "format": "prettier --check .", + "format:fix": "prettier --write ." + }, + "devDependencies": { + "prettier": "^3.7.4", + "prettier-plugin-sort-json": "^4.1.1" + } +} diff --git a/i18n/pl.json b/i18n/pl.json index 12828dca83..d98533a41e 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -5,6 +5,7 @@ "acknowledge": "Zrozumiałem/łam", "action": "Akcja", "action_common_update": "Aktualizuj", + "action_description": "Zestaw akcji do wykonania na przefiltrowanych zasobach", "actions": "Akcje", "active": "Aktywne", "active_count": "Aktywne: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Dodaj lokalizację", "add_a_name": "Dodaj nazwę", "add_a_title": "Dodaj tytuł", + "add_action": "Dodaj akcję", + "add_action_description": "Kliknij, aby dodać akcję do wykonania", + "add_assets": "Dodaj zasoby", "add_birthday": "Dodaj datę urodzin", "add_endpoint": "Dodaj punkt końcowy", "add_exclusion_pattern": "Dodaj wzÃŗr wykluczający", + "add_filter": "Dodaj filtr", + "add_filter_description": "Kliknij, aby dodać warunek filtrowania", "add_location": "Dodaj lokalizację", "add_more_users": "Dodaj więcej uÅŧytkownikÃŗw", "add_partner": "Dodaj partnera", @@ -36,6 +42,7 @@ "add_to_shared_album": "Dodaj do udostępnionego albumu", "add_upload_to_stack": "Dodaj przesłane do stosu", "add_url": "Dodaj URL", + "add_workflow_step": "Dodaj krok przepływu pracy", "added_to_archive": "Dodano do archiwum", "added_to_favorites": "Dodano do ulubionych", "added_to_favorites_count": "Dodano {count, number} do ulubionych", @@ -97,6 +104,8 @@ "image_preview_description": "Obraz średniej wielkości z wyczyszczonymi metadanymi, uÅŧywany podczas przeglądania pojedynczego zasobu i do uczenia maszynowego", "image_preview_quality_description": "Jakość podglądu od 1 do 100. WyÅŧsza jest lepsza, ale tworzy większe pliki i moÅŧe spowolnić reakcję aplikacji. Ustawienie niskiej wartości moÅŧe wpłynąć na jakość uczenia maszynowego.", "image_preview_title": "Ustawienia podglądu", + "image_progressive": "Progresywny", + "image_progressive_description": "Koduj obrazy JPEG progresywnie, aby umoÅŧliwić stopniowe ładowanie i wyświetlanie. Nie ma to wpływu na obrazy WebP.", "image_quality": "Jakość", "image_resolution": "Rozdzielczość", "image_resolution_description": "WyÅŧsze rozdzielczości pozwalają zachować więcej szczegÃŗÅ‚Ãŗw, ale wymagają dłuÅŧszego kodowania, mają większy rozmiar pliku i mogą spowalniać reakcję aplikacji.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Włącz inteligentne wyszukiwanie", "machine_learning_smart_search_enabled_description": "JeÅŧeli wyłączone, obrazy nie będą przygotowywane do inteligentnego wyszukiwania.", "machine_learning_url_description": "URL serwera uczenia maszynowego. JeÅŧeli podano więcej niÅŧ jeden URL, do kaÅŧdego serwera po kolei będzie wysłane Åŧądanie dopÃŗki chociaÅŧ jeden nie odpowie, w kolejności od pierwszego do ostatniego. Serwery ktÃŗre nie odpowiedzą, zostaną tymczasowo ignorowane aÅŧ do momentu ich przejścia w stan online.", + "maintenance_delete_backup": "Usuń kopię zapasową", + "maintenance_delete_backup_description": "Ten plik zostanie nieodwracalnie usunięty.", + "maintenance_delete_error": "Nie udało się usunąć kopii zapasowej.", + "maintenance_restore_backup": "PrzywrÃŗÄ‡ kopię zapasową", + "maintenance_restore_backup_description": "Immich zostanie wyczyszczony i przywrÃŗcony z wybranej kopii zapasowej. Przed rozpoczęciem operacji zostanie utworzona kopia zapasowa.", + "maintenance_restore_backup_different_version": "Ta kopia zapasowa została utworzona przy uÅŧyciu innej wersji Immich!", + "maintenance_restore_backup_unknown_version": "Nie moÅŧna określić wersji kopii zapasowej.", + "maintenance_restore_database_backup": "PrzywrÃŗÄ‡ kopię zapasową bazy danych", + "maintenance_restore_database_backup_description": "PowrÃŗt do poprzedniego stanu bazy danych przy uÅŧyciu pliku kopii zapasowej", "maintenance_settings": "Konserwacja", "maintenance_settings_description": "Przełącza Immich w tryb konserwacji.", - "maintenance_start": "Uruchom tryb konserwacji", + "maintenance_start": "Przełącz na tryb konserwacji", "maintenance_start_error": "Nie udało się uruchomić trybu konserwacji.", + "maintenance_upload_backup": "Prześlij plik kopii zapasowej bazy danych", + "maintenance_upload_backup_error": "Nie moÅŧna przesłać kopii zapasowej. Czy jest to plik .sql/.sql.gz?", "manage_concurrency": "Zarządzaj wspÃŗÅ‚bieÅŧnością zadań", "manage_concurrency_description": "PrzejdÅē do strony zadań, aby zarządzać wspÃŗÅ‚bieÅŧnością zadań", "manage_log_settings": "Zarządzaj ustawieniami logÃŗw", @@ -252,7 +272,7 @@ "oauth_auto_register": "Automatyczna rejestracja", "oauth_auto_register_description": "Automatycznie rejestruj nowych uÅŧytkownikÃŗw po zalogowaniu się za pomocą protokołu OAuth", "oauth_button_text": "Tekst na przycisku", - "oauth_client_secret_description": "Wymagane jeÅŧeli PKCE (Proof Key for Code Exchange) nie jest wspierane przez dostawcę OAuth", + "oauth_client_secret_description": "Wymagane dla poufnego klienta lub jeśli PKCE (Proof Key for Code Exchange) nie jest obsługiwane dla klienta publicznego.", "oauth_enable_description": "Loguj się za pomocą OAuth", "oauth_mobile_redirect_uri": "Mobilny adres zwrotny", "oauth_mobile_redirect_uri_override": "Zapasowy URI przekierowania mobilnego", @@ -331,7 +351,7 @@ "template_settings": "Szablony Powiadomień", "template_settings_description": "Zarządzaj niestandardowymi szablonami powiadomień e-mail", "theme_custom_css_settings": "Własny CSS", - "theme_custom_css_settings_description": "Własny CSS pozwala na zmianę wyglądu aplikacji Immich.", + "theme_custom_css_settings_description": "Własny CSS pozwala na zmianę wyglądu aplikacji Immich.", "theme_settings": "Ustawienia Motywu", "theme_settings_description": "Zarządzaj wyglądem aplikacji Immich w przeglądarce", "thumbnail_generation_job": "StwÃŗrz Miniaturki", @@ -431,8 +451,11 @@ "admin_password": "Hasło Administratora", "administration": "Administracja", "advanced": "Zaawansowane", - "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_clear_image_cache": "Wyczyść pamięć podręczną obrazÃŗw", + "advanced_settings_clear_image_cache_error": "Nie udało się wyczyścić pamięci podręcznej obrazÃŗw", + "advanced_settings_clear_image_cache_success": "Pomyślnie wyczyszczono {size}", + "advanced_settings_enable_alternate_media_filter_subtitle": "UÅŧyj tej opcji do filtrowania mediÃŗw podczas synchronizacji opartej na alternatywnych kryteriach. 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 albumÃŗw na urządzeniu", "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 lokalnych zasobÃŗw. Aktywuj to ustawienie, aby ładować zdalne obrazy.", "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne", @@ -467,10 +490,12 @@ "album_remove_user": "Usunąć uÅŧytkownika?", "album_remove_user_confirmation": "Na pewno chcesz usunąć {user}?", "album_search_not_found": "Nie znaleziono albumÃŗw pasujących do Twojego wyszukiwania", + "album_selected": "Wybrany album", "album_share_no_users": "Wygląda na to, Åŧe ten album albo udostępniono wszystkim uÅŧytkownikom, albo nie ma komu go udostępnić.", "album_summary": "Podsumowanie albumu", "album_updated": "Album zaktualizowany", "album_updated_setting_description": "Otrzymaj powiadomienie e-mail, gdy do udostępnionego Ci albumu zostaną dodane nowe zasoby", + "album_upload_assets": "Prześlij zasoby ze swojego komputera i dodaj je do albumu", "album_user_left": "Opuszczono {album}", "album_user_removed": "Usunięto {user}", "album_viewer_appbar_delete_confirm": "Czy na pewno chcesz usunąć ten album ze swojego konta?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Początkowa kolejność sortowania zasobÃŗw przy tworzeniu nowych albumÃŗw.", "albums_feature_description": "Kolekcje zasobÃŗw, ktÃŗre moÅŧna udostępniać innym uÅŧytkownikom.", "albums_on_device_count": "Albumy na urządzeniu ({count})", + "albums_selected": "{count, plural, one {# wybrany album} few {# wybrane albumy} other {# wybranych albumÃŗw}}", "all": "Wszystkie", "all_albums": "Wszystkie albumy", "all_people": "Wszystkie osoby", + "all_photos": "Wszystkie zdjęcia", "all_videos": "Wszystkie filmy", "allow_dark_mode": "Zezwalaj na tryb ciemny", "allow_edits": "PozwÃŗl edytować", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "ZezwÃŗl uÅŧytkownikowi publicznemu na przesyłanie plikÃŗw", "allowed": "Dozwolone", "alt_text_qr_code": "Obrazek kodu QR", + "always_keep": "Zawsze zachowuj", + "always_keep_photos_hint": "Zwolnij Miejsce zachowa wszystkie zdjęcia na tym urządzeniu.", + "always_keep_videos_hint": "Zwolnij Miejsce zachowa wszystkie filmy na tym urządzeniu.", "anti_clockwise": "Przeciwnie do ruchu wskazÃŗwek zegara", "api_key": "Klucz API", "api_key_description": "Widzisz tę wartość po raz pierwszy i ostatni, więc lepiej ją skopiuj przed zamknięciem okna.", @@ -520,14 +550,16 @@ "archive_page_title": "Archiwum {count}", "archive_size": "Rozmiar archiwum", "archive_size_description": "Podziel pobierane pliki na więcej niÅŧ jedno archiwum, jeÅŧeli rozmiar archiwum przekroczy tę wartość w GiB", - "archived": "Zarchiwizowane", + "archived": "Archiwum", "archived_count": "{count, plural, other {Zarchiwizowano #}}", "are_these_the_same_person": "Czy to jedna i ta sama osoba?", "are_you_sure_to_do_this": "Czy aby na pewno chcesz to zrobić?", + "array_field_not_fully_supported": "Elementy tablicy wymagają ręcznej edycji JSON", "asset_action_delete_err_read_only": "Nie moÅŧna usunąć zasobÃŗw tylko do odczytu, pomijam", "asset_action_share_err_offline": "Nie moÅŧna pobrać zasobÃŗw offline, pomijam", "asset_added_to_album": "Dodano do albumu", "asset_adding_to_album": "Dodawanie do albumuâ€Ļ", + "asset_created": "Utworzono zasÃŗb", "asset_description_updated": "Zaktualizowano opis zasobu", "asset_filename_is_offline": "ZasÃŗb {filename} jest offline", "asset_has_unassigned_faces": "ZasÃŗb ma nieprzypisane twarze", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Układ", "asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć", "asset_list_settings_title": "Siatka Zdjęć", + "asset_not_found_on_device_android": "Nie znaleziono zasobu na urządzeniu", + "asset_not_found_on_device_ios": "Nie znaleziono zasobu na urządzeniu. Jeśli korzystasz z usługi iCloud, zasÃŗb moÅŧe być niedostępny z powodu uszkodzonego pliku przechowywanego w usłudze iCloud", + "asset_not_found_on_icloud": "Nie znaleziono zasobu w usłudze iCloud. ZasÃŗb moÅŧe być niedostępny z powodu uszkodzonego pliku przechowywanego w usłudze iCloud", "asset_offline": "ZasÃŗb niedostępny", "asset_offline_description": "Ten zewnętrzny zasÃŗb nie jest juÅŧ dostępny na dysku. Aby uzyskać pomoc, skontaktuj się z administratorem Immich.", "asset_restored_successfully": "ZasÃŗb został pomyślnie przywrÃŗcony", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Hasła nie są zgodne", "change_password_form_reenter_new_password": "WprowadÅē ponownie Nowe Hasło", "change_pin_code": "Zmień kod PIN", + "change_trigger": "Zmień wyzwalacz", + "change_trigger_prompt": "Czy na pewno chcesz zmienić wyzwalacz? Spowoduje to usunięcie wszystkich istniejących akcji i filtrÃŗw.", "change_your_password": "Zmień swoje hasło", "changed_visibility_successfully": "Pomyślnie zmieniono widoczność", "charging": "Ładowanie", @@ -722,6 +759,18 @@ "checksum": "Suma kontrolna", "choose_matching_people_to_merge": "Wybierz osoby, aby złączyć je w jedną", "city": "Miasto", + "cleanup_confirm_description": "Immich znalazł {count} zasobÃŗw (utworzonych przed {date}) z kopią zapasową bezpiecznie przesłaną na serwer. Czy chcesz usunąć lokalne kopie z tego urządzenia?", + "cleanup_confirm_prompt_title": "Usunąć z tego urządzenia?", + "cleanup_deleted_assets": "Przeniesiono {count} zasobÃŗw do kosza urządzenia", + "cleanup_deleting": "Przenoszenie do kosza...", + "cleanup_found_assets": "Znaleziono {count} zasobÃŗw z przesłaną kopią zapasową", + "cleanup_found_assets_with_size": "Znaleziono {count} zasobÃŗw z kopią zapasową ({size})", + "cleanup_icloud_shared_albums_excluded": "Udostępniane albumy iCloud są wyłączone ze skanowania", + "cleanup_no_assets_found": "Nie znaleziono Åŧadnych zasobÃŗw spełniających podane kryteria. Zwolnij Miejsce moÅŧe usuwać jedynie zasoby, ktÃŗre posiadają kopię zapasową na serwerze", + "cleanup_preview_title": "Zasoby do usunięcia ({count})", + "cleanup_step3_description": "Wyszukaj zasoby z kopią zapasową, zgodne z Twoimi ustawieniami.", + "cleanup_step4_summary": "{count} zasoby (utworzone przed {date}) zostaną usunięte z tego urządzenia. Zdjęcia będą nadal dostępne w aplikacji Immich.", + "cleanup_trash_hint": "Aby całkowicie odzyskać miejsce w pamięci, otwÃŗrz aplikację galerii systemowej i oprÃŗÅŧnij kosz", "clear": "Wyczyść", "clear_all": "Wyczyść wszystko", "clear_all_recent_searches": "Usuń ostatnio wyszukiwane", @@ -733,6 +782,8 @@ "client_cert_import": "Importuj", "client_cert_import_success_msg": "Certyfikat klienta został zaimportowany", "client_cert_invalid_msg": "Nieprawidłowy plik certyfikatu lub nieprawidłowe hasło", + "client_cert_password_message": "WprowadÅē hasło dla tego certyfikatu", + "client_cert_password_title": "Hasło certyfikatu", "client_cert_remove_msg": "Certyfikat klienta został usunięty", "client_cert_subtitle": "Obsługuje wyłącznie format PKCS12 (.p12, .pfx). Importowanie/usuwanie certyfikatÃŗw jest dostępne wyłącznie przed zalogowaniem", "client_cert_title": "Certyfikat klienta SSL [EKSPERYMENTALNE]", @@ -787,6 +838,7 @@ "create_album": "UtwÃŗrz album", "create_album_page_untitled": "Bez tytułu", "create_api_key": "UtwÃŗrz klucz API", + "create_first_workflow": "StwÃŗrz pierwszy przepływ pracy", "create_library": "StwÃŗrz Bibliotekę", "create_link": "UtwÃŗrz link", "create_link_to_share": "UtwÃŗrz link do udostępnienia", @@ -801,17 +853,25 @@ "create_tag": "StwÃŗrz etykietę", "create_tag_description": "StwÃŗrz nową etykietę. Dla etykiet zagnieÅŧdÅŧonych, wprowadÅē pełną ścieÅŧkę etykiety zawierającą ukośniki.", "create_user": "StwÃŗrz uÅŧytkownika", + "create_workflow": "StwÃŗrz przepływ pracy", "created": "Utworzono", "created_at": "Utworzony", "creating_linked_albums": "Tworzenie połączonych albumÃŗw...", "crop": "Przytnij", + "crop_aspect_ratio_fixed": "Stałe", + "crop_aspect_ratio_free": "Dowolne", + "crop_aspect_ratio_original": "Oryginalne", "curated_object_page_title": "Rzeczy", "current_device": "Obecne urządzenie", "current_pin_code": "Aktualny kod PIN", "current_server_address": "Aktualny adres serwera", + "custom_date": "Data niestandardowa", "custom_locale": "Niestandardowy Region", "custom_locale_description": "Formatuj daty i liczby na podstawie języka i regionu", "custom_url": "Niestandardowy URL", + "cutoff_date_description": "Zachowaj zdjęcia z ostatnichâ€Ļ", + "cutoff_day": "{count, plural, one {dzień} other {dni}}", + "cutoff_year": "{count, plural, one {rok} few {lata} other {lat}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Ciemny", @@ -867,6 +927,7 @@ "deselect_all": "Odznacz wszystkie", "details": "SzczegÃŗÅ‚y", "direction": "Kierunek", + "disable": "Wyłącz", "disabled": "Wyłączone", "disallow_edits": "Nie pozwalaj edytować", "discord": "Discord", @@ -892,8 +953,9 @@ "download_include_embedded_motion_videos": "Pobierz filmy ruchomych zdjęć", "download_include_embedded_motion_videos_description": "Dołącz filmy osadzone w ruchomych zdjęciach jako oddzielny plik", "download_notfound": "Nie znaleziono pliku do pobrania", + "download_original": "Pobierz oryginał", "download_paused": "Pobieranie wstrzymane", - "download_settings": "Pobieranie", + "download_settings": "Pobierz", "download_settings_description": "Zarządzaj pobieraniem zasobÃŗw", "download_started": "Pobieranie rozpoczęte", "download_sucess": "Udane pobieranie", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Oczekiwanie na ponowną prÃŗbę", "downloading": "Pobieranie", "downloading_asset_filename": "Pobieranie zasobu {filename}", + "downloading_from_icloud": "Pobieranie z iCloud", "downloading_media": "Pobieranie multimediÃŗw", "drop_files_to_upload": "Upuść pliki w dowolnym miejscu, aby je przesłać", "duplicates": "Duplikaty", @@ -929,11 +992,22 @@ "edit_tag": "Edytuj etykietę", "edit_title": "Edytuj Tytuł", "edit_user": "Edytuj uÅŧytkownika", + "edit_workflow": "Edytuj przepływ pracy", "editor": "Edytor", "editor_close_without_save_prompt": "Zmiany nie zostaną zapisane", "editor_close_without_save_title": "Zamknąć edytor?", - "editor_crop_tool_h2_aspect_ratios": "Proporcje obrazu", - "editor_crop_tool_h2_rotation": "ObrÃŗt", + "editor_confirm_reset_all_changes": "Czy na pewno chcesz zresetować wszystkie zmiany?", + "editor_discard_edits_confirm": "Odrzuć zmiany", + "editor_discard_edits_prompt": "Masz niezapisane zmiany. Czy na pewno chcesz je odrzucić?", + "editor_discard_edits_title": "Odrzucić zmiany?", + "editor_edits_applied_error": "Nie udało się zastosować zmian", + "editor_edits_applied_success": "Zmiany zostały pomyślnie zastosowane", + "editor_flip_horizontal": "OdwrÃŗÄ‡ poziomo", + "editor_flip_vertical": "OdwrÃŗÄ‡ pionowo", + "editor_orientation": "Orientacja", + "editor_reset_all_changes": "Zresetuj zmiany", + "editor_rotate_left": "ObrÃŗÄ‡ o 90° przeciwnie do ruchu wskazÃŗwek zegara", + "editor_rotate_right": "ObrÃŗÄ‡ o 90° zgodnie z ruchem wskazÃŗwek zegara", "email": "E-mail", "email_notifications": "Powiadomienia e-mail", "empty_folder": "Ten folder jest pusty", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Nie udało się zmienić kolejności sortowania albumÃŗw", "error_delete_face": "Błąd podczas usuwania twarzy z zasobÃŗw", "error_getting_places": "Błąd podczas pozyskiwania lokalizacji", + "error_loading_albums": "Błąd podczas ładowania albumÃŗw", "error_loading_image": "Błąd podczas ładowania zdjęcia", "error_loading_partners": "Błąd podczas ładowania partnerÃŗw: {error}", + "error_retrieving_asset_information": "Błąd podczas pobierania informacji o zasobie", "error_saving_image": "Błąd: {error}", "error_tag_face_bounding_box": "Błąd przy dodawaniu etykiety dla tej twarzy - nie moÅŧe uzyskać wspÃŗÅ‚rzędnych granicznych", "error_title": "Błąd - Coś poszło nie tak", + "error_while_navigating": "Błąd podczas przechodzenia do zasobu", "errors": { "cannot_navigate_next_asset": "Nie moÅŧna przejść do następnego zasobu", "cannot_navigate_previous_asset": "Nie moÅŧna przejść do poprzedniego zasobu", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "Nie moÅŧna ukończyć logowania przy uÅŧyciu OAuth", "unable_to_connect": "Nie moÅŧna się połączyć", "unable_to_copy_to_clipboard": "Nie moÅŧna skopiować do schowka, upewnij się, Åŧe łączysz się ze stroną przez https", + "unable_to_create": "Nie moÅŧna utworzyć przepływu pracy", "unable_to_create_admin_account": "Nie moÅŧna utworzyć konta administratora", "unable_to_create_api_key": "Nie moÅŧna stworzyć Klucza API", "unable_to_create_library": "Nie moÅŧna stworzyć biblioteki", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Nie moÅŧna usunąć wzoru wykluczającego", "unable_to_delete_shared_link": "Nie moÅŧna usunąć udostępnionego linku", "unable_to_delete_user": "Nie moÅŧna usunąć uÅŧytkownika", + "unable_to_delete_workflow": "Nie moÅŧna usunąć przepływu pracy", "unable_to_download_files": "Nie moÅŧna pobrać plikÃŗw", "unable_to_edit_exclusion_pattern": "Nie moÅŧna zmienić wzoru wykluczającego", "unable_to_empty_trash": "Nie moÅŧna oprÃŗÅŧnić kosza", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "Nie moÅŧna przeskanować biblioteki", "unable_to_set_feature_photo": "Nie moÅŧna ustawić zdjęcia gÅ‚Ãŗwnego", "unable_to_set_profile_picture": "Nie moÅŧna zmienić zdjęcia profilowego", + "unable_to_set_rating": "Nie moÅŧna ustawić oceny", "unable_to_submit_job": "Nie moÅŧna przesłać zadania", "unable_to_trash_asset": "Nie moÅŧna przenieść zasobu do kosza", "unable_to_unlink_account": "Nie moÅŧna odłączyć konta", @@ -1074,10 +1154,12 @@ "unable_to_update_settings": "Nie moÅŧna zmienić ustawień", "unable_to_update_timeline_display_status": "Nie moÅŧna zaktualizować stanu wyświetlania na osi czasu", "unable_to_update_user": "Nie moÅŧna zmienić uÅŧytkownika", + "unable_to_update_workflow": "Nie moÅŧna zaktualizować przepływu pracy", "unable_to_upload_file": "Nie moÅŧna przesłać pliku" }, + "errors_text": "Błędy", "exclusion_pattern": "Szablon wykluczeń", - "exif": "Metadane EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "Dodaj Opis...", "exif_bottom_sheet_description_error": "Wystąpił błąd podczas aktualizacji opisu", "exif_bottom_sheet_details": "SZCZEGÓŁY", @@ -1120,14 +1202,17 @@ "features": "Funkcje", "features_in_development": "Funkcje w fazie rozwoju", "features_setting_description": "Zarządzaj funkcjami aplikacji", - "file_name": "Nazwa pliku", "file_name_or_extension": "Nazwie lub rozszerzeniu pliku", + "file_name_text": "Nazwa pliku", + "file_name_with_value": "Nazwa pliku: {file_name}", "file_size": "Rozmiar pliku", "filename": "Nazwa pliku", "filetype": "Typ pliku", "filter": "Filtr", + "filter_description": "Warunki filtrowania wybranych zasobÃŗw", "filter_people": "Szukaj osoby", "filter_places": "Filtruj miejsca", + "filters": "Filtry", "find_them_fast": "Wyszukuj szybciej przypisując nazwę", "first": "Pierwszy", "fix_incorrect_match": "Napraw nieprawidłowe dopasowanie", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Przeglądanie zdjęć i filmÃŗw w widoku folderÃŗw", "forgot_pin_code_question": "Nie pamiętasz kodu PIN?", "forward": "Do przodu", + "free_up_space": "Zwolnij miejsce w pamięci", + "free_up_space_description": "Przenieś zdjęcia i filmy z kopią zapasową do kosza urządzenia, aby zwolnić miejsce. Twoje kopie na serwerze pozostają bezpieczne.", + "free_up_space_settings_subtitle": "Zwolnij miejsce w pamięci urządzenia", "full_path": "Pełna ścieÅŧka: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcja , aby działać, ładuje zewnętrzne zasoby z Google.", "general": "OgÃŗlne", "geolocation_instruction_location": "Kliknij na zasÃŗb z wspÃŗÅ‚rzędnymi GPS, aby uÅŧyć jego lokalizacji, lub wybierz lokalizację bezpośrednio z mapy", "get_help": "Pomoc", + "get_people_error": "Błąd podczas pobierania osÃŗb", "get_wifiname_error": "Nie moÅŧna uzyskać nazwy Wi-Fi. Upewnij się, Åŧe udzieliłeś niezbędnych uprawnień i jesteś połączony z siecią Wi-Fi", "getting_started": "Pierwsze kroki", "go_back": "Wstecz", @@ -1175,6 +1264,7 @@ "hide_named_person": "Ukryj osobę {name}", "hide_password": "Ukryj hasło", "hide_person": "Ukryj osobę", + "hide_schema": "Ukryj schemat", "hide_text_recognition": "Ukryj rozpoznawanie tekstu", "hide_unnamed_people": "Ukryj nienazwaną osobę", "home_page_add_to_album_conflicts": "Dodano {added} zasoby do albumu {album}. {failed} zasobÃŗw jest juÅŧ w albumie.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Przetwarzanie przebiegło {dateTime}", "items_count": "{count, plural, one {# element} few {# elementy} other {# elementÃŗw}}", "jobs": "Zadania", + "json_editor": "Edytor JSON", + "json_error": "Błąd JSON", "keep": "Zachowaj", + "keep_albums": "Zachowaj albumy", + "keep_albums_count": "Zachowuję {count} {count, plural, one {album} few {albumy} other {albumÃŗw}}", "keep_all": "Zachowaj wszystko", + "keep_description": "Wybierz, co zachować na Twoim urządzeniu przy zwalnianiu miejsca.", + "keep_favorites": "Zachowaj ulubione", + "keep_on_device": "Zachowaj na urządzeniu", + "keep_on_device_hint": "Wybierz elementy, ktÃŗre chcesz zachować na tym urządzeniu", "keep_this_delete_others": "Zachowaj to, usuń pozostałe", + "keeping": "Zachowuję:{items}", "kept_this_deleted_others": "Zachowano ten zasÃŗb i usunięto {count, plural, one {#zasÃŗb} other {#zasoby}}", "keyboard_shortcuts": "SkrÃŗty klawiaturowe", "language": "Język", @@ -1343,10 +1442,28 @@ "loop_videos_description": "Włącz automatyczne odtwarzanie w pętli filmu w widoku szczegÃŗÅ‚owym.", "main_branch_warning": "UÅŧywasz wersji deweloperskiej. Zdecydowanie zalecamy korzystanie z wydanej wersji aplikacji!", "main_menu": "Menu gÅ‚Ãŗwne", + "maintenance_action_restore": "Przywracanie bazy danych", "maintenance_description": "Immich został przełączony w tryb konserwacji.", "maintenance_end": "Zakończ tryb konserwacji", "maintenance_end_error": "Nie udało się zakończyć trybu konserwacji.", "maintenance_logged_in_as": "Obecnie zalogowano jako {user}", + "maintenance_restore_from_backup": "PrzywrÃŗÄ‡ z kopii zapasowej", + "maintenance_restore_library": "PrzywrÃŗÄ‡ swoją bibliotekę", + "maintenance_restore_library_confirm": "Jeśli wszystko wygląda poprawnie, kontynuuj przywracanie kopii zapasowej!", + "maintenance_restore_library_description": "Przywracanie bazy danych", + "maintenance_restore_library_folder_has_files": "{folder} zawiera {count} folder(Ãŗw)", + "maintenance_restore_library_folder_no_files": "W {folder} brakuje plikÃŗw!", + "maintenance_restore_library_folder_pass": "z uprawnieniami odczytu i zapisu", + "maintenance_restore_library_folder_read_fail": "brak uprawnień do odczytu", + "maintenance_restore_library_folder_write_fail": "brak uprawnień do zapisu", + "maintenance_restore_library_hint_missing_files": "Być moÅŧe brakuje waÅŧnych plikÃŗw", + "maintenance_restore_library_hint_regenerate_later": "MoÅŧesz je pÃŗÅēniej odtworzyć w ustawieniach", + "maintenance_restore_library_hint_storage_template_missing_files": "Korzystasz z Szablonu Magazynu? Być moÅŧe brakuje Ci plikÃŗw", + "maintenance_restore_library_loading": "Ładowanie kontroli integralności i heurystykiâ€Ļ", + "maintenance_task_backup": "Tworzenie kopii zapasowej istniejącej bazy danychâ€Ļ", + "maintenance_task_migrations": "Przeprowadzanie migracji bazy danychâ€Ļ", + "maintenance_task_restore": "Przywracanie wybranej kopii zapasowejâ€Ļ", + "maintenance_task_rollback": "Przywracanie nie powiodło się, powrÃŗt do punktu przywracaniaâ€Ļ", "maintenance_title": "Tymczasowo niedostępne", "make": "Marka", "manage_geolocation": "Zarządzaj lokalizacją", @@ -1408,6 +1525,8 @@ "minimize": "Zminimalizuj", "minute": "Minuta", "minutes": "Minuty", + "mirror_horizontal": "Poziomo", + "mirror_vertical": "Pionowo", "missing": "Brakujące", "mobile_app": "Aplikacja mobilna", "mobile_app_download_onboarding_note": "Pobierz towarzyszącą aplikację mobilną, korzystając z następujących opcji", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Więcej", "move": "Przenieś", + "move_down": "Przesuń w dÃŗÅ‚", "move_off_locked_folder": "Przenieś z folderu zablokowanego", "move_to": "Przenieś do", + "move_to_device_trash": "Przenieś do kosza urządzenia", "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", + "move_up": "Przesuń w gÃŗrę", "moved_to_archive": "Przeniesiono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do archiwum", "moved_to_library": "Przeniesiono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}} do biblioteki", "moved_to_trash": "Przeniesiono do kosza", @@ -1430,6 +1552,7 @@ "my_albums": "Moje albumy", "name": "Nazwa", "name_or_nickname": "Nazwa lub pseudonim", + "name_required": "Imię jest wymagane", "navigate": "Nawiguj", "navigate_to_time": "Nawiguj do czasu", "network_requirement_photos_upload": "UÅŧywaj danych komÃŗrkowych do tworzenia kopii zapasowych zdjęć", @@ -1454,20 +1577,24 @@ "next": "Dalej", "next_memory": "Następne wspomnienie", "no": "Nie", + "no_actions_added": "Nie dodano jeszcze Åŧadnych akcji", + "no_albums_found": "Nie znaleziono albumÃŗw", "no_albums_message": "StwÃŗrz album, aby organizować Twoje zdjęcia i filmy", "no_albums_with_name_yet": "Wygląda na to, Åŧe nie masz jeszcze Åŧadnych albumÃŗw o tej nazwie.", "no_albums_yet": "Wygląda na to, Åŧe nie masz jeszcze Åŧadnych albumÃŗw.", "no_archived_assets_message": "Archiwizuj zdjęcia i filmy, aby ukryć je ze strony Zdjęcia", - "no_assets_message": "KLIKNIJ, ABY WYSŁAĆ PIERWSZE ZDJĘCIE", + "no_assets_message": "Kliknij, aby przesłać swoje pierwsze zdjęcie", "no_assets_to_show": "Brak zasobÃŗw do pokazania", "no_cast_devices_found": "Nie znaleziono urządzeń do przesyłania strumieniowego", "no_checksum_local": "Brak sumy kontrolnej - nie moÅŧna pobrać lokalnych zasobÃŗw", "no_checksum_remote": "Brak sumy kontrolnej - nie moÅŧna pobrać zdalnego zasobu", + "no_configuration_needed": "Nie wymaga konfiguracji", "no_devices": "Brak autoryzowanych urządzeń", "no_duplicates_found": "Nie znaleziono duplikatÃŗw.", "no_exif_info_available": "Nie znaleziono informacji exif", "no_explore_results_message": "Prześlij więcej zdjęć, aby przeglądać swÃŗj zbiÃŗr.", "no_favorites_message": "Dodaj ulubione aby szybko znaleÅēć swoje najlepsze zdjęcia i filmy", + "no_filters_added": "Nie dodano jeszcze Åŧadnych filtrÃŗw", "no_libraries_message": "StwÃŗrz bibliotekę zewnętrzną, aby przeglądać swoje zdjęcia i filmy", "no_local_assets_found": "Nie znaleziono Åŧadnych lokalnych zasobÃŗw o tej sumie kontrolnej", "no_location_set": "Nie ustawiono lokalizacji", @@ -1481,11 +1608,11 @@ "no_results_description": "SprÃŗbuj uÅŧyć synonimu lub bardziej ogÃŗlnego słowa kluczowego", "no_shared_albums_message": "StwÃŗrz album aby udostępnić zdjęcia i filmy osobom w Twojej sieci", "no_uploads_in_progress": "Brak przesyłań w toku", + "none": "Åģadne", "not_allowed": "Niedozwolone", "not_available": "Nie dotyczy", "not_in_any_album": "Bez albumu", "not_selected": "Nie wybrano", - "note_apply_storage_label_to_previously_uploaded assets": "Uwaga: Aby przypisać etykietę magazynowania do wcześniej przesłanych zasobÃŗw, uruchom", "notes": "Uwagi", "nothing_here_yet": "Nic tu jeszcze nie ma", "notification_permission_dialog_content": "Aby włączyć powiadomienia, przejdÅē do Ustawień i wybierz opcję Zezwalaj.", @@ -1528,7 +1655,7 @@ "other_devices": "Inne urządzenia", "other_entities": "Inne byty", "other_variables": "Inne zmienne", - "owned": "Posiadany", + "owned": "Posiadane", "owner": "Właściciel", "page": "Strona", "partner": "Partner", @@ -1563,6 +1690,7 @@ "people": "Osoby", "people_edits_count": "Edytowano {count, plural, one {# osoba} few {# osoby} many {# osÃŗb} other {# osÃŗb}}", "people_feature_description": "Przeglądanie zdjęć i filmÃŗw pogrupowanych według osÃŗb", + "people_selected": "{count, plural, one {# osoba wybrana} few {# osoby wybrane} other {# osÃŗb wybranych}}", "people_sidebar_description": "Pokazuj link do OsÃŗb w panelu bocznym", "permanent_deletion_warning": "OstrzeÅŧenie o trwałym usunięciu", "permanent_deletion_warning_setting_description": "PokaÅŧ ostrzeÅŧenie przy trwałym usuwaniu zasobÃŗw", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, one {# rok} few {# lata} many {# lat} other {# lat}}", "person_birthdate": "Urodzony {date}", "person_hidden": "{name}{hidden, select, true { (ukryty)} other {}}", + "person_recognized": "Osoba rozpoznana", + "person_selected": "Osoba wybrana", "photo_shared_all_users": "Wygląda na to, Åŧe udostępniłeś swoje zdjęcia wszystkim uÅŧytkownikom lub nie masz Åŧadnego uÅŧytkownika, z ktÃŗrym moÅŧna by było je udostępnić.", "photos": "Zdjęcia", "photos_and_videos": "Zdjęcia i Filmy", "photos_count": "{count, plural, one {{count, number} Zdjęcie} few {{count, number} Zdjęcia} other {{count, number} Zdjęć}}", "photos_from_previous_years": "Zdjęcia z ubiegłych lat", + "photos_only": "Tylko zdjęcia", "pick_a_location": "Oznacz lokalizację", "pick_custom_range": "Zakres niestandardowy", "pick_date_range": "Wybierz zakres dat", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", "query_asset_id": "Zapytanie o ID zasobu", "queue_status": "Kolejkowanie {count}/{total}", + "rate_asset": "Oceń zasÃŗb", "rating": "Ocena gwiazdkowa", "rating_clear": "Wyczyść ocenę", "rating_count": "{count, plural, one {# gwiazdka} other {# gwiazdek}}", "rating_description": "Wyświetl ocenę z EXIF w panelu informacji", + "rating_set": "Ocena ustawiona na {rating, plural, one {# gwiazdkę} few {# gwiazdki} other {# gwiazdek}}", "reaction_options": "Opcje reakcji", "read_changelog": "Zobacz Zmiany", "readonly_mode_disabled": "Tryb tylko do odczytu wyłączony", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "Przypisano ponownie {count, plural, one {# zasÃŗb} other {# zasobÃŗw}} do nowej osoby", "reassing_hint": "Przypisz wybrane zasoby do istniejącej osoby", "recent": "Ostatnie", - "recent-albums": "Ostatnie albumy", + "recent_albums": "Ostatnie albumy", "recent_searches": "Ostatnie wyszukiwania", "recently_added": "Ostatnio dodane", "recently_added_page_title": "Ostatnio Dodane", @@ -1770,9 +1903,11 @@ "saved_settings": "Zapisane ustawienia", "say_something": "Powiedz coś", "scaffold_body_error_occurred": "Wystąpił błąd", + "scan": "Skanuj", "scan_all_libraries": "Skanuj wszystkie biblioteki", "scan_library": "Skanuj", "scan_settings": "Ustawienia Skanowania", + "scanning": "Skanowanie", "scanning_for_album": "Skanuję album...", "search": "Szukaj", "search_albums": "Przeszukaj albumy", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Wybierz typ multimediÃŗw", "search_filter_ocr": "Wyszukaj przy uÅŧyciu OCR", "search_filter_people_title": "Wybierz osoby", + "search_filter_star_rating": "Ocena gwiazdkowa", "search_for": "Szukaj wśrÃŗd", "search_for_existing_person": "Wyszukaj istniejącą osobę", "search_no_more_result": "Brak dalszych wynikÃŗw", @@ -1836,17 +1972,23 @@ "second": "Sekunda", "see_all_people": "Zobacz wszystkie osoby", "select": "Wybierz", + "select_album": "Wybierz album", "select_album_cover": "Wybierz okładkę albumu", + "select_albums": "Wybierz albumy", "select_all": "Zaznacz wszystko", "select_all_duplicates": "Wybierz wszystkie duplikaty", "select_all_in": "Wybierz wszystkie w {group}", "select_avatar_color": "Wybierz kolor awatara", + "select_count": "{count, plural, one {Wybierz #} other {Wybierz #}}", + "select_cutoff_date": "Wybierz datę graniczną", "select_face": "Wybierz twarz", "select_featured_photo": "Zmień gÅ‚Ãŗwne zdjęcie", "select_from_computer": "Wybierz z komputera", "select_keep_all": "Zaznacz zachowaj wszystko", "select_library_owner": "Wybierz właściciela biblioteki", "select_new_face": "Wybierz nową twarz", + "select_people": "Wybierz osoby", + "select_person": "Wybierz osobę", "select_person_to_tag": "Wybierz osobę do oznaczenia", "select_photos": "Wybierz zdjęcia", "select_trash_all": "Zaznacz wszystko do kosza", @@ -1922,7 +2064,7 @@ "shared_by_you": "Udostępnione przez ciebie", "shared_from_partner": "Zdjęcia od {partner}", "shared_intent_upload_button_progress_text": "{current} / {total} Przesłano", - "shared_link_app_bar_title": "Udostępnione linki", + "shared_link_app_bar_title": "Udostępnione", "shared_link_clipboard_copied_massage": "Skopiowane do schowka", "shared_link_clipboard_text": "Link: {link}\nHasło: {password}", "shared_link_create_error": "Błąd podczas tworzenia linka do udostępnienia", @@ -1961,7 +2103,7 @@ "sharing": "Udostępnianie", "sharing_enter_password": "WprowadÅē hasło, aby wyświetlić tę stronę.", "sharing_page_album": "Udostępnione albumy", - "sharing_page_description": "TwÃŗrz wspÃŗldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.", + "sharing_page_description": "TwÃŗrz wspÃŗÅ‚dzielone albumy, aby udostępniać zdjęcia i filmy osobom w twojej sieci.", "sharing_page_empty_list": "PUSTA LISTA", "sharing_sidebar_description": "Wyświetl link do udostępniania na pasku bocznym", "sharing_silver_appbar_create_shared_album": "UtwÃŗrz wspÃŗÅ‚dzielony album", @@ -1982,6 +2124,7 @@ "show_password": "PokaÅŧ hasło", "show_person_options": "PokaÅŧ opcje osoby", "show_progress_bar": "PokaÅŧ pasek postępu", + "show_schema": "PokaÅŧ schemat", "show_search_options": "Wyświetl opcje wyszukiwania", "show_shared_links": "PokaÅŧ udostępniane linki", "show_slideshow_transition": "PokaÅŧ przejście pokazu slajdÃŗw", @@ -1999,6 +2142,8 @@ "skip_to_folders": "PrzejdÅē do folderÃŗw", "skip_to_tags": "PrzejdÅē do tagÃŗw", "slideshow": "Pokaz slajdÃŗw", + "slideshow_repeat": "PowtÃŗrz pokaz slajdÃŗw", + "slideshow_repeat_description": "Zapętl pokaz slajdÃŗw", "slideshow_settings": "Ustawienia pokazu slajdÃŗw", "sort_albums_by": "Sortuj albumy według...", "sort_created": "Data utworzenia", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Wybierz ustawienia motywu aplikacji", "theme_setting_three_stage_loading_subtitle": "TrÃŗjstopniowe ładowanie moÅŧe zwiększyć wydajność ładowania, ale powoduje znacznie większe obciąÅŧenie sieci", "theme_setting_three_stage_loading_title": "Włączenie trÃŗjstopniowego ładowania", + "then": "Wtedy", "they_will_be_merged_together": "Zostaną one ze sobą połączone", "third_party_resources": "Zasoby stron trzecich", "time": "Czas", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Wybierz zasoby", "trash_page_title": "Kosz ({count})", "trashed_items_will_be_permanently_deleted_after": "Wyrzucone zasoby zostaną trwale usunięte po {days, plural, one {jednym dniu} other {# dniach}}.", + "trigger": "Wyzwalacz", + "trigger_asset_uploaded": "Przesłano zasÃŗb", + "trigger_asset_uploaded_description": "Wyzwalane gdy przesłany zostanie nowy zasÃŗb", + "trigger_description": "Wydarzenie, ktÃŗre uruchamia przepływ pracy", + "trigger_person_recognized": "Osoba rozpoznana", + "trigger_person_recognized_description": "Wyzwalane gdy zostanie wykryta osoba", + "trigger_type": "Rodzaj wyzwalacza", "troubleshoot": "RozwiąÅŧ problemy", "type": "Typ", "unable_to_change_pin_code": "Nie moÅŧna zmienić kodu PIN", @@ -2123,6 +2276,7 @@ "unhide_person": "PrzywrÃŗÄ‡ osobę", "unknown": "Nieznany", "unknown_country": "Nieznane państwo", + "unknown_date": "Nieznana data", "unknown_year": "Rok nieznany", "unlimited": "Nieograniczony", "unlink_motion_video": "Rozłącz ruchome wideo", @@ -2139,17 +2293,19 @@ "unstack": "Rozdziel stos", "unstack_action_prompt": "{count} rozdzielono", "unstacked_assets_count": "Rozdzielono {count, plural, one {# zasÃŗb} few {# zasoby} other {# zasobÃŗw}}", + "unsupported_field_type": "Nieobsługiwany typ pola", "untagged": "Nieoznaczone", + "untitled_workflow": "Przepływ pracy bez tytułu", "up_next": "Do następnego", "update_location_action_prompt": "Zaktualizuj lokalizację {count} wybranych zasobÃŗw na:", "updated_at": "Zaktualizowany", "updated_password": "Pomyślnie zaktualizowano hasło", "upload": "Prześlij", - "upload_action_prompt": "{count} w kolejce do wysłania", "upload_concurrency": "WspÃŗÅ‚bieÅŧność wysyłania", "upload_details": "SzczegÃŗÅ‚y przesyłania", "upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobÃŗw na serwerze?", "upload_dialog_title": "Prześlij ZasÃŗb", + "upload_error_with_count": "Błąd przesyłania dla {count, plural, one {# zasobu} other {# zasobÃŗw}}", "upload_errors": "Przesyłanie zakończone z {count, plural, one {# błędem} other {# błędami}}. OdświeÅŧ stronę, aby zobaczyć nowo przesłane zasoby.", "upload_finished": "Przesyłanie zakończone", "upload_progress": "Pozostałe {remaining, number} - Przetworzone {processed, number}/{total, number}", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "UÅŧycie", "use_biometric": "UÅŧyj biometrii", - "use_current_connection": "uÅŧyj bieÅŧącego połączenia", + "use_current_connection": "UÅŧyj bieÅŧącego połączenia", "use_custom_date_range": "Zamiast tego uÅŧyj niestandardowego zakresu dat", "user": "UÅŧytkownik", "user_has_been_deleted": "Ten uÅŧytkownik został usunięty.", @@ -2185,6 +2341,7 @@ "utilities": "Narzędzia", "validate": "Walidacja", "validate_endpoint_error": "Proszę wprowadzić prawidłowy adres URL", + "validation_error": "Błąd walidacji", "variables": "Zmienne", "version": "Wersja", "version_announcement_closing": "TwÃŗj przyjaciel Aleks", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "OdtwÃŗrz miniaturę wideo po najechaniu myszką na element. Nawet jeśli jest wyłączone, odtwarzanie moÅŧna rozpocząć, najeÅŧdÅŧając kursorem na ikonę odtwarzania.", "videos": "Filmy", "videos_count": "{count, plural, one {# Film} few {# Filmy} other {# FilmÃŗw}}", + "videos_only": "Tylko filmy", "view": "Widok", "view_album": "Wyświetl Album", "view_all": "PokaÅŧ wszystkie", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "UÅŧyj jako gÅ‚Ãŗwnego zasobu", "viewer_unstack": "Rozdziel stos", "visibility_changed": "Zmieniono widoczność dla {count, plural, one {# osoby} other {# osÃŗb}}", + "visual": "Wizualny", + "visual_builder": "Edytor wizualny", "waiting": "Oczekujące", "waiting_count": "W oczekiwaniu: {count}", "warning": "OstrzeÅŧenie", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Witamy w immich", "width": "Szerokość", "wifi_name": "Nazwa Wi-Fi", - "workflow": "Przepływ pracy", + "workflow_delete_prompt": "Czy jesteś pewien, Åŧe chcesz usunąć ten przepływ pracy?", + "workflow_deleted": "Przepływ pracy usunięty", + "workflow_description": "Opis przepływu pracy", + "workflow_info": "Informacje o przepływie pracy", + "workflow_json": "JSON przepływu pracy", + "workflow_json_help": "Edytuj konfigurację przepływu pracy w formacie JSON. Zmiany zostaną zsynchronizowane z edytorem wizualnym.", + "workflow_name": "Nazwa przepływu pracy", + "workflow_navigation_prompt": "Czy na pewno chcesz wyjść bez zapisania zmian?", + "workflow_summary": "Podsumowanie przepływu pracy", + "workflow_update_success": "Przepływ pracy zaktualizowany pomyślnie", + "workflow_updated": "Zaktualizowano przepływ pracy", + "workflows": "Przepływy pracy", + "workflows_help_text": "Przepływy pracy automatyzują działania na twoich zasobach w oparciu o wyzwalacze i filtry", "wrong_pin_code": "Nieprawidłowy kod PIN", "year": "Rok", "years_ago": "{years, plural, one {# rok} few {# lata} other {# lat}} temu", "yes": "Tak", "you_dont_have_any_shared_links": "Nie masz Åŧadnych udostępnionych linkÃŗw", "your_wifi_name": "Twoja nazwa Wi-Fi", + "zero_to_clear_rating": "naciśnij 0, aby wyczyścić ocenę zasobu", "zoom_image": "Powiększ obraz", "zoom_to_bounds": "Powiększ do krawędzi" } diff --git a/i18n/pt.json b/i18n/pt.json index 7cbf66f11b..4d281c94fa 100644 --- a/i18n/pt.json +++ b/i18n/pt.json @@ -5,6 +5,7 @@ "acknowledge": "Aceitar", "action": "AÃ§ÃŖo", "action_common_update": "Atualizar", + "action_description": "Um conjunto de açÃĩes a executar nos ficheiros filtrados", "actions": "AçÃĩes", "active": "Em execuÃ§ÃŖo", "active_count": "Ativas: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Adicionar localizaÃ§ÃŖo", "add_a_name": "Adicionar um nome", "add_a_title": "Adicionar um título", + "add_action": "Adicionar aÃ§ÃŖo", + "add_action_description": "Faça clique para adicionar uma aÃ§ÃŖo a executar", + "add_assets": "Adicionar ficheiros", "add_birthday": "Definir aniversÃĄrio", "add_endpoint": "Adicionar URL", "add_exclusion_pattern": "Adicionar um padrÃŖo de exclusÃŖo", + "add_filter": "Adicionar filtro", + "add_filter_description": "Faça clique para adicionar uma condiÃ§ÃŖo para o filtro", "add_location": "Adicionar localizaÃ§ÃŖo", "add_more_users": "Adicionar mais utilizadores", "add_partner": "Adicionar parceiro", @@ -36,6 +42,7 @@ "add_to_shared_album": "Adicionar ao ÃĄlbum partilhado", "add_upload_to_stack": "Adicionar carregamento à fila", "add_url": "Adicionar URL", + "add_workflow_step": "Adicionar passo de fluxo de trabalho", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", @@ -97,6 +104,8 @@ "image_preview_description": "Imagem de tamanho mÊdio sem metadados, utilizada ao visualizar um Ãēnico ficheiro e pela aprendizagem de mÃĄquina", "image_preview_quality_description": "Qualidade de prÊ-visualizaÃ§ÃŖo de 1 a 100. Maior Ê melhor, mas produz ficheiros maiores e pode reduzir a capacidade de resposta da aplicaÃ§ÃŖo. Definir um valor demasiado baixo pode afetar a qualidade da aprendizagem de mÃĄquina.", "image_preview_title": "DefiniçÃĩes de PrÊ-visualizaÃ§ÃŖo", + "image_progressive": "Progressivo", + "image_progressive_description": "Codificar imagens JPEG de forma progressiva para exibiÃ§ÃŖo com carregamento gradual. NÃŖo tem efeito em imagens WebP.", "image_quality": "Qualidade", "image_resolution": "ResoluÃ§ÃŖo", "image_resolution_description": "ResoluçÃĩes mais altas podem ajudar a preservar mais detalhes mas demoram mais a codificar, tÃĒm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicaÃ§ÃŖo.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Ativar a Pesquisa Inteligente", "machine_learning_smart_search_enabled_description": "Se desativado, as imagens nÃŖo serÃŖo codificadas para Pesquisa Inteligente.", "machine_learning_url_description": "A URL do servidor de aprendizagem de mÃĄquina. Se for fornecido mais do que um URL, cada servidor serÃĄ testado, um a um, atÊ um deles responder com sucesso, por ordem do primeiro ao Ãēltimo. Servidores que nÃŖo responderem serÃŖo temporariamente ignorados atÊ voltarem a estar online.", + "maintenance_delete_backup": "Eliminar CÃŗpia de Segurança", + "maintenance_delete_backup_description": "Este ficheiro irÃĄ ser apagado para sempre.", + "maintenance_delete_error": "Ocorreu um erro ao eliminar a cÃŗpia de segurança.", + "maintenance_restore_backup": "Restaurar CÃŗpia de Segurança", + "maintenance_restore_backup_description": "O Immich irÃĄ ser apagado e de seguida restaurado a partir da cÃŗpia de segurança selecionada. IrÃĄ ser criada uma cÃŗpia de segurança antes de continuar.", + "maintenance_restore_backup_different_version": "Esta cÃŗpia de segurança foi criada com uma versÃŖo diferente do Immich!", + "maintenance_restore_backup_unknown_version": "NÃŖo foi possível determinar a versÃŖo da cÃŗpia de segurança.", + "maintenance_restore_database_backup": "Restaurar cÃŗpia de seguraça da base de dados", + "maintenance_restore_database_backup_description": "Reverter para um estado anterior da base de dados utilizando um ficheiro de cÃŗpia de segurança", "maintenance_settings": "ManutenÃ§ÃŖo", "maintenance_settings_description": "Colocar o Immich no modo de manutenÃ§ÃŖo.", - "maintenance_start": "Iniciar modo de manutenÃ§ÃŖo", + "maintenance_start": "Aternar para o modo de manutenÃ§ÃŖo", "maintenance_start_error": "Ocorreu um erro ao iniciar o modo de manutenÃ§ÃŖo.", + "maintenance_upload_backup": "Carregar ficheiro de cÃŗpia de segurança da base de dados", + "maintenance_upload_backup_error": "NÃŖo foi possível carregar cÃŗpia de segurança. É um ficheiro .sql/.sql.gz?", "manage_concurrency": "Gerir simultaneidade", "manage_concurrency_description": "Navegar para a pÃĄgina das tarefas para gerir as tarefas em simultÃĸneo", "manage_log_settings": "Gerir definiçÃĩes de registo", @@ -252,7 +272,7 @@ "oauth_auto_register": "Registo automÃĄtico", "oauth_auto_register_description": "Registar automaticamente novos utilizadores apÃŗs iniciarem sessÃŖo com o OAuth", "oauth_button_text": "Texto do botÃŖo", - "oauth_client_secret_description": "ObrigatÃŗrio se PKCE (Proof Key for Code Exchange) nÃŖo for suportado pelo provedor OAuth", + "oauth_client_secret_description": "ObrigatÃŗrio para o cliente confidencial, ou se a PKCE (Proof Key for Code Exchange) nÃŖo for suportada para cliente pÃēblico.", "oauth_enable_description": "Iniciar sessÃŖo com o OAuth", "oauth_mobile_redirect_uri": "URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override": "SubstituiÃ§ÃŖo de URI de redirecionamento mÃŗvel", @@ -431,6 +451,9 @@ "admin_password": "Palavra-passe do administrador", "administration": "AdministraÃ§ÃŖo", "advanced": "Avançado", + "advanced_settings_clear_image_cache": "Limpar a Cache de Imagens", + "advanced_settings_clear_image_cache_error": "Ocorreu um erro ao limpar a cache de imagens", + "advanced_settings_clear_image_cache_success": "Limpeza concluída com sucesso {size}", "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}", @@ -467,10 +490,12 @@ "album_remove_user": "Remover utilizador?", "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?", "album_search_not_found": "Nenhum ÃĄlbum encontrado segundo a pesquisa", + "album_selected": "Álbum selecionado", "album_share_no_users": "Parece que tem este ÃĄlbum partilhado com todos os utilizadores ou que nÃŖo existem utilizadores com quem o partilhar.", "album_summary": "Resumo do ÃĄlbum", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receber uma notificaÃ§ÃŖo por e-mail quando um ÃĄlbum partilhado tiver novos ficheiros", + "album_upload_assets": "Carregar ficheiros a partir do seu computador e adicionÃĄ-los ao ÃĄlbum", "album_user_left": "Saíu do {album}", "album_user_removed": "Utilizador {user} removido", "album_viewer_appbar_delete_confirm": "Tem certeza que deseja excluir este ÃĄlbum da sua conta?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Ordem inicial dos ficheiros ao criar novos ÃĄlbuns.", "albums_feature_description": "ColeçÃĩes de ficheiros que podem ser partilhados com outros utilizadores.", "albums_on_device_count": "Álbums no dispositivo ({count})", + "albums_selected": "{count, plural, one {# ÃĄlbum selecionado} other {# ÃĄlbuns selecionados}}", "all": "Todos", "all_albums": "Todos os ÃĄlbuns", "all_people": "Todas as pessoas", + "all_photos": "Todas as fotos", "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir ediçÃĩes", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permitir que utilizadores pÃēblicos façam carregamentos", "allowed": "Permitido", "alt_text_qr_code": "Imagem do cÃŗdigo QR", + "always_keep": "Manter sempre", + "always_keep_photos_hint": "Libertar Espaço irÃĄ manter todas as fotos neste dispositivo.", + "always_keep_videos_hint": "Libertar Espaço irÃĄ manter todos os vídeos neste dispositivo.", "anti_clockwise": "Sentido anti-horÃĄrio", "api_key": "Chave de API", "api_key_description": "Este valor serÃĄ apresentado apenas uma Ãēnica vez. Por favor, certifique-se que o copiou antes de fechar a janela.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {#Arquivado # item} other {Arquivados # itens}}", "are_these_the_same_person": "Estas pessoas sÃŖo a mesma pessoa?", "are_you_sure_to_do_this": "Tem a certeza de que quer fazer isto?", + "array_field_not_fully_supported": "Campos de listas necessitam de ediÃ§ÃŖo manual JSON", "asset_action_delete_err_read_only": "NÃŖo Ê possível eliminar ficheiro sÃŗ de leitura, a ignorar", "asset_action_share_err_offline": "NÃŖo foi possível obter os ficheiros offline, a ignorar", "asset_added_to_album": "Adicionado ao ÃĄlbum", "asset_adding_to_album": "A adicionar ao ÃĄlbumâ€Ļ", + "asset_created": "Ficheiro criado", "asset_description_updated": "A descriÃ§ÃŖo do ficheiro foi atualizada", "asset_filename_is_offline": "O ficheiro {filename} nÃŖo estÃĄ disponível", "asset_has_unassigned_faces": "O ficheiro tem rostos nÃŖo atribuídas", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "DisposiÃ§ÃŖo", "asset_list_settings_subtitle": "ConfiguraçÃĩes de disposiÃ§ÃŖo da grade de fotos", "asset_list_settings_title": "Grade de fotos", + "asset_not_found_on_device_android": "Ficheiro nÃŖo encontrado no dispositivo", + "asset_not_found_on_device_ios": "Ficheiro nÃŖo encontrado no dispositivo. Se estiver a utilizar o iCloud, o ficheiro pode estar inacessível devido a um ficheiro corrompido armazenado no iCloud", + "asset_not_found_on_icloud": "Ficheiro nÃŖo encontrado no iCloud. Este pode estar inacessível devido a um ficheiro corrompido armazenado no iCloud", "asset_offline": "Ficheiro Indisponível", "asset_offline_description": "Este ficheiro externo deixou de estar disponível no disco. Contacte o seu administrador do Immich para obter ajuda.", "asset_restored_successfully": "FIcheiro restaurado com sucesso", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Selecione Álbuns", "backup_album_selection_page_selection_info": "InformaçÃĩes da SeleÃ§ÃŖo", "backup_album_selection_page_total_assets": "Total de ficheiros Ãēnicos", - "backup_albums_sync": "CÃŗpia de segurança de sincronizaÃ§ÃŖo de ÃĄlbuns", + "backup_albums_sync": "CÃŗpia de Segurança de SincronizaÃ§ÃŖo de Álbuns", "backup_all": "Tudo", "backup_background_service_backup_failed_message": "Ocorreu um erro ao efetuar cÃŗpia de segurança dos ficheiros. A tentar de novoâ€Ļ", "backup_background_service_complete_notification": "CÃŗpia de conteÃēdos concluída", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "As palavras-passe nÃŖo condizem", "change_password_form_reenter_new_password": "Confirme a nova palavra-passe", "change_pin_code": "Alterar cÃŗdigo PIN", + "change_trigger": "Alterar ativador", + "change_trigger_prompt": "Tem a certeza de que quer alterar o ativador? Isto irÃĄ remover todas as açÃĩes e filtros.", "change_your_password": "Alterar a sua palavra-passe", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "charging": "A carregar", @@ -722,6 +759,18 @@ "checksum": "Teste de soma de dados", "choose_matching_people_to_merge": "Escolha pessoas correspondentes para unir", "city": "Cidade/Localidade", + "cleanup_confirm_description": "O Immich encontrou {count} ficheiro(s) (criados antes de {date}) que tÃĒm cÃŗpia de segurança neste servidor. Quer remover as cÃŗpias locais deste dispositivo?", + "cleanup_confirm_prompt_title": "Remover deste dispositivo?", + "cleanup_deleted_assets": "{count} ficheiro(s) foram movidos para a reciclagem do dispositivo", + "cleanup_deleting": "A mover para a reciclagem...", + "cleanup_found_assets": "Foram encontrados {count} ficheiro(s) com cÃŗpias de segurança", + "cleanup_found_assets_with_size": "Foram encontrados {count} ficheiros com cÃŗpia de segurança ({size})", + "cleanup_icloud_shared_albums_excluded": "Álbuns Partilhados do iCloud serÃŖo excluídos da pesquisa", + "cleanup_no_assets_found": "Nenhum ficheiro encontrado que siga os critÊrios acima. Libertar Espaço apenas pode remover ficheiros que tenham sido copiados para o servidor", + "cleanup_preview_title": "Ficheiros a serem removidos ({count})", + "cleanup_step3_description": "Procurar por ficheiros no servidor que sigam os seus critÊrios de data e se serÃŖo mantidos.", + "cleanup_step4_summary": "{count} ficheiros (criados antes de {date}) para remover do seu dispositivo local. As fotos irÃŖo manter-se acessíveis atravÊs da aplicaÃ§ÃŖo do Immich.", + "cleanup_trash_hint": "Para recuperar por completo o espaço de armazenamento, abra a aplicaÃ§ÃŖo da galeria do sistema e esvazie a reciclagem", "clear": "Limpar", "clear_all": "Limpar tudo", "clear_all_recent_searches": "Limpar todas as pesquisas recentes", @@ -733,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente foi importado", "client_cert_invalid_msg": "Certificado invÃĄlido ou palavra-passe incorreta", + "client_cert_password_message": "Insira a palavra-passe para este certificado", + "client_cert_password_title": "Palavra-passe do Certificado", "client_cert_remove_msg": "Certificado do cliente foi removido", "client_cert_subtitle": "Apenas hÃĄ suporte ao formato PKCS12 (.p12, .pfx). Importar/Remover certificados estÃĄ disponível apenas antes do início de sessÃŖo", "client_cert_title": "Certificado de Cliente SSL [EXPERIMENTAL]", @@ -787,6 +838,7 @@ "create_album": "Criar ÃĄlbum", "create_album_page_untitled": "Sem título", "create_api_key": "Criar chave de API", + "create_first_workflow": "Criar o primeiro fluxo de trabalho", "create_library": "Criar biblioteca", "create_link": "Criar link", "create_link_to_share": "Criar link para partilhar", @@ -801,17 +853,25 @@ "create_tag": "Criar etiqueta", "create_tag_description": "Criar uma nova etiqueta. Para etiquetas compostas, introduza o caminho completo, incluindo as barras.", "create_user": "Criar utilizador", + "create_workflow": "Criar fluxo de trabalho", "created": "Criado", "created_at": "Criado a", "creating_linked_albums": "A criar albuns ligados...", "crop": "Cortar", + "crop_aspect_ratio_fixed": "Fixo", + "crop_aspect_ratio_free": "Livre", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", "current_pin_code": "CÃŗdigo PIN atual", "current_server_address": "Endereço atual do servidor", + "custom_date": "Data personalizada", "custom_locale": "LocalizaÃ§ÃŖo Personalizada", "custom_locale_description": "Formatar datas e nÃēmeros baseados na língua e na regiÃŖo", "custom_url": "URL personalizado", + "cutoff_date_description": "Manter fotos dos Ãēltimosâ€Ļ", + "cutoff_day": "{count, plural, one {dia} other {dias}}", + "cutoff_year": "{count, plural, one {ano} other {anos}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", @@ -867,6 +927,7 @@ "deselect_all": "Remover seleÃ§ÃŖo de tudo", "details": "Detalhes", "direction": "DireÃ§ÃŖo", + "disable": "Desativar", "disabled": "Desativado", "disallow_edits": "NÃŖo permitir ediçÃĩes", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Vídeos incorporados", "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um ficheiro separado", "download_notfound": "NÃŖo encontrado", + "download_original": "Descarregar original", "download_paused": "Pausado", "download_settings": "Transferir", "download_settings_description": "Gerir definiçÃĩes relacionadas com a transferÃĒncia de ficheiros", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Tentando novamente", "downloading": "A transferir", "downloading_asset_filename": "A transferir o ficheiro {filename}", + "downloading_from_icloud": "A descarregar do iCloud", "downloading_media": "A descarregar ficheiro", "drop_files_to_upload": "Solte os ficheiros em qualquer lugar para os enviar", "duplicates": "Itens duplicados", @@ -929,11 +992,22 @@ "edit_tag": "Editar etiqueta", "edit_title": "Editar Título", "edit_user": "Editar utilizador", + "edit_workflow": "Editar fluxo de trabalho", "editor": "Editar", "editor_close_without_save_prompt": "As alteraçÃĩes nÃŖo serÃŖo guardadas", "editor_close_without_save_title": "Fechar editor?", - "editor_crop_tool_h2_aspect_ratios": "RelaÃ§ÃŖo de aspeto", - "editor_crop_tool_h2_rotation": "RotaÃ§ÃŖo", + "editor_confirm_reset_all_changes": "Tem a certeza de que quer desfazer todas as alteraçÃĩes?", + "editor_discard_edits_confirm": "Descartar AlteraçÃĩes", + "editor_discard_edits_prompt": "Tem alteraçÃĩes nÃŖo guardadas. Tem a certeza de que quer descartÃĄ-las?", + "editor_discard_edits_title": "Descartar alteraçÃĩes?", + "editor_edits_applied_error": "NÃŖo foi possível aplicar alteraçÃĩes", + "editor_edits_applied_success": "AlteraçÃĩes aplicadas com sucesso", + "editor_flip_horizontal": "Espelhar na horizontal", + "editor_flip_vertical": "Espelhar na vertical", + "editor_orientation": "OrientaÃ§ÃŖo", + "editor_reset_all_changes": "Desfazer alteraçÃĩes", + "editor_rotate_left": "Rodar 90° à esquerda", + "editor_rotate_right": "Rodar 90° à direita", "email": "E-mail", "email_notifications": "NotificaçÃĩes por e-mail", "empty_folder": "Esta pasta estÃĄ vazia", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Ocorreu um erro ao mudar a ordem de exibiÃ§ÃŖo", "error_delete_face": "Falha ao remover rosto do ficheiro", "error_getting_places": "Erro ao obter locais", + "error_loading_albums": "Ocorreu um erro ao carregar os ÃĄlbuns", "error_loading_image": "Erro ao carregar a imagem", "error_loading_partners": "Erro ao carregar parceiros: {error}", + "error_retrieving_asset_information": "Ocorreu um erro ao carregar as informaçÃĩes do ficheiro", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao marcar o rosto - nÃŖo foi possível localizar o rosto", "error_title": "Erro - Algo correu mal", + "error_while_navigating": "Ocorreu um erro ao navegar para o ficheiro", "errors": { "cannot_navigate_next_asset": "NÃŖo foi possível navegar para o prÃŗximo ficheiro", "cannot_navigate_previous_asset": "NÃŖo foi possível navegar para o ficheiro anterior", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "NÃŖo foi possível completar o início de sessÃŖo com OAuth", "unable_to_connect": "NÃŖo Ê possível ligar", "unable_to_copy_to_clipboard": "NÃŖo foi possível copiar para a ÃĄrea de transferÃĒncia, certifique-se de que estÃĄ a aceder à pagina atravÊs de https", + "unable_to_create": "NÃŖo foi possível criar um fluxo de trabalho", "unable_to_create_admin_account": "NÃŖo foi possível criar conta de administrador", "unable_to_create_api_key": "NÃŖo foi possível criar uma nova Chave de API", "unable_to_create_library": "NÃŖo foi possível criar a biblioteca", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "NÃŖo foi possível eliminar o padrÃŖo de exclusÃŖo", "unable_to_delete_shared_link": "NÃŖo foi possível eliminar o link compartilhado", "unable_to_delete_user": "NÃŖo foi possível eliminar o utilizador", + "unable_to_delete_workflow": "NÃŖo foi possível eliminar fluxo de trabalho", "unable_to_download_files": "NÃŖo foi possível transferir ficheiros", "unable_to_edit_exclusion_pattern": "NÃŖo foi possível editar o padrÃŖo de exclusÃŖo", "unable_to_empty_trash": "NÃŖo foi possível esvaziar a reciclagem", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "NÃŖo foi possível analisar a biblioteca", "unable_to_set_feature_photo": "NÃŖo foi possível definir a foto de destaque", "unable_to_set_profile_picture": "NÃŖo foi possível definir a foto de perfil", + "unable_to_set_rating": "NÃŖo foi possível classificar", "unable_to_submit_job": "NÃŖo foi possível enviar a tarefa", "unable_to_trash_asset": "NÃŖo foi possível enviar o ficheiro para a reciclagem", "unable_to_unlink_account": "NÃŖo foi possível desvincular conta", @@ -1074,8 +1154,10 @@ "unable_to_update_settings": "NÃŖo foi possível atualizar as definiçÃĩ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 utilizador", + "unable_to_update_workflow": "NÃŖo foi possível atualizar o fluxo de trabalho", "unable_to_upload_file": "NÃŖo foi possível carregar o ficheiro" }, + "errors_text": "Erros", "exclusion_pattern": "PadrÃŖo de exclusÃŖo", "exif": "Exif", "exif_bottom_sheet_description": "Adicionar DescriÃ§ÃŖo...", @@ -1120,14 +1202,17 @@ "features": "Funcionalidades", "features_in_development": "Funcionalidades em Desenvolvimento", "features_setting_description": "Configurar as funcionalidades da aplicaÃ§ÃŖo", - "file_name": "Nome do ficheiro", "file_name_or_extension": "Nome do ficheiro ou extensÃŖo", + "file_name_text": "Nome do ficheiro", + "file_name_with_value": "Nome do ficheiro: {file_name}", "file_size": "Tamanho do ficheiro", "filename": "Nome do ficheiro", "filetype": "Tipo de ficheiro", "filter": "Filtro", + "filter_description": "CondiçÃĩes para filtrar os ficheiros alvo", "filter_people": "Filtrar pessoas", "filter_places": "Filtrar lugares", + "filters": "Filtros", "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa", "first": "Primeiro", "fix_incorrect_match": "Corrigir correspondÃĒncia incorreta", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Navegar na vista de pastas por fotos e vídeos no sistema de ficheiros", "forgot_pin_code_question": "Esqueceu-se do seu PIN?", "forward": "Para a frente", + "free_up_space": "Libertar Espaço", + "free_up_space_description": "Mover fotos e vídeos que tenham sido copiados para o servidor para a reciclagem do seu dispositivo para libertar espaço. As cÃŗpias no servidor mantÃĒm-se seguras.", + "free_up_space_settings_subtitle": "Libertar espaço no dispositivo", "full_path": "Caminho completo: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade requer o carregamento de recursos externos da Google para poder funcionar.", "general": "Geral", "geolocation_instruction_location": "Clique num ficheiro com coordenadas GPS para usar a sua localizaÃ§ÃŖo ou selecione um local diretamente do mapa", "get_help": "Obter Ajuda", + "get_people_error": "Ocorreu um erro ao obter pessoas", "get_wifiname_error": "NÃŖo foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissÃĩes necessÃĄrias e se estÃĄ conectado a uma rede Wi-Fi", "getting_started": "Primeiros Passos", "go_back": "Regressar", @@ -1175,6 +1264,7 @@ "hide_named_person": "Ocultar pessoa {name}", "hide_password": "Ocultar palavra-passe", "hide_person": "Ocultar pessoa", + "hide_schema": "Ocultar esquema", "hide_text_recognition": "Esconder reconhecimento de texto", "hide_unnamed_people": "Ocultar pessoas sem nome", "home_page_add_to_album_conflicts": "Foram adicionados {added} ficheiros ao ÃĄlbum {album}. {failed} ficheiros jÃĄ estÃŖo no ÃĄlbum.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Processamento executado em {dateTime}", "items_count": "{count, plural, one {item #} other {itens #}}", "jobs": "Tarefas", + "json_editor": "Editor JSON", + "json_error": "Erro JSON", "keep": "Manter", + "keep_albums": "Manter ÃĄlbuns", + "keep_albums_count": "A manter {count} {count, plural, one {ÃĄlbum} other {ÃĄlbuns}}", "keep_all": "Manter Todos", + "keep_description": "Escolha o que fica no seu dispositivo quando liberta espaço.", + "keep_favorites": "Manter favoritos", + "keep_on_device": "Manter no dispositivo", + "keep_on_device_hint": "Selecionar itens para manter neste dispositivo", "keep_this_delete_others": "Manter este ficheiro, eliminar os outros", + "keeping": "A manter: {items}", "kept_this_deleted_others": "Foi mantido ficheiro e {count, plural, one {eliminado # outro} other {eliminados # outros}}", "keyboard_shortcuts": "Atalhos do teclado", "language": "Idioma", @@ -1343,10 +1442,28 @@ "loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibiÃ§ÃŖo.", "main_branch_warning": "EstÃĄ a usar uma versÃŖo de desenvolvimento; recomendamos vivamente que use uma versÃŖo de lançamento!", "main_menu": "Menu Principal", + "maintenance_action_restore": "A Restaurar Base de Dados", "maintenance_description": "O Immich foi colocado em modo de manutenÃ§ÃŖo.", "maintenance_end": "Desativar modo de manutenÃ§ÃŖo", "maintenance_end_error": "Ocorreu um erro ao desativar o modo de manutenÃ§ÃŖo.", "maintenance_logged_in_as": "SessÃŖo iniciada como {user}", + "maintenance_restore_from_backup": "Restaurar a partir de uma cÃŗpia de segurança", + "maintenance_restore_library": "Restaurar a Sua Biblioteca", + "maintenance_restore_library_confirm": "Se isto parecer correto, continue para restaurar uma cÃŗpia de segurança!", + "maintenance_restore_library_description": "A Restaurar Base de Dados", + "maintenance_restore_library_folder_has_files": "{folder} tem {count} pasta(s)", + "maintenance_restore_library_folder_no_files": "{folder} tem ficheiros em falta!", + "maintenance_restore_library_folder_pass": "leitura e escrita possível", + "maintenance_restore_library_folder_read_fail": "leitura impossível", + "maintenance_restore_library_folder_write_fail": "escrita impossível", + "maintenance_restore_library_hint_missing_files": "Pode ter ficheiros importantes em falta", + "maintenance_restore_library_hint_regenerate_later": "Pode regenerÃĄ-las mais tarde nas definiçÃĩes", + "maintenance_restore_library_hint_storage_template_missing_files": "EstÃĄ a utilizar um modelo de armazenamento? Pode ter ficheiros em falta", + "maintenance_restore_library_loading": "A carregar verificaçÃĩes de integradade e heurísticasâ€Ļ", + "maintenance_task_backup": "A criar uma cÃŗpia de segurança da base de dados existenteâ€Ļ", + "maintenance_task_migrations": "A migrar base de dadosâ€Ļ", + "maintenance_task_restore": "A restaurar a cÃŗpia de segurança selecionadaâ€Ļ", + "maintenance_task_rollback": "NÃŖo foi possível restaurar, a reverter para o ponto de restauroâ€Ļ", "maintenance_title": "Temporariamente Indisponível", "make": "Marca", "manage_geolocation": "Gerir localizaÃ§ÃŖo", @@ -1408,6 +1525,8 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Em falta", "mobile_app": "App mÃŗvel", "mobile_app_download_onboarding_note": "Descarregue a aplicaÃ§ÃŖo para dispositivos mÃŗveis com as seguintes opçÃĩes", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mais", "move": "Mover", + "move_down": "Mover para baixo", "move_off_locked_folder": "Mover para fora da pasta trancada", "move_to": "Mover para", + "move_to_device_trash": "Mover para a reciclagem do dispositivo", "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", + "move_up": "Mover para cima", "moved_to_archive": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para o arquivo", "moved_to_library": "{count, plural, one {Foi movido # ficheiro} other {Foram movidos # ficheiros}} para a biblioteca", "moved_to_trash": "Enviado para a reciclagem", @@ -1430,6 +1552,7 @@ "my_albums": "Os meus ÃĄlbuns", "name": "Nome", "name_or_nickname": "Nome ou alcunha", + "name_required": "O nome Ê obrigatÃŗrio", "navigate": "Navegar", "navigate_to_time": "Navegar para HorÃĄrio", "network_requirement_photos_upload": "Usar dados mÃŗveis para fazer cÃŗpia de segurança de fotos", @@ -1454,20 +1577,24 @@ "next": "Avançar", "next_memory": "PrÃŗxima memÃŗria", "no": "NÃŖo", + "no_actions_added": "Ainda nÃŖo foram adicionadas açÃĩes", + "no_albums_found": "Nenhum ÃĄlbum encontrado", "no_albums_message": "Crie um ÃĄlbum para organizar as suas fotos e vídeos", "no_albums_with_name_yet": "Parece que ainda nÃŖo tem nenhum ÃĄlbum com este nome.", "no_albums_yet": "Parece que 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": "FAÇA CLIQUE PARA CARREGAR A SUA PRIMEIRA FOTO", + "no_assets_message": "Clique para carregar a sua primeira foto", "no_assets_to_show": "NÃŖo hÃĄ ficheiros para exibir", "no_cast_devices_found": "Nenhum dispositivo de transmissÃŖo encontrado", "no_checksum_local": "Sem cÃĄlculo de verificaÃ§ÃŖo disponível - nÃŖo pode capturar conteÃēdos locais", "no_checksum_remote": "Soma de verificaÃ§ÃŖo (checksum) nÃŖo disponível - nÃŖo Ê possível obter o recurso remoto", + "no_configuration_needed": "ConfiguraÃ§ÃŖo nÃŖo Ê necessÃĄria", "no_devices": "Nenhum dispositivo autorizado", "no_duplicates_found": "Nenhum item duplicado foi encontrado.", "no_exif_info_available": "Sem informaçÃĩes exif disponíveis", "no_explore_results_message": "Carregue mais fotos para explorar a sua coleÃ§ÃŖo.", "no_favorites_message": "Adicione aos favoritos para encontrar as suas melhores fotos e vídeos rapidamente", + "no_filters_added": "Ainda nÃŖo foram adicionados filtros", "no_libraries_message": "Crie uma biblioteca externa para ver as suas fotos e vídeos", "no_local_assets_found": "Sem cÃĄlculo de verificaÃ§ÃŖo disponível", "no_location_set": "Sem localizaÃ§ÃŖo definida", @@ -1481,11 +1608,11 @@ "no_results_description": "Tente um sinÃŗnimo ou uma palavra-chave mais comum", "no_shared_albums_message": "Crie um ÃĄlbum para partilhar fotos e vídeos com pessoas na sua rede", "no_uploads_in_progress": "Nenhum carregamento em curso", + "none": "Nenhum", "not_allowed": "NÃŖo permitido", "not_available": "N/A", "not_in_any_album": "NÃŖo estÃĄ em nenhum ÃĄlbum", "not_selected": "NÃŖo selecionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o RÃŗtulo de Armazenamento a ficheiros carregados anteriormente, execute o", "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.", @@ -1563,6 +1690,7 @@ "people": "Pessoas", "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", "people_feature_description": "Navegar por fotos e vídeos agrupados por pessoas", + "people_selected": "{count, plural, one {# pessoa selecionada} other {# pessoas selecionadas}}", "people_sidebar_description": "Exibir o link Pessoas na barra lateral", "permanent_deletion_warning": "Aviso de eliminaÃ§ÃŖo permanente", "permanent_deletion_warning_setting_description": "Exibir um aviso ao eliminar ficheiros de forma permanente", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# anos}} de idade", "person_birthdate": "Nasceu a {date}", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "person_recognized": "Pessoa reconhecida", + "person_selected": "Pessoa selecionada", "photo_shared_all_users": "Parece que jÃĄ partilhou as suas fotos com todos os utilizadores ou nÃŖo tem nenhum utilizador com quem partilhar.", "photos": "Fotos", "photos_and_videos": "Fotos & Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", + "photos_only": "Apenas fotografias", "pick_a_location": "Selecione uma localizaÃ§ÃŖo", "pick_custom_range": "Intervalo personalizado", "pick_date_range": "Selecione um intervalo de datas", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "A chave de produto do servidor Ê gerida pelo administrador", "query_asset_id": "Consultar ID do ficheiro", "queue_status": "Em fila {count}/{total}", + "rate_asset": "Classificar ficheiro", "rating": "ClassificaÃ§ÃŖo por estrelas", "rating_clear": "Limpar classificaÃ§ÃŖo", "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", "rating_description": "Mostrar a classificaÃ§ÃŖo EXIF no painel de informaçÃĩes", + "rating_set": "ClassificaÃ§ÃŖo definida para {rating, plural, one {# estrela} other {# estrelas}}", "reaction_options": "OpçÃĩes de reaÃ§ÃŖo", "read_changelog": "Ler Novidades", "readonly_mode_disabled": "Modo sÃŗ de leitura desativado", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa", "reassing_hint": "Atribuir ficheiros selecionados a uma pessoa existente", "recent": "Recentes", - "recent-albums": "Álbuns recentes", + "recent_albums": "Álbuns recentes", "recent_searches": "Pesquisas recentes", "recently_added": "Adicionados Recentemente", "recently_added_page_title": "Adicionado recentemente", @@ -1770,9 +1903,11 @@ "saved_settings": "DefiniçÃĩes guardadas", "say_something": "Diga alguma coisa", "scaffold_body_error_occurred": "Ocorreu um erro", + "scan": "Analisar", "scan_all_libraries": "Analisar todas as bibliotecas", "scan_library": "Analisar", "scan_settings": "OpçÃĩes de anÃĄlise", + "scanning": "A analisar", "scanning_for_album": "A analisar por ÃĄlbum...", "search": "Pesquisar", "search_albums": "Pesquisar ÃĄlbuns", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Selecione o tipo do ficheiro", "search_filter_ocr": "Pesquisar por OCR", "search_filter_people_title": "Selecionar pessoas", + "search_filter_star_rating": "ClassificaÃ§ÃŖo", "search_for": "Pesquisar por", "search_for_existing_person": "Pesquisar por pessoas existentes", "search_no_more_result": "Sem mais resultados", @@ -1836,17 +1972,23 @@ "second": "Segundo", "see_all_people": "Ver todas as pessoas", "select": "Selecionar", + "select_album": "Selecionar ÃĄlbum", "select_album_cover": "Escolher capa do ÃĄlbum", + "select_albums": "Selecionar ÃĄlbuns", "select_all": "Selecionar todos", "select_all_duplicates": "Selecionar todos os itens duplicados", "select_all_in": "Selecionar tudo em {group}", "select_avatar_color": "Selecionar cor do avatar", + "select_count": "{count, plural, one {Selecionar #} other {Selecionar #}}", + "select_cutoff_date": "Selecionar data limite", "select_face": "Selecionar rosto", "select_featured_photo": "Selecionar foto principal", "select_from_computer": "Selecionar a partir do computador", "select_keep_all": "Selecionar manter todos", "select_library_owner": "Selecionar o dono da biblioteca", "select_new_face": "Selecionar novo rosto", + "select_people": "Selecionar pessoas", + "select_person": "Selecionar pessoa", "select_person_to_tag": "Selecione uma pessoa para etiquetar", "select_photos": "Selecionar fotos", "select_trash_all": "Selecionar todos para reciclagem", @@ -1982,6 +2124,7 @@ "show_password": "Mostrar palavra-passe", "show_person_options": "Exibir opçÃĩes da pessoa", "show_progress_bar": "Exibir barra de progresso", + "show_schema": "Mostrar esquema", "show_search_options": "Mostrar opçÃĩes de pesquisa", "show_shared_links": "Mostrar links partilhados", "show_slideshow_transition": "Mostrar transiçÃĩes no Modo de ApresentaÃ§ÃŖo", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Saltar para pastas", "skip_to_tags": "Saltar para as etiquetas", "slideshow": "ApresentaÃ§ÃŖo", + "slideshow_repeat": "Repetir apresentaÃ§ÃŖo de diapositivos", + "slideshow_repeat_description": "Repetir do inicio quando a apresentaÃ§ÃŖo acabar", "slideshow_settings": "DefiniçÃĩes de apresentaÃ§ÃŖo", "sort_albums_by": "Ordenar ÃĄlbuns por...", "sort_created": "Data de criaÃ§ÃŖo", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Escolha a configuraÃ§ÃŖo do tema da aplicaÃ§ÃŖo", "theme_setting_three_stage_loading_subtitle": "O carregamento em trÃĒs estÃĄgios pode aumentar o desempenho do carregamento, mas causa uma carga de rede significativamente maior", "theme_setting_three_stage_loading_title": "Habilitar carregamento em trÃĒs estÃĄgios", + "then": "Depois", "they_will_be_merged_together": "Eles serÃŖo unidos", "third_party_resources": "Recursos de terceiros", "time": "Hora", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Selecionar ficheiros", "trash_page_title": "Reciclagem ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem sÃŖo eliminados permanentemente apÃŗs {days, plural, one {# dia} other {# dias}}.", + "trigger": "Ativador", + "trigger_asset_uploaded": "Ficheiro Carregado", + "trigger_asset_uploaded_description": "Ativado quando um novo ficheiro Ê carregado", + "trigger_description": "Um evento que irÃĄ começar o fluxo de trabalho", + "trigger_person_recognized": "Pessoa Reconhecida", + "trigger_person_recognized_description": "Ativado quando uma pessoa for detetada", + "trigger_type": "Tipo de ativador", "troubleshoot": "Diagnosticar problemas", "type": "Tipo", "unable_to_change_pin_code": "NÃŖo foi possível alterar o cÃŗdigo PIN", @@ -2123,6 +2276,7 @@ "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", "unknown_country": "País desconhecido", + "unknown_date": "Data desconhecida", "unknown_year": "Ano desconhecido", "unlimited": "Ilimitado", "unlink_motion_video": "Remover relaÃ§ÃŖo com video animado", @@ -2139,17 +2293,19 @@ "unstack": "Desempilhar", "unstack_action_prompt": "{count} desempilhados", "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}", + "unsupported_field_type": "Tipo de campo nÃŖo suportado", "untagged": "Sem etiqueta", + "untitled_workflow": "Fluxo de trabalho sem nome", "up_next": "A seguir", "update_location_action_prompt": "Atualize a localizaÃ§ÃŖo de {count} ficheiros selecionados com:", "updated_at": "Atualizado a", "updated_password": "Palavra-passe atualizada", "upload": "Carregar", - "upload_action_prompt": "{count} à espera de carregar", "upload_concurrency": "Carregamentos em simultÃĸneo", "upload_details": "Detalhes do Carregamento", "upload_dialog_info": "Deseja realizar uma cÃŗpia de segurança dos ficheiros selecionados para o servidor?", "upload_dialog_title": "Enviar ficheiro", + "upload_error_with_count": "Erro ao carregar {count, plural, one {# ficheiro} other {# ficheiros}}", "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a pÃĄgina para ver os novos ficheiros enviados.", "upload_finished": "Carregamento acabado", "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "UtilizaÃ§ÃŖo", "use_biometric": "Utilizar dados biomÊtricos", - "use_current_connection": "usar conexÃŖo atual", + "use_current_connection": "Utilizar a ligaÃ§ÃŖo atual", "use_custom_date_range": "Utilizar um intervalo de datas personalizado", "user": "Utilizador", "user_has_been_deleted": "Este utilizador for eliminado.", @@ -2185,6 +2341,7 @@ "utilities": "Ferramentas", "validate": "Validar", "validate_endpoint_error": "Digite uma URL vÃĄlida", + "validation_error": "Erro de validaÃ§ÃŖo", "variables": "VariÃĄveis", "version": "VersÃŖo", "version_announcement_closing": "O seu amigo, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o cursor estÃĄ sobre o item. Mesmo quando estÃĄ desativado, a reproduÃ§ÃŖo ainda pode ser iniciada passando sobre o ícone de reproduzir.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "videos_only": "Apenas vídeos", "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver tudo", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Usar como foto principal", "viewer_unstack": "Desempilhar", "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", + "visual": "Visual", + "visual_builder": "Construtor visual", "waiting": "Em fila", "waiting_count": "Em espera: {count}", "warning": "Aviso", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Bem-vindo(a) ao Immich", "width": "Largura", "wifi_name": "Nome da rede Wi-Fi", - "workflow": "Fluxo de trabalho", + "workflow_delete_prompt": "Tem a certeza de que quer eliminar este fluxo de trabalho?", + "workflow_deleted": "Fluxo de trabalho eliminado", + "workflow_description": "DescriÃ§ÃŖo do fluxo de trabalho", + "workflow_info": "InformaÃ§ÃŖo do fluxo de trabalho", + "workflow_json": "Fluxo de trabalho JSON", + "workflow_json_help": "Editar a configuraÃ§ÃŖo do fluxo de trabalho em formato JSON. Mudanças irÃŖo ser sincronizadas com o construtor visual.", + "workflow_name": "Nome do fluxo de trabalho", + "workflow_navigation_prompt": "Tem a certeza de que quer sair sem guardar as alteraçÃĩes?", + "workflow_summary": "Resumo do fluxo de trabalho", + "workflow_update_success": "Fluxo de trabalho atualizado com sucesso", + "workflow_updated": "Fluxo de trabalho atualizado", + "workflows": "Fluxos de trabalho", + "workflows_help_text": "Fluxos de trabalho automatizam açÃĩes nos seus ficheiros baseados em ativadores e filtros", "wrong_pin_code": "CÃŗdigo PIN errado", "year": "Ano", "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", + "zero_to_clear_rating": "Carregue no 0 para retirar a classificaÃ§ÃŖo", "zoom_image": "Ampliar/Reduzir imagem", "zoom_to_bounds": "Aproximar aos limites" } diff --git a/i18n/pt_BR.json b/i18n/pt_BR.json index 20eb16a938..7f2845fc53 100644 --- a/i18n/pt_BR.json +++ b/i18n/pt_BR.json @@ -5,19 +5,25 @@ "acknowledge": "Entendi", "action": "AÃ§ÃŖo", "action_common_update": "Atualizar", + "action_description": "Um conjunto de açÃĩes a serem executadas nos arquivos filtrados", "actions": "AçÃĩes", "active": "Em execuÃ§ÃŖo", "active_count": "Ativo: {count}", "activity": "Atividade", - "activity_changed": "A atividade estÃĄ {enabled, select, true {ativada} other {desativada}}", + "activity_changed": "Atividade foi {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", "add_a_description": "Adicionar uma descriÃ§ÃŖo", "add_a_location": "Adicionar uma localizaÃ§ÃŖo", "add_a_name": "Adicionar um nome", "add_a_title": "Adicionar um título", + "add_action": "Adicionar aÃ§ÃŖo", + "add_action_description": "Clique para adicionar uma aÃ§ÃŖo", + "add_assets": "Adicionar arquivos", "add_birthday": "Definir aniversÃĄrio", "add_endpoint": "Adicionar URL", "add_exclusion_pattern": "Adicionar padrÃŖo de exclusÃŖo", + "add_filter": "Adicionar filtro", + "add_filter_description": "Clique para adicional uma condiÃ§ÃŖo no filtro", "add_location": "Adicionar local", "add_more_users": "Adicionar mais usuÃĄrios", "add_partner": "Adicionar parceiro", @@ -28,7 +34,7 @@ "add_to_album": "Adicionar ao ÃĄlbum", "add_to_album_bottom_sheet_added": "Adicionado ao {album}", "add_to_album_bottom_sheet_already_exists": "JÃĄ existe em {album}", - "add_to_album_bottom_sheet_some_local_assets": "Alguns arquivos nÃŖo puderam ser adicionados ao ÃĄlbum", + "add_to_album_bottom_sheet_some_local_assets": "Alguns arquivos locais nÃŖo puderam ser adicionados ao ÃĄlbum", "add_to_album_toggle": "Alternar a seleÃ§ÃŖo de {album}", "add_to_albums": "Adicionar aos ÃĄlbuns", "add_to_albums_count": "Adicionar aos ÃĄlbuns ({count})", @@ -36,13 +42,14 @@ "add_to_shared_album": "Adicionar ao ÃĄlbum compartilhado", "add_upload_to_stack": "Adicionar ao grupo", "add_url": "Adicionar URL", + "add_workflow_step": "Adicionar uma etapa no fluxo", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { "add_exclusion_pattern_description": "Adicione padrÃĩes de exclusÃŖo. Utilizar *, ** ou ? sÃŖo suportados. Para ignorar todos os arquivos em qualquer diretÃŗrio chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que terminam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", "admin_user": "UsuÃĄrio Administrador", - "asset_offline_description": "Este arquivo nÃŖo foi encontrado na biblioteca externa, entÃŖo foi enviado para a lixeira. Se o arquivo foi movido para outra pasta dentro da biblioteca, verifique sua linha do tempo para encontrar o arquivo novamente. Para restaurar este arquivo, certifique-se de que o caminho descrito abaixo pode ser acessado pelo Immich e entÃŖo escaneie a biblioteca.", + "asset_offline_description": "Este arquivo externo nÃŖo foi encontrado no disco e foi movido para a lixeira. Se o arquivo foi movido para outra pasta da biblioteca externa, verifique se ele estÃĄ disponível na linha do tempo. Para restaurar este arquivo, certifique-se de que o caminho abaixo Ê acessível pelo Immich e escaneie a biblioteca novamente.", "authentication_settings": "ConfiguraçÃĩes de AutenticaÃ§ÃŖo", "authentication_settings_description": "Gerenciar senhas, OAuth, e outras configuraçÃĩes de autenticaÃ§ÃŖo", "authentication_settings_disable_all": "Tem certeza de que deseja desativar todos os mÊtodos de login? O login serÃĄ completamente desativado.", @@ -70,7 +77,7 @@ "confirm_user_pin_code_reset": "Tem certeza de que deseja redefinir o cÃŗdigo PIN do usuÃĄrio {user}?", "copy_config_to_clipboard_description": "Copiar as configuraçÃĩes do sistema como um objeto JSON para a ÃĄrea de transferÃĒncia", "create_job": "Criar tarefa", - "cron_expression": "ExpressÃŖo CRON", + "cron_expression": "ExpressÃŖo cron", "cron_expression_description": "Defina o intervalo de anÃĄlise no formato Cron. Para mais informaçÃĩes, por favor veja o Crontab Guru", "cron_expression_presets": "SugestÃĩes de expressÃŖo Cron", "disable_login": "Desabilitar login", @@ -97,6 +104,8 @@ "image_preview_description": "Imagem de tamanho mÊdio sem os metadados, utilizado quando visualizando um Ãēnico arquivo e tambÊm pelo aprendizado de mÃĄquina", "image_preview_quality_description": "Qualidade da prÊ-visualizaÃ§ÃŖo, de 1-100. Maior Ê melhor, mas produz arquivos maiores e pode reduzir a velocidade do aplicativo. Definir um valor muito baixo pode afetar a qualidade do aprendizado de mÃĄquina.", "image_preview_title": "ConfiguraçÃĩes de prÊ-visualizaÃ§ÃŖo", + "image_progressive": "Progressivo", + "image_progressive_description": "Codifique imagens JPEG de forma progressiva para exibiÃ§ÃŖo com carregamento gradual. Isso nÃŖo tem efeito em imagens WebP.", "image_quality": "Qualidade", "image_resolution": "ResoluÃ§ÃŖo", "image_resolution_description": "ResoluçÃĩes mais altas preservam mais detalhes, porÊm demoram mais para processar, tem um tamanho de arquivo maior e pode reduzir a velocidade do aplicativo.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Habilitar a Pesquisa Inteligente", "machine_learning_smart_search_enabled_description": "Se desativado, as imagens nÃŖo serÃŖo codificadas para pesquisa inteligente.", "machine_learning_url_description": "A URL do servidor de aprendizado de mÃĄquina. Se mais de uma URL for fornecida, elas serÃŖo tentadas, uma de cada vez e na ordem indicada, atÊ que uma responda com sucesso. Servidores que nÃŖo responderem serÃŖo ignorados temporariamente atÊ voltarem a estar conectados.", + "maintenance_delete_backup": "Excluir Backup", + "maintenance_delete_backup_description": "Este arquivo serÃĄ excluído de forma irreversível.", + "maintenance_delete_error": "Falha ao excluir o backup.", + "maintenance_restore_backup": "Restaurar Backup", + "maintenance_restore_backup_description": "O Immich serÃĄ apagado e restaurado a partir do backup escolhido. Um backup serÃĄ criado antes de continuar.", + "maintenance_restore_backup_different_version": "Este backup foi criado com uma versÃŖo diferente do Immich!", + "maintenance_restore_backup_unknown_version": "NÃŖo foi possível determinar a versÃŖo do backup.", + "maintenance_restore_database_backup": "Restaurar backup do banco de dados", + "maintenance_restore_database_backup_description": "Reverter para um estado anterior do banco de dados usando um arquivo de backup", "maintenance_settings": "ManutenÃ§ÃŖo", "maintenance_settings_description": "Coloque o Immich em modo de manutenÃ§ÃŖo.", - "maintenance_start": "Iniciar modo de manutenÃ§ÃŖo", + "maintenance_start": "Alternar para o modo de manutenÃ§ÃŖo", "maintenance_start_error": "Ocorreu um erro ao iniciar o modo de manutenÃ§ÃŖo.", + "maintenance_upload_backup": "Carregar arquivo de backup do banco de dados", + "maintenance_upload_backup_error": "NÃŖo foi possível carregar o backup. É um arquivo .sql/.sql.gz?", "manage_concurrency": "Gerenciar simultaneidade", "manage_concurrency_description": "Acesse a pÃĄgina de tarefas para gerenciar a simultaneidade de tarefas", "manage_log_settings": "Gerenciar configuraçÃĩes de log", @@ -252,7 +272,7 @@ "oauth_auto_register": "Registro automÃĄtico", "oauth_auto_register_description": "Registre automaticamente novos usuÃĄrios apÃŗs fazer login com OAuth", "oauth_button_text": "BotÃŖo de texto", - "oauth_client_secret_description": "ObrigatÃŗrio se PKCE (Proof Key for Code Exchange) nÃŖo for suportado pelo provedor OAuth", + "oauth_client_secret_description": "ObrigatÃŗrio para cliente confidencial ou quando o PKCE (Proof Key for Code Exchange) nÃŖo Ê suportado para cliente pÃēblico.", "oauth_enable_description": "Faça login com OAuth", "oauth_mobile_redirect_uri": "URI de redirecionamento mÃŗvel", "oauth_mobile_redirect_uri_override": "SubstituiÃ§ÃŖo de URI de redirecionamento mÃŗvel", @@ -263,9 +283,9 @@ "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": "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_label_claim_description": "Definir automaticamente o rÃŗtulo de armazenamento do usuÃĄrio com o valor desta declaraÃ§ÃŖo.", "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_claim_description": "Definir automaticamente a cota de armazenamento do usuÃĄrio com o valor desta declaraÃ§ÃŖo.", "oauth_storage_quota_default": "Cota de armazenamento padrÃŖo (GiB)", "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", @@ -431,6 +451,9 @@ "admin_password": "Senha do administrador", "administration": "AdministraÃ§ÃŖo", "advanced": "Avançado", + "advanced_settings_clear_image_cache": "Limpar cache de imagens", + "advanced_settings_clear_image_cache_error": "Falha ao limpar o cache de imagens", + "advanced_settings_clear_image_cache_success": "Limpeza concluída com sucesso {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "Use esta opÃ§ÃŖo para filtrar mídias durante a sincronizaÃ§ÃŖo com base em critÊrios alternativos. Tente esta opÃ§ÃŖo somente se o aplicativo estiver com problemas para detectar todos os ÃĄlbuns.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizar filtro alternativo de sincronizaÃ§ÃŖo de ÃĄlbum de dispositivo", "advanced_settings_log_level_title": "Nível de log: {level}", @@ -467,10 +490,12 @@ "album_remove_user": "Remover usuÃĄrio?", "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", "album_search_not_found": "NÃŖo hÃĄ ÃĄlbum que corresponda à sua pesquisa", + "album_selected": "Álbum selecionado", "album_share_no_users": "Parece que vocÃĒ jÃĄ compartilhou este ÃĄlbum com todos os usuÃĄrios ou nÃŖo hÃĄ nenhum usuÃĄrio para compartilhar.", "album_summary": "Resumo do ÃĄlbum", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receba uma notificaÃ§ÃŖo por e-mail quando um ÃĄlbum compartilhado tiver novos recursos", + "album_upload_assets": "Enviar arquivos do seu computador e adicionar ao ÃĄlbum", "album_user_left": "Saiu do ÃĄlbum {album}", "album_user_removed": "UsuÃĄrio {user} foi removido", "album_viewer_appbar_delete_confirm": "Tem certeza de que deseja excluir este ÃĄlbum da sua conta?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Ordem padrÃŖo dos arquivos ao criar novos ÃĄlbuns.", "albums_feature_description": "ColeçÃĩes de arquivos que podem ser compartilhados com outros usuÃĄrios.", "albums_on_device_count": "Álbuns no dispositivo ({count})", + "albums_selected": "{count, plural, one {# ÃĄlbum selecionado} other {# ÃĄlbuns selecionados}}", "all": "Todos", "all_albums": "Todos os ÃĄlbuns", "all_people": "Todas as pessoas", + "all_photos": "Todas as fotos", "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir ediçÃĩes", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Permitir que usuÃĄrios pÃēblicos enviem novos arquivos", "allowed": "Permitido", "alt_text_qr_code": "Imagem do cÃŗdigo QR", + "always_keep": "Manter sempre", + "always_keep_photos_hint": "Liberar espaço manterÃĄ todas as fotos neste dispositivo.", + "always_keep_videos_hint": "Liberar espaço manterÃĄ todos os vídeos neste dispositivo.", "anti_clockwise": "Anti-horÃĄrio", "api_key": "Chave de API", "api_key_description": "Este valor serÃĄ mostrado apenas uma vez. Por favor, certifique-se de copiÃĄ-lo antes de fechar a janela.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {# Arquivado} other {# Arquivados}}", "are_these_the_same_person": "Essas pessoas sÃŖo a mesma pessoa?", "are_you_sure_to_do_this": "Tem certeza de que deseja fazer isso?", + "array_field_not_fully_supported": "Campos array exigem ediÃ§ÃŖo manual do JSON", "asset_action_delete_err_read_only": "NÃŖo Ê possível excluir arquivo sÃŗ leitura, ignorando", "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_created": "Arquivo foi criado", "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", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "ConfiguraçÃĩes de layout da grade de fotos", "asset_list_settings_title": "Grade de Fotos", + "asset_not_found_on_device_android": "Arquivo nÃŖo encontrado no dispositivo", + "asset_not_found_on_device_ios": "Arquivo nÃŖo encontrado no dispositivo. Se estiver usando o iCloud, o arquivo pode estar inacessível devido a um arquivo corrompido armazenado no iCloud", + "asset_not_found_on_icloud": "Arquivo nÃŖo encontrado no iCloud. o arquivo pode estar inacessível devido a um arquivo corrompido armazenado no iCloud", "asset_offline": "Arquivo indisponível", "asset_offline_description": "Este arquivo externo nÃŖo estÃĄ mais disponível. Contate seu administrador do Immich para obter ajuda.", "asset_restored_successfully": "Arquivo restaurado", @@ -587,11 +622,11 @@ "backup": "Backup", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({count})", "backup_album_selection_page_albums_tap": "Toque para incluir, toque duas vezes para excluir", - "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.", + "backup_album_selection_page_assets_scatter": "Os arquivos podem se espalhar por vÃĄrios ÃĄlbuns. Assim, os ÃĄlbuns podem ser incluídos ou excluídos durante o processo de backup.", "backup_album_selection_page_select_albums": "Selecionar ÃĄlbuns", "backup_album_selection_page_selection_info": "InformaçÃĩes da SeleÃ§ÃŖo", - "backup_album_selection_page_total_assets": "Total de recursos exclusivos", - "backup_albums_sync": "Backup de sincronizaÃ§ÃŖo de ÃĄlbuns", + "backup_album_selection_page_total_assets": "Total de arquivos Ãēnicos", + "backup_albums_sync": "SincronizaÃ§ÃŖo de ÃĄlbuns", "backup_all": "Todos", "backup_background_service_backup_failed_message": "Falha ao fazer backup. Tentando novamenteâ€Ļ", "backup_background_service_complete_notification": "Backup dos arquivos concluído", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "As senhas nÃŖo estÃŖo iguais", "change_password_form_reenter_new_password": "Confirme a nova senha", "change_pin_code": "Alterar cÃŗdigo PIN", + "change_trigger": "Alterar gatilho", + "change_trigger_prompt": "Tem certeza de que deseja alterar o gatilho? Isso removerÃĄ todas as açÃĩes e filtros existentes.", "change_your_password": "Alterar sua senha", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "charging": "Carregando", @@ -722,6 +759,18 @@ "checksum": "Checksum", "choose_matching_people_to_merge": "Escolha pessoas correspondentes para mesclar", "city": "Cidade", + "cleanup_confirm_description": "O Immich encontrou {count} arquivos (criados antes de {date}) salvos com segurança no servidor. Deseja remover as cÃŗpias locais deste dispositivo?", + "cleanup_confirm_prompt_title": "Remover deste dispositivo?", + "cleanup_deleted_assets": "{count} mídias movidas para a lixeira do dispositivo", + "cleanup_deleting": "Movendo para a lixeira...", + "cleanup_found_assets": "Encontrados {count} arquivos com backup", + "cleanup_found_assets_with_size": "Foram encontrados {count} arquivos com backup ({size})", + "cleanup_icloud_shared_albums_excluded": "Álbuns compartilhados do iCloud nÃŖo serÃŖo incluídos", + "cleanup_no_assets_found": "NÃŖo foram encontrados arquivos que correspondam aos seus critÊrios. Liberar Espaço sÃŗ pode remover arquivos que foram copiados para o servidor", + "cleanup_preview_title": "Remover {count} arquivos", + "cleanup_step3_description": "Procure por arquivos de backup que correspondam à sua data e manter configuraçÃĩes.", + "cleanup_step4_summary": "{count} arquivos criados antes de {date} foram selecionados para liberar espaço do seu dispositivo. Fotos permanecerÃŖo acessíveis atravÊs do app do Immich.", + "cleanup_trash_hint": "Para liberar espaço imediatamente, abra a galeria de fotos original do dispositivo e esvazie a lixeira", "clear": "Limpar", "clear_all": "Limpar tudo", "clear_all_recent_searches": "Limpar todas as buscas recentes", @@ -733,6 +782,8 @@ "client_cert_import": "Importar", "client_cert_import_success_msg": "Certificado do cliente importado", "client_cert_invalid_msg": "Arquivo de certificado invÃĄlido ou senha errada", + "client_cert_password_message": "Entre com a senha para esse certificado", + "client_cert_password_title": "Senha do certificado", "client_cert_remove_msg": "Certificado do cliente removido", "client_cert_subtitle": "Suporta apenas o formato PKCS12 (.p12, .pfx). A importaÃ§ÃŖo/remoÃ§ÃŖo de certificados estÃĄ disponível apenas antes do login", "client_cert_title": "Certificado de cliente SSL [EXPERIMENTAL]", @@ -787,6 +838,7 @@ "create_album": "Criar ÃĄlbum", "create_album_page_untitled": "Sem título", "create_api_key": "Criar chave de API", + "create_first_workflow": "Criar primeiro fluxo", "create_library": "Criar biblioteca", "create_link": "Criar link", "create_link_to_share": "Criar link e compartilhar", @@ -801,17 +853,25 @@ "create_tag": "Criar marcador", "create_tag_description": "Cria um novo marcador. Para marcadores multi nível, digite o caminho completo do marcador, inclusive as barras.", "create_user": "Criar usuÃĄrio", + "create_workflow": "Criar fluxo", "created": "Criado", "created_at": "Criado em", "creating_linked_albums": "Criando ÃĄlbuns relacionados...", "crop": "Cortar", + "crop_aspect_ratio_fixed": "Fixo", + "crop_aspect_ratio_free": "Livre", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Objetos", "current_device": "Dispositivo atual", "current_pin_code": "CÃŗdigo PIN atual", "current_server_address": "Endereço atual do servidor", + "custom_date": "Data específica", "custom_locale": "LocalizaÃ§ÃŖo Customizada", - "custom_locale_description": "Formatar datas e nÃēmeros baseados na linguagem e regiÃŖo", + "custom_locale_description": "Formatar datas e nÃēmeros baseado no idioma e na regiÃŖo", "custom_url": "URL personalizada", + "cutoff_date_description": "Manter fotos dos Ãēltimosâ€Ļ", + "cutoff_day": "{count, plural, one {dia} other {dias}}", + "cutoff_year": "{count, plural, one {ano} other {anos}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Escuro", @@ -867,6 +927,7 @@ "deselect_all": "Desselecionar tudo", "details": "Detalhes", "direction": "DireÃ§ÃŖo", + "disable": "Desativar", "disabled": "Desativado", "disallow_edits": "NÃŖo permitir ediçÃĩes", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Vídeos inclusos", "download_include_embedded_motion_videos_description": "Baixar os vídeos inclusos de uma foto em movimento em um arquivo separado", "download_notfound": "NÃŖo encontrado", + "download_original": "Baixar original", "download_paused": "Pausado", "download_settings": "Baixar", "download_settings_description": "Gerenciar configuraçÃĩes relacionadas a transferÃĒncia de arquivos", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Aguardando para tentar novamente", "downloading": "Baixando", "downloading_asset_filename": "Baixando arquivo {filename}", + "downloading_from_icloud": "Baixando do iCloud", "downloading_media": "Baixando mídia", "drop_files_to_upload": "Solte os arquivos em qualquer lugar para enviar", "duplicates": "Duplicados", @@ -929,11 +992,22 @@ "edit_tag": "Editar marcador", "edit_title": "Editar Título", "edit_user": "Editar usuÃĄrio", + "edit_workflow": "Editar fluxo", "editor": "Editar", "editor_close_without_save_prompt": "As alteraçÃĩes nÃŖo serÃŖo salvas", "editor_close_without_save_title": "Fechar editor?", - "editor_crop_tool_h2_aspect_ratios": "ProporçÃĩes", - "editor_crop_tool_h2_rotation": "RotaÃ§ÃŖo", + "editor_confirm_reset_all_changes": "Tem certeza que deseja desfazer todas alteraçÃĩes?", + "editor_discard_edits_confirm": "Descartar alteraçÃĩes", + "editor_discard_edits_prompt": "VocÃĒ possui alteraçÃĩes que nÃŖo foram salvas. Tem certeza que deseja descarta-las?", + "editor_discard_edits_title": "Desfazer as alteraçÃĩes?", + "editor_edits_applied_error": "Falhou ao salvar as alteraçÃĩes", + "editor_edits_applied_success": "AlteraçÃĩes salvas com sucesso", + "editor_flip_horizontal": "Virar na horizontal", + "editor_flip_vertical": "Virar na vertical", + "editor_orientation": "OrientaÃ§ÃŖo", + "editor_reset_all_changes": "Desfazer alteraçÃĩes", + "editor_rotate_left": "Girar 90° em sentido anti-horÃĄrio", + "editor_rotate_right": "Girar 90° em sentido horÃĄrio", "email": "E-mail", "email_notifications": "NotificaçÃĩes por e-mail", "empty_folder": "A pasta estÃĄ vazia", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Falha ao alterar a ordem de exibiÃ§ÃŖo", "error_delete_face": "Erro ao remover face do arquivo", "error_getting_places": "Erro ao buscar os locais", + "error_loading_albums": "Erro ao carregar ÃĄlbuns", "error_loading_image": "Erro ao carregar a pÃĄgina", "error_loading_partners": "Erro ao carregar parceiros: {error}", + "error_retrieving_asset_information": "Erro ao recuperar informaçÃĩes do arquivo", "error_saving_image": "Erro: {error}", "error_tag_face_bounding_box": "Erro ao marcar o rosto - nÃŖo foi possível localizar o rosto", "error_title": "Erro - Algo deu errado", + "error_while_navigating": "Erro ao navegar para o arquivo", "errors": { "cannot_navigate_next_asset": "NÃŖo foi possível navegar para o prÃŗximo arquivo", "cannot_navigate_previous_asset": "NÃŖo foi possível navegar para o arquivo anterior", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "NÃŖo foi possível concluir o login OAuth", "unable_to_connect": "NÃŖo foi possível conectar", "unable_to_copy_to_clipboard": "NÃŖo Ê possível copiar para a ÃĄrea de transferÃĒncia, certifique-se que estÃĄ acessando a pagina atravÊs de https", + "unable_to_create": "NÃŖo foi possível criar fluxo", "unable_to_create_admin_account": "NÃŖo foi possível criar uma conta de administrador", "unable_to_create_api_key": "NÃŖo foi possível criar uma nova Chave de API", "unable_to_create_library": "NÃŖo foi possível criar a biblioteca", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "NÃŖo foi possível deletar o padrÃŖo de exclusÃŖo", "unable_to_delete_shared_link": "NÃŖo foi possível deletar o link compartilhado", "unable_to_delete_user": "NÃŖo foi possível deletar o usuÃĄrio", + "unable_to_delete_workflow": "NÃŖo foi possível excluir fluxo", "unable_to_download_files": "NÃŖo foi possível baixar os arquivos", "unable_to_edit_exclusion_pattern": "NÃŖo foi possível editar o padrÃŖo de exclusÃŖo", "unable_to_empty_trash": "NÃŖo foi possível esvaziar a lixeira", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "NÃŖo foi possível escanear a biblioteca", "unable_to_set_feature_photo": "NÃŖo foi possível definir a foto de destaque", "unable_to_set_profile_picture": "NÃŖo foi possível definir a foto de perfil", + "unable_to_set_rating": "NÃŖo foi possível classificar", "unable_to_submit_job": "NÃŖo foi possível enviar a tarefa", "unable_to_trash_asset": "NÃŖo foi possível enviar o arquivo para a lixeira", "unable_to_unlink_account": "NÃŖo foi possível desvincular conta", @@ -1074,8 +1154,10 @@ "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_update_workflow": "NÃŖo foi possível atualizar fluxo", "unable_to_upload_file": "NÃŖo foi possível enviar o arquivo" }, + "errors_text": "Erros", "exclusion_pattern": "PadrÃŖo de exclusÃŖo", "exif": "Exif", "exif_bottom_sheet_description": "Adicionar descriÃ§ÃŖo...", @@ -1120,14 +1202,17 @@ "features": "Funcionalidades", "features_in_development": "FunçÃĩes em desenvolvimento", "features_setting_description": "Gerenciar as funcionalidades da aplicaÃ§ÃŖo", - "file_name": "Nome do arquivo", "file_name_or_extension": "Nome do arquivo ou extensÃŖo", + "file_name_text": "Nome do arquivo", + "file_name_with_value": "Nome do arquivo: {file_name}", "file_size": "Tamanho do arquivo", "filename": "Nome do arquivo", "filetype": "Tipo de arquivo", "filter": "Filtro", + "filter_description": "CondiçÃĩes para filtrar os arquivos enviados", "filter_people": "Filtrar pessoas", "filter_places": "Filtrar lugares", + "filters": "Filtros", "find_them_fast": "Encontre pelo nome em uma pesquisa", "first": "Primeiro", "fix_incorrect_match": "Corrigir correspondÃĒncia incorreta", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Navegar pelas pastas das fotos e vídeos no sistema de arquivos", "forgot_pin_code_question": "Esqueceu seu PIN?", "forward": "Para frente", + "free_up_space": "Liberar espaço", + "free_up_space_description": "Mova as fotos e vídeos de backup para a lixeira do seu dispositivo para liberar espaço. Suas cÃŗpias no servidor permanecem seguras.", + "free_up_space_settings_subtitle": "Liberar espaço no dispositivo", "full_path": "Caminho completo: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Esta funcionalidade carrega recursos externos do Google para funcionar.", "general": "Geral", "geolocation_instruction_location": "Selecione um arquivo com as coordenadas de GPS desejada, ou selecione a localizaÃ§ÃŖo diretamente no mapa", "get_help": "Obter Ajuda", + "get_people_error": "Erro ao obter pessoas", "get_wifiname_error": "NÃŖo foi possível obter o nome do Wi-Fi. Verifique se concedeu as permissÃĩes necessÃĄrias e se estÃĄ conectado a uma rede Wi-Fi", "getting_started": "Primeiros passos", "go_back": "Voltar", @@ -1175,6 +1264,7 @@ "hide_named_person": "Esconder {name}", "hide_password": "Ocultar senha", "hide_person": "Ocultar pessoa", + "hide_schema": "Ocultar esquema", "hide_text_recognition": "Esconder reconhecimento de texto", "hide_unnamed_people": "Esconder pessoas sem nome", "home_page_add_to_album_conflicts": "{added} arquivos adicionados ao ÃĄlbum {album}. {failed} arquivos jÃĄ estÃŖo no ÃĄlbum.", @@ -1247,20 +1337,29 @@ "ios_debug_info_processing_ran_at": "processamento executado em {dateTime}", "items_count": "{count, plural, one {# item} other {# itens}}", "jobs": "Tarefas", + "json_editor": "Editor JSON", + "json_error": "Erro no JSON", "keep": "Manter", + "keep_albums": "Manter ÃĄlbuns", + "keep_albums_count": "Mantendo {count} {count, plural, one {ÃĄlbum} other {ÃĄlbuns}}", "keep_all": "Manter Todos", + "keep_description": "Escolha o que fica no seu dispositivo ao liberar espaço.", + "keep_favorites": "Manter favoritos", + "keep_on_device": "Manter no dispositivo", + "keep_on_device_hint": "Selecione os itens que deseja manter neste dispositivo", "keep_this_delete_others": "Manter este, excluir o resto", + "keeping": "Mantendo: {items}", "kept_this_deleted_others": "Este foi mantido e {count, plural, one {# arquivo foi excluído} other {# arquivos foram excluídos}}", "keyboard_shortcuts": "Atalhos do teclado", "language": "Idioma", "language_no_results_subtitle": "tente refinar seu termo de pesquisa", "language_no_results_title": "nenhum idioma encontrado", "language_search_hint": "Procure idiomas...", - "language_setting_description": "Selecione seu Idioma preferido", + "language_setting_description": "Selecione seu idioma preferido", "large_files": "Arquivos Grandes", "last": "Último", - "last_months": "{count, plural, one {Last month} other {Last # months}}", - "last_seen": "Visto pela ultima vez", + "last_months": "{count, plural, one {MÃĒs passado} other {Últimos # meses}}", + "last_seen": "Visto pela Ãēltima vez", "latest_version": "VersÃŖo mais recente", "latitude": "Latitude", "leave": "Sair", @@ -1297,7 +1396,7 @@ "local_network_sheet_info": "O aplicativo irÃĄ se conectar ao servidor atravÊs deste endereço quando estiver na rede Wi-Fi especificada", "location": "LocalizaÃ§ÃŖo", "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_permission_content": "Para usar o recurso de alternÃĸncia automÃĄtica, o Immich requer permissÃŖo de localizaÃ§ÃŖo precisa para poder ler o nome da rede Wi-Fi atual", "location_picker_choose_on_map": "Escolha no mapa", "location_picker_latitude_error": "Digite uma latitude vÃĄlida", "location_picker_latitude_hint": "Digite a latitude", @@ -1343,10 +1442,28 @@ "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibiÃ§ÃŖo.", "main_branch_warning": "VocÃĒ estÃĄ utilizando uma versÃŖo de desenvolvimento. É fortemente recomendado que utilize uma versÃŖo estÃĄvel!", "main_menu": "Menu Principal", + "maintenance_action_restore": "Restaurando Banco de Dados", "maintenance_description": "O Immich foi colocado em modo de manutenÃ§ÃŖo.", "maintenance_end": "Desativar modo de manutenÃ§ÃŖo", "maintenance_end_error": "Ocorreu um erro ao desativar o modo de manutenÃ§ÃŖo.", "maintenance_logged_in_as": "UsuÃĄrio atual: {user}", + "maintenance_restore_from_backup": "Restaurar a partir de Backup", + "maintenance_restore_library": "Restaurar Sua Biblioteca", + "maintenance_restore_library_confirm": "Se tudo parecer correto, prossiga com a restauraÃ§ÃŖo do backup!", + "maintenance_restore_library_description": "Restaurando o Banco de Dados", + "maintenance_restore_library_folder_has_files": "{folder} possui {count} pasta(s)", + "maintenance_restore_library_folder_no_files": "{folder} estÃĄ faltando arquivos!", + "maintenance_restore_library_folder_pass": "legível e escrevível", + "maintenance_restore_library_folder_read_fail": "ilegível", + "maintenance_restore_library_folder_write_fail": "nÃŖo gravÃĄvel", + "maintenance_restore_library_hint_missing_files": "Talvez estejam faltando arquivos importantes", + "maintenance_restore_library_hint_regenerate_later": "VocÃĒ pode regenerÃĄ-los depois nas configuraçÃĩes", + "maintenance_restore_library_hint_storage_template_missing_files": "EstÃĄ usando um modelo de armazenamento? Podem estar faltando arquivos", + "maintenance_restore_library_loading": "Carregando verificaçÃĩes de integridade e heurísticasâ€Ļ", + "maintenance_task_backup": "Criando um backup do banco de dados existenteâ€Ļ", + "maintenance_task_migrations": "Executando migraçÃĩes do banco de dadosâ€Ļ", + "maintenance_task_restore": "Restaurando o backup escolhidoâ€Ļ", + "maintenance_task_rollback": "Falha na restauraÃ§ÃŖo, voltando para o ponto de restauraÃ§ÃŖoâ€Ļ", "maintenance_title": "Temporariamente Indisponível", "make": "Marca", "manage_geolocation": "Gerenciar localizaÃ§ÃŖo", @@ -1408,6 +1525,8 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Faltando", "mobile_app": "Aplicativo MÃŗvel", "mobile_app_download_onboarding_note": "Baixe o aplicativo mÃŗvel usando as opçÃĩes abaixo", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mais", "move": "Mover", + "move_down": "Mover para baixo", "move_off_locked_folder": "Mover para fora da pasta com senha", "move_to": "Mover para", + "move_to_device_trash": "Mover para lixeira", "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", + "move_up": "Mover para cima", "moved_to_archive": "{count, plural, one {# mídia foi arquivada} other {# mídias foram arquivadas}}", "moved_to_library": "{count, plural, one {# arquivo foi enviado} other {# arquivos foram enviados}} à biblioteca", "moved_to_trash": "Enviado para a lixeira", @@ -1430,6 +1552,7 @@ "my_albums": "Meus Álbuns", "name": "Nome", "name_or_nickname": "Nome ou apelido", + "name_required": "Nome Ê obrigatÃŗrio", "navigate": "Navegar", "navigate_to_time": "Navegar para HorÃĄrio", "network_requirement_photos_upload": "Use a rede mÃŗvel para enviar fotos", @@ -1454,20 +1577,24 @@ "next": "Avançar", "next_memory": "PrÃŗxima memÃŗria", "no": "NÃŖo", + "no_actions_added": "Nenhuma aÃ§ÃŖo foi adicionada ainda", + "no_albums_found": "Nenhum ÃĄlbum encontrado", "no_albums_message": "Crie um ÃĄlbum para organizar suas fotos e vídeos", "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 ENVIAR SUA PRIMEIRA FOTO", + "no_assets_message": "Clique aqui para enviar sua primeira foto", "no_assets_to_show": "NÃŖo hÃĄ arquivos para exibir", "no_cast_devices_found": "Nenhum dispositivo encontrado", "no_checksum_local": "Nenhum checksum disponível - nÃŖo foi possível carregar os arquivos locais", "no_checksum_remote": "Nenhum checksum disponível - nÃŖo foi possível carregar os arquivos remotos", + "no_configuration_needed": "Nenhuma configuraÃ§ÃŖo necessÃĄria", "no_devices": "Nenhum dispostivio autorizado", "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", "no_exif_info_available": "Sem informaçÃĩes exif disponíveis", "no_explore_results_message": "Envie mais fotos para explorar sua coleÃ§ÃŖo.", "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente", + "no_filters_added": "Nenhum filtro adicionado ainda", "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos", "no_local_assets_found": "Nenhum arquivo local foi encontrado com este checksum", "no_location_set": "Sem localizaÃ§ÃŖo", @@ -1481,11 +1608,11 @@ "no_results_description": "Tente um sinônimo ou uma palavra-chave mais geral", "no_shared_albums_message": "Crie um ÃĄlbum para compartilhar fotos e vídeos com pessoas em sua rede", "no_uploads_in_progress": "Nenhum envio em progresso", + "none": "Nenhum", "not_allowed": "NÃŖo permitido", "not_available": "N/A", "not_in_any_album": "Fora de ÃĄlbum", "not_selected": "NÃŖo selecionado", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rÃŗtulo de armazenamento a arquivos enviados anteriormente, execute o", "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.", @@ -1563,6 +1690,7 @@ "people": "Pessoas", "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", "people_feature_description": "Navegar por fotos e vídeos agrupados por pessoas", + "people_selected": "{count, plural, one {# pessoa selecionada} other {# pessoas selecionadas}}", "people_sidebar_description": "Exibe o link Pessoas na barra lateral", "permanent_deletion_warning": "Aviso para deletar permanentemente", "permanent_deletion_warning_setting_description": "Exibe um aviso ao deletar arquivos de forma permanente", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# anos}}", "person_birthdate": "Nasceu em {date}", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "person_recognized": "Pessoa reconhecida", + "person_selected": "Pessoa selecionada", "photo_shared_all_users": "Parece que vocÃĒ compartilhou suas fotos com todos os usuÃĄrios ou nÃŖo tem nenhum usuÃĄrio com quem compartilhar.", "photos": "Fotos", "photos_and_videos": "Fotos e Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", + "photos_only": "Somente fotos", "pick_a_location": "Selecione uma localizaÃ§ÃŖo", "pick_custom_range": "Intervalo customizado", "pick_date_range": "Selecione o intervalo de datas", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "A chave do produto para servidor Ê gerenciada pelo administrador", "query_asset_id": "Consultar ID do Ativo", "queue_status": "Na fila {count} de {total}", + "rate_asset": "Classificar arquivo", "rating": "Estrelas", "rating_clear": "Limpar classificaÃ§ÃŖo", "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", "rating_description": "Exibir o EXIF de classificaÃ§ÃŖo no painel de informaçÃĩes", + "rating_set": "ClassificaÃ§ÃŖo alterada para {rating, plural, one {# estrela} other {# estrelas}}", "reaction_options": "OpçÃĩes de reaÃ§ÃŖo", "read_changelog": "Ler Novidades", "readonly_mode_disabled": "Modo apenas visualizaÃ§ÃŖo desativado", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente", "recent": "Recente", - "recent-albums": "Álbuns recentes", + "recent_albums": "Álbuns recentes", "recent_searches": "Pesquisas recentes", "recently_added": "Adicionado recentemente", "recently_added_page_title": "Adicionados recentemente", @@ -1770,9 +1903,11 @@ "saved_settings": "ConfiguraçÃĩes salvas", "say_something": "Diga algo", "scaffold_body_error_occurred": "Ocorreu um erro", + "scan": "Escanear", "scan_all_libraries": "Escanear Todas Bibliotecas", "scan_library": "Escanear", "scan_settings": "OpçÃĩes de escanear", + "scanning": "Escaneando", "scanning_for_album": "Escaneando por ÃĄlbum...", "search": "Pesquisar", "search_albums": "Pesquisar ÃĄlbuns", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Selecione o tipo de mídia", "search_filter_ocr": "Buscar por OCR", "search_filter_people_title": "Selecione pessoas", + "search_filter_star_rating": "AvaliaÃ§ÃŖo", "search_for": "Pesquisar por", "search_for_existing_person": "Pesquisar por pessoas", "search_no_more_result": "NÃŖo hÃĄ mais resultados", @@ -1836,17 +1972,23 @@ "second": "Segundo", "see_all_people": "Ver todas as pessoas", "select": "Selecionar", + "select_album": "Selecionar ÃĄlbum", "select_album_cover": "Escolher capa do ÃĄlbum", + "select_albums": "Selecionar ÃĄlbuns", "select_all": "Selecionar todos", "select_all_duplicates": "Selecionar todas as duplicatas", "select_all_in": "Selecionar tudo em {group}", "select_avatar_color": "Selecionar cor do avatar", + "select_count": "{count, plural, one {Selecionar #} other {Selecionar #}}", + "select_cutoff_date": "Selecione a data limite", "select_face": "Selecionar rosto", "select_featured_photo": "Selecionar foto principal", "select_from_computer": "Selecionar do computador", "select_keep_all": "Marcar manter em todos", "select_library_owner": "Selecione o dono da biblioteca", "select_new_face": "Selecionar novo rosto", + "select_people": "Selecionar pessoas", + "select_person": "Selecionar pessoa", "select_person_to_tag": "Selecione uma pessoa para marcar", "select_photos": "Selecionar fotos", "select_trash_all": "Marcar lixo em todos", @@ -1856,7 +1998,7 @@ "selected_gps_coordinates": "Coordenadas de GPS Selecionada", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", - "server_endpoint": "URL do servidor", + "server_endpoint": "URL do Servidor", "server_info_box_app_version": "VersÃŖo do aplicativo", "server_info_box_server_url": "Endereço", "server_offline": "Servidor Indisponível", @@ -1982,6 +2124,7 @@ "show_password": "Exibir senha", "show_person_options": "Exibir opçÃĩes da pessoa", "show_progress_bar": "Exibir barra de progresso", + "show_schema": "Exibir esquema", "show_search_options": "Exibir opçÃĩes de pesquisa", "show_shared_links": "Mostrar links compartilhados", "show_slideshow_transition": "Usar transiçÃĩes no modo de apresentaÃ§ÃŖo", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Ir para pastas", "skip_to_tags": "Ir para os marcadores", "slideshow": "ApresentaÃ§ÃŖo", + "slideshow_repeat": "Repetir apresentaÃ§ÃŖo de slides", + "slideshow_repeat_description": "Voltar para o início quando a apresentaÃ§ÃŖo terminar", "slideshow_settings": "OpçÃĩes de apresentaÃ§ÃŖo", "sort_albums_by": "Ordenar ÃĄlbuns por...", "sort_created": "Data de criaÃ§ÃŖo", @@ -2030,7 +2175,7 @@ "storage": "Espaço de armazenamento", "storage_label": "RÃŗtulo de armazenamento", "storage_quota": "Quota de armazenamento", - "storage_usage": "Utilizado {used} de {available}", + "storage_usage": "Utilizando {used} de {available}", "submit": "Enviar", "success": "Sucesso", "suggestions": "SugestÃĩes", @@ -2061,7 +2206,7 @@ "text_recognition": "Reconhecimento de texto", "theme": "Tema", "theme_selection": "Selecionar tema", - "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferÃĒncia do sistema do seu navegador", + "theme_selection_description": "Definir automaticamente o tema como claro ou escuro com base nas preferÃĒncias do sistema do seu navegador", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento na grade de fotos", "theme_setting_asset_list_tiles_per_row_title": "Quantidade de arquivos por linha ({count})", "theme_setting_colorful_interface_subtitle": "Aplica a cor primÃĄria ao fundo.", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Escolha a configuraÃ§ÃŖo de tema do app", "theme_setting_three_stage_loading_subtitle": "O carregamento em trÃĒs estÃĄgios oferece a imagem de melhor qualidade em troca de uma velocidade de carregamento mais lenta", "theme_setting_three_stage_loading_title": "Ative o carregamento em trÃĒs estÃĄgios", + "then": "Antes", "they_will_be_merged_together": "Eles serÃŖo mesclados", "third_party_resources": "Recursos de terceiros", "time": "Hora", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Selecionar arquivos", "trash_page_title": "Lixeira ({count})", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira serÃŖo deletados permanentemente apÃŗs {days, plural, one {# dia} other {# dias}}.", + "trigger": "Gatilho", + "trigger_asset_uploaded": "Arquivo enviado", + "trigger_asset_uploaded_description": "Acionado quando um novo arquivo Ê enviado", + "trigger_description": "Um evento que dÃĄ início ao fluxo", + "trigger_person_recognized": "Pessoa reconhecida", + "trigger_person_recognized_description": "Acionado quando uma pessoa Ê detectada", + "trigger_type": "Tipo de gatilho", "troubleshoot": "Diagnosticar", "type": "Tipo", "unable_to_change_pin_code": "NÃŖo foi possível alterar o cÃŗdigo PIN", @@ -2123,6 +2276,7 @@ "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", "unknown_country": "País desconhecido", + "unknown_date": "Data desconhecida", "unknown_year": "Ano desconhecido", "unlimited": "Ilimitado", "unlink_motion_video": "Remover relaÃ§ÃŖo com video animado", @@ -2139,17 +2293,19 @@ "unstack": "Desagrupar", "unstack_action_prompt": "{count} desagrupados", "unstacked_assets_count": "{count, plural, one {# arquivo retirado} other {# arquivos retirados}} do grupo", + "unsupported_field_type": "Tipo de campo nÃŖo suportado", "untagged": "Marcador removido", + "untitled_workflow": "Fluxo sem título", "up_next": "A seguir", "update_location_action_prompt": "Atualizar a localizaÃ§ÃŖo de {count} arquivos selecionados para:", "updated_at": "Atualizado em", "updated_password": "Senha atualizada", "upload": "Enviar", - "upload_action_prompt": "{count} na fila de envio", "upload_concurrency": "Envios simultÃĸneos", "upload_details": "Detalhes do envio", "upload_dialog_info": "Deseja fazer o backup dos arquivos selecionados no servidor?", "upload_dialog_title": "Enviar arquivo", + "upload_error_with_count": "Erro de envio para {count, plural, one {# arquivo} other {# arquivos}}", "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a pÃĄgina para ver os novos arquivos.", "upload_finished": "Envio finalizado", "upload_progress": "{remaining, number} restantes - {processed, number}/{total, number} jÃĄ processados", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "Uso", "use_biometric": "Usar biometria", - "use_current_connection": "usar conexÃŖo atual", + "use_current_connection": "Usar a conexÃŖo atual", "use_custom_date_range": "Usar intervalo de datas personalizado", "user": "UsuÃĄrio", "user_has_been_deleted": "Este usuÃĄrio foi excluído.", @@ -2185,6 +2341,7 @@ "utilities": "Ferramentas", "validate": "Validar", "validate_endpoint_error": "Digite uma URL vÃĄlida", + "validation_error": "Erro de validaÃ§ÃŖo", "variables": "VariÃĄveis", "version": "VersÃŖo", "version_announcement_closing": "De seu amigo, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Reproduzir a miniatura do vídeo ao passar o mouse sobre o item. Mesmo quando desativado, a reproduÃ§ÃŖo pode ser iniciada ao passar o mouse sobre o ícone de reproduÃ§ÃŖo.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", + "videos_only": "Somente videos", "view": "Ver", "view_album": "Ver ÃĄlbum", "view_all": "Ver tudo", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Usar como foto principal", "viewer_unstack": "Desagrupar", "visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}", + "visual": "Visual", + "visual_builder": "Construtor visual", "waiting": "Na fila", "waiting_count": "Esperando: {count}", "warning": "Aviso", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Bem-vindo(a) ao Immich", "width": "Largura", "wifi_name": "Nome do Wi-Fi", - "workflow": "AutomaÃ§ÃŖo", + "workflow_delete_prompt": "Tem certeza de que deseja excluir este fluxo?", + "workflow_deleted": "Fluxo excluído", + "workflow_description": "DescriÃ§ÃŖo do fluxo", + "workflow_info": "InformaçÃĩes sobre fluxo", + "workflow_json": "Fluxo em JSON", + "workflow_json_help": "Edite a configuraÃ§ÃŖo do fluxo em formato JSON. As alteraçÃĩes serÃŖo sincronizadas com o construtor visual.", + "workflow_name": "Nome do fluxo", + "workflow_navigation_prompt": "Tem certeza de que deseja sair sem salvar as alteraçÃĩes?", + "workflow_summary": "Resumo do fluxo", + "workflow_update_success": "Fluxo atualizado com sucesso", + "workflow_updated": "Fluxo atualizado", + "workflows": "Fluxos", + "workflows_help_text": "Fluxos utilizam gatilhos e filtros para automatizar açÃĩes sobre os arquivos", "wrong_pin_code": "CÃŗdigo PIN incorreto", "year": "Ano", "years_ago": "{years, plural, one {# ano} other {# anos}} atrÃĄs", "yes": "Sim", "you_dont_have_any_shared_links": "NÃŖo hÃĄ links compartilhados", "your_wifi_name": "Nome do seu Wi-Fi", + "zero_to_clear_rating": "Tecle 0 para remover a classificaÃ§ÃŖo", "zoom_image": "Ampliar imagem", "zoom_to_bounds": "Ampliar para preencher" } diff --git a/i18n/ro.json b/i18n/ro.json index 90cdc5ddbf..b36ae22d36 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -2,9 +2,10 @@ "about": "Despre", "account": "Cont", "account_settings": "Setări cont", - "acknowledge": "Confirmare", + "acknowledge": "Am ÃŽnțeles", "action": "AcÅŖiune", "action_common_update": "Actualizează", + "action_description": "Un set de acțiuni de efectuat asupra elementelor filtrate", "actions": "AcÅŖiuni", "active": "Active", "active_count": "Activ: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Adaugă o locație", "add_a_name": "Adaugă un nume", "add_a_title": "Adaugă un titlu", + "add_action": "Adaugă acÅŖiune", + "add_action_description": "Click pentru a adăuga o acțiune de rulat", + "add_assets": "Adaugă elemente", "add_birthday": "Adaugă zi de naștere", "add_endpoint": "Adaugă punct final", "add_exclusion_pattern": "Adăugă un model de excludere", + "add_filter": "Adaugă filtru", + "add_filter_description": "Click pentru a adăuga o condiție de filtrare", "add_location": "Adaugă locație", "add_more_users": "Adaugă mai mulți utilizatori", "add_partner": "Adaugă partener", @@ -36,8 +42,9 @@ "add_to_shared_album": "Adaugă la album partajat", "add_upload_to_stack": "Încarcă și adaugă la stivă", "add_url": "Adaugă adresa URL", + "add_workflow_step": "Adaugă un pas ÃŽn workflow", "added_to_archive": "Adăugat la arhivă", - "added_to_favorites": "Adaugă la favorite", + "added_to_favorites": "Adăugat la favorite", "added_to_favorites_count": "Adăugat {count, number} la favorite", "admin": { "add_exclusion_pattern_description": "Adaugă 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/**”.", @@ -97,6 +104,8 @@ "image_preview_description": "Imagine de dimensiune medie cu metadate eliminate, utilizată la vizualizarea unui singur element și pentru ÃŽnvățarea automată", "image_preview_quality_description": "Calitatea previzualizării de la 1 la 100. O valoare mai mare oferă o calitate mai bună, dar produce fișiere mai mari și poate reduce receptivitatea aplicației. Setarea unei valori scăzute poate afecta calitatea ÃŽnvățării automate.", "image_preview_title": "Previzualizați setările", + "image_progressive": "Progresiv", + "image_progressive_description": "Encodează imaginile JPEG progresiv, pentru ÃŽncărcare graduală.Fără efect pentru imaginile WebP", "image_quality": "Calitate", "image_resolution": "Rezolutie", "image_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru a fi codificate, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.", @@ -110,9 +119,10 @@ "job_created": "Sarcină creată", "job_not_concurrency_safe": "Această sarcină nu este sigură pentru a rula ÃŽn concurență.", "job_settings": "Setări sarcină", - "job_settings_description": "Administrează concurența sarcinilor", + "job_settings_description": "Gestionează sarcinile paralele", "jobs_delayed": "{jobCount, plural, other {# ÃŽntÃĸrziat}}", "jobs_failed": "{jobCount, plural, other {# eșuat}}", + "jobs_over_time": "Sarcini de-a lungul timpului", "library_created": "Librărie creată: {library}", "library_deleted": "Bibliotecă ștearsă", "library_details": "Detalii bibliotecă", @@ -180,12 +190,23 @@ "machine_learning_smart_search_enabled": "Activează căutarea inteligentă", "machine_learning_smart_search_enabled_description": "Dacă este dezactivată, imaginile nu vor fi codificate pentru căutarea inteligentă.", "machine_learning_url_description": "URL-ul serverului de ÃŽnvățare automată. Dacă sunt furnizate mai multe URL-uri, fiecare server va fi ÃŽncercat pe rÃĸnd, pÃĸnă cÃĸnd unul răspunde cu succes, ÃŽn ordine de la primul pÃĸnă la ultimul. Serverele care nu răspund vor fi ignorate temporar pÃĸnă revin online.", + "maintenance_delete_backup": "Sterge Backup", + "maintenance_delete_backup_description": "Acest fisier va fi sters permanent.", + "maintenance_delete_error": "Stergerea backup-ului nu a reusit.", + "maintenance_restore_backup": "Restaureaza Backup", + "maintenance_restore_backup_description": "Immich va fi șters si restaurat din backup-ul ales. Va fi creat un nou backup ÃŽnainte de a continua.", + "maintenance_restore_backup_different_version": "Acest backup a fost creat folosind o versiune diferita de Immich!", + "maintenance_restore_backup_unknown_version": "Versiunea de backup nu a putut fi determinată.", + "maintenance_restore_database_backup": "Restaurează baza de date din backup", + "maintenance_restore_database_backup_description": "Restaureaza la o bază de date precedentă folosind un fisier backup", "maintenance_settings": "Întreținere", "maintenance_settings_description": "Puneți Immich ÃŽn modul de ÃŽntreținere.", - "maintenance_start": "Pornește modul de ÃŽntreținere", + "maintenance_start": "Schimbă la modul de ÃŽntreținere", "maintenance_start_error": "Nu s-a putut porni modul de ÃŽntreținere.", - "manage_concurrency": "Gestionarea simultaneității", - "manage_concurrency_description": "Accesează pagina de joburi pentru a gestiona concurența lor.", + "maintenance_upload_backup": "Încarcă fișier backup pentru baza de date", + "maintenance_upload_backup_error": "Nu s-a putut ÃŽncărca backupul, e un fișier .sql/.sql.gz?", + "manage_concurrency": "Gestionează sarcinile paralele", + "manage_concurrency_description": "Accesează pagina de joburi pentru a gestiona concurența lor", "manage_log_settings": "Administrați setările jurnalului", "map_dark_style": "Mod ÃŽntunecat", "map_enable_description": "Activează funcțiile hărții", @@ -251,7 +272,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_client_secret_description": "Necesar pentru un client confidențial sau dacă PKCE (Proof Key for Code Exchange) nu este suportat pentru un client public.", "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ă", @@ -277,10 +298,12 @@ "person_cleanup_job": "Ștergere persoane", "queue_details": "Detalii coadă", "queues": "Cozi de joburi", + "queues_page_description": "Pagina cu cozi de sarcini administrative", "quota_size_gib": "Spațiu de stocare alocat (GiB)", "refreshing_all_libraries": "Bibliotecile sunt ÃŽn curs de reÃŽmprospĮŽtare", "registration": "Înregistrare Administratori", "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastră.", + "remove_failed_jobs": "Elimina sarcinile eșuate", "require_password_change_on_login": "ObligĮŽ utilizatorul sĮŽ ÃŽČ™i schimbe parola la prima autentificare", "reset_settings_to_default": "ReseteazĮŽ setĮŽrile la valorile implicite", "reset_settings_to_recent_saved": "ReseteazĮŽ setĮŽrile la valorile salvate recent", @@ -428,6 +451,9 @@ "admin_password": "Parolă administrator", "administration": "Administrare", "advanced": "Avansat", + "advanced_settings_clear_image_cache": "Șterge cache-ul", + "advanced_settings_clear_image_cache_error": "Ștergerea cache-ului de imagini a eșuat", + "advanced_settings_clear_image_cache_success": "{size} șterși cu succes", "advanced_settings_enable_alternate_media_filter_subtitle": "Utilizați această opțiune pentru a filtra conținutul media ÃŽn timpul sincronizării pe baza unor criterii alternative. Încercați numai dacă ÃŽntÃĸmpinați probleme cu aplicația la detectarea tuturor albumelor.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTAL] Utilizați filtrul alternativ de sincronizare a albumelor de pe dispozitiv", "advanced_settings_log_level_title": "Nivel log: {level}", @@ -464,10 +490,12 @@ "album_remove_user": "Eliminare utilizator?", "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?", "album_search_not_found": "Nu s-au găsit albume care să corespundă căutării dumneavoastră", + "album_selected": "Album selectat", "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.", "album_summary": "Rezumat album", "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail cÃĸnd un album partajat are elemente noi", + "album_upload_assets": "Încarcă elemente din calculatorul personal și adaugă in album", "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?", @@ -485,9 +513,11 @@ "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.", "albums_on_device_count": "{count} albume pe dispozitiv", + "albums_selected": "{număr, plural, unul {# album selectat} altele {# albumuri selectate}}", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", + "all_photos": "Toate fotografiile", "all_videos": "Toate videoclipurile", "allow_dark_mode": "Permite mod ÃŽntunecat", "allow_edits": "Permite editări", @@ -495,6 +525,9 @@ "allow_public_user_to_upload": "Permite utilizatorului public să ÃŽncarce", "allowed": "Permis", "alt_text_qr_code": "Cod QR", + "always_keep": "Păstrează ÃŽntotdeauna", + "always_keep_photos_hint": "Eliberează Spațiu va păstra toate fotografiile de pe acest dispozitiv.", + "always_keep_videos_hint": "Eliberează Spațiu va păstra toate video-urile de pe acest dispozitiv.", "anti_clockwise": "În sens invers acelor de ceasornic", "api_key": "Cheie API", "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați ÃŽnainte de a ÃŽnchide fereastra.", @@ -521,10 +554,12 @@ "archived_count": "{count, plural, one {Arhivat} few {# arhivate} other {# arhivate}}", "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?", + "array_field_not_fully_supported": "CÃĸmpurile necesită editare manuală JSON", "asset_action_delete_err_read_only": "Fișierele cu permisiuni doar de citire nu au putut fi șterse, omitere", "asset_action_share_err_offline": "Fișierele offline nu au putut accesate, omitere", "asset_added_to_album": "Adăugat la album", "asset_adding_to_album": "Se adaugă la albumâ€Ļ", + "asset_created": "Resurse create", "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", @@ -537,6 +572,9 @@ "asset_list_layout_sub_title": "Aspect", "asset_list_settings_subtitle": "Setări format grilă fotografii", "asset_list_settings_title": "Grilă fotografii", + "asset_not_found_on_device_android": "Obiect negăsit pe dispozitiv", + "asset_not_found_on_device_ios": "Obiect negăsit pe dispozitiv.Dacă folosești iCloud, obiectul poate fi inaccesibil din cauza stocării incorecte pe iCloud", + "asset_not_found_on_icloud": "Obiect negăsit pe iCloud. Obiectul poate fi inaccesibil din cauza stocării incorecte pe iCloud", "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", @@ -588,7 +626,7 @@ "backup_album_selection_page_select_albums": "Selectează albume", "backup_album_selection_page_selection_info": "Informații selecție", "backup_album_selection_page_total_assets": "Total resurse unice", - "backup_albums_sync": "Sincronizarea albumelor de backup", + "backup_albums_sync": "Sincronizarea albumelor de rezervă", "backup_all": "Toate", "backup_background_service_backup_failed_message": "Eșuare backup resurse. ReÃŽncercareâ€Ļ", "backup_background_service_complete_notification": "Backup resurse finalizat", @@ -649,6 +687,7 @@ "backup_options_page_title": "Opțiuni copie de rezervă", "backup_setting_subtitle": "Schimbă opțiuni pentru backup ÃŽn prim-plan și ÃŽn fundal", "backup_settings_subtitle": "Gestionați setările de ÃŽncărcare", + "backup_upload_details_page_more_details": "Apasa pentru mai multe detalii", "backward": "În sens invers", "biometric_auth_enabled": "Autentificare biometrică activată", "biometric_locked_out": "Sunteți blocați de la autentificare biometrică", @@ -681,8 +720,8 @@ "camera": "CamerĮŽ", "camera_brand": "MarcĮŽ cameră", "camera_model": "Model cameră", - "cancel": "Anulați", - "cancel_search": "Anulați căutarea", + "cancel": "Anuleaza", + "cancel_search": "Anuleaza căutarea", "canceled": "Anulat", "canceling": "În curs de anulare", "cannot_merge_people": "Nu se pot ÃŽmbina persoanele", @@ -707,6 +746,8 @@ "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_trigger": "mecanism de schimbare", + "change_trigger_prompt": "Ești sigur ca vrei sa schimbi mecanismul? Aceasta va șterge toate actiunile și filtrele existente.", "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbare vizibilitate cu succes", "charging": "Încărcare", @@ -715,8 +756,21 @@ "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", + "checksum": "Suma de control", "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona", "city": "Oraș", + "cleanup_confirm_description": "Immich a găsit {count} materiale (create ÃŽnainte de {date}) salvate ÃŽn siguranță pe server. Eliminați copiile locale de pe acest dispozitiv?", + "cleanup_confirm_prompt_title": "Elimina de pe dispozitiv?", + "cleanup_deleted_assets": "Muta {count} materiale in coșul de gunoi", + "cleanup_deleting": "Se șterge...", + "cleanup_found_assets": "Am găsit {count} materiale in copia de rezerva", + "cleanup_found_assets_with_size": "{count} obiecte găsite ({size})", + "cleanup_icloud_shared_albums_excluded": "Albumele partajate iCLoud sunt excluse de la cautare", + "cleanup_no_assets_found": "Nu au fost găsite fișiere care să corespundă criteriilor de mai sus. „Eliberare spațiu” poate șterge doar fișierele care au fost deja salvate pe server.", + "cleanup_preview_title": "Materiale sa fie șterse ({count})", + "cleanup_step3_description": "Scanează fișierele salvate pe server care corespund setărilor tale de dată și păstrare.", + "cleanup_step4_summary": "{count} elemente create ÃŽnainte de {date} sunt puse ÃŽn coadă pentru a fi eliminate de pe dispozitiv", + "cleanup_trash_hint": "Pentru a recupera complet spațiu de stocare, deschideți aplicația Galerie și goliți coșul de gunoi", "clear": "Curățați", "clear_all": "Curățați tot", "clear_all_recent_searches": "Curățați toate căutările recente", @@ -728,6 +782,8 @@ "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_password_message": "Introduceți parola pentru acest certificat", + "client_cert_password_title": "Parola certificatului", "client_cert_remove_msg": "Certificatul de client este șters", "client_cert_subtitle": "Este suportat doar formatul PKCS12 (.p12, .pfx). Importul/ștergerea certificatului este disponibil(ă) doar ÃŽnainte de autentificare", "client_cert_title": "Certificat SSL pentru client [EXPERIMENTAL]", @@ -782,6 +838,7 @@ "create_album": "Creează album", "create_album_page_untitled": "Fără nume", "create_api_key": "Creează cheie API", + "create_first_workflow": "Creați primul flux de lucru", "create_library": "Creează Bibliotecă", "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", @@ -796,17 +853,25 @@ "create_tag": "Creează etichetă", "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", + "create_workflow": "Creați un flux de lucru", "created": "Creat", "created_at": "Creat", "creating_linked_albums": "Crearea albumelor cu link...", "crop": "Decupează", + "crop_aspect_ratio_fixed": "Reparat", + "crop_aspect_ratio_free": "Liber", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Obiecte", "current_device": "Dispozitiv curent", "current_pin_code": "Codul PIN actual", "current_server_address": "Adresa actuală a serverului", + "custom_date": "Data personalizată", "custom_locale": "Setare Regională Personalizată", "custom_locale_description": "Formatați datele și numerele ÃŽn funcție de limbă și regiune", "custom_url": "URL personalizat", + "cutoff_date_description": "Păstrează fotografiile din ultimeleâ€Ļ", + "cutoff_day": "{număr, plural, o {day} mai multe {days}}", + "cutoff_year": "{count, plural, =0 {0 ani} one {# an} few {# ani} other {# de ani}}", "daily_title_text_date": "E, LLL zz", "daily_title_text_date_year": "E, LLL zz, aaaa", "dark": "Întunecat", @@ -862,6 +927,7 @@ "deselect_all": "Deselectează toate", "details": "Detalii", "direction": "Direcție", + "disable": "Dezactivare", "disabled": "Dezactivat", "disallow_edits": "Interzice modificările", "discord": "Server Discord", @@ -887,6 +953,7 @@ "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_notfound": "Descărcare negăsită", + "download_original": "Descarcă originalul", "download_paused": "Descărcarea a fost ÃŽntreruptă", "download_settings": "Descărcați", "download_settings_description": "Gestionați setările legate de descărcarea resurselor", @@ -896,6 +963,7 @@ "download_waiting_to_retry": "Se așteaptă o nouă ÃŽncercare", "downloading": "Se descarcă", "downloading_asset_filename": "Se descarcă resursa {filename}", + "downloading_from_icloud": "Se descarcă din iCloud", "downloading_media": "Se descarcă fișierele media", "drop_files_to_upload": "Trageți fișierele aici pentru a le ÃŽncărca", "duplicates": "Duplicate", @@ -924,11 +992,22 @@ "edit_tag": "Editare etichetă", "edit_title": "Editare Titlu", "edit_user": "Editare utilizator", + "edit_workflow": "Modifică fluxul de lucru", "editor": "Editor", "editor_close_without_save_prompt": "Schimbările nu vor fi salvate", "editor_close_without_save_title": "Închideți editorul?", - "editor_crop_tool_h2_aspect_ratios": "Raporturi de aspect", - "editor_crop_tool_h2_rotation": "Rotire", + "editor_confirm_reset_all_changes": "Sigur vrei să resetezi toate modificările?", + "editor_discard_edits_confirm": "Renunță modificările", + "editor_discard_edits_prompt": "Ai modificări nesalvate. Ești sigur că vrei să le renunți?", + "editor_discard_edits_title": "Renunți la modificări?", + "editor_edits_applied_error": "Nu s-au putut aplica modificările", + "editor_edits_applied_success": "Modificările au fost aplicate cu succes", + "editor_flip_horizontal": "Întoarceți orizontal", + "editor_flip_vertical": "Întoarceți vertical", + "editor_orientation": "Orientare", + "editor_reset_all_changes": "Resetați modificările", + "editor_rotate_left": "Rotiți cu 90° ÃŽn sens invers acelor de ceasornic", + "editor_rotate_right": "Rotiți cu 90° ÃŽn sensul acelor de ceasornic", "email": "Adresă de mail", "email_notifications": "Notificări e-mail", "empty_folder": "Acest dosar este gol", @@ -947,11 +1026,14 @@ "error_change_sort_album": "Nu s-a putut modifica ordinea de sortare a albumului", "error_delete_face": "Eroare la ștergerea feței din activ", "error_getting_places": "Eroare la obținerea locațiilor", + "error_loading_albums": "Eroare la ÃŽncărcarea albumelor", "error_loading_image": "Eroare la ÃŽncărcarea imaginii", "error_loading_partners": "Eroare la ÃŽncărcarea partenerilor: {error}", + "error_retrieving_asset_information": "Eroare la colectarea informațiilor obiectului", "error_saving_image": "Eroare: {error}", "error_tag_face_bounding_box": "Eroare la etichetarea feței - nu se pot obține coordonatele casetei de delimitare", "error_title": "Eroare - ceva nu a mers", + "error_while_navigating": "Eroare la navigarea spre obiect", "errors": { "cannot_navigate_next_asset": "Nu se poate naviga către următoarea resursă", "cannot_navigate_previous_asset": "Nu se poate naviga la resursa anterioară", @@ -1009,6 +1091,7 @@ "unable_to_complete_oauth_login": "Nu s-a realizat logarea prin OAuth", "unable_to_connect": "Nu se poate conecta", "unable_to_copy_to_clipboard": "Nu poate fi copiat, asigură-te că accesezi pagina prin https", + "unable_to_create": "Nu se poate crea fluxul de lucru", "unable_to_create_admin_account": "Nu se poate crea contul de administrator", "unable_to_create_api_key": "Nu se poate crea o nouă cheie API", "unable_to_create_library": "Nu se poate crea biblioteca", @@ -1019,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Nu se poate șterge modelul de excludere", "unable_to_delete_shared_link": "Nu se poate șterge linkul partajat", "unable_to_delete_user": "Nu se poate șterge userul", + "unable_to_delete_workflow": "Nu se poate șterge fluxul de lucru", "unable_to_download_files": "Nu se pot descărca fișierele", "unable_to_edit_exclusion_pattern": "Nu se poate edita modelul de excludere", "unable_to_empty_trash": "Nu se poate goli coșul de gunoi", @@ -1058,6 +1142,7 @@ "unable_to_scan_library": "Nu se poate scana librăria", "unable_to_set_feature_photo": "Nu se poate seta fotografia principală", "unable_to_set_profile_picture": "Nu se poate seta fotografia de profil", + "unable_to_set_rating": "Nu se poate seta evaluarea", "unable_to_submit_job": "Imposibil de trimis sarcina", "unable_to_trash_asset": "Nu se poate elimina resursa", "unable_to_unlink_account": "Nu se poate deconecta contul", @@ -1069,8 +1154,10 @@ "unable_to_update_settings": "Nu se pot actualiza setările", "unable_to_update_timeline_display_status": "Nu se poate actualiza starea de afișare a cronologiei", "unable_to_update_user": "Nu se poate actualiza utilizatorul", + "unable_to_update_workflow": "Nu se poate actualiza fluxul de lucru", "unable_to_upload_file": "Nu se poate ÃŽncărca fișierul" }, + "errors_text": "Erori", "exclusion_pattern": "Model de excludere", "exif": "Format comutabil pentru fișiere imagine", "exif_bottom_sheet_description": "Adaugă Descriere...", @@ -1102,6 +1189,7 @@ "external_network_sheet_info": "CÃĸnd nu se află ÃŽn rețeaua Wi-Fi preferată, aplicația se va conecta la server prin prima dintre adresele URL de mai jos pe care o poate accesa, ÃŽncepÃĸnd de sus ÃŽn jos", "face_unassigned": "Nealocat", "failed": "Eșuat", + "failed_count": "Eșuat: {count}", "failed_to_authenticate": "Autentificarea nu a reușit", "failed_to_load_assets": "Nu s-au ÃŽncărcat activele", "failed_to_load_folder": "Nu s-a putut ÃŽncărca folderul", @@ -1114,14 +1202,17 @@ "features": "Caracteristici", "features_in_development": "Funcții ÃŽn dezvoltare", "features_setting_description": "Gestionați funcțiile aplicației", - "file_name": "Nume de fișier", "file_name_or_extension": "Numele sau extensia fișierului", + "file_name_text": "Nume fișier", + "file_name_with_value": "Nume fișier: {file_name}", "file_size": "Mărime fișier", "filename": "Numele fișierului", "filetype": "Tipul fișierului", "filter": "Filtre", + "filter_description": "Condiții pentru filtrarea activelor țintă", "filter_people": "Filtrați persoanele", "filter_places": "Filtrează locurile", + "filters": "Filtre", "find_them_fast": "Găsiți-le rapid prin căutare după nume", "first": "Primul", "fix_incorrect_match": "Remediați potrivirea incorectă", @@ -1131,12 +1222,16 @@ "folders_feature_description": "Răsfoire ÃŽn conținutul folderului pentru fotografiile și videoclipurile din sistemul de fișiere", "forgot_pin_code_question": "Ai uitat codul PIN?", "forward": "Redirecționare", + "free_up_space": "Eliberați spațiu", + "free_up_space_description": "Mută fotografiile și videoclipurile salvate ÃŽn coșul de gunoi al dispozitivului pentru a elibera spațiu. Copiile tale de pe server rămÃĸn ÃŽn siguranță.", + "free_up_space_settings_subtitle": "Eliberați spațiul de stocare al dispozitivului", "full_path": "Calea completă: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Această funcție ÃŽncarcă resurse externe de la Google pentru a funcționa.", "general": "General", "geolocation_instruction_location": "Apasă pe o resursă cu coordonate GPS pentru a folosi locația sa, sau selectează direct o locație de pe hartă", "get_help": "Obțineți Ajutor", + "get_people_error": "Eroare la obținerea datelor despre persoane", "get_wifiname_error": "Nu s-a putut obține numele rețelei Wi-Fi. Asigurați-vă că ați acordat permisiunile necesare și că sunteți conectat la o rețea Wi-Fi", "getting_started": "Noțiuni de Bază", "go_back": "Întoarcere", @@ -1162,12 +1257,14 @@ "header_settings_header_name_input": "Numele antetului", "header_settings_header_value_input": "Valoarea antetului", "headers_settings_tile_title": "Header-uri proxy personalizate", + "height": "Înălțime", "hi_user": "Bună {name} ({email})", "hide_all_people": "Ascundeți toate persoanele", "hide_gallery": "Ascundeți galeria", "hide_named_person": "Ascundeți persoana {name}", "hide_password": "Ascundeți parola", "hide_person": "Ascundeți persoana", + "hide_schema": "Ascunde schema", "hide_text_recognition": "Ascunde recunoașterea textului", "hide_unnamed_people": "Ascundeți persoanele fără nume", "home_page_add_to_album_conflicts": "Au fost adăugate {added} de resurse ÃŽn albumul {album}. {failed} de resurse sunt deja adăugate ÃŽn album.", @@ -1240,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Procesarea a rulat {dateTime}", "items_count": "{count, plural, one {# element} other{# elemente}}", "jobs": "Sarcini", + "json_editor": "Editor JSON", + "json_error": "Eroare JSON", "keep": "Păstrați", + "keep_albums": "Păstreaza albume", + "keep_albums_count": "Păstrez {count} {count, plural, one {album} few {albume} other {de albume}}", "keep_all": "Păstrați Tot", + "keep_description": "Alege ce să rămÃĸnă pe dispozitiv cÃĸnd eliberezi spațiu.", + "keep_favorites": "Păstrați favoritele", + "keep_on_device": "Păstrează pe dispozitiv", + "keep_on_device_hint": "Selectează ce să rămÃĸnă pe dispozitiv", "keep_this_delete_others": "Păstrați asta, ștergeți celelalte", + "keeping": "Păstrez: {items}", "kept_this_deleted_others": "S-a păstrat acest material și s-au șters {count, plural, one {# material} other {# materiale}}", "keyboard_shortcuts": "Comenzi rapide de tastatură", "language": "Limbă", @@ -1284,6 +1390,7 @@ "local": "Local", "local_asset_cast_failed": "Nu se poate converti un element care nu este ÃŽncărcat pe server", "local_assets": "Asset-uri locale", + "local_id": "ID local", "local_media_summary": "Rezumatul fișierelor media locale", "local_network": "Rețea locală", "local_network_sheet_info": "Aplicația se va conecta la server prin intermediul acestei adrese URL atunci cÃĸnd utilizează rețeaua Wi-Fi specificată", @@ -1335,10 +1442,28 @@ "loop_videos_description": "Activați pentru a rula in buclă automat un videoclip ÃŽn vizualizatorul de detalii.", "main_branch_warning": "Utilizați o versiune de dezvoltare; vă recomandăm insistent să utilizați o versiune de lansare!", "main_menu": "Meniu principal", + "maintenance_action_restore": "Restaurare bază de date", "maintenance_description": "Immich a fost pus ÃŽn modul de ÃŽntreținere.", "maintenance_end": "Ieșire din modul de ÃŽntreținere", "maintenance_end_error": "Nu s-a reușit ieșirea din modul de ÃŽntreținere.", "maintenance_logged_in_as": "Conectat ÃŽn prezent ca {user}", + "maintenance_restore_from_backup": "Restaurează din backup", + "maintenance_restore_library": "Restaurează-ți biblioteca", + "maintenance_restore_library_confirm": "Dacă pare corect, continuă spre a restaura un backup!", + "maintenance_restore_library_description": "Restaurare bază de date", + "maintenance_restore_library_folder_has_files": "{folder} are {count} {count, plural, one {fișier} few {fișiere} other {de fișiere}}", + "maintenance_restore_library_folder_no_files": "Lipsesc fișiere din {folder}!", + "maintenance_restore_library_folder_pass": "permite scrierea și citirea", + "maintenance_restore_library_folder_read_fail": "nu permite citirea", + "maintenance_restore_library_folder_write_fail": "nu permite scrierea", + "maintenance_restore_library_hint_missing_files": "Posibil să lipsească fișiere importante", + "maintenance_restore_library_hint_regenerate_later": "Poți regenera mai tarziu ÃŽn setări", + "maintenance_restore_library_hint_storage_template_missing_files": "Folosesti șablonul de stocare? Posibil să-ți lipsească fișiere", + "maintenance_restore_library_loading": "Încarc verificările de integritate si euristiceâ€Ļ", + "maintenance_task_backup": "Creez backupul bazei de date existenteâ€Ļ", + "maintenance_task_migrations": "Rulez migrările bazei de dateâ€Ļ", + "maintenance_task_restore": "Restaurez backupul alesâ€Ļ", + "maintenance_task_rollback": "Restaurarea a eșuat, ÃŽntorc la punctul de restaurareâ€Ļ", "maintenance_title": "Temporar indisponibil", "make": "Marcă", "manage_geolocation": "Gestionați locația", @@ -1400,6 +1525,8 @@ "minimize": "Minimizare", "minute": "Minut", "minutes": "Minute", + "mirror_horizontal": "Orizontal", + "mirror_vertical": "Vertical", "missing": "Lipsă", "mobile_app": "Aplicație Mobilă", "mobile_app_download_onboarding_note": "Descarcă aplicația mobilă folosind următoarele opțiuni", @@ -1408,11 +1535,14 @@ "monthly_title_text_date_format": "LLLL a", "more": "Mai mult", "move": "Mută", + "move_down": "Mută ÃŽn jos", "move_off_locked_folder": "Mutați din folderul blocat", "move_to": "Mutare la", + "move_to_device_trash": "Mutare ÃŽn coșul de gunoi al dispozitivului", "move_to_lock_folder_action_prompt": "{count} adăugate ÃŽn dosarul blocat", "move_to_locked_folder": "Mută ÃŽn dosarul blocat", "move_to_locked_folder_confirmation": "Aceste fotografii și videoclipuri vor fi eliminate din toate albumele și vor putea fi vizualizate doar din dosarul blocat", + "move_up": "Mută sus", "moved_to_archive": "Au fost mutate {count, plural, one {# element} other {# elemente}} ÃŽn arhivă", "moved_to_library": "Au fost mutate {count, plural, one {# element} other {# elemente}} la bibliotecă", "moved_to_trash": "Mutat ÃŽn coșul de gunoi", @@ -1422,6 +1552,7 @@ "my_albums": "Albumele mele", "name": "Nume", "name_or_nickname": "Nume sau poreclĮŽ", + "name_required": "Numele este obligatoriu", "navigate": "Navighează", "navigate_to_time": "Navigheaza la Timp", "network_requirement_photos_upload": "Utilizați datele mobile pentru a face copii de rezervă ale fotografiilor", @@ -1446,20 +1577,24 @@ "next": "Următorul", "next_memory": "Următoarea amintire", "no": "Nu", + "no_actions_added": "Nu s-au adăugat ÃŽncă acțiuni", + "no_albums_found": "Niciun album găsit", "no_albums_message": "Creați un album pentru a vă organiza fotografiile și videoclipurile", "no_albums_with_name_yet": "Se pare că nu aveți ÃŽncă niciun album cu acest nume.", "no_albums_yet": "Se pare că nu aveți ÃŽncă niciun album.", "no_archived_assets_message": "Arhivați fotografii și videoclipuri pentru a le ascunde din vizualizarea fotografii", - "no_assets_message": "CLICK PENTRU A ÎNCĂRCA PRIMA TA FOTOGRAFIE", + "no_assets_message": "Apasă pentru a ÃŽncărca prima ta fotografie.", "no_assets_to_show": "Nicio resursă de afișat", "no_cast_devices_found": "Nu s-au găsit dispozitive de difuzare", "no_checksum_local": "Nu există checksum – nu se pot prelua resursele locale", "no_checksum_remote": "Nu există checksum – nu se pot prelua resursele la distanță", + "no_configuration_needed": "Nu este necesară nicio configurare", "no_devices": "Nu există dispozitive autorizate", "no_duplicates_found": "Nu au fost găsite duplicate.", "no_exif_info_available": "Nu există informații exif disponibile", "no_explore_results_message": "Încarcați mai multe fotografii pentru a vă explora colecția.", "no_favorites_message": "Adaugă favorite pentru a găsi rapid cele mai bune fotografii și videoclipuri", + "no_filters_added": "Nu s-au adăugat ÃŽncă filtre", "no_libraries_message": "Creați o bibliotecă externă pentru a vă vizualiza fotografiile și videoclipurile", "no_local_assets_found": "Nicio resursă locală găsită cu acest checksum", "no_location_set": "Locație neconfigurată", @@ -1473,11 +1608,11 @@ "no_results_description": "Încercați un sinonim sau un cuvÃĸnt cheie mai general", "no_shared_albums_message": "Creați un album pentru a partaja fotografii și videoclipuri cu persoanele din rețeaua dvs", "no_uploads_in_progress": "Nicio ÃŽncărcare ÃŽn curs", + "none": "Niciunul", "not_allowed": "Nu este permis", "not_available": "N/A", "not_in_any_album": "Nu există ÃŽn niciun album", "not_selected": "Neselectat", - "note_apply_storage_label_to_previously_uploaded assets": "Notă: Pentru a aplica eticheta de stocare la resursele ÃŽncărcate anterior, rulați", "notes": "Note", "nothing_here_yet": "Nimic aici ÃŽncă", "notification_permission_dialog_content": "Pentru a activa notificările, mergi ÃŽn Setări > Immich și selectează permite.", @@ -1555,6 +1690,7 @@ "people": "Persoane", "people_edits_count": "Editat {count, plural, one {# persoană} other {# persoane}}", "people_feature_description": "Răsfoiți fotografii și videoclipuri grupate după persoane", + "people_selected": "{count, plural,one {# persoană selectată} few {# persoane selectate}other {# de persoane selectate}}", "people_sidebar_description": "Afișează un link către persoane ÃŽn bara laterală", "permanent_deletion_warning": "Avertisment de ștergere permanentă", "permanent_deletion_warning_setting_description": "Afișează un avertisment la ștergerea definitivă a resurselor", @@ -1576,14 +1712,17 @@ "person": "PersoanĮŽ", "person_age_months": "{months, plural, one {# lună} other {# luni}}", "person_age_year_months": "1 an, {months, plural, one {# lună} other {# luni}}", - "person_age_years": "{years, plural, other {# years}} vechime", + "person_age_years": "{years, plural, other {# ani}}", "person_birthdate": "Născut pe {date}", "person_hidden": "{name}{hidden, select, true { (ascuns)} other {}}", + "person_recognized": "Persoană recunoscută", + "person_selected": "Persoana selectată", "photo_shared_all_users": "Se pare că ți-ai partajat fotografiile tuturor utilizatorilor sau că nu ai niciun utilizator căruia să le distribui.", "photos": "Fotografii", "photos_and_videos": "Fotografii și Videoclipuri", "photos_count": "{count, plural, one {{count, number} imagine} other{{count, number} imagini}}", "photos_from_previous_years": "Fotografii din anii anteriori", + "photos_only": "Numai fotografii", "pick_a_location": "Alegeți o locație", "pick_custom_range": "Interval personalizat", "pick_date_range": "Selectați un interval de date", @@ -1659,10 +1798,12 @@ "purchase_settings_server_activated": "Cheia de produs a serverului este gestionată de administrator", "query_asset_id": "Interoghează ID-ul resursei", "queue_status": "Se pun ÃŽn coadă {count}/{total}", + "rate_asset": "Dă o notă", "rating": "Evaluare cu stele", - "rating_clear": "Anulați evaluarea", + "rating_clear": "Anuleaza evaluarea", "rating_count": "{count, plural, one {# stea} other {# stele}}", "rating_description": "Afișați evaluarea EXIF ÃŽn panoul de informații", + "rating_set": "Evaluare setată la {rating, plural, o {# star} alte {# stars}}", "reaction_options": "Opțiuni de reacție", "read_changelog": "Citiți Jurnalul de Modificări", "readonly_mode_disabled": "Modul doar citire dezactivat", @@ -1673,7 +1814,7 @@ "reassigned_assets_to_new_person": "Re-alocat {count, plural, one {# resursă} other {# resurse}} unei noi persoane", "reassing_hint": "Atribuiți resursele selectate unei persoane existente", "recent": "Recent", - "recent-albums": "Albume recente", + "recent_albums": "Albume recente", "recent_searches": "Căutări recente", "recently_added": "Adăugate recent", "recently_added_page_title": "Adăugate recent", @@ -1762,9 +1903,11 @@ "saved_settings": "Setări salvate", "say_something": "Spuneți ceva", "scaffold_body_error_occurred": "A apărut o eroare", + "scan": "Scanare", "scan_all_libraries": "Scanați toate bibliotecile", "scan_library": "Scanare", "scan_settings": "Setări Scanare", + "scanning": "Scanare", "scanning_for_album": "Se scanează după album...", "search": "Căutați", "search_albums": "Căutați albume", @@ -1794,6 +1937,7 @@ "search_filter_media_type_title": "Selectați tipul media", "search_filter_ocr": "Caută dupa OCR", "search_filter_people_title": "Selectați persoane", + "search_filter_star_rating": "După rating ÃŽn stele", "search_for": "Căutare după", "search_for_existing_person": "Caută o persoană existentă", "search_no_more_result": "Nu mai există rezultate", @@ -1826,19 +1970,25 @@ "search_your_photos": "Căutarea fotografiilor dvs", "searching_locales": "Se caută regionale...", "second": "SecundĮŽ", - "see_all_people": "Vizualizați toate persoanele", + "see_all_people": "Vizualizează toate persoanele", "select": "Selectează", + "select_album": "Selectează album", "select_album_cover": "Selectați coperta albumului", + "select_albums": "Selectează albume", "select_all": "Selectați tot", "select_all_duplicates": "Selectați toate duplicatele", "select_all_in": "Selectați tot ÃŽn {group}", "select_avatar_color": "Selectați culoarea avatarului", + "select_count": "{count, plural, one {Selectează #} few {Selectează #} other {Selectează #}}", + "select_cutoff_date": "Selectează data limită", "select_face": "Selectați fața", "select_featured_photo": "Selectați fotografia recomandată", "select_from_computer": "Selectați din calculator", "select_keep_all": "Selectați tot pentru păstrare", "select_library_owner": "Selectați proprietarul bibliotecii", "select_new_face": "Selectați o nouĮŽ fațĮŽ", + "select_people": "Selectează oameni", + "select_person": "Selectează persoana", "select_person_to_tag": "Selectați o persoană pentru a o eticheta", "select_photos": "Selectați fotografii", "select_trash_all": "Selectați tot pentru ștergere", @@ -1974,6 +2124,7 @@ "show_password": "Afișați parola", "show_person_options": "Afișați opțiunile persoanelor", "show_progress_bar": "Afișați Bara de Progres", + "show_schema": "Arată schema", "show_search_options": "Afișați opțiunile de căutare", "show_shared_links": "Afișare linkuri partajate", "show_slideshow_transition": "Afișați tranziția de prezentare", @@ -1984,13 +2135,15 @@ "shuffle": "Amestecați", "sidebar": "Bara laterală", "sidebar_display_description": "Afișați un link către vizualizare ÃŽn bara laterală", - "sign_out": "Vă deconectați", + "sign_out": "Deconectare", "sign_up": "Vă ÃŽnregistrați", "size": "Dimensiune", "skip_to_content": "Treceți la conținut", "skip_to_folders": "Treceți la foldere", "skip_to_tags": "Treceți la etichete", "slideshow": "Prezentare de diapozitive", + "slideshow_repeat": "Repetă prezentarea", + "slideshow_repeat_description": "ReÃŽntoarce-te la ÃŽnceput cand prezentarea se ÃŽncheie", "slideshow_settings": "Setări pentru prezentarea de diapozitive", "sort_albums_by": "Sortați albumele după...", "sort_created": "Data creării", @@ -2067,6 +2220,7 @@ "theme_setting_theme_subtitle": "Alege tema aplicației", "theme_setting_three_stage_loading_subtitle": "Încărcarea ÃŽn trei etape are putea crește performanța ÃŽncărcării dar generează un volum semnificativ mai mare de trafic pe rețea", "theme_setting_three_stage_loading_title": "Pornește ÃŽncărcarea ÃŽn 3 etape", + "then": "Atunci", "they_will_be_merged_together": "Vor fi ÃŽmbinate ÃŽmpreună", "third_party_resources": "Resurse Terță Parte", "time": "Timp", @@ -2101,6 +2255,13 @@ "trash_page_select_assets_btn": "Selectează resurse", "trash_page_title": "Coș ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementele din coșul de gunoi vor fi șterse definitiv după {days, plural, one {# zi} other {# zile}}.", + "trigger": "Declanșator", + "trigger_asset_uploaded": "Fișier ÃŽncărcat", + "trigger_asset_uploaded_description": "Declanșează cand un fișier este ÃŽncarcat", + "trigger_description": "Un eveniment care declanșează fluxul de lucru", + "trigger_person_recognized": "Persoană Recunoscută", + "trigger_person_recognized_description": "Declanșat atunci cÃĸnd este detectată o persoană", + "trigger_type": "Tip de declanșare", "troubleshoot": "Depanați", "type": "Tip", "unable_to_change_pin_code": "Nu se poate schimba codul PIN", @@ -2115,6 +2276,7 @@ "unhide_person": "Dezvăluie persoana", "unknown": "Necunoscut", "unknown_country": "Țară necunoscută", + "unknown_date": "Dată necunoscută", "unknown_year": "An Necunoscut", "unlimited": "Nelimitat", "unlink_motion_video": "Deconectați videoclipul ÃŽn mișcare", @@ -2131,17 +2293,19 @@ "unstack": "Dezasamblați", "unstack_action_prompt": "{count} neÃŽmpachetate", "unstacked_assets_count": "Nestivuit {count, plural, one {# resursă} other {# resurse}}", + "unsupported_field_type": "Tip de cÃĸmp neacceptat", "untagged": "Neetichetat", + "untitled_workflow": "Flux de lucru fără titlu", "up_next": "Mai departe", "update_location_action_prompt": "Actualizează locația pentru {count} resurse selectate cu:", "updated_at": "Actualizat", "updated_password": "Parolă actualizată", "upload": "Încărcați", - "upload_action_prompt": "{count} ÃŽn coadă pentru ÃŽncărcare", "upload_concurrency": "Încărcați simultan", "upload_details": "Detalii ÃŽncărcare", "upload_dialog_info": "Vrei să backup resursele selectate pe server?", "upload_dialog_title": "Încarcă resursă", + "upload_error_with_count": "Eroare la ÃŽncărcare pentru {număr, plural, un {# fișier} alte {# fișiere}}", "upload_errors": "Încărcare finalizată cu {count, plural, one {# eroare} other {# erori}}, reÃŽmprospătați pagina pentru a reÃŽncărca noile resurse.", "upload_finished": "Încărcarea s-a finalizat", "upload_progress": "Rămas {remaining, number} - Procesat {processed, number}/{total, number}", @@ -2156,7 +2320,7 @@ "url": "URL", "usage": "Utilizare", "use_biometric": "Folosește biometrice", - "use_current_connection": "folosește conexiunea curentă", + "use_current_connection": "Folosește conexiunea curentă", "use_custom_date_range": "Utilizați ÃŽn schimb un interval de date personalizat", "user": "Utilizator", "user_has_been_deleted": "Acest utilizator a fost șters.", @@ -2177,6 +2341,7 @@ "utilities": "UtilitĮŽČ›i", "validate": "Validați", "validate_endpoint_error": "Vă rugăm să introduceți o adresă URL validă", + "validation_error": "Eroare de validare", "variables": "Variabile", "version": "Versiune", "version_announcement_closing": "Prietenul tĮŽu, Alex", @@ -2188,38 +2353,57 @@ "video_hover_setting_description": "Redați miniatura video cÃĸnd mouse-ul trece peste element. Chiar și atunci cÃĸnd este dezactivată, redarea poate fi pornită trecÃĸnd cu mouse-ul peste pictograma de redare.", "videos": "Videoclipuri", "videos_count": "{count, plural, one {# Videoclip} other {# Videoclipuri}}", - "view": "Vizualizați", - "view_album": "Vizualizați Album", - "view_all": "Vizualizați Tot", + "videos_only": "Doar videoclipuri", + "view": "Secțiune", + "view_album": "Vizualizează Album", + "view_all": "Vizualizează Tot", "view_all_users": "Vizulizați toți utilizatorii", + "view_asset_owners": "Vezi proprietarii resursei", "view_details": "Vedeți detaliile", - "view_in_timeline": "Vizualizați ÃŽn cronologie", + "view_in_timeline": "Vizualizează ÃŽn cronologie", "view_link": "Vezi link", - "view_links": "Vizualizați scurtĮŽturi", + "view_links": "Vizualizează link-urile", "view_name": "Vizualizare", - "view_next_asset": "Vizualizați următoarea resursă", - "view_previous_asset": "Vizualizați resursa anterioară", + "view_next_asset": "Vizualizează următoarea resursă", + "view_previous_asset": "Vizualizează resursa anterioară", "view_qr_code": "Vezi cod QR", - "view_similar_photos": "Vizualizați poze similare", + "view_similar_photos": "Vizualizează poze similare", "view_stack": "Vizualizare stivă", "view_user": "Vizualizare utilizator", "viewer_remove_from_stack": "Șterge din grup", "viewer_stack_use_as_main_asset": "Folosește ca resursă principală", "viewer_unstack": "Anulează grup", "visibility_changed": "Vizibilitatea schimbată pentru {count, plural, one {# persoană} other {# persoane}}", + "visual": "Vizual", + "visual_builder": "Constructor vizual", "waiting": "În așteptare", + "waiting_count": "În așteptare: {count}", "warning": "Avertisment", "week": "SĮŽptĮŽmÃĸnĮŽ", "welcome": "Bun venit", "welcome_to_immich": "Bun venit la Immich", + "width": "Lățime", "wifi_name": "Nume Wi-Fi", - "workflow": "Flux de lucru", + "workflow_delete_prompt": "Ești sigur că vrei să ștergi acest flux de lucru?", + "workflow_deleted": "Flux de lucru șters", + "workflow_description": "Descrierea fluxului de lucru", + "workflow_info": "Informații despre fluxul de lucru", + "workflow_json": "Flux de lucru JSON", + "workflow_json_help": "Editează configurația fluxului de lucru ÃŽn format JSON. Modificările vor fi sincronizate cu constructorul vizual.", + "workflow_name": "Numele fluxului de lucru", + "workflow_navigation_prompt": "Ești sigur că vrei să părăsești fără să salvezi modificările?", + "workflow_summary": "Rezumatul fluxului de lucru", + "workflow_update_success": "Fluxul de lucru a fost actualizat cu succes", + "workflow_updated": "Fluxul de lucru a fost actualizat", + "workflows": "Fluxuri de lucru", + "workflows_help_text": "Fluxurile de lucru automatizează acțiuni pe resurse, folosind declanșatori și filtre", "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": "Numele rețelei tale WiFi", + "zero_to_clear_rating": "apasă 0 pentru a reseta evaluarea resursei", "zoom_image": "Măriți Imaginea", "zoom_to_bounds": "Mărește la margini" } diff --git a/i18n/ru.json b/i18n/ru.json index b7561b084c..a1e542a2e5 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -5,6 +5,7 @@ "acknowledge": "ĐŸĐžĐ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚ŅŒ", "action": "ДĐĩĐšŅŅ‚Đ˛Đ¸Đĩ", "action_common_update": "ОбĐŊĐžĐ˛Đ¸Ņ‚ŅŒ", + "action_description": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ, Đ˛Ņ‹ĐŋĐžĐģĐŊŅĐĩĐŧŅ‹Đĩ ҁ ĐžŅ‚ĐžĐąŅ€Đ°ĐŊĐŊŅ‹Đŧи ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧи", "actions": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ", "active": "Đ’Ņ‹ĐŋĐžĐģĐŊŅĐĩŅ‚ŅŅ", "active_count": "Đ’Ņ‹ĐŋĐžĐģĐŊŅŅŽŅ‚ŅŅ: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "add_a_name": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ иĐŧŅ", "add_a_title": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŊаСваĐŊиĐĩ", + "add_action": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đĩ", + "add_action_description": "НаĐļĐŧĐ¸Ņ‚Đĩ Đ´ĐģŅ дОйавĐģĐĩĐŊĐ¸Ņ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ", + "add_assets": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ", "add_birthday": "ĐŖĐēĐ°ĐˇĐ°Ņ‚ŅŒ Đ´Đ°Ņ‚Ņƒ Ņ€ĐžĐļĐ´ĐĩĐŊĐ¸Ņ", "add_endpoint": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Đ°Đ´Ņ€Đĩҁ", "add_exclusion_pattern": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ŅˆĐ°ĐąĐģĐžĐŊ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ", + "add_filter": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Ņ„Đ¸ĐģŅŒŅ‚Ņ€", + "add_filter_description": "НаĐļĐŧĐ¸Ņ‚Đĩ Đ´ĐģŅ дОйавĐģĐĩĐŊĐ¸Ņ ҃ҁĐģĐžĐ˛Đ¸Ņ ĐžŅ‚ĐąĐžŅ€Đ°", "add_location": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "add_more_users": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Đĩ҉ґ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš", "add_partner": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐŋĐ°Ņ€Ņ‚ĐŊŅ‘Ņ€Đ°", @@ -36,6 +42,7 @@ "add_to_shared_album": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в ĐžĐąŅ‰Đ¸Đš аĐģŅŒĐąĐžĐŧ", "add_upload_to_stack": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ и Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ в ĐŗŅ€ŅƒĐŋĐŋ҃", "add_url": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ URL", + "add_workflow_step": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ŅˆĐ°Đŗ Ņ€Đ°ĐąĐžŅ‡ĐĩĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅŅĐ°", "added_to_archive": "ДобавĐģĐĩĐŊĐž в Đ°Ņ€Ņ…Đ¸Đ˛", "added_to_favorites": "ДобавĐģĐĩĐŊĐž в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", "added_to_favorites_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ дОйавĐģĐĩĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ дОйавĐģĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° дОйавĐģĐĩĐŊĐž}} в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", @@ -77,7 +84,7 @@ "duplicate_detection_job_description": "ЗаĐŋ҃ҁĐēаĐĩŅ‚ ĐžĐŋŅ€ĐĩĐ´ĐĩĐģĐĩĐŊиĐĩ ĐŋĐžŅ…ĐžĐļĐ¸Ņ… Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиК ĐŋŅ€Đ¸ ĐŋĐžĐŧĐžŅ‰Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐˇŅ€ĐĩĐŊĐ¸Ņ (ĐˇĐ°Đ˛Đ¸ŅĐ¸Ņ‚ ĐžŅ‚ ҃ĐŧĐŊĐžĐŗĐž ĐŋĐžĐ¸ŅĐēа)", "exclusion_pattern_description": "ШайĐģĐžĐŊŅ‹ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊиК ĐŋОСвОĐģŅŅŽŅ‚ Đ¸ĐŗĐŊĐžŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŊĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ Ņ„Đ°ĐšĐģŅ‹ и ĐŋаĐŋĐēи ĐŋŅ€Đ¸ ҁĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°ĐŊии йийĐģĐ¸ĐžŅ‚ĐĩĐēи. Đ­Ņ‚Đž ĐŋĐžĐģĐĩСĐŊĐž, ĐĩҁĐģи в ĐŋаĐŋĐēĐĩ ĐĩŅŅ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ĐŊĐĩ ĐŊ҃ĐļĐŊĐž иĐŧĐŋĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ. НаĐŋŅ€Đ¸ĐŧĐĩŅ€ RAW-Ņ„Đ°ĐšĐģŅ‹.", "export_config_as_json_description": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ Ņ‚ĐĩĐēŅƒŅ‰ŅƒŅŽ ĐēĐžĐŊŅ„Đ¸ĐŗŅƒŅ€Đ°Ņ†Đ¸ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹ в Ņ„Đ°ĐšĐģ JSON", - "external_libraries_page_description": "АдĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ вĐŊĐĩ҈ĐŊĐĩĐš йийĐģĐ¸ĐžŅ‚ĐĩĐēи", + "external_libraries_page_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ вĐŊĐĩ҈ĐŊиĐŧи йийĐģĐ¸ĐžŅ‚ĐĩĐēаĐŧи", "face_detection": "ОбĐŊĐ°Ņ€ŅƒĐļĐĩĐŊиĐĩ ĐģĐ¸Ņ†", "face_detection_description": "ОбĐŊĐ°Ņ€ŅƒĐļиваĐĩŅ‚ ĐģĐ¸Ņ†Đ° ĐŊа ĐžĐąŅŠĐĩĐēŅ‚Đ°Ņ… ҁ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩĐŧ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐžĐąŅƒŅ‡ĐĩĐŊĐ¸Ņ. ДĐģŅ видĐĩĐž аĐŊаĐģĐ¸ĐˇĐ¸Ņ€ŅƒĐĩŅ‚ŅŅ Ņ‚ĐžĐģҌĐēĐž ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Đ°. КĐŊĐžĐŋĐēа \"ОбĐŊĐžĐ˛Đ¸Ņ‚ŅŒ\" СаĐŋ҃ҁĐēаĐĩŅ‚ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊŅƒŅŽ ĐžĐąŅ€Đ°ĐąĐžŅ‚Đē҃ Đ˛ŅĐĩŅ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛. \"ĐĄĐąŅ€ĐžŅ\" — Đ´ĐžĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊĐž ŅƒĐ´Đ°ĐģŅĐĩŅ‚ Đ˛ŅĐĩ иĐŧĐĩŅŽŅ‰Đ¸ĐĩŅŅ даĐŊĐŊŅ‹Đĩ Đž ĐģĐ¸Ņ†Đ°Ņ…. \"ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Đĩ\" — ŅŅ‚Đ°Đ˛Đ¸Ņ‚ в ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ Đĩ҉ґ ĐŊĐĩ ĐąŅ‹Đģи ĐžĐąŅ€Đ°ĐąĐžŅ‚Đ°ĐŊŅ‹. ОбĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐģĐ¸Ņ†Đ° ĐŋĐžĐŧĐĩŅ‰Đ°ŅŽŅ‚ŅŅ в ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ Đ´ĐģŅ ĐˇĐ°Đ´Đ°Ņ‡Đ¸ Đ Đ°ŅĐŋОСĐŊаваĐŊиĐĩ ĐģĐ¸Ņ† и ĐŋĐžŅĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐš Đ¸Ņ… ĐŋŅ€Đ¸Đ˛ŅĐˇĐēи Đē ŅŅƒŅ‰ĐĩŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Đŧ иĐģи ĐŊĐžĐ˛Ņ‹Đŧ ĐģŅŽĐ´ŅĐŧ.", "facial_recognition_job_description": "Đ“Ņ€ŅƒĐŋĐŋĐ¸Ņ€ŅƒĐĩŅ‚ и ĐŊаСĐŊĐ°Ņ‡Đ°ĐĩŅ‚ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐģĐ¸Ņ†Đ° ĐģŅŽĐ´ŅĐŧ. Đ’Ņ‹ĐŋĐžĐģĐŊŅĐĩŅ‚ŅŅ ĐŋĐžŅĐģĐĩ СавĐĩŅ€ŅˆĐĩĐŊĐ¸Ņ ĐˇĐ°Đ´Đ°Ņ‡Đ¸ ОбĐŊĐ°Ņ€ŅƒĐļĐĩĐŊиĐĩ ĐģĐ¸Ņ†. КĐŊĐžĐŋĐēа \"ĐĄĐąŅ€ĐžŅ\" (ĐŋĐĩŅ€Đĩ)ĐŊаСĐŊĐ°Ņ‡Đ°ĐĩŅ‚ Đ˛ŅĐĩ ĐģĐ¸Ņ†Đ°. \"ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Đĩ\" — дОйавĐģŅĐĩŅ‚ в ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ ĐžĐąŅ€Đ°ĐąĐžŅ‚Đēи ĐģĐ¸Ņ†Đ°, ĐŊĐĩ ĐŋŅ€Đ¸Đ˛ŅĐˇĐ°ĐŊĐŊŅ‹Đĩ Đē ҇ĐĩĐģОвĐĩĐē҃.", @@ -97,6 +104,8 @@ "image_preview_description": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиĐĩ ҁҀĐĩĐ´ĐŊĐĩĐŗĐž Ņ€Đ°ĐˇĐŧĐĩŅ€Đ° ĐąĐĩС ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊҋ҅, Đ¸ŅĐŋĐžĐģŅŒĐˇŅƒĐĩĐŧĐžĐĩ ĐŋŅ€Đ¸ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đĩ ĐžŅ‚Đ´ĐĩĐģҌĐŊҋ҅ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ и Đ´ĐģŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐžĐąŅƒŅ‡ĐĩĐŊĐ¸Ņ", "image_preview_quality_description": "ĐšĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģҌĐŊĐžĐŗĐž ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ° ĐžŅ‚ 1 Đ´Đž 100. ЧĐĩĐŧ Đ˛Ņ‹ŅˆĐĩ, Ņ‚ĐĩĐŧ ĐģŅƒŅ‡ŅˆĐĩ, ĐŊĐž ŅĐžĐˇĐ´Đ°ŅŽŅ‚ŅŅ Ņ„Đ°ĐšĐģŅ‹ йОĐģҌ҈ĐĩĐŗĐž Ņ€Đ°ĐˇĐŧĐĩŅ€Đ°, и ĐŧĐžĐļĐĩŅ‚ ҁĐŊĐ¸ĐˇĐ¸Ņ‚ŅŒŅŅ ҁĐēĐžŅ€ĐžŅŅ‚ŅŒ ĐžŅ‚ĐēĐģиĐēа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ. ĐŖŅŅ‚Đ°ĐŊОвĐēа ĐŊиСĐēĐžĐŗĐž СĐŊĐ°Ņ‡ĐĩĐŊĐ¸Ņ ĐŧĐžĐļĐĩŅ‚ ĐŋОвĐģĐ¸ŅŅ‚ŅŒ ĐŊа ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐžĐąŅƒŅ‡ĐĩĐŊĐ¸Ņ.", "image_preview_title": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģҌĐŊĐžĐŗĐž ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ°", + "image_progressive": "ĐŸŅ€ĐžĐŗŅ€ĐĩŅŅĐ¸Đ˛ĐŊŅ‹Đš JPEG", + "image_progressive_description": "Đ˜ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ ҁ ĐŋŅ€ĐžĐŗŅ€ĐĩŅŅĐ¸Đ˛ĐŊŅ‹Đŧ ĐēĐžĐ´Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩĐŧ ĐˇĐ°ĐŗŅ€ŅƒĐļĐ°ŅŽŅ‚ŅŅ ĐąŅ‹ŅŅ‚Ņ€ĐĩĐĩ, ĐŋĐžŅŅ‚ĐĩĐŋĐĩĐŊĐŊĐž ҃ĐģŅƒŅ‡ŅˆĐ°Ņ ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž. ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŊĐĩ вĐģĐ¸ŅĐĩŅ‚ ĐŊа Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ в Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đĩ WebP.", "image_quality": "ĐšĐ°Ņ‡ĐĩŅŅ‚Đ˛Đž", "image_resolution": "Đ Đ°ĐˇŅ€Đĩ҈ĐĩĐŊиĐĩ", "image_resolution_description": "БоĐģĐĩĐĩ Đ˛Ņ‹ŅĐžĐēĐžĐĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊиĐĩ ĐŋОСвОĐģŅĐĩŅ‚ ŅĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ йОĐģҌ҈Đĩ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš, ĐŊĐž ҂ҀĐĩĐąŅƒĐĩŅ‚ йОĐģҌ҈Đĩ Đ˛Ņ€ĐĩĐŧĐĩĐŊи Đ´ĐģŅ ĐēĐžĐ´Đ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ, ĐŋŅ€Đ¸Đ˛ĐžĐ´Đ¸Ņ‚ Đē ŅƒĐ˛ĐĩĐģĐ¸Ņ‡ĐĩĐŊĐ¸ŅŽ Ņ€Đ°ĐˇĐŧĐĩŅ€Đ° Ņ„Đ°ĐšĐģОв и ĐŧĐžĐļĐĩŅ‚ ҁĐŊĐ¸ĐˇĐ¸Ņ‚ŅŒ ҁĐēĐžŅ€ĐžŅŅ‚ŅŒ ĐžŅ‚ĐēĐģиĐēа ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ.", @@ -113,7 +122,7 @@ "job_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŋĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒŅŽ Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ ĐˇĐ°Đ´Đ°Ņ‡", "jobs_delayed": "{jobCount, plural, one {# ĐžŅ‚ĐģĐžĐļĐĩĐŊа} other {# ĐžŅ‚ĐģĐžĐļĐĩĐŊĐž}}", "jobs_failed": "{jobCount, plural, other {# ĐŊĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ Đ˛Ņ‹ĐŋĐžĐģĐŊĐ¸Ņ‚ŅŒ}}", - "jobs_over_time": "Đ—Đ°Đ´Đ°Ņ‡Đ¸ вО Đ˛Ņ€ĐĩĐŧĐĩĐŊи", + "jobs_over_time": "Đ“Ņ€Đ°Ņ„Đ¸Đē ĐžĐąŅ€Đ°ĐąĐžŅ‚Đēи", "library_created": "ХОСдаĐŊа ĐŊĐžĐ˛Đ°Ņ йийĐģĐ¸ĐžŅ‚ĐĩĐēа: {library}", "library_deleted": "БибĐģĐ¸ĐžŅ‚ĐĩĐēа ŅƒĐ´Đ°ĐģĐĩĐŊа", "library_details": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ йийĐģĐ¸ĐžŅ‚ĐĩĐēи", @@ -181,12 +190,23 @@ "machine_learning_smart_search_enabled": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ иĐŊŅ‚ĐĩĐģĐģĐĩĐēŅ‚ŅƒĐ°ĐģҌĐŊŅ‹Đš ĐŋĐžĐ¸ŅĐē", "machine_learning_smart_search_enabled_description": "ĐŸŅ€Đ¸ ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊии ŅŅ‚ĐžĐš Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ ĐēĐžĐ´Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒŅŅ Đ´ĐģŅ иĐŊŅ‚ĐĩĐģĐģĐĩĐēŅ‚ŅƒĐ°ĐģҌĐŊĐžĐŗĐž ĐŋĐžĐ¸ŅĐēа.", "machine_learning_url_description": "URL-Đ°Đ´Ņ€Đĩҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐžĐąŅƒŅ‡ĐĩĐŊĐ¸Ņ. Đ•ŅĐģи ҃ĐēаСаĐŊĐž ĐŊĐĩҁĐēĐžĐģҌĐēĐž, СаĐŋŅ€ĐžŅŅ‹ ĐąŅƒĐ´ŅƒŅ‚ ĐžŅ‚ĐŋŅ€Đ°Đ˛ĐģŅŅ‚ŅŒŅŅ ĐŋĐž ĐžŅ‡ĐĩŅ€Đĩди ĐŊа ĐēаĐļĐ´Ņ‹Đš, ĐŋĐžĐēа ĐžŅ‚ ОдĐŊĐžĐŗĐž иС ĐŊĐ¸Ņ… ĐŊĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋĐžĐģŅƒŅ‡ĐĩĐŊ ҃ҁĐŋĐĩ҈ĐŊŅ‹Đš ĐžŅ‚Đ˛ĐĩŅ‚. ĐĄĐĩŅ€Đ˛ĐĩҀҋ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ĐŊĐĩ ĐžŅ‚Đ˛ĐĩŅ‡Đ°ŅŽŅ‚, ĐąŅƒĐ´ŅƒŅ‚ Đ˛Ņ€ĐĩĐŧĐĩĐŊĐŊĐž Đ¸ĐŗĐŊĐžŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒŅŅ Đ´Đž Ņ‚ĐĩŅ… ĐŋĐžŅ€, ĐŋĐžĐēа ĐŊĐĩ ŅŅ‚Đ°ĐŊŅƒŅ‚ ҁĐŊОва Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹.", + "maintenance_delete_backup": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅƒŅŽ ĐēĐžĐŋĐ¸ŅŽ", + "maintenance_delete_backup_description": "Đ­Ņ‚Đ° Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ°Ņ ĐēĐžĐŋĐ¸Ņ ĐąŅƒĐ´ĐĩŅ‚ ĐąĐĩĐˇĐ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‚ĐŊĐž ŅƒĐ´Đ°ĐģĐĩĐŊа.", + "maintenance_delete_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅƒŅŽ ĐēĐžĐŋĐ¸ŅŽ.", + "maintenance_restore_backup": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅƒŅŽ ĐēĐžĐŋĐ¸ŅŽ", + "maintenance_restore_backup_description": "База даĐŊĐŊҋ҅ Immich ĐąŅƒĐ´ĐĩŅ‚ ĐžŅ‡Đ¸Ņ‰ĐĩĐŊа и ĐˇĐ°Ņ‚ĐĩĐŧ Đ˛ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊа иС Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊОК Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии. ĐĸĐĩĐēŅƒŅ‰ĐĩĐĩ ŅĐžŅŅ‚ĐžŅĐŊиĐĩ Ņ‚ĐžĐļĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋŅ€ĐĩĐ´Đ˛Đ°Ņ€Đ¸Ņ‚ĐĩĐģҌĐŊĐž ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐž.", + "maintenance_restore_backup_different_version": "Đ­Ņ‚Đ° Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ°Ņ ĐēĐžĐŋĐ¸Ņ ĐąŅ‹Đģа ŅĐ´ĐĩĐģаĐŊа ĐŊа Đ´Ņ€ŅƒĐŗĐžĐš вĐĩŅ€ŅĐ¸Đ¸ Immich!", + "maintenance_restore_backup_unknown_version": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžĐŋŅ€ĐĩĐ´ĐĩĐģĐ¸Ņ‚ŅŒ вĐĩŅ€ŅĐ¸ŅŽ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии.", + "maintenance_restore_database_backup": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅƒŅŽ ĐēĐžĐŋĐ¸ŅŽ ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", + "maintenance_restore_database_backup_description": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ ĐŋŅ€ĐĩĐ´Ņ‹Đ´ŅƒŅ‰ĐĩĐŗĐž ŅĐžŅŅ‚ĐžŅĐŊĐ¸Ņ ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅ иС Ņ„Đ°ĐšĐģа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии", "maintenance_settings": "ĐžĐąŅĐģ҃ĐļиваĐŊиĐĩ", "maintenance_settings_description": "ПĐĩŅ€ĐĩвОд ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich в Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ.", "maintenance_start": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ", "maintenance_start_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ в Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ.", + "maintenance_upload_backup": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Ņ„Đ°ĐšĐģ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", + "maintenance_upload_backup_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅƒŅŽ ĐēĐžĐŋĐ¸ŅŽ. Đ­Ņ‚Đž Ņ‚ĐžŅ‡ĐŊĐž Ņ„Đ°ĐšĐģ .sql/.sql.gz?", "manage_concurrency": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŋĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒŅŽ", - "manage_concurrency_description": "ПĐĩŅ€ĐĩŅ…ĐžĐ´ ĐŊа ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи ĐˇĐ°Đ´Đ°Ņ‡ Đ´ĐģŅ ҃ĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐ¸Ņ ĐŋĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒŅŽ Đ¸Ņ… Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ", + "manage_concurrency_description": "ПĐĩŅ€ĐĩŅ…ĐžĐ´ Đē ҃ĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐ¸ŅŽ ĐŋĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒŅŽ Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ ĐˇĐ°Đ´Đ°Ņ‡", "manage_log_settings": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ĐļŅƒŅ€ĐŊаĐģа", "map_dark_style": "ĐĸŅ‘ĐŧĐŊŅ‹Đš ŅŅ‚Đ¸ĐģҌ", "map_enable_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Ņ„ŅƒĐŊĐēŅ†Đ¸Đ¸ ĐēĐ°Ņ€Ņ‚Ņ‹", @@ -252,7 +272,7 @@ "oauth_auto_register": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēĐ°Ņ Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ", "oauth_auto_register_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēи Ņ€ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐŊĐžĐ˛Ņ‹Ņ… ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš ĐŋŅ€Đ¸ Đ˛Ņ…ĐžĐ´Đĩ в ŅĐ¸ŅŅ‚ĐĩĐŧ҃ ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ OAuth", "oauth_button_text": "ĐĸĐĩĐēҁ҂ ĐēĐŊĐžĐŋĐēи", - "oauth_client_secret_description": "ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ ĐĩҁĐģи PKCE (Proof Key for Code Exchange) ĐŊĐĩ ĐŋОддĐĩŅ€ĐļиваĐĩŅ‚ŅŅ OAuth ĐŋŅ€ĐžĐ˛Đ°ĐšĐ´ĐĩŅ€ĐžĐŧ", + "oauth_client_secret_description": "ĐĸŅ€ĐĩĐąŅƒĐĩŅ‚ŅŅ Đ´ĐģŅ ĐēĐžĐŊŅ„Đ¸Đ´ĐĩĐŊŅ†Đ¸Đ°ĐģҌĐŊҋ҅ ĐēĐģиĐĩĐŊŅ‚ĐžĐ˛ иĐģи ĐĩҁĐģи PKCE (Proof Key for Code Exchange) ĐŊĐĩ ĐŋОддĐĩŅ€ĐļиваĐĩŅ‚ŅŅ Đ´ĐģŅ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ĐēĐģиĐĩĐŊŅ‚ĐžĐ˛.", "oauth_enable_description": "Đ’Ņ…ĐžĐ´ ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ OAuth", "oauth_mobile_redirect_uri": "URI Ņ€ĐĩĐ´Đ¸Ņ€ĐĩĐēŅ‚Đ° Đ´ĐģŅ ĐŧОйиĐģҌĐŊҋ҅", "oauth_mobile_redirect_uri_override": "ПĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ URI Đ´ĐģŅ ĐŧОйиĐģҌĐŊҋ҅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", @@ -277,8 +297,8 @@ "paths_validated_successfully": "Đ’ŅĐĩ ĐŋŅƒŅ‚Đ¸ ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€ĐžŅˆĐģи ĐŋŅ€ĐžĐ˛ĐĩŅ€Đē҃", "person_cleanup_job": "ĐžŅ‡Đ¸ŅŅ‚Đēа ĐŋĐĩŅ€ŅĐžĐŊŅ‹", "queue_details": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐžŅ‡ĐĩŅ€Đĩди", - "queues": "ĐžŅ‡ĐĩŅ€Đĩди ĐˇĐ°Đ´Đ°Ņ‡", - "queues_page_description": "ĐĄŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ° ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи СаĐŋĐģаĐŊĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊҋ҅ ĐˇĐ°Đ´Đ°Ņ‡", + "queues": "Đ—Đ°Đ´Đ°Ņ‡Đ¸", + "queues_page_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ Ņ€ĐĩĐŗĐģаĐŧĐĩĐŊŅ‚ĐŊŅ‹Đŧи ĐˇĐ°Đ´Đ°Ņ‡Đ°Đŧи и ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€ ŅŅ‚Đ°Ņ‚ŅƒŅĐ° Đ¸Ņ… Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ", "quota_size_gib": "РаСĐŧĐĩŅ€ ĐēĐ˛ĐžŅ‚Ņ‹ (GiB)", "refreshing_all_libraries": "ОбĐŊОвĐģĐĩĐŊиĐĩ Đ˛ŅĐĩŅ… йийĐģĐ¸ĐžŅ‚ĐĩĐē", "registration": "Đ ĐĩĐŗĐ¸ŅŅ‚Ņ€Đ°Ņ†Đ¸Ņ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", @@ -291,15 +311,15 @@ "search_jobs": "ĐŸĐžĐ¸ŅĐē ĐˇĐ°Đ´Đ°Ņ‡â€Ļ", "send_welcome_email": "ĐžŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŋŅ€Đ¸Đ˛ĐĩŅ‚ŅŅ‚Đ˛ĐĩĐŊĐŊĐžĐĩ ĐŋĐ¸ŅŅŒĐŧĐž", "server_external_domain_settings": "ВĐŊĐĩ҈ĐŊиК Đ´ĐžĐŧĐĩĐŊ", - "server_external_domain_settings_description": "ДоĐŧĐĩĐŊ Đ´ĐģŅ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ҁҁҋĐģĐžĐē, вĐēĐģŅŽŅ‡Đ°Ņ http(s)://", + "server_external_domain_settings_description": "ДоĐŧĐĩĐŊ Đ´ĐģŅ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ҁҁҋĐģĐžĐē", "server_public_users": "ĐŸŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģи", "server_public_users_description": "Đ’Ņ‹Đ˛ĐžĐ´Đ¸Ņ‚ŅŒ ҁĐŋĐ¸ŅĐžĐē ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš (иĐŧĐĩĐŊа и email) в ĐžĐąŅ‰Đ¸Ņ… аĐģŅŒĐąĐžĐŧĐ°Ņ…. ĐšĐžĐŗĐ´Đ° ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐž, ҁĐŋĐ¸ŅĐžĐē Đ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ Ņ‚ĐžĐģҌĐēĐž адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°Đŧ, ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģи ҁĐŧĐžĐŗŅƒŅ‚ Đ´ĐĩĐģĐ¸Ņ‚ŅŒŅŅ Ņ‚ĐžĐģҌĐēĐž ҁҁҋĐģĐēОК.", "server_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "server_stats_page_description": "ĐĄŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ° ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "server_stats_page_description": "ХвОдĐŊĐ°Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ ĐŋĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧ и ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧ", "server_welcome_message": "ĐŸŅ€Đ¸Đ˛ĐĩŅ‚ŅŅ‚Đ˛ĐĩĐŊĐŊĐžĐĩ ŅĐžĐžĐąŅ‰ĐĩĐŊиĐĩ", "server_welcome_message_description": "ĐĄĐžĐžĐąŅ‰ĐĩĐŊиĐĩ, ĐēĐžŅ‚ĐžŅ€ĐžĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°Ņ‚ŅŒŅŅ ĐŊа ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đĩ Đ˛Ņ…ĐžĐ´Đ°.", - "settings_page_description": "ĐĄŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ° ĐŊĐ°ŅŅ‚Ņ€ĐžĐĩĐē ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "settings_page_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "sidecar_job": "МĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊŅ‹Đĩ иС sidecar-Ņ„Đ°ĐšĐģОв", "sidecar_job_description": "ОбĐŊĐ°Ņ€ŅƒĐļиваĐĩŅ‚ и ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ŅƒĐĩŅ‚ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐŊŅ‹Đĩ иС sidecar-Ņ„Đ°ĐšĐģОв", "slideshow_duration_description": "ДĐģĐ¸Ņ‚ĐĩĐģҌĐŊĐžŅŅ‚ŅŒ ĐŋĐžĐēаСа ҁĐģаКдОв в ҁĐĩĐē҃ĐŊĐ´Đ°Ņ…", @@ -419,7 +439,7 @@ "user_settings": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌҁĐēиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи", "user_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš", "user_successfully_removed": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ {email} ҃ҁĐŋĐĩ҈ĐŊĐž ŅƒĐ´Đ°ĐģĐĩĐŊ.", - "users_page_description": "ĐĄŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đ° ҃ĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐ¸Ņ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи", + "users_page_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи ŅĐ¸ŅŅ‚ĐĩĐŧŅ‹", "version_check_enabled_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đē҃ ĐŊаĐģĐ¸Ņ‡Đ¸Ņ ĐŊĐžĐ˛Ņ‹Ņ… вĐĩŅ€ŅĐ¸Đš", "version_check_implications": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ ĐŋŅ€ĐžĐ˛ĐĩŅ€Đēи вĐĩŅ€ŅĐ¸Đ¸ ĐŋĐĩŅ€Đ¸ĐžĐ´Đ¸Ņ‡ĐĩҁĐēи ĐžĐąŅ€Đ°Ņ‰Đ°ĐĩŅ‚ŅŅ Đē ŅĐ°ĐšŅ‚Ņƒ github.com", "version_check_settings": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đēа вĐĩŅ€ŅĐ¸Đ¸", @@ -431,6 +451,9 @@ "admin_password": "ĐŸĐ°Ņ€ĐžĐģҌ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "administration": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ", "advanced": "Đ Đ°ŅŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ‹Đĩ", + "advanced_settings_clear_image_cache": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēŅŅˆ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиК", + "advanced_settings_clear_image_cache_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēŅŅˆ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊиК", + "advanced_settings_clear_image_cache_success": "ĐŖŅĐŋĐĩ҈ĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž {size}", "advanced_settings_enable_alternate_media_filter_subtitle": "ĐŸĐžĐ´ĐąĐžŅ€ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ Đ´ĐģŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸ ĐŊа ĐžŅĐŊОвĐĩ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊҋ҅ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸Đĩв. ĐŸŅ€ĐžĐąŅƒĐšŅ‚Đĩ вĐēĐģŅŽŅ‡Đ°Ņ‚ŅŒ Ņ‚ĐžĐģҌĐēĐž в Ņ‚ĐžĐŧ ҁĐģŅƒŅ‡Đ°Đĩ, ĐĩҁĐģи в ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊии ĐĩŅŅ‚ŅŒ ĐŋŅ€ĐžĐąĐģĐĩĐŧŅ‹ ҁ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊиĐĩĐŧ Đ˛ŅĐĩŅ… аĐģŅŒĐąĐžĐŧОв.", "advanced_settings_enable_alternate_media_filter_title": "[ЭКСПЕРИМЕНĐĸАЛĐŦНО] Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°ĐŊиĐĩ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊĐžĐŗĐž ҁĐŋĐžŅĐžĐąĐ° ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ°Ņ†Đ¸Đ¸ аĐģŅŒĐąĐžĐŧОв ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "advanced_settings_log_level_title": "ĐŖŅ€ĐžĐ˛ĐĩĐŊҌ ĐģĐžĐŗĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ: {level}", @@ -467,10 +490,12 @@ "album_remove_user": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ?", "album_remove_user_confirmation": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ {user}?", "album_search_not_found": "НĐĩ ĐŊаКдĐĩĐŊĐž аĐģŅŒĐąĐžĐŧОв ĐŋĐž Đ˛Đ°ŅˆĐĩĐŧ҃ СаĐŋŅ€ĐžŅŅƒ", + "album_selected": "АĐģŅŒĐąĐžĐŧ Đ˛Ņ‹ĐąŅ€Đ°ĐŊ", "album_share_no_users": "НĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊҋ҅ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš, ҁ ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž ĐŋОдĐĩĐģĐ¸Ņ‚ŅŒŅŅ аĐģŅŒĐąĐžĐŧĐžĐŧ.", "album_summary": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Ой аĐģŅŒĐąĐžĐŧĐĩ", "album_updated": "АĐģŅŒĐąĐžĐŧ ОйĐŊОвĐģŅ‘ĐŊ", "album_updated_setting_description": "ПоĐģŅƒŅ‡Đ°Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊиĐĩ ĐŋĐž ŅĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊОК ĐŋĐžŅ‡Ņ‚Đĩ ĐŋŅ€Đ¸ дОйавĐģĐĩĐŊии ĐŊĐžĐ˛Ņ‹Ņ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ в ĐžĐąŅ‰Đ¸Đš аĐģŅŒĐąĐžĐŧ", + "album_upload_assets": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ҁ ĐēĐžĐŧĐŋŅŒŅŽŅ‚ĐĩŅ€Đ° и Đ´ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ Đ¸Ņ… в аĐģŅŒĐąĐžĐŧ", "album_user_left": "Đ’Ņ‹ ĐŋĐžĐēиĐŊ҃Đģи {album}", "album_user_removed": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌ {user} ŅƒĐ´Đ°ĐģĐĩĐŊ", "album_viewer_appbar_delete_confirm": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ иС ŅĐ˛ĐžĐĩĐš ŅƒŅ‡ĐĩŅ‚ĐŊОК СаĐŋĐ¸ŅĐ¸?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "ПĐĩŅ€Đ˛ĐžĐŊĐ°Ņ‡Đ°ĐģҌĐŊŅ‹Đš ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đēи, ŅƒŅŅ‚Đ°ĐŊавĐģиваĐĩĐŧŅ‹Đš в ĐŊĐžĐ˛Ņ‹Ņ… аĐģŅŒĐąĐžĐŧĐ°Ņ….", "albums_feature_description": "КоĐģĐģĐĩĐēŅ†Đ¸Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž, ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž Đ´ĐĩĐģĐ¸Ņ‚ŅŒŅŅ ҁ Đ´Ņ€ŅƒĐŗĐ¸Đŧи ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи.", "albums_on_device_count": "АĐģŅŒĐąĐžĐŧŅ‹ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ ({count})", + "albums_selected": "{count, plural, one {Đ’Ņ‹ĐąŅ€Đ°ĐŊ # аĐģŅŒĐąĐžĐŧ} many {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # аĐģŅŒĐąĐžĐŧОв} other {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # аĐģŅŒĐąĐžĐŧа}}", "all": "Đ’ŅĐĩ", "all_albums": "Đ’ŅĐĩ аĐģŅŒĐąĐžĐŧŅ‹", "all_people": "Đ’ŅĐĩ ĐģŅŽĐ´Đ¸", + "all_photos": "Đ’ŅĐĩ Ņ„ĐžŅ‚Đž", "all_videos": "Đ’ŅĐĩ видĐĩĐž", "allow_dark_mode": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ ҂ґĐŧĐŊŅ‹Đš Ņ€ĐĩĐļиĐŧ", "allow_edits": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Đ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒ дОйавĐģĐĩĐŊиĐĩ Ņ„Đ°ĐšĐģОв", "allowed": "Đ Đ°ĐˇŅ€Đĩ҈ĐĩĐŊĐž", "alt_text_qr_code": "QR-ĐēОд", + "always_keep": "Đ’ŅĐĩĐŗĐ´Đ° ĐžŅŅ‚Đ°Đ˛ĐģŅŅ‚ŅŒ", + "always_keep_photos_hint": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ ĐžŅĐ˛ĐžĐąĐžĐļĐ´ĐĩĐŊĐ¸Ņ ĐŧĐĩŅŅ‚Đ° ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ Đ˛ŅĐĩ Ņ„ĐžŅ‚Đž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ.", + "always_keep_videos_hint": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ ĐžŅĐ˛ĐžĐąĐžĐļĐ´ĐĩĐŊĐ¸Ņ ĐŧĐĩŅŅ‚Đ° ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ Đ˛ŅĐĩ видĐĩĐž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ.", "anti_clockwise": "ĐŸŅ€ĐžŅ‚Đ¸Đ˛ Ņ‡Đ°ŅĐžĐ˛ĐžĐš", "api_key": "API ĐēĐģŅŽŅ‡", "api_key_description": "Đ­Ņ‚Đž СĐŊĐ°Ņ‡ĐĩĐŊиĐĩ ĐąŅƒĐ´ĐĩŅ‚ ĐŋĐžĐēаСаĐŊĐž Ņ‚ĐžĐģҌĐēĐž ОдиĐŊ Ņ€Đ°Đˇ. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ŅƒĐąĐĩĐ´Đ¸Ņ‚ĐĩҁҌ, Ņ‡Ņ‚Đž ҁĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Đģи ĐĩĐŗĐž ĐŋĐĩŅ€ĐĩĐ´ СаĐēŅ€Ņ‹Ņ‚Đ¸ĐĩĐŧ ĐžĐēĐŊа.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŊĐĩҁґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐž}} в Đ°Ņ€Ņ…Đ¸Đ˛", "are_these_the_same_person": "Đ­Ņ‚Đž ОдиĐŊ и Ņ‚ĐžŅ‚ ĐļĐĩ ҇ĐĩĐģОвĐĩĐē?", "are_you_sure_to_do_this": "Đ’Ņ‹ ŅƒĐ˛ĐĩŅ€ĐĩĐŊŅ‹, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅŅ‚Đž ŅĐ´ĐĩĐģĐ°Ņ‚ŅŒ?", + "array_field_not_fully_supported": "ПоĐģŅ ĐŧĐ°ŅŅĐ¸Đ˛ĐžĐ˛ ҂ҀĐĩĐąŅƒŅŽŅ‚ Ņ€ŅƒŅ‡ĐŊĐžĐŗĐž Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ JSON", "asset_action_delete_err_read_only": "НĐĩвОСĐŧĐžĐļĐŊĐž ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐēŅ‚(Ņ‹) Ņ‚ĐžĐģҌĐēĐž Đ´ĐģŅ ҇҂ĐĩĐŊĐ¸Ņ, ĐŋŅ€ĐžĐŋ҃ҁĐē", "asset_action_share_err_offline": "НĐĩвОСĐŧĐžĐļĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ĐžŅ„Ņ„ĐģаКĐŊ-ĐžĐąŅŠĐĩĐēŅ‚(Ņ‹), ĐŋŅ€ĐžĐŋ҃ҁĐē", "asset_added_to_album": "ДобавĐģĐĩĐŊĐž в аĐģŅŒĐąĐžĐŧ", "asset_adding_to_album": "ДобавĐģĐĩĐŊиĐĩ в аĐģŅŒĐąĐžĐŧâ€Ļ", + "asset_created": "ĐžĐąŅŠĐĩĐēŅ‚ ŅĐžĐˇĐ´Đ°ĐŊ", "asset_description_updated": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩ ОйĐŊОвĐģĐĩĐŊĐž", "asset_filename_is_offline": "ĐžĐąŅŠĐĩĐēŅ‚ {filename} ĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚ŅŅ в ĐžŅ„ĐģаКĐŊ-Ņ€ĐĩĐļиĐŧĐĩ", "asset_has_unassigned_faces": "Đ•ŅŅ‚ŅŒ ĐŊĐĩ Ņ€Đ°ŅĐŋОСĐŊаĐŊĐŊŅ‹Đĩ ĐģĐ¸Ņ†Đ°", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "РаСĐŧĐĩŅ‚Đēа", "asset_list_settings_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа ҁĐĩŅ‚Đēи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš", "asset_list_settings_title": "ĐĄĐĩŅ‚Đēа Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš", + "asset_not_found_on_device_android": "ĐžĐąŅŠĐĩĐēŅ‚ ĐŊĐĩ ĐŊаКдĐĩĐŊ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", + "asset_not_found_on_device_ios": "ĐžĐąŅŠĐĩĐēŅ‚ ĐŊĐĩ ĐŊаКдĐĩĐŊ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ. Đ•ŅĐģи Đ¸ŅĐŋĐžĐģŅŒĐˇŅƒĐĩŅ‚ŅŅ iCloud, Đ´ĐžŅŅ‚ŅƒĐŋ Đē ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŧĐžĐļĐĩŅ‚ ĐąŅ‹Ņ‚ŅŒ ĐˇĐ°Ņ‚Ņ€ŅƒĐ´ĐŊĐĩĐŊ иС-Са ĐŊĐĩĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊĐžĐŗĐž Ņ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ Ņ„Đ°ĐšĐģа в iCloud.", + "asset_not_found_on_icloud": "ĐžĐąŅŠĐĩĐēŅ‚ ĐŊĐĩ ĐŊаКдĐĩĐŊ в iCloud. ВозĐŧĐžĐļĐŊĐž, Ņ„Đ°ĐšĐģ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐĩĐŊ иС-Са ĐŊĐĩĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊĐžĐŗĐž Ņ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ в iCloud.", "asset_offline": "ĐžĐąŅŠĐĩĐēŅ‚ ĐžŅ‚ĐēĐģŅŽŅ‡Ņ‘ĐŊ", "asset_offline_description": "Đ­Ņ‚ĐžŅ‚ вĐŊĐĩ҈ĐŊиК Ņ„Đ°ĐšĐģ ĐŊĐĩ ĐŊаКдĐĩĐŊ ĐŊа Đ´Đ¸ŅĐēĐĩ. ПоĐļаĐģŅƒĐšŅŅ‚Đ°, ŅĐ˛ŅĐļĐ¸Ņ‚ĐĩҁҌ ҁ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ Immich Đ´ĐģŅ ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐ¸Ņ ĐŋĐžĐŧĐžŅ‰Đ¸.", "asset_restored_successfully": "ĐžĐąŅŠĐĩĐēŅ‚ ҃ҁĐŋĐĩ҈ĐŊĐž Đ˛ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "ĐŸĐ°Ņ€ĐžĐģи ĐŊĐĩ ŅĐžĐ˛ĐŋĐ°Đ´Đ°ŅŽŅ‚", "change_password_form_reenter_new_password": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ввĐĩĐ´Đ¸Ņ‚Đĩ ĐŊĐžĐ˛Ņ‹Đš ĐŋĐ°Ņ€ĐžĐģҌ", "change_pin_code": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ PIN-ĐēОд", + "change_trigger": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Ņ‚Ņ€Đ¸ĐŗĐŗĐĩŅ€", + "change_trigger_prompt": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ‚Đž ŅĐžĐąŅ‹Ņ‚Đ¸Đĩ? ИСĐŧĐĩĐŊĐĩĐŊиĐĩ ŅĐžĐąŅ‹Ņ‚Đ¸Ņ ĐŋŅ€Đ¸Đ˛ĐĩĐ´Ņ‘Ņ‚ Đē ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸ŅŽ ҃ĐļĐĩ ŅĐžĐˇĐ´Đ°ĐŊĐŊҋ҅ ĐžŅ‚ĐąĐžŅ€ĐžĐ˛ и Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đš.", "change_your_password": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅĐ˛ĐžĐš ĐŋĐ°Ņ€ĐžĐģҌ", "changed_visibility_successfully": "ВидиĐŧĐžŅŅ‚ŅŒ ҃ҁĐŋĐĩ҈ĐŊĐž иСĐŧĐĩĐŊĐĩĐŊа", "charging": "ĐŸŅ€Đ¸ ĐˇĐ°Ņ€ŅĐ´ĐēĐĩ", @@ -722,6 +759,18 @@ "checksum": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊĐ°Ņ ҁ҃ĐŧĐŧа", "choose_matching_people_to_merge": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐŋĐžĐ´Ņ…ĐžĐ´ŅŅ‰Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš Đ´ĐģŅ ҁĐģĐ¸ŅĐŊĐ¸Ņ", "city": "Đ“ĐžŅ€ĐžĐ´", + "cleanup_confirm_description": "ОбĐŊĐ°Ņ€ŅƒĐļĐĩĐŊŅ‹ ĐžĐąŅŠĐĩĐē҂ҋ ({count} ŅˆŅ‚.), ŅĐžĐˇĐ´Đ°ĐŊĐŊŅ‹Đĩ Đ´Đž {date} и ҃ĐļĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€. ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Đ¸Ņ… ĐēĐžĐŋии ҁ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°?", + "cleanup_confirm_prompt_title": "ĐŖĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ҁ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°?", + "cleanup_deleted_assets": "ĐžĐąŅŠĐĩĐē҂ҋ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊŅ‹ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ° ({count} ŅˆŅ‚.)", + "cleanup_deleting": "ПĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊиĐĩ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃...", + "cleanup_found_assets": "НайдĐĩĐŊŅ‹ ҃ĐļĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐžĐąŅŠĐĩĐē҂ҋ ({count} ŅˆŅ‚.)", + "cleanup_found_assets_with_size": "НайдĐĩĐŊŅ‹ ŅĐžŅ…Ņ€Đ°ĐŊŅ‘ĐŊĐŊŅ‹Đĩ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐžĐąŅŠĐĩĐē҂ҋ ({count} ŅˆŅ‚.) ({size})", + "cleanup_icloud_shared_albums_excluded": "ĐžĐąŅ‰Đ¸Đĩ аĐģŅŒĐąĐžĐŧŅ‹ iCloud Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊŅ‹ иС ҁĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ", + "cleanup_no_assets_found": "НĐĩ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐž ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐž ҃ĐēаСаĐŊĐŊŅ‹Đŧ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Đ¸ŅĐŧ. ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚Đž ĐŧĐžĐļĐŊĐž Ņ‚ĐžĐģҌĐēĐž ŅƒĐ´Đ°Đģив ĐžĐąŅŠĐĩĐē҂ҋ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊŅ‹ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€.", + "cleanup_preview_title": "ĐžĐąŅŠĐĩĐē҂ҋ Đ´ĐģŅ ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸Ņ ({count} ŅˆŅ‚.)", + "cleanup_step3_description": "ĐŸĐžĐ¸ŅĐē ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ҃ĐļĐĩ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€Đĩ, ŅĐžĐžŅ‚Đ˛ĐĩŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Ņ… Đ´Đ°Ņ‚Đĩ и ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊиК.", + "cleanup_step4_summary": "ĐžĐąŅŠĐĩĐē҂ҋ ({count} ŅˆŅ‚.), ŅĐžĐˇĐ´Đ°ĐŊĐŊŅ‹Đĩ Đ´Đž {date}, в ĐžŅ‡ĐĩŅ€Đĩди ĐŊа ŅƒĐ´Đ°ĐģĐĩĐŊиĐĩ ҁ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°. ОĐŊи ĐŋĐž-ĐŋŅ€ĐĩĐļĐŊĐĩĐŧ҃ ĐąŅƒĐ´ŅƒŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹ в ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊии Immich.", + "cleanup_trash_hint": "Đ§Ņ‚ĐžĐąŅ‹ ĐŋĐžĐģĐŊĐžŅŅ‚ŅŒŅŽ ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚Đž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ, ĐžŅ‚ĐēŅ€ĐžĐšŅ‚Đĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊОК ĐŗĐ°ĐģĐĩŅ€Đĩи и ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đĩ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", "clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ", "clear_all": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ Đ˛ŅŅ‘", "clear_all_recent_searches": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ Đ˛ŅĐĩ ĐŊĐĩдавĐŊиĐĩ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ‹ ĐŋĐžĐ¸ŅĐēа", @@ -733,6 +782,8 @@ "client_cert_import": "ИĐŧĐŋĐžŅ€Ņ‚", "client_cert_import_success_msg": "КĐģиĐĩĐŊ҂ҁĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ иĐŧĐŋĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊ", "client_cert_invalid_msg": "НĐĩвĐĩŅ€ĐŊŅ‹Đš Ņ„Đ°ĐšĐģ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚Đ° иĐģи ĐŊĐĩвĐĩŅ€ĐŊŅ‹Đš ĐŋĐ°Ņ€ĐžĐģҌ", + "client_cert_password_message": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ ŅŅ‚ĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚Đ°", + "client_cert_password_title": "ĐŸĐ°Ņ€ĐžĐģҌ ĐžŅ‚ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚Đ°", "client_cert_remove_msg": "КĐģиĐĩĐŊ҂ҁĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ ŅƒĐ´Đ°ĐģĐĩĐŊ", "client_cert_subtitle": "ПоддĐĩŅ€ĐļиваĐĩŅ‚ŅŅ Ņ‚ĐžĐģҌĐēĐž Ņ„ĐžŅ€ĐŧĐ°Ņ‚ PKCS12 (.p12, .pfx). ИĐŧĐŋĐžŅ€Ņ‚/ŅƒĐ´Đ°ĐģĐĩĐŊиĐĩ ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐž Ņ‚ĐžĐģҌĐēĐž ĐŋĐĩŅ€ĐĩĐ´ Đ˛Ņ…ĐžĐ´ĐžĐŧ в ŅĐ¸ŅŅ‚ĐĩĐŧ҃.", "client_cert_title": "[ЭКСПЕРИМЕНĐĸАЛĐŦНО] КĐģиĐĩĐŊ҂ҁĐēиК SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚", @@ -743,6 +794,11 @@ "color": "ĐĻвĐĩŅ‚", "color_theme": "ĐĻвĐĩŅ‚ĐžĐ˛Đ°Ņ Ņ‚ĐĩĐŧа", "command": "КоĐŧаĐŊда", + "command_palette_prompt": "Đ‘Ņ‹ŅŅ‚Ņ€Ņ‹Đš ĐŋĐžĐ¸ŅĐē ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†, Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Đš иĐģи ĐēĐžĐŧаĐŊĐ´", + "command_palette_to_close": "СаĐēŅ€Ņ‹Ņ‚ŅŒ", + "command_palette_to_navigate": "ĐŊĐ°Đ˛Đ¸ĐŗĐ°Ņ†Đ¸Ņ", + "command_palette_to_select": "Đ˛Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ", + "command_palette_to_show_all": "ĐŋĐžĐēĐ°ĐˇĐ°Ņ‚ŅŒ Đ˛ŅĐĩ", "comment_deleted": "КоĐŧĐŧĐĩĐŊŅ‚Đ°Ņ€Đ¸Đš ŅƒĐ´Đ°ĐģŅ‘ĐŊ", "comment_options": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ ҁ ĐēĐžĐŧĐŧĐĩĐŊŅ‚Đ°Ņ€Đ¸ĐĩĐŧ", "comments_and_likes": "КоĐŧĐŧĐĩĐŊŅ‚Đ°Ņ€Đ¸Đ¸ и ĐžŅ‚ĐŧĐĩŅ‚Đēи \"ĐŊŅ€Đ°Đ˛Đ¸Ņ‚ŅŅ\"", @@ -787,6 +843,7 @@ "create_album": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ", "create_album_page_untitled": "БĐĩС ĐŊаСваĐŊĐ¸Ņ", "create_api_key": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ API ĐēĐģŅŽŅ‡", + "create_first_workflow": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐŋĐĩŅ€Đ˛Ņ‹Đš Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "create_library": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", "create_link": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃", "create_link_to_share": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ҁҁҋĐģĐē҃ ĐžĐąŅ‰ĐĩĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋа", @@ -801,17 +858,25 @@ "create_tag": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ Ņ‚ĐĩĐŗ", "create_tag_description": "ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ ĐŊĐžĐ˛Ņ‹Đš Ņ‚ĐĩĐŗ. ДĐģŅ вĐģĐžĐļĐĩĐŊĐŊҋ҅ Ņ‚ĐĩĐŗĐžĐ˛ ввĐĩĐ´Đ¸Ņ‚Đĩ ĐŋĐžĐģĐŊŅ‹Đš ĐŋŅƒŅ‚ŅŒ Đē Ņ‚ĐĩĐŗŅƒ, вĐēĐģŅŽŅ‡Đ°Ņ ҁĐģŅŅˆĐ¸.", "create_user": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "create_workflow": "ĐĄĐžĐˇĐ´Đ°Ņ‚ŅŒ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "created": "ХОСдаĐŊ", "created_at": "ХОСдаĐŊ", "creating_linked_albums": "ХОСдаĐŊиĐĩ ŅĐ˛ŅĐˇĐ°ĐŊĐŊҋ҅ аĐģŅŒĐąĐžĐŧОв...", "crop": "ĐžĐąŅ€ĐĩĐˇĐ°Ņ‚ŅŒ", + "crop_aspect_ratio_fixed": "ФиĐēŅĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊŅ‹Đš", + "crop_aspect_ratio_free": "ХвОйОдĐŊĐž", + "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", "curated_object_page_title": "ĐŸŅ€ĐĩĐ´ĐŧĐĩ҂ҋ", "current_device": "ĐĸĐĩĐēŅƒŅ‰ĐĩĐĩ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đž", "current_pin_code": "ĐĸĐĩĐēŅƒŅ‰Đ¸Đš PIN-ĐēОд", "current_server_address": "ĐĸĐĩĐēŅƒŅ‰Đ¸Đš Đ°Đ´Ņ€Đĩҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "custom_date": "ĐŸŅ€ĐžĐ¸ĐˇĐ˛ĐžĐģҌĐŊĐ°Ņ Đ´Đ°Ņ‚Đ°", "custom_locale": "ПоĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģҌҁĐēиК Ņ€ĐĩĐŗĐ¸ĐžĐŊ", "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ Đ´Đ°Ņ‚ и Ņ‡Đ¸ŅĐĩĐģ в ĐˇĐ°Đ˛Đ¸ŅĐ¸ĐŧĐžŅŅ‚Đ¸ ĐžŅ‚ ŅĐˇŅ‹Đēа и Ņ€ĐĩĐŗĐ¸ĐžĐŊа", "custom_url": "ХвОК URL", + "cutoff_date_description": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ Ņ„ĐžŅ‚Đž Са ĐŋĐžŅĐģĐĩĐ´ĐŊиĐĩâ€Ļ", + "cutoff_day": "{count, plural, one {Đ´ĐĩĐŊҌ} many {Đ´ĐŊĐĩĐš} other {Đ´ĐŊŅ}}", + "cutoff_year": "{count, plural, one {ĐŗĐžĐ´} many {ĐģĐĩŅ‚} other {ĐŗĐžĐ´Đ°}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "ĐĸŅ‘ĐŧĐŊĐ°Ņ", @@ -867,6 +932,7 @@ "deselect_all": "ĐĄĐŊŅŅ‚ŅŒ Đ˛Ņ‹Đ´ĐĩĐģĐĩĐŊиĐĩ", "details": "ĐŸĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸", "direction": "НаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ", + "disable": "ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ", "disabled": "ĐžŅ‚ĐēĐģŅŽŅ‡ĐĩĐŊĐž", "disallow_edits": "ЗаĐŋŅ€ĐĩŅ‚Đ¸Ņ‚ŅŒ Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Đ’ŅŅ‚Ņ€ĐžĐĩĐŊĐŊŅ‹Đĩ видĐĩĐž", "download_include_embedded_motion_videos_description": "ĐĄĐžŅ…Ņ€Đ°ĐŊŅŅ‚ŅŒ видĐĩĐž, Đ˛ŅŅ‚Ņ€ĐžĐĩĐŊĐŊŅ‹Đĩ в ĐļĐ¸Đ˛Ņ‹Đĩ Ņ„ĐžŅ‚Đž, в видĐĩ ĐžŅ‚Đ´ĐĩĐģҌĐŊҋ҅ Ņ„Đ°ĐšĐģОв", "download_notfound": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŊĐĩ ĐŊаКдĐĩĐŊа", + "download_original": "ĐĄĐēĐ°Ņ‡Đ°Ņ‚ŅŒ ĐžŅ€Đ¸ĐŗĐ¸ĐŊаĐģ", "download_paused": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŋŅ€Đ¸ĐžŅŅ‚Đ°ĐŊОвĐģĐĩĐŊа", "download_settings": "ĐĄĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊиĐĩ", "download_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēаĐŧи ҁĐēĐ°Ņ‡Đ¸Đ˛Đ°ĐŊĐ¸Ņ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "ОĐļидаĐŊиĐĩ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊОК ĐŋĐžĐŋҋ҂Đēи", "downloading": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа", "downloading_asset_filename": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐžĐąŅŠĐĩĐēŅ‚Đ° {filename}", + "downloading_from_icloud": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа иС iCloud", "downloading_media": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŧĐĩдиа", "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅĐ¸Ņ‚Đĩ Ņ„Đ°ĐšĐģŅ‹ в ĐģŅŽĐąĐžĐĩ ĐŧĐĩŅŅ‚Đž Đ´ĐģŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "duplicates": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚Ņ‹", @@ -929,11 +997,22 @@ "edit_tag": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Ņ‚ĐĩĐŗ", "edit_title": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐˇĐ°ĐŗĐžĐģОвОĐē", "edit_user": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "edit_workflow": "Đ ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "editor_close_without_save_prompt": "ИСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹", "editor_close_without_save_title": "ЗаĐēŅ€Ņ‹Ņ‚ŅŒ Ņ€ĐĩдаĐēŅ‚ĐžŅ€?", - "editor_crop_tool_h2_aspect_ratios": "ĐĄĐžĐžŅ‚ĐŊĐžŅˆĐĩĐŊĐ¸Ņ ŅŅ‚ĐžŅ€ĐžĐŊ", - "editor_crop_tool_h2_rotation": "Đ’Ņ€Đ°Ņ‰ĐĩĐŊиĐĩ", + "editor_confirm_reset_all_changes": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛ŅĐĩ ŅĐ´ĐĩĐģаĐŊĐŊŅ‹Đĩ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ?", + "editor_discard_edits_confirm": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ", + "editor_discard_edits_prompt": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ Đ˛ŅĐĩ ĐŊĐĩ ŅĐžŅ…Ņ€Đ°ĐŊŅ‘ĐŊĐŊŅ‹Đĩ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ?", + "editor_discard_edits_title": "ĐžŅ‚ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ?", + "editor_edits_applied_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋŅ€Đ¸ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ", + "editor_edits_applied_success": "ИСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ ҃ҁĐŋĐĩ҈ĐŊĐž ĐŋŅ€Đ¸ĐŧĐĩĐŊĐĩĐŊŅ‹", + "editor_flip_horizontal": "ĐžŅ‚Ņ€Đ°ĐˇĐ¸Ņ‚ŅŒ ĐŗĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊĐž", + "editor_flip_vertical": "ĐžŅ‚Ņ€Đ°ĐˇĐ¸Ņ‚ŅŒ вĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊĐž", + "editor_orientation": "ĐžŅ€Đ¸ĐĩĐŊŅ‚Đ°Ņ†Đ¸Ņ", + "editor_reset_all_changes": "ĐĄĐąŅ€ĐžŅĐ¸Ņ‚ŅŒ иСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ", + "editor_rotate_left": "ПовĐĩŅ€ĐŊŅƒŅ‚ŅŒ ĐŊа 90° ĐŋŅ€ĐžŅ‚Đ¸Đ˛ Ņ‡Đ°ŅĐžĐ˛ĐžĐš ҁ҂ҀĐĩĐģĐēи", + "editor_rotate_right": "ПовĐĩŅ€ĐŊŅƒŅ‚ŅŒ ĐŊа 90° ĐŋĐž Ņ‡Đ°ŅĐžĐ˛ĐžĐš ҁ҂ҀĐĩĐģĐēĐĩ", "email": "Đ­ĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐ°Ņ ĐŋĐžŅ‡Ņ‚Đ°", "email_notifications": "ĐŖĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ ĐŋĐž ŅĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊОК ĐŋĐžŅ‡Ņ‚Đĩ", "empty_folder": "ĐŸŅƒŅŅ‚Đ°Ņ ĐŋаĐŋĐēа", @@ -952,11 +1031,14 @@ "error_change_sort_album": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đēи аĐģŅŒĐąĐžĐŧа", "error_delete_face": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ŅƒĐ´Đ°ĐģĐĩĐŊии ĐģĐ¸Ņ†Đ° иС ĐžĐąŅŠĐĩĐēŅ‚Đ°", "error_getting_places": "ĐžŅˆĐ¸ĐąĐēа ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐ¸Ņ ĐŧĐĩҁ҂", + "error_loading_albums": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ аĐģŅŒĐąĐžĐŧОв", "error_loading_image": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ Đ¸ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ", "error_loading_partners": "ĐžŅˆĐ¸ĐąĐēа ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи ĐŋĐ°Ņ€Ņ‚ĐŊŅ‘Ņ€ĐžĐ˛: {error}", + "error_retrieving_asset_information": "ĐžŅˆĐ¸ĐąĐēа ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐ¸Ņ иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Đ¸ Ой ĐžĐąŅŠĐĩĐēŅ‚Đĩ", "error_saving_image": "ĐžŅˆĐ¸ĐąĐēа: {error}", "error_tag_face_bounding_box": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ дОйавĐģĐĩĐŊии ĐžŅ‚ĐŧĐĩŅ‚Đēи - ĐŊĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Ņ‹ Ņ€Đ°ĐŧĐēи ĐģĐ¸Ņ†Đ°", "error_title": "ĐžŅˆĐ¸ĐąĐēа - Đ§Ņ‚Đž-Ņ‚Đž ĐŋĐžŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", + "error_while_navigating": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Đĩ Đē ĐžĐąŅŠĐĩĐēŅ‚Ņƒ", "errors": { "cannot_navigate_next_asset": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đē ҁĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐŧ҃ ĐžĐąŅŠĐĩĐēŅ‚Ņƒ", "cannot_navigate_previous_asset": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đē ĐŋŅ€ĐĩĐ´Ņ‹Đ´ŅƒŅ‰ĐĩĐŧ҃ ĐžĐąŅŠĐĩĐēŅ‚Ņƒ", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ Đ˛Ņ‹ĐŋĐžĐģĐŊĐ¸Ņ‚ŅŒ Đ˛Ņ…ĐžĐ´ ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ OAuth", "unable_to_connect": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋОдĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒŅŅ", "unable_to_copy_to_clipboard": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ҁĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧĐĩĐŊа, ŅƒĐąĐĩĐ´Đ¸Ņ‚ĐĩҁҌ, Ņ‡Ņ‚Đž Đ˛Ņ‹ ĐŋĐžĐģŅƒŅ‡Đ°ĐĩŅ‚Đĩ Đ´ĐžŅŅ‚ŅƒĐŋ Đē ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Đĩ ĐŋĐž ĐŋŅ€ĐžŅ‚ĐžĐēĐžĐģ҃ https", + "unable_to_create": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅĐžĐˇĐ´Đ°Ņ‚ŅŒ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "unable_to_create_admin_account": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅĐžĐˇĐ´Đ°Ņ‚ŅŒ ŅƒŅ‡ĐĩŅ‚ĐŊŅƒŅŽ СаĐŋĐ¸ŅŅŒ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "unable_to_create_api_key": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅĐžĐˇĐ´Đ°Ņ‚ŅŒ ĐŊĐžĐ˛Ņ‹Đš API ĐēĐģŅŽŅ‡", "unable_to_create_library": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅĐžĐˇĐ´Đ°Ņ‚ŅŒ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ŅˆĐ°ĐąĐģĐžĐŊ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ", "unable_to_delete_shared_link": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊŅƒŅŽ ҁҁҋĐģĐē҃", "unable_to_delete_user": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "unable_to_delete_workflow": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "unable_to_download_files": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ҁĐēĐ°Ņ‡Đ°Ņ‚ŅŒ Ņ„Đ°ĐšĐģŅ‹", "unable_to_edit_exclusion_pattern": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‚Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ŅˆĐ°ĐąĐģĐžĐŊ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊĐ¸Ņ", "unable_to_empty_trash": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋŅ€ĐžŅĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", "unable_to_set_feature_photo": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅŽ ĐŊа ОйĐģĐžĐļĐē҃", "unable_to_set_profile_picture": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ„ĐžŅ‚Đž ĐŋŅ€ĐžŅ„Đ¸ĐģŅ", + "unable_to_set_rating": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ŅƒŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "unable_to_submit_job": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‚ĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ĐˇĐ°Đ´Đ°Ņ‡Ņƒ ĐŊа Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊиĐĩ", "unable_to_trash_asset": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐēŅ‚ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", "unable_to_unlink_account": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‚ŅĐžĐĩдиĐŊĐ¸Ņ‚ŅŒ ŅƒŅ‡Ņ‘Ņ‚ĐŊŅƒŅŽ СаĐŋĐ¸ŅŅŒ", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēи", "unable_to_update_timeline_display_status": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ иСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐĩĐŊĐ¸Ņ ĐŊа ҈ĐēаĐģĐĩ Đ˛Ņ€ĐĩĐŧĐĩĐŊи", "unable_to_update_user": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅ", + "unable_to_update_workflow": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ОйĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", "unable_to_upload_file": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Ņ„Đ°ĐšĐģ" }, + "errors_text": "ĐžŅˆĐ¸ĐąĐēи", "exclusion_pattern": "ШайĐģĐžĐŊŅ‹ Đ¸ŅĐēĐģŅŽŅ‡ĐĩĐŊиК", "exif": "Exif", "exif_bottom_sheet_description": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ ĐžĐŋĐ¸ŅĐ°ĐŊиĐĩ...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "ЛЮДИ", "exif_bottom_sheet_person_add_person": "Đ”ĐžĐąĐ°Đ˛Đ¸Ņ‚ŅŒ иĐŧŅ", "exit_slideshow": "Đ’Ņ‹ĐšŅ‚Đ¸ иС ҁĐģаКд-ŅˆĐžŅƒ", + "expand": "РаСвĐĩŅ€ĐŊŅƒŅ‚ŅŒ", "expand_all": "РаСвĐĩŅ€ĐŊŅƒŅ‚ŅŒ Đ˛ŅŅ‘", "experimental_settings_new_asset_list_subtitle": "В Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚ĐēĐĩ", "experimental_settings_new_asset_list_title": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊŅƒŅŽ ҁĐĩŅ‚Đē҃ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš", @@ -1120,14 +1208,17 @@ "features": "ДоĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đĩ вОСĐŧĐžĐļĐŊĐžŅŅ‚Đ¸", "features_in_development": "Đ¤ŅƒĐŊĐēŅ†Đ¸Đ¸ в Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚ĐēĐĩ", "features_setting_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ Đ´ĐžĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đŧи вОСĐŧĐžĐļĐŊĐžŅŅ‚ŅĐŧи ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ", - "file_name": "ИĐŧŅ Ņ„Đ°ĐšĐģа", "file_name_or_extension": "ИĐŧŅ Ņ„Đ°ĐšĐģа иĐģи Ņ€Đ°ŅŅˆĐ¸Ņ€ĐĩĐŊиĐĩ", + "file_name_text": "ИĐŧŅ Ņ„Đ°ĐšĐģа", + "file_name_with_value": "ИĐŧŅ Ņ„Đ°ĐšĐģа: {file_name}", "file_size": "РаСĐŧĐĩŅ€ Ņ„Đ°ĐšĐģа", "filename": "ИĐŧŅ Ņ„Đ°ĐšĐģа", "filetype": "ĐĸиĐŋ Ņ„Đ°ĐšĐģа", "filter": "ФиĐģŅŒŅ‚Ņ€", + "filter_description": "ĐŖŅĐģĐžĐ˛Đ¸Ņ ĐžŅ‚ĐąĐžŅ€Đ° ҆ĐĩĐģĐĩĐ˛Ņ‹Ņ… ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", "filter_people": "ФиĐģŅŒŅ‚Ņ€ ĐŋĐž ĐģŅŽĐ´ŅĐŧ", "filter_places": "ФиĐģŅŒŅ‚Ņ€ ĐŋĐž ĐŧĐĩŅŅ‚Đ°Đŧ", + "filters": "ФиĐģŅŒŅ‚Ņ€Ņ‹", "find_them_fast": "Đ‘Ņ‹ŅŅ‚Ņ€Đž ĐŊĐ°ĐšĐ´Đ¸Ņ‚Đĩ Đ¸Ņ… ĐŋĐž иĐŧĐĩĐŊи ҁ ĐŋĐžĐŧĐžŅ‰ŅŒŅŽ ĐŋĐžĐ¸ŅĐēа", "first": "ПĐĩŅ€Đ˛Ņ‹Đš", "fix_incorrect_match": "Đ˜ŅĐŋŅ€Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐžĐĩ ŅĐžĐžŅ‚Đ˛ĐĩŅ‚ŅŅ‚Đ˛Đ¸Đĩ", @@ -1137,12 +1228,16 @@ "folders_feature_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ ĐŋаĐŋĐžĐē ҁ Ņ„ĐžŅ‚Đž и видĐĩĐž в Ņ„Đ°ĐšĐģОвОК ŅĐ¸ŅŅ‚ĐĩĐŧĐĩ", "forgot_pin_code_question": "Đ—Đ°ĐąŅ‹Đģи PIN-ĐēОд?", "forward": "ВĐŋĐĩŅ€Ņ‘Đ´", + "free_up_space": "ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚Đž", + "free_up_space_description": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ҁĐēĐžĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊŅ‹Đĩ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Ņ„ĐžŅ‚Đž и видĐĩĐž в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ° Đ´ĐģŅ ĐžŅĐ˛ĐžĐąĐžĐļĐ´ĐĩĐŊĐ¸Ņ ĐŧĐĩŅŅ‚Đ°. КоĐŋии ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€Đĩ ĐžŅŅ‚Đ°ĐŊŅƒŅ‚ŅŅ ĐŊĐĩŅ‚Ņ€ĐžĐŊŅƒŅ‚Ņ‹Đŧи.", + "free_up_space_settings_subtitle": "ĐžŅĐ˛ĐžĐąĐžĐ´Đ¸Ņ‚ŅŒ ĐŧĐĩŅŅ‚Đž ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "full_path": "ПоĐģĐŊŅ‹Đš ĐŋŅƒŅ‚ŅŒ: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "ДĐģŅ Ņ€Đ°ĐąĐžŅ‚Ņ‹ ҂ҀĐĩĐąŅƒĐĩŅ‚ŅŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēа вĐŊĐĩ҈ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅĐžĐ˛ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐ˛ Google.", "general": "ĐžĐąŅ‰Đ¸Đĩ", "geolocation_instruction_location": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐžĐąŅŠĐĩĐēŅ‚ ҁ иĐŧĐĩŅŽŅ‰Đ¸ĐŧĐ¸ŅŅ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ°Đŧи, Ņ‡Ņ‚ĐžĐąŅ‹ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ Đ¸Ņ…, ĐģийО Đ˛Ņ€ŅƒŅ‡ĐŊŅƒŅŽ ҃ĐēаĐļĐ¸Ņ‚Đĩ ĐŧĐĩŅŅ‚Đž ĐŊа ĐēĐ°Ņ€Ņ‚Đĩ", "get_help": "ПоĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ĐŋĐžĐŧĐžŅ‰ŅŒ", + "get_people_error": "ĐžŅˆĐ¸ĐąĐēа ĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐ¸Ņ ĐģŅŽĐ´ĐĩĐš", "get_wifiname_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ иĐŧŅ Wi-Fi ҁĐĩŅ‚Đ¸. ĐŖĐąĐĩĐ´Đ¸Ņ‚ĐĩҁҌ, Ņ‡Ņ‚Đž Đ˛Ņ‹ ĐŋОдĐēĐģŅŽŅ‡ĐĩĐŊŅ‹ Đē ҁĐĩŅ‚Đ¸ и ĐŋŅ€ĐĩĐ´ĐžŅŅ‚Đ°Đ˛Đ¸Đģи ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸ŅŽ ĐŊĐĩĐžĐąŅ…ĐžĐ´Đ¸ĐŧŅ‹Đĩ Ņ€Đ°ĐˇŅ€Đĩ҈ĐĩĐŊĐ¸Ņ", "getting_started": "ĐĄŅ‚Đ°Ņ€Ņ‚", "go_back": "Назад", @@ -1175,6 +1270,7 @@ "hide_named_person": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ {name}", "hide_password": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", "hide_person": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", + "hide_schema": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ ҁ҅ĐĩĐŧ҃", "hide_text_recognition": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ Ņ€Đ°ŅĐŋОСĐŊаĐŊĐŊŅ‹Đš Ņ‚ĐĩĐēҁ҂", "hide_unnamed_people": "ĐĄĐēŅ€Ņ‹Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš ĐąĐĩС иĐŧĐĩĐŊи", "home_page_add_to_album_conflicts": "ДобавĐģĐĩĐŊĐž {added} ĐŧĐĩдиа в аĐģŅŒĐąĐžĐŧ {album}. {failed} ĐŧĐĩдиа ҃ĐļĐĩ в аĐģŅŒĐąĐžĐŧĐĩ.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "ĐžĐąŅ€Đ°ĐąĐžŅ‚Đēа СаĐŋŅƒŅ‰ĐĩĐŊа {dateTime}", "items_count": "{count, plural, one {# ŅĐģĐĩĐŧĐĩĐŊŅ‚} many {# ŅĐģĐĩĐŧĐĩĐŊŅ‚ĐžĐ˛} other {# ŅĐģĐĩĐŧĐĩĐŊŅ‚Đ°}}", "jobs": "Đ—Đ°Đ´Đ°Ņ‡Đ¸", + "json_editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€ JSON", + "json_error": "ĐžŅˆĐ¸ĐąĐēа JSON", "keep": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ", + "keep_albums": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧŅ‹", + "keep_albums_count": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ {count, plural, one {# аĐģŅŒĐąĐžĐŧ} many {# аĐģŅŒĐąĐžĐŧОв} other {# аĐģŅŒĐąĐžĐŧа}}", "keep_all": "ĐĄĐžŅ…Ņ€Đ°ĐŊĐ¸Ņ‚ŅŒ Đ˛ŅĐĩ", + "keep_description": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ, Ņ‡Ņ‚Đž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ ĐŋŅ€Đ¸ ĐžŅĐ˛ĐžĐąĐžĐļĐ´ĐĩĐŊии ĐŧĐĩŅŅ‚Đ°.", + "keep_favorites": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ", + "keep_on_device": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", + "keep_on_device_hint": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐžĐąŅŠĐĩĐē҂ҋ, ĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ ĐŊ҃ĐļĐŊĐž ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "keep_this_delete_others": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚, ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ĐžŅŅ‚Đ°ĐģҌĐŊŅ‹Đĩ", + "keeping": "ĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ: {items}", "kept_this_deleted_others": "ĐĄĐžŅ…Ņ€Đ°ĐŊŅ‘ĐŊ ŅŅ‚ĐžŅ‚ ĐžĐąŅŠĐĩĐēŅ‚ и {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ŅƒĐ´Đ°ĐģŅ‘ĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ŅƒĐ´Đ°ĐģĐĩĐŊĐž} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ŅƒĐ´Đ°ĐģĐĩĐŊĐž}}", "keyboard_shortcuts": "ĐĄĐžŅ‡ĐĩŅ‚Đ°ĐŊĐ¸Ņ ĐēĐģĐ°Đ˛Đ¸Ņˆ", "language": "Đ¯ĐˇŅ‹Đē", @@ -1343,10 +1448,28 @@ "loop_videos_description": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēиК ĐŋĐžĐ˛Ņ‚ĐžŅ€ видĐĩĐž ĐŋŅ€Đ¸ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đĩ.", "main_branch_warning": "Đ’Ņ‹ Đ¸ŅĐŋĐžĐģŅŒĐˇŅƒĐĩŅ‚Đĩ вĐĩŅ€ŅĐ¸ŅŽ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ Đ´ĐģŅ Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Đēи. ĐĐ°ŅŅ‚ĐžŅŅ‚ĐĩĐģҌĐŊĐž Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒĐĩŅ‚ŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ ĐŊа Ņ€ĐĩĐģиСĐŊŅƒŅŽ вĐĩŅ€ŅĐ¸ŅŽ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ!", "main_menu": "ГĐģавĐŊĐžĐĩ ĐŧĐĩĐŊŅŽ", + "maintenance_action_restore": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", "maintenance_description": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ Immich ĐŋĐĩŅ€ĐĩвĐĩĐ´Ņ‘ĐŊ в Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ.", "maintenance_end": "ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ", "maintenance_end_error": "НĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ ĐžŅ‚ĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģ҃ĐļиваĐŊĐ¸Ņ.", "maintenance_logged_in_as": "В ĐŊĐ°ŅŅ‚ĐžŅŅ‰ĐĩĐĩ Đ˛Ņ€ĐĩĐŧŅ Đ˛Ņ‹ Đ˛ĐžŅˆĐģи в ŅĐ¸ŅŅ‚ĐĩĐŧ҃ ĐēаĐē {user}", + "maintenance_restore_from_backup": "Đ’ĐžŅŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ иС Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии", + "maintenance_restore_library": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ йийĐģĐ¸ĐžŅ‚ĐĩĐēи", + "maintenance_restore_library_confirm": "Đ•ŅĐģи Đ˛ŅŅ‘ Đ˛Ņ‹ĐŗĐģŅĐ´Đ¸Ņ‚ ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐž, ĐŊĐ°Ņ‡Đ¸ĐŊĐ°ĐšŅ‚Đĩ Đ˛ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ иС Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии!", + "maintenance_restore_library_description": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅", + "maintenance_restore_library_folder_has_files": "{folder} ŅĐžĐ´ĐĩŅ€ĐļĐ¸Ņ‚ {count} ĐŋаĐŋĐžĐē", + "maintenance_restore_library_folder_no_files": "В ĐŋаĐŋĐēĐĩ {folder} ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ Ņ„Đ°ĐšĐģŅ‹!", + "maintenance_restore_library_folder_pass": "Đ´ĐžŅŅ‚ŅƒĐŋ Đ´ĐģŅ ҇҂ĐĩĐŊĐ¸Ņ и СаĐŋĐ¸ŅĐ¸", + "maintenance_restore_library_folder_read_fail": "ĐŊĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋа ĐŊа ҇҂ĐĩĐŊиĐĩ", + "maintenance_restore_library_folder_write_fail": "ĐŊĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋа Đ´ĐģŅ СаĐŋĐ¸ŅĐ¸", + "maintenance_restore_library_hint_missing_files": "ВозĐŧĐžĐļĐŊĐž ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ ваĐļĐŊŅ‹Đĩ Ņ„Đ°ĐšĐģŅ‹", + "maintenance_restore_library_hint_regenerate_later": "МоĐļĐŊĐž ĐąŅƒĐ´ĐĩŅ‚ ĐŋĐžŅ‚ĐžĐŧ СаĐŊОвО ŅĐŗĐĩĐŊĐĩŅ€Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ в ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēĐ°Ņ…", + "maintenance_restore_library_hint_storage_template_missing_files": "Đ˜ŅĐŋĐžĐģŅŒĐˇŅƒĐĩŅ‚Đĩ ŅˆĐ°ĐąĐģĐžĐŊ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ°? ВозĐŧĐžĐļĐŊĐž, ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ ĐŊĐĩĐēĐžŅ‚ĐžŅ€Ņ‹Đĩ Ņ„Đ°ĐšĐģŅ‹.", + "maintenance_restore_library_loading": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐŋŅ€ĐžĐ˛ĐĩŅ€ĐžĐē ҆ĐĩĐģĐžŅŅ‚ĐŊĐžŅŅ‚Đ¸ и ŅĐ˛Ņ€Đ¸ŅŅ‚Đ¸Ņ‡ĐĩҁĐēĐ¸Ņ… аĐģĐŗĐžŅ€Đ¸Ņ‚ĐŧОвâ€Ļ", + "maintenance_task_backup": "ХОСдаĐŊиĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋии ŅŅƒŅ‰ĐĩŅŅ‚Đ˛ŅƒŅŽŅ‰ĐĩĐš ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅â€Ļ", + "maintenance_task_migrations": "ĐœĐ¸ĐŗŅ€Đ°Ņ†Đ¸Ņ ĐąĐ°ĐˇŅ‹ даĐŊĐŊҋ҅â€Ļ", + "maintenance_task_restore": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊОК Ņ€ĐĩСĐĩŅ€Đ˛ĐŊОК ĐēĐžĐŋииâ€Ļ", + "maintenance_task_rollback": "Đ’ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊиĐĩ ĐŊĐĩ ŅƒĐ´Đ°ĐģĐžŅŅŒ, ĐžŅ‚ĐēĐ°Ņ‚ Đē Ņ‚ĐžŅ‡ĐēĐĩ Đ˛ĐžŅŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐ¸Ņâ€Ļ", "maintenance_title": "Đ’Ņ€ĐĩĐŧĐĩĐŊĐŊĐž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐž", "make": "ĐŸŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ĐĩĐģҌ", "manage_geolocation": "ĐŖĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ĐŧĐĩŅŅ‚Đ°Đŧи ŅŅŠŅ‘ĐŧĐēи", @@ -1408,6 +1531,8 @@ "minimize": "МиĐŊиĐŧĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "minute": "МиĐŊŅƒŅ‚Đ°", "minutes": "МиĐŊŅƒŅ‚Ņ‹", + "mirror_horizontal": "Đ“ĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊĐž", + "mirror_vertical": "ВĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊĐž", "missing": "ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‰Đ¸Đĩ", "mobile_app": "МобиĐģҌĐŊĐžĐĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ", "mobile_app_download_onboarding_note": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚Đĩ ĐŧОйиĐģҌĐŊĐžĐĩ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊиĐĩ Immich ĐģŅŽĐąŅ‹Đŧ иС ҁĐģĐĩĐ´ŅƒŅŽŅ‰Đ¸Ņ… ҁĐŋĐžŅĐžĐąĐžĐ˛", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "ДоĐŋĐžĐģĐŊĐ¸Ņ‚ĐĩĐģҌĐŊŅ‹Đĩ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ", "move": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ", + "move_down": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ вĐŊиС", "move_off_locked_folder": "ĐŖĐąŅ€Đ°Ņ‚ŅŒ иС ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēи", "move_to": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в", + "move_to_device_trash": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đ°", "move_to_lock_folder_action_prompt": "ĐžĐąŅŠĐĩĐē҂ҋ дОйавĐģĐĩĐŊŅ‹ в ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃ ({count} ŅˆŅ‚.)", "move_to_locked_folder": "В ĐģĐ¸Ņ‡ĐŊŅƒŅŽ ĐŋаĐŋĐē҃", "move_to_locked_folder_confirmation": "Đ­Ņ‚Đ¸ Ņ„ĐžŅ‚Đž и видĐĩĐž ĐąŅƒĐ´ŅƒŅ‚ ŅƒĐ´Đ°ĐģĐĩĐŊŅ‹ иС Đ˛ŅĐĩŅ… аĐģŅŒĐąĐžĐŧОв и ĐąŅƒĐ´ŅƒŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ‹ Ņ‚ĐžĐģҌĐēĐž в ĐģĐ¸Ņ‡ĐŊОК ĐŋаĐŋĐēĐĩ", + "move_up": "ПĐĩŅ€ĐĩĐŧĐĩŅŅ‚Đ¸Ņ‚ŅŒ ĐŊавĐĩҀ҅", "moved_to_archive": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊŅ‹} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊŅ‹}} в Đ°Ņ€Ņ…Đ¸Đ˛", "moved_to_library": "{count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ґĐŊ} many {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊŅ‹} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ° ĐŋĐĩŅ€ĐĩĐŧĐĩ҉ĐĩĐŊŅ‹}} в йийĐģĐ¸ĐžŅ‚ĐĩĐē҃", "moved_to_trash": "ПĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐž в ĐēĐžŅ€ĐˇĐ¸ĐŊ҃", @@ -1430,6 +1558,7 @@ "my_albums": "Мои аĐģŅŒĐąĐžĐŧŅ‹", "name": "ИĐŧŅ", "name_or_nickname": "ИĐŧŅ иĐģи ĐŊиĐē", + "name_required": "ИĐŧŅ ĐžĐąŅĐˇĐ°Ņ‚ĐĩĐģҌĐŊĐž Đ´ĐģŅ СаĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ", "navigate": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸", "navigate_to_time": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đē Đ´Đ°Ņ‚Đĩ", "network_requirement_photos_upload": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ ĐŧОйиĐģҌĐŊŅ‹Đš иĐŊŅ‚ĐĩŅ€ĐŊĐĩŅ‚ Đ´ĐģŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи Ņ„ĐžŅ‚Đž", @@ -1454,20 +1583,24 @@ "next": "ДаĐģĐĩĐĩ", "next_memory": "ĐĄĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐĩ Đ˛ĐžŅĐŋĐžĐŧиĐŊаĐŊиĐĩ", "no": "НĐĩŅ‚", + "no_actions_added": "ДĐĩĐšŅŅ‚Đ˛Đ¸Đš ĐŋĐžĐēа ĐŊĐĩ дОйавĐģĐĩĐŊĐž", + "no_albums_found": "АĐģŅŒĐąĐžĐŧОв ĐŊĐĩ ĐŊаКдĐĩĐŊĐž", "no_albums_message": "ĐĄĐžĐˇĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ аĐģŅŒĐąĐžĐŧŅ‹ Đ´ĐģŅ ŅĐ¸ŅŅ‚ĐĩĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Đ¸Đ¸ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš и видĐĩĐž", "no_albums_with_name_yet": "ĐŸĐžŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ĐŋĐžĐēа ĐŊĐĩŅ‚ аĐģŅŒĐąĐžĐŧОв ҁ Ņ‚Đ°ĐēиĐŧ ĐŊаСваĐŊиĐĩĐŧ.", "no_albums_yet": "ĐŸĐžŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ĐŋĐžĐēа ĐŊĐĩŅ‚ аĐģŅŒĐąĐžĐŧОв.", "no_archived_assets_message": "ĐŅ€Ņ…Đ¸Đ˛Đ¸Ņ€ŅƒĐšŅ‚Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸ и видĐĩĐž, Ņ‡Ņ‚ĐžĐąŅ‹ ҁĐēŅ€Ņ‹Ņ‚ŅŒ Đ¸Ņ… ĐŋŅ€Đ¸ ĐžĐąŅ‰ĐĩĐŧ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đĩ", - "no_assets_message": "НАЖМИĐĸЕ Đ”Đ›Đ¯ Đ—ĐĐ“Đ ĐŖĐ—ĐšĐ˜ ВАШЕГО ПЕРВОГО ФОĐĸО", + "no_assets_message": "НаĐļĐŧĐ¸Ņ‚Đĩ Đ´ĐģŅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи Đ˛Đ°ŅˆĐĩĐŗĐž ĐŋĐĩŅ€Đ˛ĐžĐŗĐž Ņ„ĐžŅ‚Đž", "no_assets_to_show": "МĐĩдиа ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚", "no_cast_devices_found": "НĐĩ ĐŊаКдĐĩĐŊĐž ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛ Đ´ĐģŅ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Đ¸Đ¸", "no_checksum_local": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊŅ‹Đĩ ҁ҃ĐŧĐŧŅ‹ ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ - ĐŊĐĩвОСĐŧĐžĐļĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ", "no_checksum_remote": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊŅ‹Đĩ ҁ҃ĐŧĐŧŅ‹ ĐžŅ‚ŅŅƒŅ‚ŅŅ‚Đ˛ŅƒŅŽŅ‚ - ĐŊĐĩвОСĐŧĐžĐļĐŊĐž ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐē҂ҋ ҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "no_configuration_needed": "КоĐŊŅ„Đ¸ĐŗŅƒŅ€Đ°Ņ†Đ¸Ņ ĐŊĐĩ ҂ҀĐĩĐąŅƒĐĩŅ‚ŅŅ", "no_devices": "НĐĩŅ‚ Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊĐŊҋ҅ ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛", "no_duplicates_found": "Đ”ŅƒĐąĐģиĐēĐ°Ņ‚ĐžĐ˛ ĐŊĐĩ ОйĐŊĐ°Ņ€ŅƒĐļĐĩĐŊĐž.", "no_exif_info_available": "НĐĩŅ‚ Đ´ĐžŅŅ‚ŅƒĐŋĐŊОК иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Đ¸ exif", "no_explore_results_message": "Đ—Đ°ĐŗŅ€ŅƒĐļĐ°ĐšŅ‚Đĩ йОĐģҌ҈Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš, Ņ‡Ņ‚ĐžĐąŅ‹ ĐŊĐ°ŅĐģаĐļĐ´Đ°Ņ‚ŅŒŅŅ Đ˛Đ°ŅˆĐĩĐš ĐēĐžĐģĐģĐĩĐēŅ†Đ¸ĐĩĐš.", "no_favorites_message": "ДобавĐģŅĐšŅ‚Đĩ ĐžĐąŅŠĐĩĐē҂ҋ в Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ, Ņ‡Ņ‚ĐžĐąŅ‹ ĐąŅ‹ŅŅ‚Ņ€ĐĩĐĩ ĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚ŅŒ ŅĐ˛ĐžĐ¸ ĐģŅƒŅ‡ŅˆĐ¸Đĩ Ņ„ĐžŅ‚Đž и видĐĩĐž", + "no_filters_added": "ФиĐģŅŒŅ‚Ņ€ĐžĐ˛ ĐŋĐžĐēа ĐŊĐĩ дОйавĐģĐĩĐŊĐž", "no_libraries_message": "ĐĄĐžĐˇĐ´Đ°ĐšŅ‚Đĩ вĐŊĐĩ҈ĐŊŅŽŅŽ йийĐģĐ¸ĐžŅ‚ĐĩĐē҃ Đ´ĐģŅ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ° в Immich ŅŅ‚ĐžŅ€ĐžĐŊĐŊĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đš и видĐĩĐž", "no_local_assets_found": "На ŅƒŅŅ‚Ņ€ĐžĐšŅŅ‚Đ˛Đĩ ĐŊĐĩ ĐŊаКдĐĩĐŊĐž ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ҁ Ņ‚Đ°ĐēОК ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊОК ҁ҃ĐŧĐŧОК", "no_location_set": "МĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ ĐŊĐĩ ŅƒŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž", @@ -1481,11 +1614,11 @@ "no_results_description": "ПоĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ Đ¸ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ ŅĐ¸ĐŊĐžĐŊиĐŧŅ‹ иĐģи йОĐģĐĩĐĩ ĐžĐąŅ‰Đ¸Đĩ ҁĐģОва", "no_shared_albums_message": "ĐĄĐžĐˇĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ аĐģŅŒĐąĐžĐŧŅ‹ Đ´ĐģŅ ОйĐŧĐĩĐŊа Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅĐŧи и видĐĩОСаĐŋĐ¸ŅŅĐŧи ҁ ĐģŅŽĐ´ŅŒĐŧи в Đ˛Đ°ŅˆĐĩĐš ҁĐĩŅ‚Đ¸", "no_uploads_in_progress": "НĐĩŅ‚ аĐēŅ‚Đ¸Đ˛ĐŊҋ҅ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐžĐē", + "none": "ĐĐ¸Ņ‡ĐĩĐŗĐž", "not_allowed": "ЗаĐŋŅ€Đĩ҉ĐĩĐŊĐž", "not_available": "НĐĩŅ‚ даĐŊĐŊҋ҅", "not_in_any_album": "Ни в ОдĐŊĐžĐŧ аĐģŅŒĐąĐžĐŧĐĩ", "not_selected": "НĐĩ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐž", - "note_apply_storage_label_to_previously_uploaded assets": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ: Đ§Ņ‚ĐžĐąŅ‹ ĐŋŅ€Đ¸ĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐĩŅ‚Đē҃ Ņ…Ņ€Đ°ĐŊиĐģĐ¸Ņ‰Đ° Đē Ņ€Đ°ĐŊĐĩĐĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đŧ ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧ, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đĩ", "notes": "ĐŸŅ€Đ¸ĐŧĐĩŅ‡Đ°ĐŊиĐĩ", "nothing_here_yet": "ЗдĐĩҁҌ ĐŋĐžĐēа ĐŊĐ¸Ņ‡ĐĩĐŗĐž ĐŊĐĩŅ‚", "notification_permission_dialog_content": "Đ§Ņ‚ĐžĐąŅ‹ вĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ŅƒĐ˛ĐĩĐ´ĐžĐŧĐģĐĩĐŊĐ¸Ņ, ĐŋĐĩŅ€ĐĩĐšĐ´Đ¸Ņ‚Đĩ в ÂĢĐĐ°ŅŅ‚Ņ€ĐžĐšĐēиÂģ и Đ˛Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ÂĢĐ Đ°ĐˇŅ€ĐĩŅˆĐ¸Ņ‚ŅŒÂģ.", @@ -1515,6 +1648,7 @@ "online": "Đ”ĐžŅŅ‚ŅƒĐŋĐĩĐŊ", "only_favorites": "ĐĸĐžĐģҌĐēĐž Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ", "open": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ", + "open_calendar": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ ĐēаĐģĐĩĐŊĐ´Đ°Ņ€ŅŒ", "open_in_map_view": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ в Ņ€ĐĩĐļиĐŧĐĩ ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Đ° ĐēĐ°Ņ€Ņ‚Ņ‹", "open_in_openstreetmap": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ в OpenStreetMap", "open_the_search_filters": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ Ņ„Đ¸ĐģŅŒŅ‚Ņ€Ņ‹ ĐŋĐžĐ¸ŅĐēа", @@ -1563,6 +1697,7 @@ "people": "Đ›ŅŽĐ´Đ¸", "people_edits_count": "{count, plural, one {ИСĐŧĐĩĐŊŅ‘ĐŊ # ҇ĐĩĐģОвĐĩĐē} many {ИСĐŧĐĩĐŊĐĩĐŊĐž # ҇ĐĩĐģОвĐĩĐē} other {ИСĐŧĐĩĐŊĐĩĐŊĐž # ҇ĐĩĐģОвĐĩĐēа}}", "people_feature_description": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€ Ņ„ĐžŅ‚Đž и видĐĩĐž, ŅĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐŊҋ҅ ĐŋĐž ĐģŅŽĐ´ŅĐŧ", + "people_selected": "{count, plural, one {Đ’Ņ‹ĐąŅ€Đ°ĐŊ # ҇ĐĩĐģОвĐĩĐē} many {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # ҇ĐĩĐģОвĐĩĐē} other {Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž # ҇ĐĩĐģОвĐĩĐēа}}", "people_sidebar_description": "ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°Ņ‚ŅŒ Đŋ҃ĐŊĐēŅ‚ ĐŧĐĩĐŊŅŽ \"Đ›ŅŽĐ´Đ¸\" в йОĐēОвОК ĐŋаĐŊĐĩĐģи", "permanent_deletion_warning": "ĐŸŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´ĐĩĐŊиĐĩ Ой ŅƒĐ´Đ°ĐģĐĩĐŊии", "permanent_deletion_warning_setting_description": "ĐŸŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´Đ°Ņ‚ŅŒ ĐŋĐĩŅ€ĐĩĐ´ ĐąĐĩĐˇĐ˛ĐžĐˇĐ˛Ņ€Đ°Ņ‚ĐŊŅ‹Đŧ ŅƒĐ´Đ°ĐģĐĩĐŊиĐĩĐŧ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, one {# ĐŗĐžĐ´} many {# ĐģĐĩŅ‚} other {# ĐŗĐžĐ´Đ°}}", "person_birthdate": "Đ”Đ°Ņ‚Đ° Ņ€ĐžĐļĐ´ĐĩĐŊĐ¸Ņ: {date}", "person_hidden": "{name}{hidden, select, true { (ҁĐēҀҋ҂)} other {}}", + "person_recognized": "ЧĐĩĐģОвĐĩĐē Ņ€Đ°ŅĐŋОСĐŊаĐŊ", + "person_selected": "ЧĐĩĐģОвĐĩĐē Đ˛Ņ‹ĐąŅ€Đ°ĐŊ", "photo_shared_all_users": "ĐŸĐžŅ…ĐžĐļĐĩ, Ņ‡Ņ‚Đž Đ˛Ņ‹ ĐŋОдĐĩĐģиĐģĐ¸ŅŅŒ ŅĐ˛ĐžĐ¸Đŧи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸ŅĐŧи ŅĐž Đ˛ŅĐĩĐŧи ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģŅĐŧи иĐģи ҃ Đ˛Đ°Ņ ĐŊĐĩŅ‚ ĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ĐĩĐģĐĩĐš, ҁ ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи ĐŧĐžĐļĐŊĐž ĐŋОдĐĩĐģĐ¸Ņ‚ŅŒŅŅ.", "photos": "Đ¤ĐžŅ‚Đž", "photos_and_videos": "Đ¤ĐžŅ‚Đž и видĐĩĐž", "photos_count": "{count, plural, one {{count, number} Ņ„ĐžŅ‚Đž} other {{count, number} Ņ„ĐžŅ‚Đž}}", "photos_from_previous_years": "Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸ ĐŋŅ€ĐžŅˆĐģҋ҅ ĐģĐĩŅ‚ в ŅŅ‚ĐžŅ‚ Đ´ĐĩĐŊҌ", + "photos_only": "ĐĸĐžĐģҌĐēĐž Ņ„ĐžŅ‚Đž", "pick_a_location": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ĐŧĐĩŅŅ‚ĐžĐŋĐžĐģĐžĐļĐĩĐŊиĐĩ", "pick_custom_range": "ĐŸŅ€ĐžĐ¸ĐˇĐ˛ĐžĐģҌĐŊŅ‹Đš ĐŋĐĩŅ€Đ¸ĐžĐ´", "pick_date_range": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐŋĐĩŅ€Đ¸ĐžĐ´", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "КĐģŅŽŅ‡ĐžĐŧ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Đ° ҃ĐŋŅ€Đ°Đ˛ĐģŅĐĩŅ‚ адĐŧиĐŊĐ¸ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "query_asset_id": "ИдĐĩĐŊŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ‚ĐžŅ€ Đ¸ŅŅ…ĐžĐ´ĐŊĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°", "queue_status": "В ĐžŅ‡ĐĩŅ€Đĩди {count}/{total}", + "rate_asset": "ĐŖŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating": "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating_clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚ŅŒ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating_count": "{count, plural, one {# СвĐĩСда} many {# СвĐĩСд} other {# СвĐĩĐˇĐ´Ņ‹}}", "rating_description": "ĐĄĐ¸ŅŅ‚ĐĩĐŧа ĐžŅ†ĐĩĐŊĐēи ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ в ĐŋаĐŊĐĩĐģи иĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Đ¸", + "rating_set": "ĐŖŅŅ‚Đ°ĐŊОвĐģĐĩĐŊ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ {rating, plural, one {# СвĐĩСда} many {# СвĐĩСд} other {# СвĐĩĐˇĐ´Ņ‹}}", "reaction_options": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ ҁ ĐžŅ‚ĐŧĐĩŅ‚ĐēОК", "read_changelog": "Đ˜ŅŅ‚ĐžŅ€Đ¸Ņ Ņ€ĐĩĐģиСОв", "readonly_mode_disabled": "Đ ĐĩĐļиĐŧ ÂĢŅ‚ĐžĐģҌĐēĐž ĐŋŅ€ĐžŅĐŧĐžŅ‚Ņ€Âģ ĐžŅ‚ĐēĐģŅŽŅ‡Ņ‘ĐŊ", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "Đ›Đ¸Ņ†Đ° ĐŊа {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚Đĩ} other {# ĐžĐąŅŠĐĩĐēŅ‚Đ°Ņ…}} ĐŋĐĩŅ€ĐĩĐŊаСĐŊĐ°Ņ‡ĐĩĐŊŅ‹ ĐŊа ĐŊĐžĐ˛ĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", "reassing_hint": "НазĐŊĐ°Ņ‡Đ¸Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ ҃ĐēаСаĐŊĐŊĐžĐŧ҃ ҇ĐĩĐģОвĐĩĐē҃", "recent": "НĐĩдавĐŊиĐĩ", - "recent-albums": "НĐĩдавĐŊиĐĩ аĐģŅŒĐąĐžĐŧŅ‹", + "recent_albums": "НĐĩдавĐŊиĐĩ аĐģŅŒĐąĐžĐŧŅ‹", "recent_searches": "НĐĩдавĐŊиĐĩ ĐŋĐžĐ¸ŅĐēĐžĐ˛Ņ‹Đĩ СаĐŋŅ€ĐžŅŅ‹", "recently_added": "НĐĩдавĐŊĐž дОйавĐģĐĩĐŊĐŊŅ‹Đĩ", "recently_added_page_title": "НĐĩдавĐŊĐž дОйавĐģĐĩĐŊĐŊŅ‹Đĩ", @@ -1770,9 +1910,11 @@ "saved_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊŅ‹", "say_something": "НаĐŋĐ¸ŅˆĐ¸Ņ‚Đĩ Ņ‡Ņ‚Đž-ĐŊĐ¸ĐąŅƒĐ´ŅŒ", "scaffold_body_error_occurred": "ВозĐŊиĐēĐģа ĐžŅˆĐ¸ĐąĐēа", + "scan": "ĐŸĐžĐ¸ŅĐē", "scan_all_libraries": "ĐĄĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ Đ˛ŅĐĩ йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "scan_library": "ĐĄĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "scan_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ҁĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐ¸Ņ", + "scanning": "ĐŸĐžĐ¸ŅĐē ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛", "scanning_for_album": "ĐĄĐēаĐŊĐ¸Ņ€ĐžĐ˛Đ°ĐŊиĐĩ аĐģŅŒĐąĐžĐŧа...", "search": "ĐŸĐžĐ¸ŅĐē", "search_albums": "ĐŸĐžĐ¸ŅĐē аĐģŅŒĐąĐžĐŧОв", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ Ņ‚Đ¸Đŋ ĐŧĐĩдиа", "search_filter_ocr": "ĐŸĐžĐ¸ŅĐē Ņ‚ĐĩĐēŅŅ‚Đ°", "search_filter_people_title": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐģŅŽĐ´ĐĩĐš", + "search_filter_star_rating": "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "search_for": "ĐŸĐžĐ¸ŅĐē ĐŋĐž", "search_for_existing_person": "ĐŸĐžĐ¸ŅĐē ŅŅƒŅ‰ĐĩŅŅ‚Đ˛ŅƒŅŽŅ‰ĐĩĐŗĐž ҇ĐĩĐģОвĐĩĐēа", "search_no_more_result": "БоĐģҌ҈Đĩ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚ĐžĐ˛ ĐŊĐĩŅ‚", @@ -1836,17 +1979,23 @@ "second": "ĐĄĐĩĐē҃ĐŊда", "see_all_people": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ Đ˛ŅĐĩŅ… ĐģŅŽĐ´ĐĩĐš", "select": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ", + "select_album": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ аĐģŅŒĐąĐžĐŧ", "select_album_cover": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ОйĐģĐžĐļĐē҃ аĐģŅŒĐąĐžĐŧа", + "select_albums": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ аĐģŅŒĐąĐžĐŧŅ‹", "select_all": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ", "select_all_duplicates": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ", "select_all_in": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ в {group}", "select_avatar_color": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ Ņ†Đ˛ĐĩŅ‚ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°", + "select_count": "Đ’Ņ‹ĐąŅ€Đ°ĐŊĐž: {count, plural, other {#}}", + "select_cutoff_date": "ĐŖĐēаĐļĐ¸Ņ‚Đĩ Đ´Đ°Ņ‚Ņƒ ĐžŅ‚ŅĐĩ҇ĐĩĐŊĐ¸Ņ", "select_face": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ĐģĐ¸Ņ†Đž", "select_featured_photo": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ¸ĐˇĐąŅ€Đ°ĐŊĐŊĐžĐĩ Ņ„ĐžŅ‚Đž", "select_from_computer": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ ҁ ĐēĐžĐŧĐŋŅŒŅŽŅ‚ĐĩŅ€Đ°", "select_keep_all": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ", "select_library_owner": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ вĐģадĐĩĐģŅŒŅ†Đ° йийĐģĐ¸ĐžŅ‚ĐĩĐēи", "select_new_face": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ´Ņ€ŅƒĐŗĐžĐŗĐž ҇ĐĩĐģОвĐĩĐēа", + "select_people": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ĐģŅŽĐ´ĐĩĐš", + "select_person": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ ҇ĐĩĐģОвĐĩĐēа", "select_person_to_tag": "Đ’Ņ‹Đ´ĐĩĐģĐ¸Ņ‚Đĩ ĐģĐ¸Ņ†Đž ҇ĐĩĐģОвĐĩĐēа, ĐēĐžŅ‚ĐžŅ€ĐžĐŗĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ĐžŅ‚ĐŧĐĩŅ‚Đ¸Ņ‚ŅŒ", "select_photos": "Đ’Ņ‹ĐąĐĩŅ€Đ¸Ņ‚Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Đ¸", "select_trash_all": "Đ’Ņ‹ĐąŅ€Đ°Ņ‚ŅŒ Đ˛ŅĐĩ Đ´ĐģŅ ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸Ņ", @@ -1938,7 +2087,7 @@ "shared_link_edit_expire_after_option_year": "{count} ĐģĐĩŅ‚", "shared_link_edit_password_hint": "Đ—Đ°Ņ‰Đ¸Ņ‚Đ¸Ņ‚Đĩ Đ´ĐžŅŅ‚ŅƒĐŋ ĐŋĐ°Ņ€ĐžĐģĐĩĐŧ", "shared_link_edit_submit_button": "ОбĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ҁҁҋĐģĐē҃", - "shared_link_error_server_url_fetch": "НĐĩвОСĐŧĐžĐļĐŊĐž СаĐŋŅ€ĐžŅĐ¸Ņ‚ŅŒ URL ҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "shared_link_error_server_url_fetch": "НĐĩ ŅƒĐ´Đ°ĐĩŅ‚ŅŅ ĐŋĐžĐģŅƒŅ‡Đ¸Ņ‚ŅŒ URL-Đ°Đ´Ņ€Đĩҁ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "shared_link_expires_day": "Đ˜ŅŅ‚Đĩ҇ґ҂ ҇ĐĩŅ€ĐĩС {count} Đ´ĐĩĐŊҌ", "shared_link_expires_days": "Đ˜ŅŅ‚Đĩ҇ґ҂ ҇ĐĩŅ€ĐĩС {count} Đ´ĐŊĐĩĐš", "shared_link_expires_hour": "Đ˜ŅŅ‚Đĩ҇ґ҂ ҇ĐĩŅ€ĐĩС {count} Ņ‡Đ°Ņ", @@ -1982,10 +2131,11 @@ "show_password": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", "show_person_options": "ДĐĩĐšŅŅ‚Đ˛Đ¸Ņ ҁ ҇ĐĩĐģОвĐĩĐēĐžĐŧ", "show_progress_bar": "ĐžŅ‚ĐžĐąŅ€Đ°ĐļĐ°Ņ‚ŅŒ иĐŊдиĐēĐ°Ņ‚ĐžŅ€ Đ˛Ņ‹ĐŋĐžĐģĐŊĐĩĐŊĐ¸Ņ", + "show_schema": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҁ҅ĐĩĐŧ҃", "show_search_options": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁҋ ĐŋĐžĐ¸ŅĐēа", "show_shared_links": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊŅ‹Đĩ ҁҁҋĐģĐēи", "show_slideshow_transition": "ПĐģавĐŊŅ‹Đš ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´", - "show_supporter_badge": "ЗĐŊĐ°Ņ‡ĐžĐē ĐŋОддĐĩŅ€ĐļĐēи", + "show_supporter_badge": "ЗĐŊĐ°Ņ‡ĐžĐē ҁĐŋĐžĐŊŅĐžŅ€ŅŅ‚Đ˛Đ°", "show_supporter_badge_description": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ СĐŊĐ°Ņ‡ĐžĐē ĐŋОддĐĩŅ€ĐļĐēи", "show_text_recognition": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ Ņ€Đ°ŅĐŋОСĐŊаĐŊĐŊŅ‹Đš Ņ‚ĐĩĐēҁ҂", "show_text_search_menu": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ĐŧĐĩĐŊŅŽ Ņ‚ĐĩĐēŅŅ‚ĐžĐ˛ĐžĐŗĐž ĐŋĐžĐ¸ŅĐēа", @@ -1999,6 +2149,8 @@ "skip_to_folders": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đē ĐŋаĐŋĐēаĐŧ", "skip_to_tags": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đē Ņ‚ĐĩĐŗĐ°Đŧ", "slideshow": "ĐĄĐģаКд-ŅˆĐžŅƒ", + "slideshow_repeat": "Đ—Đ°Ņ†Đ¸ĐēĐģĐ¸Ņ‚ŅŒ ҁĐģаКд-ŅˆĐžŅƒ", + "slideshow_repeat_description": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŅ‚ŅŒ ҁĐģаКд-ŅˆĐžŅƒ ĐŋĐžŅĐģĐĩ ĐĩĐŗĐž ĐžĐēĐžĐŊŅ‡Đ°ĐŊĐ¸Ņ", "slideshow_settings": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēи ҁĐģаКд-ŅˆĐžŅƒ", "sort_albums_by": "ĐĄĐžŅ€Ņ‚Đ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧŅ‹ ĐŋĐž...", "sort_created": "Đ”Đ°Ņ‚Đ° ŅĐžĐˇĐ´Đ°ĐŊĐ¸Ņ", @@ -2038,6 +2190,7 @@ "support": "ПоддĐĩŅ€ĐļĐēа", "support_and_feedback": "ПоддĐĩŅ€ĐļĐēа и ĐžĐąŅ€Đ°Ņ‚ĐŊĐ°Ņ ŅĐ˛ŅĐˇŅŒ", "support_third_party_description": "Đ’Đ°ŅˆĐ° ŅƒŅŅ‚Đ°ĐŊОвĐēа immich ĐąŅ‹Đģа ҃ĐŋаĐēОваĐŊа ŅŅ‚ĐžŅ€ĐžĐŊĐŊиĐŧ Ņ€Đ°ĐˇŅ€Đ°ĐąĐžŅ‚Ņ‡Đ¸ĐēĐžĐŧ. ĐŸŅ€ĐžĐąĐģĐĩĐŧŅ‹, ҁ ĐēĐžŅ‚ĐžŅ€Ņ‹Đŧи Đ˛Ņ‹ ŅŅ‚ĐžĐģĐēĐŊ҃ĐģĐ¸ŅŅŒ, ĐŧĐžĐŗŅƒŅ‚ ĐąŅ‹Ņ‚ŅŒ Đ˛Ņ‹ĐˇĐ˛Đ°ĐŊŅ‹ ŅŅ‚Đ¸Đŧ ĐŋаĐēĐĩŅ‚ĐžĐŧ, ĐŋĐžŅŅ‚ĐžĐŧ҃, ĐŋĐžĐļаĐģŅƒĐšŅŅ‚Đ°, в ĐŋĐĩŅ€Đ˛ŅƒŅŽ ĐžŅ‡ĐĩŅ€ĐĩĐ´ŅŒ ĐžĐąŅ€Đ°Ņ‰Đ°ĐšŅ‚ĐĩҁҌ Đē ĐŊиĐŧ, Đ¸ŅĐŋĐžĐģŅŒĐˇŅƒŅ ҁҁҋĐģĐēи ĐŊиĐļĐĩ.", + "supporter": "ĐĄĐŋĐžĐŊŅĐžŅ€ Immich", "swap_merge_direction": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиĐĩ ҁĐģĐ¸ŅĐŊĐ¸Ņ", "sync": "ХиĐŊŅ…Ņ€.", "sync_albums": "ХиĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧŅ‹", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "ĐĐ°ŅŅ‚Ņ€ĐžĐšĐēа Ņ‚ĐĩĐŧŅ‹ ĐŋŅ€Đ¸ĐģĐžĐļĐĩĐŊĐ¸Ņ", "theme_setting_three_stage_loading_subtitle": "ĐĸŅ€ĐĩŅ…ŅŅ‚Đ°ĐŋĐŊĐ°Ņ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēа ĐŧĐžĐļĐĩŅ‚ ĐŋĐžĐ˛Ņ‹ŅĐ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ĐĩĐģҌĐŊĐžŅŅ‚ŅŒ, ĐŊĐž СĐŊĐ°Ņ‡Đ¸Ņ‚ĐĩĐģҌĐŊĐž ĐŊĐ°ĐŗŅ€ŅƒĐļаĐĩŅ‚ ҁĐĩŅ‚ŅŒ", "theme_setting_three_stage_loading_title": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚ŅŒ ҂ҀĐĩŅ…ŅŅ‚Đ°ĐŋĐŊŅƒŅŽ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐē҃", + "then": "Đ—Đ°Ņ‚ĐĩĐŧ", "they_will_be_merged_together": "ОĐŊи ĐąŅƒĐ´ŅƒŅ‚ ĐžĐąŅŠĐĩдиĐŊĐĩĐŊŅ‹ вĐŧĐĩҁ҂Đĩ", "third_party_resources": "ĐĄŅ‚ĐžŅ€ĐžĐŊĐŊиĐĩ Ņ€ĐĩŅŅƒŅ€ŅŅ‹", "time": "Đ’Ņ€ĐĩĐŧŅ", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Đ’Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ", "trash_page_title": "ĐšĐžŅ€ĐˇĐ¸ĐŊа ({count})", "trashed_items_will_be_permanently_deleted_after": "ĐžĐąŅŠĐĩĐē҂ҋ, Ņ…Ņ€Đ°ĐŊŅŅ‰Đ¸ĐĩŅŅ в ĐēĐžŅ€ĐˇĐ¸ĐŊĐĩ йОĐģĐĩĐĩ {days, plural, one {# Đ´ĐŊŅ} other {# Đ´ĐŊĐĩĐš}}, ŅƒĐ´Đ°ĐģŅŅŽŅ‚ŅŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐĩҁĐēи.", + "trigger": "ĐĸŅ€Đ¸ĐŗĐŗĐĩŅ€", + "trigger_asset_uploaded": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа ĐžĐąŅŠĐĩĐēŅ‚Đ°", + "trigger_asset_uploaded_description": "ĐĄŅ€Đ°ĐąĐ°Ņ‚Ņ‹Đ˛Đ°ĐĩŅ‚ ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ ĐŊĐžĐ˛ĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°", + "trigger_description": "ĐĄĐžĐąŅ‹Ņ‚Đ¸Đĩ, ĐēĐžŅ‚ĐžŅ€ĐžĐĩ СаĐŋ҃ҁĐēаĐĩŅ‚ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", + "trigger_person_recognized": "Đ Đ°ŅĐŋОСĐŊаваĐŊиĐĩ ҇ĐĩĐģОвĐĩĐēа", + "trigger_person_recognized_description": "ĐĄŅ€Đ°ĐąĐ°Ņ‚Ņ‹Đ˛Đ°ĐĩŅ‚ ĐŋŅ€Đ¸ Ņ€Đ°ŅĐŋОСĐŊаваĐŊии ҇ĐĩĐģОвĐĩĐēа", + "trigger_type": "ĐĸиĐŋ Ņ‚Ņ€Đ¸ĐŗĐŗĐĩŅ€Đ°", "troubleshoot": "Đ”Đ¸Đ°ĐŗĐŊĐžŅŅ‚Đ¸Đēа", "type": "ĐĸиĐŋ", "unable_to_change_pin_code": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ иСĐŧĐĩĐŊĐĩĐŊии PIN-ĐēОда", @@ -2123,6 +2284,7 @@ "unhide_person": "ПоĐēĐ°ĐˇĐ°Ņ‚ŅŒ ҇ĐĩĐģОвĐĩĐēа", "unknown": "НĐĩиСвĐĩҁ҂ĐŊĐž", "unknown_country": "НĐĩиСвĐĩҁ҂ĐŊĐ°Ņ ŅŅ‚Ņ€Đ°ĐŊа", + "unknown_date": "Đ”Đ°Ņ‚Đ° ĐŊĐĩиСвĐĩҁ҂ĐŊа", "unknown_year": "НĐĩиСвĐĩҁ҂ĐŊŅ‹Đš Год", "unlimited": "НĐĩ ĐžĐŗŅ€Đ°ĐŊĐ¸Ņ‡ĐĩĐŊĐž", "unlink_motion_video": "ĐžŅ‚ŅĐžĐĩдиĐŊĐ¸Ņ‚ŅŒ двиĐļŅƒŅ‰ĐĩĐĩŅŅ видĐĩĐž", @@ -2139,17 +2301,19 @@ "unstack": "Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "unstack_action_prompt": "ĐžĐąŅŠĐĩĐē҂ҋ Ņ€Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊŅ‹ ({count} ŅˆŅ‚.)", "unstacked_assets_count": "{count, plural, one {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊ # ĐžĐąŅŠĐĩĐēŅ‚} many {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛} other {Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°ĐŊĐž # ĐžĐąŅŠĐĩĐēŅ‚Đ°}}", + "unsupported_field_type": "НĐĩĐŋОддĐĩŅ€ĐļиваĐĩĐŧŅ‹Đš Ņ‚Đ¸Đŋ ĐŋĐžĐģŅ", "untagged": "БĐĩС Ņ‚ĐĩĐŗĐžĐ˛", + "untitled_workflow": "Đ Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ ĐąĐĩС ĐŊаСваĐŊĐ¸Ņ", "up_next": "ĐĄĐģĐĩĐ´ŅƒŅŽŅ‰ĐĩĐĩ", "update_location_action_prompt": "ĐŖŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚ŅŒ ҁĐģĐĩĐ´ŅƒŅŽŅ‰Đ¸Đĩ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Ņ‹ ҃ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊҋ҅ ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛ ({count} ŅˆŅ‚.):", "updated_at": "ОбĐŊОвĐģŅ‘ĐŊ", "updated_password": "ĐŸĐ°Ņ€ĐžĐģҌ иСĐŧĐĩĐŊŅ‘ĐŊ", "upload": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ", - "upload_action_prompt": "ĐžĐąŅŠĐĩĐē҂ҋ ĐžĐļĐ¸Đ´Đ°ŅŽŅ‚ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи ({count} ŅˆŅ‚.)", "upload_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐģĐĩĐģҌĐŊĐžŅŅ‚ŅŒ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "upload_details": "ĐŸĐžĐ´Ņ€ĐžĐąĐŊĐžŅŅ‚Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēи", "upload_dialog_info": "ĐĨĐžŅ‚Đ¸Ņ‚Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ Đ˛Ņ‹ĐąŅ€Đ°ĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€?", "upload_dialog_title": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐ¸Ņ‚ŅŒ ĐžĐąŅŠĐĩĐēŅ‚", + "upload_error_with_count": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐˇĐ°ĐŗŅ€ŅƒĐˇĐēĐĩ {count, plural, one {# ĐžĐąŅŠĐĩĐēŅ‚Đ°} other {# ĐžĐąŅŠĐĩĐēŅ‚ĐžĐ˛}}", "upload_errors": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа СавĐĩŅ€ŅˆĐĩĐŊа ҁ {count, plural, one {# ĐžŅˆĐ¸ĐąĐēОК} other {# ĐžŅˆĐ¸ĐąĐēаĐŧи}}, ОйĐŊĐžĐ˛Đ¸Ņ‚Đĩ ŅŅ‚Ņ€Đ°ĐŊĐ¸Ņ†Ņƒ, Ņ‡Ņ‚ĐžĐąŅ‹ ŅƒĐ˛Đ¸Đ´ĐĩŅ‚ŅŒ ĐŊĐžĐ˛Ņ‹Đĩ ĐˇĐ°ĐŗŅ€ŅƒĐļĐĩĐŊĐŊŅ‹Đĩ ĐžĐąŅŠĐĩĐē҂ҋ.", "upload_finished": "Đ—Đ°ĐŗŅ€ŅƒĐˇĐēа СавĐĩŅ€ŅˆĐĩĐŊа", "upload_progress": "ĐžŅŅ‚Đ°ĐģĐžŅŅŒ {remaining, number} - ĐžĐąŅ€Đ°ĐąĐžŅ‚Đ°ĐŊĐž {processed, number}/{total, number}", @@ -2185,6 +2349,7 @@ "utilities": "ĐŖŅ‚Đ¸ĐģĐ¸Ņ‚Ņ‹", "validate": "ĐŸŅ€ĐžĐ˛ĐĩŅ€Đ¸Ņ‚ŅŒ", "validate_endpoint_error": "ВвĐĩĐ´Đ¸Ņ‚Đĩ ĐēĐžŅ€Ņ€ĐĩĐēŅ‚ĐŊŅ‹Đš URL", + "validation_error": "ĐžŅˆĐ¸ĐąĐēа ĐŋŅ€Đ¸ ĐŋŅ€ĐžĐ˛ĐĩŅ€ĐēĐĩ", "variables": "ПĐĩŅ€ĐĩĐŧĐĩĐŊĐŊŅ‹Đĩ", "version": "ВĐĩŅ€ŅĐ¸Ņ", "version_announcement_closing": "ĐĸвОК Đ´Ņ€ŅƒĐŗ АĐģĐĩĐēҁ", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Đ’ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐžĐ´Đ¸Ņ‚ŅŒ видĐĩĐž ĐŋŅ€Đ¸ ĐŊавĐĩĐ´ĐĩĐŊии ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŧŅ‹ŅˆĐ¸ ĐŊа ĐŧиĐŊĐ¸Đ°Ņ‚ŅŽŅ€Ņƒ. ДаĐļĐĩ ĐĩҁĐģи ŅŅ‚Đ° Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ Đ˛Ņ‹ĐēĐģŅŽŅ‡ĐĩĐŊа, Đ˛ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐ´ĐĩĐŊиĐĩ ĐŧĐžĐļĐŊĐž СаĐŋŅƒŅŅ‚Đ¸Ņ‚ŅŒ, ĐŊавĐĩĐ´Ņ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа СĐŊĐ°Ņ‡ĐžĐē Đ˛ĐžŅĐŋŅ€ĐžĐ¸ĐˇĐ˛ĐĩĐ´ĐĩĐŊĐ¸Ņ.", "videos": "ВидĐĩĐž", "videos_count": "{count, plural, one {# видĐĩĐž} other {# видĐĩĐž}}", + "videos_only": "ĐĸĐžĐģҌĐēĐž видĐĩĐž", "view": "ĐŸŅ€ĐžŅĐŧĐžŅ‚Ņ€", "view_album": "ĐžŅ‚ĐēŅ€Ņ‹Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ", "view_all": "ĐŸĐžŅĐŧĐžŅ‚Ņ€ĐĩŅ‚ŅŒ Đ˛ŅŅ‘", @@ -2216,21 +2382,36 @@ "viewer_stack_use_as_main_asset": "Đ˜ŅĐŋĐžĐģŅŒĐˇĐžĐ˛Đ°Ņ‚ŅŒ в ĐēĐ°Ņ‡ĐĩŅŅ‚Đ˛Đĩ ĐžŅĐŊОвĐŊĐžĐŗĐž ĐžĐąŅŠĐĩĐēŅ‚Đ°", "viewer_unstack": "Đ Đ°ĐˇĐŗŅ€ŅƒĐŋĐŋĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ", "visibility_changed": "ИСĐŧĐĩĐŊĐĩĐŊа видиĐŧĐžŅŅ‚ŅŒ ҃ {count, plural, one {# ҇ĐĩĐģОвĐĩĐēа} other {# ҇ĐĩĐģОвĐĩĐē}}", + "visual": "Đ’Đ¸ĐˇŅƒĐ°ĐģҌĐŊŅ‹Đš", + "visual_builder": "Đ’Đ¸ĐˇŅƒĐ°ĐģҌĐŊŅ‹Đš ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", "waiting": "В ĐžŅ‡ĐĩŅ€Đĩди", - "waiting_count": "ОĐļĐ¸Đ´Đ°ŅŽŅ‚ СаĐŋ҃ҁĐēа: {count}", + "waiting_count": "ОĐļĐ¸Đ´Đ°ŅŽŅ‚: {count}", "warning": "ĐŸŅ€ĐĩĐ´ŅƒĐŋŅ€ĐĩĐļĐ´ĐĩĐŊиĐĩ", "week": "НĐĩĐ´ĐĩĐģŅ", "welcome": "Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ", "welcome_to_immich": "Đ”ĐžĐąŅ€Đž ĐŋĐžĐļаĐģĐžĐ˛Đ°Ņ‚ŅŒ в Immich", "width": "Đ¨Đ¸Ņ€Đ¸ĐŊа", "wifi_name": "ИĐŧŅ ҁĐĩŅ‚Đ¸", - "workflow": "Đ Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ", + "workflow_delete_prompt": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ ŅƒĐ´Đ°ĐģĐ¸Ņ‚ŅŒ ŅŅ‚ĐžŅ‚ Ņ€Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ?", + "workflow_deleted": "Đ Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ ŅƒĐ´Đ°ĐģŅ‘ĐŊ", + "workflow_description": "ОĐŋĐ¸ŅĐ°ĐŊиĐĩ Ņ€Đ°ĐąĐžŅ‡ĐĩĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅŅĐ°", + "workflow_info": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Ņ€Đ°ĐąĐžŅ‡ĐĩĐŧ ĐŋŅ€ĐžŅ†ĐĩҁҁĐĩ", + "workflow_json": "JSON Ņ€Đ°ĐąĐžŅ‡ĐĩĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅŅĐ°", + "workflow_json_help": "ĐžŅ‚Ņ€ĐĩдаĐēŅ‚Đ¸Ņ€ŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Đ¸ĐŗŅƒŅ€Đ°Ņ†Đ¸ŅŽ Ņ€Đ°ĐąĐžŅ‡ĐĩĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅŅĐ° в JSON Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đĩ. ИСĐŧĐĩĐŊĐĩĐŊĐ¸Ņ ĐąŅƒĐ´ŅƒŅ‚ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊĐ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°ĐŊŅ‹ в Đ˛Đ¸ĐˇŅƒĐ°ĐģҌĐŊŅ‹Đš ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€.", + "workflow_name": "ИĐŧŅ Ņ€Đ°ĐąĐžŅ‡ĐĩĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅŅĐ°", + "workflow_navigation_prompt": "Đ’Ņ‹ Đ´ĐĩĐšŅŅ‚Đ˛Đ¸Ņ‚ĐĩĐģҌĐŊĐž Ņ…ĐžŅ‚Đ¸Ņ‚Đĩ Đ˛Ņ‹ĐšŅ‚Đ¸ ĐąĐĩС ŅĐžŅ…Ņ€Đ°ĐŊĐĩĐŊĐ¸Ņ иСĐŧĐĩĐŊĐĩĐŊиК?", + "workflow_summary": "ИĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Đ¸Ņ Đž Ņ€Đ°ĐąĐžŅ‡ĐĩĐŧ ĐŋŅ€ĐžŅ†ĐĩҁҁĐĩ", + "workflow_update_success": "Đ Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ ҃ҁĐŋĐĩ҈ĐŊĐž ОйĐŊОвĐģŅ‘ĐŊ", + "workflow_updated": "Đ Đ°ĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁҁ ОйĐŊОвĐģŅ‘ĐŊ", + "workflows": "Đ Đ°ĐąĐžŅ‡Đ¸Đĩ ĐŋŅ€ĐžŅ†Đĩҁҁҋ", + "workflows_help_text": "Đ Đ°ĐąĐžŅ‡Đ¸Đĩ ĐŋŅ€ĐžŅ†Đĩҁҁҋ ĐŋОСвОĐģŅŅŽŅ‚ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ¸Ņ€ĐžĐ˛Đ°Ņ‚ŅŒ ĐžĐŋĐĩŅ€Đ°Ņ†Đ¸Đ¸ ҁ ĐžĐąŅŠĐĩĐēŅ‚Đ°Đŧи ĐŊа ĐžŅĐŊОваĐŊии ŅĐžĐąŅ‹Ņ‚Đ¸Đš и Ņ„Đ¸ĐģŅŒŅ‚Ņ€ĐžĐ˛", "wrong_pin_code": "НĐĩвĐĩŅ€ĐŊŅ‹Đš PIN-ĐēОд", "year": "Год", "years_ago": "{years, plural, one {# ĐŗĐžĐ´} few {# ĐŗĐžĐ´Đ°} many {# ĐģĐĩŅ‚} other {# ĐŗĐžĐ´Đ°}} ĐŊаСад", "yes": "Да", "you_dont_have_any_shared_links": "ĐŖ Đ˛Đ°Ņ ĐŊĐĩŅ‚ ĐŋŅƒĐąĐģĐ¸Ņ‡ĐŊҋ҅ ҁҁҋĐģĐžĐē", "your_wifi_name": "ИĐŧŅ Đ˛Đ°ŅˆĐĩĐš Wi-Fi ҁĐĩŅ‚Đ¸", + "zero_to_clear_rating": "ĐŊаĐļĐŧĐ¸Ņ‚Đĩ 0 Đ´ĐģŅ ŅƒĐ´Đ°ĐģĐĩĐŊĐ¸Ņ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗĐ°", "zoom_image": "ИСĐŧĐĩĐŊĐ¸Ņ‚ŅŒ ĐŧĐ°ŅŅˆŅ‚Đ°Đą", "zoom_to_bounds": "ĐŖĐ˛ĐĩĐģĐ¸Ņ‡Đ¸Ņ‚ŅŒ Đ´Đž ĐŗŅ€Đ°ĐŊĐ¸Ņ†" } diff --git a/i18n/sk.json b/i18n/sk.json index e92d2f2541..c8c3c69802 100644 --- a/i18n/sk.json +++ b/i18n/sk.json @@ -5,6 +5,7 @@ "acknowledge": "Rozumiem", "action": "Akcia", "action_common_update": "AktualizovaÅĨ", + "action_description": "SÃēbor akcií, ktorÊ sa majÃē vykonaÅĨ na filtrovanÃŊch poloÅžkÃĄch", "actions": "Akcie", "active": "Aktívne", "active_count": "Aktívne: {count}", @@ -15,9 +16,14 @@ "add_a_location": "PridaÅĨ polohu", "add_a_name": "PridaÅĨ meno", "add_a_title": "PridaÅĨ nÃĄzov", + "add_action": "PridaÅĨ akciu", + "add_action_description": "Kliknutím pridÃĄte akciu, ktorÃē chcete vykonaÅĨ", + "add_assets": "PridaÅĨ poloÅžky", "add_birthday": "PridaÅĨ narodeniny", "add_endpoint": "PridaÅĨ koncovÃŊ bod", "add_exclusion_pattern": "PridaÅĨ vzor vylÃēčenia", + "add_filter": "PridaÅĨ filter", + "add_filter_description": "Kliknutím pridÃĄte podmienku filtra", "add_location": "PridaÅĨ polohu", "add_more_users": "PridaÅĨ viac pouŞívateÄžov", "add_partner": "PridaÅĨ partnera", @@ -36,6 +42,7 @@ "add_to_shared_album": "PridaÅĨ do zdieÄžanÊho albumu", "add_upload_to_stack": "NahraÅĨ a pridaÅĨ do zoskupenÃŊch", "add_url": "PridaÅĨ URL", + "add_workflow_step": "PridaÅĨ krok pracovnÊho postupu", "added_to_archive": "PridanÊ do archívu", "added_to_favorites": "PridanÊ do obÄžÃēbenÃŊch", "added_to_favorites_count": "PridanÊ {count, number} do obÄžÃēbenÃŊch", @@ -97,6 +104,8 @@ "image_preview_description": "Stredne veÄžkÃŊ obrÃĄzok s odstrÃĄnenÃŊmi metadÃĄtami, pouŞívanÃŊ pri prezeraní jednej poloÅžky a na strojovÊ učenie", "image_preview_quality_description": "Kvalita nÃĄhÄžadu 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. Nastavenie niÅžÅĄej hodnoty môŞe ovplyvniÅĨ kvalitu strojovÊho učenia.", "image_preview_title": "NÃĄhÄžady", + "image_progressive": "Progresívne", + "image_progressive_description": "Progresívne kÃŗdovaÅĨ JPEG obrÃĄzky pre postupnÊ načítanie zobrazenia. Toto nemÃĄ Åžiadny vplyv na WebP obrÃĄzky.", "image_quality": "Kvalita", "image_resolution": "RozlÃ­ÅĄenie", "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.", @@ -181,10 +190,21 @@ "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.", + "maintenance_delete_backup": "VymazaÅĨ zÃĄlohu", + "maintenance_delete_backup_description": "Tento sÃēbor bude nezvratne vymazanÃŊ.", + "maintenance_delete_error": "Nepodarilo sa vymazaÅĨ zÃĄlohu.", + "maintenance_restore_backup": "ObnoviÅĨ zÃĄlohu", + "maintenance_restore_backup_description": "Immich bude vymazanÃŊ a obnovenÃŊ zo zvolenej zÃĄlohy. Pred pokračovaním bude vytvorenÃĄ zÃĄloha.", + "maintenance_restore_backup_different_version": "TÃĄto zÃĄloha bola vytvorenÃĄ pomocou inej verzie aplikÃĄcie Immich!", + "maintenance_restore_backup_unknown_version": "Nepodarilo sa zistiÅĨ verziu zÃĄlohy.", + "maintenance_restore_database_backup": "ObnoviÅĨ zÃĄlohu databÃĄzy", + "maintenance_restore_database_backup_description": "VrÃĄtiÅĨ sa do predchÃĄdzajÃēceho stavu databÃĄzy pomocou zÃĄloÅžnÊho sÃēboru", "maintenance_settings": "ÚdrÅžba", "maintenance_settings_description": "PrepnÃēÅĨ Immich do reÅžimu ÃēdrÅžby.", - "maintenance_start": "SpustiÅĨ reÅžim ÃēdrÅžby", + "maintenance_start": "PrepnÃēÅĨ do reÅžimu ÃēdrÅžby", "maintenance_start_error": "Nepodarilo sa spustiÅĨ reÅžim ÃēdrÅžby.", + "maintenance_upload_backup": "NahraÅĨ zÃĄlohu databÃĄzy na server", + "maintenance_upload_backup_error": "Nepodarilo sa nahraÅĨ zÃĄlohu, je to sÃēbor .sql/.sql.gz?", "manage_concurrency": "SpravovaÅĨ sÃēbeÅžnosÅĨ", "manage_concurrency_description": "PrejsÅĨ na strÃĄnku Ãēloh, kde môŞete spravovaÅĨ sÃēbeÅžnosÅĨ Ãēloh", "manage_log_settings": "SpravovaÅĨ nastavenia ukladania zÃĄznamov", @@ -252,7 +272,7 @@ "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_client_secret_description": "VyÅžadovanÊ pre dôvernÊho klienta alebo ak 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", @@ -291,7 +311,7 @@ "search_jobs": "VyhÄžadaÅĨ Ãēlohyâ€Ļ", "send_welcome_email": "OdoslaÅĨ uvítací e-mail", "server_external_domain_settings": "ExternÃĄ domÊna", - "server_external_domain_settings_description": "VerejnÃĄ domÊna pre zdieÄžanÊ odkazy, vrÃĄtane http(s)://", + "server_external_domain_settings_description": "DomÊna pouŞívanÃĄ pre externÊ odkazy", "server_public_users": "Verejní pouŞívatelia", "server_public_users_description": "VÅĄetci pouŞívatelia (meno a email) sÃē uvedení pri pridÃĄvaní pouŞívateÄža do zdieÄžanÃŊch albumov. Ak je tÃĄto funkcia vypnutÃĄ, zoznam pouŞívateÄžov bude dostupnÃŊ iba sprÃĄvcom.", "server_settings": "Server", @@ -431,6 +451,9 @@ "admin_password": "AdministrÃĄtorskÊ heslo", "administration": "AdministrÃĄcia", "advanced": "PokročilÊ", + "advanced_settings_clear_image_cache": "VyčistiÅĨ vyrovnÃĄvaciu pamäÅĨ obrÃĄzkov", + "advanced_settings_clear_image_cache_error": "Nepodarilo sa vyčistiÅĨ vyrovnÃĄvaciu pamäÅĨ obrÃĄzkov", + "advanced_settings_clear_image_cache_success": "ÚspeÅĄne vyčistenÃŊch {size}", "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}", @@ -467,10 +490,12 @@ "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_selected": "VybranÃŊ album", "album_share_no_users": "VyzerÃĄ to, Åže ste tento album zdieÄžali so vÅĄetkÃŊmi pouŞívateÄžmi alebo nemÃĄte Åžiadneho pouŞívateÄža, s ktorÃŊm by ste ho mohli zdieÄžaÅĨ.", "album_summary": "SÃēhrn albumu", "album_updated": "Album bol aktualizovanÃŊ", "album_updated_setting_description": "ObdrÅžaÅĨ e-mailovÊ upozornenie, keď v zdieÄžanom albume pribudnÃē novÊ poloÅžky", + "album_upload_assets": "Nahrajte sÃēbory zo svojho počítača a pridajte ich do albumu", "album_user_left": "Opustil {album}", "album_user_removed": "OdstrÃĄnenÃŊ {user}", "album_viewer_appbar_delete_confirm": "Ste si istÃŊ Åže chcete vymazaÅĨ tento album z vÃĄÅĄho Ãēčtu?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {# vybranÃŊ album} few {# vybranÊ albumy} other {# vybranÃŊch albumov}}", "all": "VÅĄetko", "all_albums": "VÅĄetky albumy", "all_people": "VÅĄetci Äžudia", + "all_photos": "VÅĄetky fotky", "all_videos": "VÅĄetky videa", "allow_dark_mode": "PovoliÅĨ tmavÃŊ reÅžim", "allow_edits": "PovoliÅĨ Ãēpravy", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "UmoÅžniÅĨ verejnÊmu pouŞívateÄžovi nahraÅĨ", "allowed": "PovolenÊ", "alt_text_qr_code": "ObrÃĄzok QR kÃŗdu", + "always_keep": "VÅždy ponechaÅĨ", + "always_keep_photos_hint": "Funkcia UvoÄžniÅĨ miesto ponechÃĄ vÅĄetky fotografie v tomto zariadení.", + "always_keep_videos_hint": "Funkcia UvoÄžniÅĨ miesto ponechÃĄ vÅĄetky videÃĄ v tomto zariadení.", "anti_clockwise": "Proti smeru hodinovÃŊch ručičiek", "api_key": "API KlÃēč", "api_key_description": "TÃĄto hodnota sa zobrazí iba raz. Pred zatvorením okna ju určite skopírujte.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {ArchivovanÃŊ #} few {ArchivovanÊ #} other {ArchivovanÃŊch #}}", "are_these_the_same_person": "Ide o tÃē istÃē osobu?", "are_you_sure_to_do_this": "Ste si istÃŊ, Åže to chcete urobiÅĨ?", + "array_field_not_fully_supported": "Polia vyÅžadujÃē ručnÊ Ãēpravy JSON", "asset_action_delete_err_read_only": "NemoÅžno vymazaÅĨ poloÅžku len na čítanie, preskakujem", "asset_action_share_err_offline": "NemoÅžno načítaÅĨ offline poloÅžku, preskakujem", "asset_added_to_album": "PridanÊ do albumu", "asset_adding_to_album": "PridÃĄva sa do albumuâ€Ļ", + "asset_created": "PoloÅžka bola vytvorenÃĄ", "asset_description_updated": "Popis mÊdia bol aktualizovanÃŊ", "asset_filename_is_offline": "MÊdium {filename} je offline", "asset_has_unassigned_faces": "PoloÅžka mÃĄ nepriradenÊ tvÃĄre", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Rozvrhnutie", "asset_list_settings_subtitle": "Nastavenia rozloÅženia mrieÅžky fotografií", "asset_list_settings_title": "MrieÅžka fotografií", + "asset_not_found_on_device_android": "PoloÅžka nebola nÃĄjdenÃĄ v zariadení", + "asset_not_found_on_device_ios": "PoloÅžka nebola nÃĄjdenÃĄ v zariadení. Ak pouŞívate iCloud, poloÅžka môŞe byÅĨ nedostupnÃĄ kvôli poÅĄkodenÊmu sÃēboru uloÅženÊmu v iCloude", + "asset_not_found_on_icloud": "PoloÅžka nebola nÃĄjdenÃĄ v iCloude. PoloÅžka môŞe byÅĨ nedostupnÃĄ kvôli poÅĄkodenÊmu sÃēboru uloÅženÊmu v iCloude", "asset_offline": "MÊdium je offline", "asset_offline_description": "Tento externÃĄ poloÅžka sa uÅž nenachÃĄdza na disku. Pre pomoc sa prosím obrÃĄÅĨte na sprÃĄvcu systÊmu Immich.", "asset_restored_successfully": "PoloÅžky boli ÃēspeÅĄne obnovenÊ", @@ -646,13 +681,13 @@ "backup_info_card_assets": "poloÅžiek", "backup_manual_cancelled": "ZruÅĄenÊ", "backup_manual_in_progress": "NahrÃĄvanie uÅž prebieha. VyskÃēÅĄajte neskôr", - "backup_manual_success": "Úspech", + "backup_manual_success": "Hotovo", "backup_manual_title": "Stav nahrÃĄvania", "backup_options": "MoÅžnosti zÃĄlohovania", "backup_options_page_title": "MoÅžnosti zÃĄlohovania", "backup_setting_subtitle": "SpravovaÅĨ nastavenia odosielania na pozadí a v popredí", "backup_settings_subtitle": "SpravovaÅĨ nastavenia nahrÃĄvania", - "backup_upload_details_page_more_details": "Klikni pre viac info", + "backup_upload_details_page_more_details": "Ťukni pre viac info", "backward": "Dozadu", "biometric_auth_enabled": "BiometrickÊ overovanie je povolenÊ", "biometric_locked_out": "Ste vymknutí z biometrickÊho overovania", @@ -711,6 +746,8 @@ "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_trigger": "ZmeniÅĨ spÃēÅĄtač", + "change_trigger_prompt": "Naozaj chcete zmeniÅĨ spÃēÅĄÅĨač? TÃŊmto krokom sa odstrÃĄnia vÅĄetky existujÃēce akcie a filtre.", "change_your_password": "ZmeniÅĨ heslo", "changed_visibility_successfully": "ViditeÄžnosÅĨ bola ÃēspeÅĄne zmenenÃĄ", "charging": "Nabíja sa", @@ -722,6 +759,18 @@ "checksum": "KontrolnÃŊ sÃēčet", "choose_matching_people_to_merge": "Vyberte rovnakÃŊch Äžudí na zlÃēčenie", "city": "Mesto", + "cleanup_confirm_description": "Immich naÅĄiel {count} poloÅžiek (vytvorenÃŊch pred {date}) bezpečne zÃĄlohovanÃŊch na serveri. OdstrÃĄniÅĨ lokÃĄlne kÃŗpie z tohto zariadenia?", + "cleanup_confirm_prompt_title": "OdstrÃĄniÅĨ z tohto zariadenia?", + "cleanup_deleted_assets": "PresunutÃŊch {count} poloÅžiek do koÅĄa na zariadení", + "cleanup_deleting": "PresÃēvanie do koÅĄa...", + "cleanup_found_assets": "NaÅĄlo sa {count} zÃĄlohovanÃŊch poloÅžiek", + "cleanup_found_assets_with_size": "NÃĄjdenÃŊch {count} zÃĄlohovanÃŊch poloÅžiek ({size})", + "cleanup_icloud_shared_albums_excluded": "ZdieÄžanÊ albumy iCloud sÃē vylÃēčenÊ zo skenovania", + "cleanup_no_assets_found": "NenaÅĄli sa Åžiadne poloÅžky zodpovedajÃēce vyÅĄÅĄie uvedenÃŊm kritÊriÃĄm. Funkcia UvoÄžniÅĨ miesto môŞe odstrÃĄniÅĨ len tie poloÅžky, ktorÊ boli zÃĄlohovanÊ na server", + "cleanup_preview_title": "PoloÅžiek na odstrÃĄnenie ({count})", + "cleanup_step3_description": "VyhÄžadaÅĨ zÃĄlohovanÊ sÃēbory zodpovedajÃēce vaÅĄim nastaveniam dÃĄtumu a ponechania.", + "cleanup_step4_summary": "{count} poloÅžiek (vytvorenÃŊch pred {date}) na odstrÃĄnenie z vÃĄÅĄho lokÃĄlneho zariadenia. Fotografie zostanÃē dostupnÊ v aplikÃĄcii Immich.", + "cleanup_trash_hint": "Ak chcete Ãēplne uvoÄžniÅĨ ÃēloÅžnÃŊ priestor, otvorte aplikÃĄciu systÊmovej galÊrie a vyprÃĄzdnite koÅĄ", "clear": "VyčistiÅĨ", "clear_all": "VyčistiÅĨ vÅĄetko", "clear_all_recent_searches": "VyčistiÅĨ nedÃĄvne vyhÄžadÃĄvania", @@ -733,6 +782,8 @@ "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_password_message": "Zadajte heslo pre tento certifikÃĄt", + "client_cert_password_title": "Heslo certifikÃĄtu", "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 [EXPERIMENTÁLNE]", @@ -743,6 +794,11 @@ "color": "Farba", "color_theme": "Farba tÊmy", "command": "Príkaz", + "command_palette_prompt": "RÃŊchlo vyhÄžadajte strÃĄnky, akcie alebo príkazy ako", + "command_palette_to_close": "zatvoriÅĨ", + "command_palette_to_navigate": "vloÅžiÅĨ", + "command_palette_to_select": "vybraÅĨ", + "command_palette_to_show_all": "zobraziÅĨ vÅĄetko", "comment_deleted": "KomentÃĄr bol odstrÃĄnenÃŊ", "comment_options": "MoÅžnosti komentÃĄra", "comments_and_likes": "KomentÃĄre a pÃĄÄi sa mi to", @@ -787,6 +843,7 @@ "create_album": "VytvoriÅĨ album", "create_album_page_untitled": "Bez nÃĄzvu", "create_api_key": "VytvoriÅĨ API kÄžÃēč", + "create_first_workflow": "Vytvorte prvÃŊ pracovnÃŊ postup", "create_library": "VytvoriÅĨ kniÅžnicu", "create_link": "VytvoriÅĨ odkaz", "create_link_to_share": "VytvoriÅĨ odkaz na zdieÄžanie", @@ -795,23 +852,31 @@ "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_add_assets": "PRIDAŤ POLOÅŊKY", "create_shared_album_page_share_select_photos": "VybraÅĨ fotografie", "create_shared_link": "VytvoriÅĨ zdieÄžanÃŊ odkaz", "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", + "create_workflow": "VytvoriÅĨ pracovnÃŊ postup", "created": "VytvorenÊ", "created_at": "VytvorenÊ", "creating_linked_albums": "VytvÃĄranie prepojenÃŊch albumov...", "crop": "OrezaÅĨ", + "crop_aspect_ratio_fixed": "PevnÃŊ pomer", + "crop_aspect_ratio_free": "VoÄžnÃŊ", + "crop_aspect_ratio_original": "OriginÃĄlny", "curated_object_page_title": "Veci", "current_device": "SÃēčasnÊ zariadenie", "current_pin_code": "AktuÃĄlny PIN kÃŗd", "current_server_address": "AktuÃĄlna adresa servera", + "custom_date": "VlastnÃŊ dÃĄtum", "custom_locale": "VlastnÊ nastavenie jazyka", "custom_locale_description": "FormÃĄtovanie dÃĄtumov a čísel podÄža jazyka a regiÃŗnu", "custom_url": "VlastnÃĄ URL adresa", + "cutoff_date_description": "PonechaÅĨ fotografie z poslednÊho obdobiaâ€Ļ", + "cutoff_day": "{count, plural, one {deň} few {dni} other {dní}}", + "cutoff_year": "{count, plural, one {rok} few {roky} other {rokov}}", "daily_title_text_date": "EEEE, d. MMMM", "daily_title_text_date_year": "EEEE, d. MMMM y", "dark": "TmavÃĄ", @@ -867,6 +932,7 @@ "deselect_all": "ZruÅĄiÅĨ vÃŊber vÅĄetkÃŊch", "details": "Podrobnosti", "direction": "Smer", + "disable": "VypnÃēÅĨ", "disabled": "VypnutÊ", "disallow_edits": "ZakÃĄzaÅĨ Ãēpravy", "discord": "Discord", @@ -892,6 +958,7 @@ "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_original": "StiahnuÅĨ originÃĄl", "download_paused": "Stiahnutie pozastavenÊ", "download_settings": "StiahnuÅĨ", "download_settings_description": "SpravovaÅĨ nastavenia sÃēvisiace so sÅĨahovaním poloÅžiek", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "ČakÃĄ sa na opakovanie pokusu", "downloading": "SÅĨahuje sa", "downloading_asset_filename": "SÅĨahuje sa poloÅžka {filename}", + "downloading_from_icloud": "SÅĨahuje sa z iCloud", "downloading_media": "SÅĨahovanie mÊdií", "drop_files_to_upload": "Umiestnite sÃēbory kamkoÄžvek na nahratie", "duplicates": "DuplikÃĄty", @@ -929,11 +997,22 @@ "edit_tag": "UpraviÅĨ ÅĄtítok", "edit_title": "UpraviÅĨ nÃĄzov", "edit_user": "UpraviÅĨ pouŞívateÄža", + "edit_workflow": "UpraviÅĨ pracovnÃŊ postup", "editor": "Editor", "editor_close_without_save_prompt": "Úpravy nebudÃē uloÅženÊ", "editor_close_without_save_title": "ZavrieÅĨ editor?", - "editor_crop_tool_h2_aspect_ratios": "Pomer strÃĄn", - "editor_crop_tool_h2_rotation": "Otočenie", + "editor_confirm_reset_all_changes": "Naozaj chcete zruÅĄiÅĨ vÅĄetky zmeny?", + "editor_discard_edits_confirm": "ZruÅĄiÅĨ Ãēpravy", + "editor_discard_edits_prompt": "MÃĄte neuloÅženÊ Ãēpravy. Naozaj ich chcete zruÅĄiÅĨ?", + "editor_discard_edits_title": "ZruÅĄiÅĨ Ãēpravy?", + "editor_edits_applied_error": "Nepodarilo sa pouÅžiÅĨ Ãēpravy", + "editor_edits_applied_success": "Úpravy boli ÃēspeÅĄne vykonanÊ", + "editor_flip_horizontal": "PrevrÃĄtiÅĨ horizontÃĄlne", + "editor_flip_vertical": "PrevrÃĄtiÅĨ vertikÃĄlne", + "editor_orientation": "OrientÃĄcia", + "editor_reset_all_changes": "ZruÅĄiÅĨ zmeny", + "editor_rotate_left": "OtočiÅĨ o 90° doÄžava", + "editor_rotate_right": "OtočiÅĨ o 90° doprava", "email": "E-mail", "email_notifications": "E-mailovÊ oznÃĄmenia", "empty_folder": "Tento priečinok je prÃĄzdny", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Nepodarilo sa zmeniÅĨ poradie albumu", "error_delete_face": "Chyba pri odstraňovaní tvÃĄre z poloÅžky", "error_getting_places": "Chyba pri získavaní polôh", + "error_loading_albums": "Chyba pri načítaní albumov", "error_loading_image": "Nepodarilo sa načítaÅĨ obrÃĄzok", "error_loading_partners": "Chyba pri načítaní partnerov: {error}", + "error_retrieving_asset_information": "Chyba pri načítaní informÃĄcií o poloÅžke", "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", + "error_while_navigating": "Chyba pri prechode na poloÅžku", "errors": { "cannot_navigate_next_asset": "Nie je moÅžnÊ prejsÅĨ na ďalÅĄiu poloÅžku", "cannot_navigate_previous_asset": "Nie je moÅžnÊ prejsÅĨ na predoÅĄlÃē poloÅžku", @@ -1014,6 +1096,7 @@ "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": "Nie je moÅžnÊ vytvoriÅĨ pracovnÃŊ postup", "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", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Nie je moÅžnÊ vymazaÅĨ vylučovací vzor", "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_delete_workflow": "Nie je moÅžnÊ odstrÃĄniÅĨ pracovnÃŊ postup", "unable_to_download_files": "Nie je moÅžnÊ stiahnuÅĨ sÃēbory", "unable_to_edit_exclusion_pattern": "Nie je moÅžnÊ upraviÅĨ vzorec vylÃēčenia", "unable_to_empty_trash": "Nie je moÅžnÊ vyprÃĄzdniÅĨ kÃ´ÅĄ", @@ -1061,8 +1145,9 @@ "unable_to_save_settings": "Nie je moÅžnÊ uloÅžiÅĨ nastavenia", "unable_to_scan_libraries": "Nie je moÅžnÊ prehÄžadaÅĨ kniÅžnice", "unable_to_scan_library": "Nie je moÅžnÊ prehÄžadaÅĨ kniÅžnicu", - "unable_to_set_feature_photo": "Nie je moÅžnÊ nastaviÅĨ hlavnÃŊ obrÃĄzok", + "unable_to_set_feature_photo": "Nie je moÅžnÊ nastaviÅĨ profilovÃē fotku", "unable_to_set_profile_picture": "Nie je moÅžnÊ nastaviÅĨ profilovÃŊ obrÃĄzok", + "unable_to_set_rating": "Nie je moÅžnÊ nastaviÅĨ hodnotenie", "unable_to_submit_job": "Nie je moÅžnÊ odoslaÅĨ Ãēlohu", "unable_to_trash_asset": "Nie je moÅžnÊ presunÃēÅĨ poloÅžku do koÅĄa", "unable_to_unlink_account": "Nie je moÅžnÊ odpojiÅĨ Ãēčet", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Nie je moÅžnÊ aktualizovaÅĨ nastavenia", "unable_to_update_timeline_display_status": "Nie je moÅžnÊ aktualizovaÅĨ stav zobrazenia časovej osi", "unable_to_update_user": "Nie je moÅžnÊ aktualizovaÅĨ pouŞívateÄža", + "unable_to_update_workflow": "Nie je moÅžnÊ aktualizovaÅĨ pracovnÃŊ postup", "unable_to_upload_file": "Nie je moÅžnÊ nahraÅĨ sÃēbor" }, + "errors_text": "Chyby", "exclusion_pattern": "Vzor vylÃēčenia", "exif": "Exif", "exif_bottom_sheet_description": "PridaÅĨ popis...", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "ÄŊUDIA", "exif_bottom_sheet_person_add_person": "PridaÅĨ meno", "exit_slideshow": "OpustiÅĨ prezentÃĄciu", + "expand": "RozbaliÅĨ", "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í", @@ -1116,18 +1204,21 @@ "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ÃŊ", + "feature_photo_updated": "ProfilovÃĄ fotka bola aktualizovanÃĄ", "features": "Funkcie", "features_in_development": "Funkcie vo vÃŊvoji", "features_setting_description": "SpravovaÅĨ funkcie aplikÃĄcie", - "file_name": "NÃĄzov sÃēboru", "file_name_or_extension": "NÃĄzov alebo prípona sÃēboru", + "file_name_text": "NÃĄzov sÃēboru", + "file_name_with_value": "NÃĄzov sÃēboru: {file_name}", "file_size": "VeÄžkosÅĨ sÃēboru", "filename": "NÃĄzov sÃēboru", "filetype": "Typ sÃēboru", "filter": "Filter", + "filter_description": "Podmienky na filtrovanie cieÄžovÃŊch poloÅžiek", "filter_people": "FiltrovaÅĨ Äžudí", "filter_places": "FiltrovaÅĨ miesta", + "filters": "Filtre", "find_them_fast": "NÃĄjdite ich rÃŊchlejÅĄie podÄža mena", "first": "PrvÊ", "fix_incorrect_match": "OpraviÅĨ nesprÃĄvnu zhodu", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Prezeranie zobrazenia priečinkov fotografií a videí v systÊme sÃēborov", "forgot_pin_code_question": "Zabudli ste svoj PIN kÃŗd?", "forward": "Dopredu", + "free_up_space": "UvoÄžniÅĨ priestor", + "free_up_space_description": "Presuňte zÃĄlohovanÊ fotografie a videÃĄ do koÅĄa vÃĄÅĄho zariadenia, aby ste uvoÄžnili miesto. VaÅĄe kÃŗpie na serveri zostanÃē v bezpečí.", + "free_up_space_settings_subtitle": "UvoÄžniÅĨ ÃēloÅžisko zariadenia", "full_path": "CelÃĄ cesta: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "TÃĄto funkcia načítava externÊ zdroje zo spoločnosti Google, aby mohla fungovaÅĨ.", "general": "VÅĄeobecnÊ", "geolocation_instruction_location": "Kliknite na poloÅžku s GPS sÃēradnicami, aby ste pouÅžili jej polohu, alebo vyberte polohu priamo z mapy", "get_help": "ZískaÅĨ pomoc", + "get_people_error": "Chyba pri načítaní Äžudí", "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äÅĨ", @@ -1175,6 +1270,7 @@ "hide_named_person": "SkryÅĨ osobu {name}", "hide_password": "SkryÅĨ heslo", "hide_person": "SkryÅĨ osobu", + "hide_schema": "SkryÅĨ schÊmu", "hide_text_recognition": "SkryÅĨ rozpoznÃĄvanie textu", "hide_unnamed_people": "SkryÅĨ osoby bez mena", "home_page_add_to_album_conflicts": "PridanÊ {added} poloÅžiek do albumu {album}. {failed} poloÅžiek uÅž je v albume.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "Spracovanie prebehlo {dateTime}", "items_count": "{count, plural, one {# poloÅžka} few {# poloÅžky} other {# poloÅžiek}}", "jobs": "Úlohy", + "json_editor": "Editor JSON", + "json_error": "Chyba JSON", "keep": "PonechaÅĨ", + "keep_albums": "PonechaÅĨ albumy", + "keep_albums_count": "PonechÃĄ sa {count} {count, plural, one {album} few {albumy} other {albumov}}", "keep_all": "PonechaÅĨ vÅĄetko", + "keep_description": "Pri uvoĞňovaní miesta vyberte, čo sa mÃĄ ponechaÅĨ na vaÅĄom zariadení.", + "keep_favorites": "PonechaÅĨ obÄžÃēbenÊ", + "keep_on_device": "PonechaÅĨ na zariadení", + "keep_on_device_hint": "Vyberte poloÅžky, ktorÊ chcete ponechaÅĨ v tomto zariadení", "keep_this_delete_others": "PonechaÅĨ tÃēto, odstrÃĄniÅĨ ostatnÊ", + "keeping": "PonechÃĄ sa: {items}", "kept_this_deleted_others": "TÃĄto poloÅžka bola ponechanÃĄ a {count, plural, one {odstrÃĄnila sa # poloÅžka} few {odstrÃĄnili sa # poloÅžky} other {odstrÃĄnilo sa # poloÅžiek}}", "keyboard_shortcuts": "KlÃĄvesovÊ skratky", "language": "Jazyk", @@ -1343,10 +1448,28 @@ "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!", "main_menu": "HlavnÃĄ ponuka", + "maintenance_action_restore": "Obnovuje sa databÃĄza", "maintenance_description": "Immich bol prepnutÃŊ do reÅžimu ÃēdrÅžby.", "maintenance_end": "UkončiÅĨ reÅžim ÃēdrÅžby", "maintenance_end_error": "Nepodarilo sa ukončiÅĨ reÅžim ÃēdrÅžby.", "maintenance_logged_in_as": "AktuÃĄlne prihlÃĄsenÃŊ ako {user}", + "maintenance_restore_from_backup": "ObnoviÅĨ zo zÃĄlohy", + "maintenance_restore_library": "Obnovte svoju kniÅžnicu", + "maintenance_restore_library_confirm": "Ak sa vÃĄm to zdÃĄ sprÃĄvne, pokračujte v obnovovaní zÃĄlohy!", + "maintenance_restore_library_description": "Obnovuje sa databÃĄza", + "maintenance_restore_library_folder_has_files": "{folder} mÃĄ {count, plural, one {# priečinok} few {# priečinky} other {# priečinkov}}", + "maintenance_restore_library_folder_no_files": "{folder} neobsahuje sÃēbory!", + "maintenance_restore_library_folder_pass": "čitateÄžnÃŊ a zapisovateÄžnÃŊ", + "maintenance_restore_library_folder_read_fail": "nedÃĄ sa čítaÅĨ", + "maintenance_restore_library_folder_write_fail": "nedÃĄ sa zapísaÅĨ", + "maintenance_restore_library_hint_missing_files": "MoÅžno vÃĄm chÃŊbajÃē dôleÅžitÊ sÃēbory", + "maintenance_restore_library_hint_regenerate_later": "Tieto môŞete neskôr znovu vytvoriÅĨ v nastaveniach", + "maintenance_restore_library_hint_storage_template_missing_files": "PouŞívate ÅĄablÃŗny ÃēloÅžiska? MôŞu vÃĄm chÃŊbaÅĨ nejakÊ sÃēbory", + "maintenance_restore_library_loading": "Načítanie kontrol integrity a heuristikyâ€Ļ", + "maintenance_task_backup": "VytvÃĄranie zÃĄlohy sÃēčasnej databÃĄzyâ€Ļ", + "maintenance_task_migrations": "Prebieha migrÃĄcia databÃĄzyâ€Ļ", + "maintenance_task_restore": "Obnovuje sa zo zvolenej zÃĄlohyâ€Ļ", + "maintenance_task_rollback": "Obnovenie sa nepodarilo, nÃĄvrat k bodu obnoveniaâ€Ļ", "maintenance_title": "Dočasne nedostupnÊ", "make": "VÃŊrobca", "manage_geolocation": "SpravovaÅĨ polohu", @@ -1408,6 +1531,8 @@ "minimize": "MinimalizovaÅĨ", "minute": "MinÃēta", "minutes": "MinÃēt", + "mirror_horizontal": "HorizontÃĄlne", + "mirror_vertical": "VertikÃĄlne", "missing": "ChÃŊbajÃēce", "mobile_app": "MobilnÃĄ aplikÃĄcia", "mobile_app_download_onboarding_note": "Stiahnite si sprievodnÃē mobilnÃē aplikÃĄciu pomocou nasledujÃēcich moÅžností", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "LLLL y", "more": "Viac", "move": "PresunÃēÅĨ", + "move_down": "PresunÃēÅĨ dole", "move_off_locked_folder": "PresunÃēÅĨ zo zamknutÊho priečinka", "move_to": "PresunÃēÅĨ do", + "move_to_device_trash": "PresunÃēÅĨ do koÅĄa na zariadení", "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Ãē odobranÊ zo vÅĄetkÃŊch albumov a bude ich moÅžnÊ zobraziÅĨ len v zamknutom priečinku", + "move_up": "PresunÃēÅĨ hore", "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", @@ -1430,6 +1558,7 @@ "my_albums": "Moje albumy", "name": "Meno", "name_or_nickname": "Meno alebo prezÃŊvka", + "name_required": "Meno je povinnÊ", "navigate": "PrejsÅĨ", "navigate_to_time": "PrejsÅĨ na čas", "network_requirement_photos_upload": "PouÅžiÅĨ mobilnÊ dÃĄta na zÃĄlohovanie fotografií", @@ -1454,20 +1583,24 @@ "next": "Ďalej", "next_memory": "ĎalÅĄia spomienka", "no": "Nie", + "no_actions_added": "ZatiaÄž neboli pridanÊ Åžiadne akcie", + "no_albums_found": "NenaÅĄli sa Åžiadne albumy", "no_albums_message": "Vytvorte album na usporiadanie svojich 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": "Archivujte fotografie a videÃĄ a skryte ich z vÃĄÅĄho zobrazenia fotografií", - "no_assets_message": "KLIKNITE A NAHRAJTE SVOJU PRVÚ FOTKU", + "no_assets_message": "Kliknite a nahrajte svoju prvÃē fotku", "no_assets_to_show": "ÅŊiadne poloÅžky", "no_cast_devices_found": "NenaÅĄli sa Åžiadne zariadenia na prenos", "no_checksum_local": "Kontrola sÃēčtu nie je k dispozícii – nie je moÅžnÊ načítaÅĨ lokÃĄlne poloÅžky", "no_checksum_remote": "Kontrola sÃēčtu nie je k dispozícii – nie je moÅžnÊ načítaÅĨ vzdialenÊ poloÅžky", + "no_configuration_needed": "Nie je potrebnÃĄ Åžiadna konfigurÃĄcia", "no_devices": "ÅŊiadne autorizovanÊ zariadenia", "no_duplicates_found": "NenaÅĄli sa Åžiadne duplicity.", "no_exif_info_available": "Nie sÃē dostupnÊ exif Ãēdaje", "no_explore_results_message": "Nahrajte viac fotiek na objavovanie vaÅĄej zbierky.", "no_favorites_message": "Pridajte si obÄžÃēbenÊ, aby ste rÃŊchlo naÅĄli svoje najlepÅĄie obrÃĄzky a videÃĄ", + "no_filters_added": "ZatiaÄž neboli pridanÊ Åžiadne filtre", "no_libraries_message": "Vytvorte externÃē kniÅžnicu na prezeranie fotiek a videí", "no_local_assets_found": "Neboli nÃĄjdenÊ Åžiadne lokÃĄlne poloÅžky s touto kontrolnou sumou", "no_location_set": "Nie je nastavenÃĄ Åžiadna poloha", @@ -1481,11 +1614,11 @@ "no_results_description": "SkÃēste synonymum alebo vÅĄeobecnejÅĄÃ­ vÃŊraz", "no_shared_albums_message": "Vytvorte album na zdieÄžanie fotiek a videí s Äžuďmi vo vaÅĄej sieti", "no_uploads_in_progress": "ÅŊiadne prebiehajÃēce nahrÃĄvanie", + "none": "ÅŊiadne", "not_allowed": "NepovolenÊ", "not_available": "NedostupnÊ", "not_in_any_album": "Nie je v Åžiadnom albume", "not_selected": "NevybranÊ", - "note_apply_storage_label_to_previously_uploaded assets": "PoznÃĄmka: Ak chcete pouÅžiÅĨ Å títok ÃēloÅžiska na predtÃŊm nahranÊ mÊdiÃĄ, spustite príkaz", "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ÅĨ.", @@ -1515,6 +1648,7 @@ "online": "Online", "only_favorites": "Len obÄžÃēbenÊ", "open": "OtvoriÅĨ", + "open_calendar": "OtvoriÅĨ kalendÃĄr", "open_in_map_view": "OtvoriÅĨ v mape", "open_in_openstreetmap": "OtvoriÅĨ v OpenStreetMap", "open_the_search_filters": "OtvoriÅĨ vyhÄžadÃĄvacie filtre", @@ -1563,6 +1697,7 @@ "people": "ÄŊudia", "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_selected": "{count, plural, one {# vybranÃĄ osoba} few {# vybranÊ osoby} other {# vybranÃŊch osôb}}", "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", @@ -1587,11 +1722,14 @@ "person_age_years": "mÃĄ {years, plural, one {# rok} few {# roky} other {# rokov}}", "person_birthdate": "NarodenÃŊ/ÃĄ dňa {date}", "person_hidden": "{name}{hidden, select, true { (skrytÊ)} other {}}", + "person_recognized": "Osoba rozpoznanÃĄ", + "person_selected": "Osoba vybranÃĄ", "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 a videÃĄ", "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", + "photos_only": "Iba fotky", "pick_a_location": "Vyberte polohu", "pick_custom_range": "VlastnÃŊ rozsah", "pick_date_range": "VybraÅĨ rozsah dÃĄtumov", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "ProduktovÃŊ kÄžÃēč servera spravuje admin", "query_asset_id": "ID poÅžiadavky poloÅžky", "queue_status": "V poradí {count}/{total}", + "rate_asset": "OhodnotiÅĨ poloÅžku", "rating": "Hodnotenie hviezdičkami", "rating_clear": "VyčistiÅĨ hodnotenie", "rating_count": "{count, plural, one {# hviezdička} few {# hviezdičky} other {# hviezdičiek}}", "rating_description": "ZobraziÅĨ EXIF hodnotenie v informačnom paneli", + "rating_set": "Hodnotenie nastavenÊ na {rating, plural, one {# hviezdičku} few {# hviezdičky} other {# hviezdičiek}}", "reaction_options": "MoÅžnosti reakcie", "read_changelog": "PrečítaÅĨ zoznam zmien", "readonly_mode_disabled": "ReÅžim iba na čítanie je vypnutÃŊ", @@ -1681,7 +1821,7 @@ "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_albums": "PoslednÊ albumy", "recent_searches": "PoslednÊ vyhÄžadÃĄvania", "recently_added": "NedÃĄvno pridanÊ", "recently_added_page_title": "NedÃĄvno pridanÊ", @@ -1770,9 +1910,11 @@ "saved_settings": "Nastavenia boli uloÅženÊ", "say_something": "NapÃ­ÅĄte niečo", "scaffold_body_error_occurred": "Vyskytla sa chyba", + "scan": "SkenovaÅĨ", "scan_all_libraries": "PreskenovaÅĨ vÅĄetky kniÅžnice", "scan_library": "SkenovaÅĨ", "scan_settings": "Nastavenia skenovania", + "scanning": "Skenovanie", "scanning_for_album": "Skenujem pre album...", "search": "HÄžadaÅĨ", "search_albums": "HÄžadaÅĨ albumy", @@ -1782,7 +1924,7 @@ "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_by_ocr": "HÄžadaÅĨ podÄža OCR", - "search_by_ocr_example": "Latte", + "search_by_ocr_example": "LattÊ", "search_camera_lens_model": "HÄžadaÅĨ model objektívu...", "search_camera_make": "HÄžadaÅĨ značku fotoaparÃĄtu...", "search_camera_model": "HÄžadaÅĨ model fotoaparÃĄtu...", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Vyberte typ mÊdia", "search_filter_ocr": "HÄžadaÅĨ podÄža OCR", "search_filter_people_title": "Vyberte Äžudí", + "search_filter_star_rating": "Hodnotenie hviezdičkami", "search_for": "VyhÄžadaÅĨ", "search_for_existing_person": "HÄžadaÅĨ existujÃēcu osobu", "search_no_more_result": "ÅŊiadne ďalÅĄie vÃŊsledky", @@ -1836,17 +1979,23 @@ "second": "Sekundy", "see_all_people": "PozrieÅĨ vÅĄetky osoby", "select": "VybraÅĨ", + "select_album": "VybraÅĨ album", "select_album_cover": "Vyberte obal albumu", + "select_albums": "VybraÅĨ albumy", "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_count": "{count, plural, one {VybraÅĨ #} other {VybraÅĨ #}}", + "select_cutoff_date": "VybraÅĨ cieÄžovÃŊ dÃĄtum", "select_face": "Vyberte tvÃĄr", "select_featured_photo": "Vyberte nÃĄhÄžadovÃē fotku", "select_from_computer": "VybraÅĨ z počítača", "select_keep_all": "VybraÅĨ ponechaÅĨ vÅĄetky", "select_library_owner": "VybraÅĨ vlastníka kniÅžnice", "select_new_face": "VybraÅĨ novÃē tvÃĄr", + "select_people": "VybraÅĨ osoby", + "select_person": "VybraÅĨ osobu", "select_person_to_tag": "Vyberte osobu, ktorÃē chcete označiÅĨ", "select_photos": "VybraÅĨ fotky", "select_trash_all": "VybraÅĨ zahodiÅĨ vÅĄetky", @@ -1938,7 +2087,7 @@ "shared_link_edit_expire_after_option_year": "{count} roky", "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_error_server_url_fetch": "Nie je moÅžnÊ načítaÅĨ URL adresu servera", "shared_link_expires_day": "VyprÅĄÃ­ o {count} deň", "shared_link_expires_days": "VyprÅĄÃ­ o {count} dní", "shared_link_expires_hour": "VyprÅĄÃ­ o {count} hodinu", @@ -1982,6 +2131,7 @@ "show_password": "ZobraziÅĨ heslo", "show_person_options": "ZobraziÅĨ moÅžnosti osoby", "show_progress_bar": "ZobraziÅĨ ukazovateÄž priebehu", + "show_schema": "ZobraziÅĨ schÊmu", "show_search_options": "ZobraziÅĨ moÅžnosti vyhÄžadÃĄvania", "show_shared_links": "ZobraziÅĨ zdieÄžanÊ odkazy", "show_slideshow_transition": "ZobraziÅĨ prechody v prezentÃĄcii", @@ -1999,6 +2149,8 @@ "skip_to_folders": "PreskočiÅĨ do priečinkov", "skip_to_tags": "PreskočiÅĨ ku ÅĄtítkom", "slideshow": "PrezentÃĄcia", + "slideshow_repeat": "OpakovaÅĨ prezentÃĄciu", + "slideshow_repeat_description": "Po skončení prezentÃĄcie sa vrÃĄtiÅĨ späÅĨ na začiatok", "slideshow_settings": "Nastavenia prezentÃĄcie", "sort_albums_by": "ZoradiÅĨ albumy podÄža...", "sort_created": "DÃĄtum vytvorenia", @@ -2032,12 +2184,13 @@ "storage_quota": "ÚloÅžnÃŊ limit", "storage_usage": "VyuÅžitÃŊch {used} z {available}", "submit": "OdoslaÅĨ", - "success": "Úspech", + "success": "Hotovo", "suggestions": "NÃĄvrhy", "sunrise_on_the_beach": "VÃŊchod slnka na plÃĄÅži", "support": "Podpora", "support_and_feedback": "Podpora a spätnÃĄ väzba", "support_third_party_description": "VaÅĄa inÅĄtalÃĄcia Immich bola pripravenÃĄ treÅĨou stranou. ProblÊmy, ktorÊ sa vyskytli, môŞu byÅĨ spôsobenÊ tÃŊmto balíčkom, preto sa na nich obrÃĄÅĨte v prvom rade cez nasledujÃēce odkazy.", + "supporter": "PodporovateÄž", "swap_merge_direction": "VymeniÅĨ smer zlÃēčenia", "sync": "SynchronizovaÅĨ", "sync_albums": "SynchronizovaÅĨ albumy", @@ -2075,6 +2228,7 @@ "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", + "then": "Potom", "they_will_be_merged_together": "ZlÃēčia sa dokopy", "third_party_resources": "Zdroje tretích strÃĄn", "time": "Čas", @@ -2109,6 +2263,13 @@ "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}}.", + "trigger": "SpÃēÅĄÅĨač", + "trigger_asset_uploaded": "PoloÅžky boli nahranÊ", + "trigger_asset_uploaded_description": "Spustí sa pri nahratí novej poloÅžky", + "trigger_description": "UdalosÅĨ, ktorÃĄ spustí pracovnÃŊ postup", + "trigger_person_recognized": "Osoba bola rozpoznanÃĄ", + "trigger_person_recognized_description": "Spustí sa, keď bude objavenÃĄ osoba", + "trigger_type": "Typ spÃēÅĄÅĨača", "troubleshoot": "RieÅĄenie problÊmov", "type": "Typ", "unable_to_change_pin_code": "Nie je moÅžnÊ zmeniÅĨ PIN kÃŗd", @@ -2123,6 +2284,7 @@ "unhide_person": "Znovu zobraziÅĨ osobu", "unknown": "NeznÃĄme", "unknown_country": "NeznÃĄma krajina", + "unknown_date": "NeznÃĄmy dÃĄtum", "unknown_year": "NeznÃĄmy rok", "unlimited": "NeobmedzenÊ", "unlink_motion_video": "OdpojiÅĨ pohyblivÊ video", @@ -2139,17 +2301,19 @@ "unstack": "ZruÅĄiÅĨ zoskupenie", "unstack_action_prompt": "{count} nezoskupenÃŊch", "unstacked_assets_count": "ZruÅĄenÊ zoskupenia pre {count, plural, one {# poloÅžku} few {# poloÅžky} other {# poloÅžiek}}", + "unsupported_field_type": "NepodporovanÃŊ typ poÄža", "untagged": "Bez ÅĄtítku", + "untitled_workflow": "PracovnÃŊ postup bez nÃĄzvu", "up_next": "To je vÅĄetko", "update_location_action_prompt": "AktualizovaÅĨ polohu {count} vybranÃŊch poloÅžiek pomocou:", "updated_at": "AktualizovanÊ", "updated_password": "Heslo zmenenÊ", "upload": "NahraÅĨ", - "upload_action_prompt": "{count} v poradí na nahratie", "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_error_with_count": "Chyba pri nahrÃĄvaní {count, plural, one {# poloÅžky} few {# poloÅžiek} other {# poloÅžiek}}", "upload_errors": "NahrÃĄvanie ukončenÊ s {count, plural, one {# chybou} other {# chybami}}, obnovte strÃĄnku, aby sa zobrazili novÊ poloÅžky.", "upload_finished": "NahrÃĄvanie dokončenÊ", "upload_progress": "OstÃĄva {remaining, number} - SpracovanÃŊch {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "Odkaz URL", "usage": "PouÅžitie", "use_biometric": "PouÅžiÅĨ biometrickÊ Ãēdaje", - "use_current_connection": "pouÅžiÅĨ aktuÃĄlne pripojenie", + "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ÃŊ.", @@ -2185,6 +2349,7 @@ "utilities": "NÃĄstroje", "validate": "OveriÅĨ", "validate_endpoint_error": "Zadajte prosím platnÃē URL adresu", + "validation_error": "Chyba overenia", "variables": "PremennÊ", "version": "Verzia", "version_announcement_closing": "Tvoj kamarÃĄt, Alex", @@ -2196,6 +2361,7 @@ "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í}}", + "videos_only": "Iba videÃĄ", "view": "Zobrazenie", "view_album": "ZobraziÅĨ Album", "view_all": "ZobraziÅĨ vÅĄetky", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "PouÅžiÅĨ ako hlavnÃē fotku", "viewer_unstack": "ZruÅĄiÅĨ zoskupenie", "visibility_changed": "ViditeÄžnosÅĨ zmenenÃĄ pre {count, plural, one {# osobu} few {# osoby} other {# osôb}}", + "visual": "VizuÃĄlny", + "visual_builder": "VizuÃĄlny nÃĄstroj na tvorbu", "waiting": "ČakajÃēce", "waiting_count": "V poradí: {count}", "warning": "Varovanie", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "Vitajte v Immich", "width": "Šírka", "wifi_name": "NÃĄzov Wi-Fi", - "workflow": "PracovnÃŊ postup", + "workflow_delete_prompt": "Naozaj chcete odstrÃĄniÅĨ tento pracovnÃŊ postup?", + "workflow_deleted": "PracovnÃŊ postup bol vymazanÃŊ", + "workflow_description": "Popis pracovnÊho postupu", + "workflow_info": "InformÃĄcie o pracovnom postupe", + "workflow_json": "PracovnÃŊ postup JSON", + "workflow_json_help": "Upravte konfigurÃĄciu pracovnÊho postupu vo formÃĄte JSON. Zmeny sa synchronizujÃē s vizuÃĄlnym nÃĄstrojom na tvorbu.", + "workflow_name": "NÃĄzov pracovnÊho postupu", + "workflow_navigation_prompt": "Naozaj chcete odísÅĨ bez uloÅženia zmien?", + "workflow_summary": "SÃēhrn pracovnÊho postupu", + "workflow_update_success": "PracovnÃŊ postup bol ÃēspeÅĄne aktualizovanÃŊ", + "workflow_updated": "PracovnÃŊ postup bol aktualizovanÃŊ", + "workflows": "PracovnÊ postupy", + "workflows_help_text": "PracovnÊ postupy automatizujÃē akcie tÃŊkajÃēce sa vaÅĄich poloÅžiek na zÃĄklade spÃēÅĄÅĨačov a filtrov", "wrong_pin_code": "NesprÃĄvny PIN kÃŗd", "year": "Rok", "years_ago": "pred {years, plural, one {# rokom} other {# rokmi}}", "yes": "Áno", "you_dont_have_any_shared_links": "NemÃĄte Åžiadne zdielanÊ odkazy", "your_wifi_name": "VÃĄÅĄ nÃĄzov siete Wi-Fi", + "zero_to_clear_rating": "stlačte 0 pre vyčistenie hodnotenia poloÅžky", "zoom_image": "PriblíŞiÅĨ obrÃĄzok", "zoom_to_bounds": "ZvÃ¤ÄÅĄiÅĨ na okraje" } diff --git a/i18n/sl.json b/i18n/sl.json index 0ff0bec8ca..b4f899bb4d 100644 --- a/i18n/sl.json +++ b/i18n/sl.json @@ -5,6 +5,7 @@ "acknowledge": "Sem seznanjen", "action": "Dejanje", "action_common_update": "Posodobi", + "action_description": "Nabor dejanj, ki jih je treba izvesti na filtriranih sredstvih", "actions": "Dejanja", "active": "Aktivno", "active_count": "Aktivno: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Dodaj lokacijo", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_action": "Dodaj dejanje", + "add_action_description": "Kliknite, če Åželite dodati dejanje, ki ga Åželite izvesti", + "add_assets": "Dodaj sredstva", "add_birthday": "Dodaj rojstni dan", "add_endpoint": "Dodaj končno točko", "add_exclusion_pattern": "Dodaj vzorec izključitve", + "add_filter": "Dodaj filter", + "add_filter_description": "Kliknite za dodajanje pogoja filtra", "add_location": "Dodaj lokacijo", "add_more_users": "Dodaj več uporabnikov", "add_partner": "Dodaj partnerja", @@ -36,6 +42,7 @@ "add_to_shared_album": "Dodaj k deljenemu albumu", "add_upload_to_stack": "Dodaj nalaganje v sklad", "add_url": "Dodaj URL", + "add_workflow_step": "Dodaj korak poteka dela", "added_to_archive": "Dodano v arhiv", "added_to_favorites": "Dodano med priljubljene", "added_to_favorites_count": "{count, number} dodanih med priljubljene", @@ -97,6 +104,8 @@ "image_preview_description": "Slika srednje velikosti z odstranjenimi metapodatki, ki se uporablja pri ogledu posameznega sredstva in za strojno učenje", "image_preview_quality_description": "Kakovost predogleda od 1-100. ViÅĄje je boljÅĄe, vendar ustvarja večje datoteke in lahko zmanjÅĄa odzivnost aplikacije. Nastavitev nizke vrednosti lahko vpliva na kakovost strojnega učenja.", "image_preview_title": "Nastavitve predogleda", + "image_progressive": "Napredno", + "image_progressive_description": "Za postopno nalaganje slik JPEG kodirajte postopoma. To ne vpliva na slike WebP.", "image_quality": "Kvaliteta", "image_resolution": "Resolucija", "image_resolution_description": "ViÅĄje ločljivosti lahko ohranijo več podrobnosti, vendar kodiranje traja dlje, imajo večje velikosti datotek in lahko zmanjÅĄajo odzivnost aplikacije.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Omogoči pametno iskanje", "machine_learning_smart_search_enabled_description": "Če je onemogočeno, slike ne bodo kodirane za pametno iskanje.", "machine_learning_url_description": "URL streÅžnika za strojno učenje. Če je na voljo več kot en URL, bo vsak streÅžnik poskusen posamično, dokler se eden ne odzove uspeÅĄno, v vrstnem redu od prvega do zadnjega. StreÅžniki, ki se ne odzovejo, bodo začasno prezrti, dokler se spet ne vzpostavijo.", + "maintenance_delete_backup": "IzbriÅĄi varnostno kopijo", + "maintenance_delete_backup_description": "Ta datoteka bo nepreklicno izbrisana.", + "maintenance_delete_error": "Varnostne kopije ni bilo mogoče izbrisati.", + "maintenance_restore_backup": "Obnovi varnostno kopijo", + "maintenance_restore_backup_description": "Immich bo izbrisan in obnovljen iz izbrane varnostne kopije. Pred nadaljevanjem bo ustvarjena varnostna kopija.", + "maintenance_restore_backup_different_version": "Ta varnostna kopija je bila ustvarjena z drugačno različico programa Immich!", + "maintenance_restore_backup_unknown_version": "Varnostne različice ni bilo mogoče določiti.", + "maintenance_restore_database_backup": "Obnovi varnostno kopijo baze podatkov", + "maintenance_restore_database_backup_description": "Povrnitev na prejÅĄnje stanje baze podatkov z uporabo varnostne kopije", "maintenance_settings": "VzdrÅževanje", "maintenance_settings_description": "Preklopite Immich v vzdrÅževalni način.", - "maintenance_start": "ZaÅženi način vzdrÅževanja", + "maintenance_start": "Preklopi v način vzdrÅževanja", "maintenance_start_error": "VzdrÅževalnega načina ni bilo mogoče zagnati.", + "maintenance_upload_backup": "NaloÅži datoteko varnostne kopije baze podatkov", + "maintenance_upload_backup_error": "Varnostne kopije ni bilo mogoče naloÅžiti. Ali gre za datoteko .sql/.sql.gz?", "manage_concurrency": "Upravljanje sočasnosti", "manage_concurrency_description": "Pomaknite se na stran z opravili, da upravljate sočasnost opravil", "manage_log_settings": "Upravljanje nastavitev dnevnika", @@ -252,7 +272,7 @@ "oauth_auto_register": "Samodejna registracija", "oauth_auto_register_description": "Samodejna registracija novih uporabnikov po prijavi z OAuth", "oauth_button_text": "Besedilo gumba", - "oauth_client_secret_description": "Zahtevano, če ponudnik OAuth ne podpira PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Zahtevano za zaupnega odjemalca ali če PKCE (dokazni ključ za izmenjavo kode) ni podprt za javnega odjemalca.", "oauth_enable_description": "Prijava z OAuth", "oauth_mobile_redirect_uri": "Mobilni preusmeritveni URI", "oauth_mobile_redirect_uri_override": "Preglasitev URI preusmeritve za mobilne naprave", @@ -291,7 +311,7 @@ "search_jobs": "IÅĄÄi opravilaâ€Ļ", "send_welcome_email": "PoÅĄlji pozdravno e-poÅĄto", "server_external_domain_settings": "Zunanja domena", - "server_external_domain_settings_description": "Domena za javne skupne povezave, vključno s http(s)://", + "server_external_domain_settings_description": "Domena, uporabljena za zunanje povezave", "server_public_users": "Javni uporabniki", "server_public_users_description": "Vsi uporabniki (ime in e-poÅĄta) so navedeni pri dodajanju uporabnika v albume v skupni rabi. Ko je onemogočen, bo seznam uporabnikov na voljo samo skrbniÅĄkim uporabnikom.", "server_settings": "Nastavitve streÅžnika", @@ -431,6 +451,9 @@ "admin_password": "SkrbniÅĄko geslo", "administration": "Administracija", "advanced": "Napredno", + "advanced_settings_clear_image_cache": "Počisti predpomnilnik slik", + "advanced_settings_clear_image_cache_error": "Brisanje predpomnilnika slik ni uspelo", + "advanced_settings_clear_image_cache_success": "UspeÅĄno počiÅĄÄeno {size}", "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}", @@ -467,10 +490,12 @@ "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_selected": "Izbran album", "album_share_no_users": "Videti je, da ste ta album dali v skupno rabo z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi ga lahko delili.", "album_summary": "Povzetek albuma", "album_updated": "Album posodobljen", "album_updated_setting_description": "Prejmite e-poÅĄtno obvestilo, ko ima album v skupni rabi nova sredstva", + "album_upload_assets": "NaloÅžite sredstva iz računalnika in jih dodajte v album", "album_user_left": "Zapustil {album}", "album_user_removed": "Odstranjen {user}", "album_viewer_appbar_delete_confirm": "Ali ste prepričani, da Åželite izbrisati ta album iz svojega računa?", @@ -488,9 +513,11 @@ "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})", + "albums_selected": "{count, plural, one {izbran # album} two {izbrana # albuma} few {izbrani # albumi} other {izbranih # albumov}}", "all": "Vse", "all_albums": "Vsi albumi", "all_people": "Vsi ljudje", + "all_photos": "Vse fotografije", "all_videos": "Vsi videi", "allow_dark_mode": "Dovoli temni način", "allow_edits": "Dovoli urejanja", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Dovolite javnemu uporabniku nalaganje", "allowed": "Dovoljeno", "alt_text_qr_code": "Slika QR kode", + "always_keep": "Vedno ohrani", + "always_keep_photos_hint": "S funkcijo \"Sprosti prostor\" bodo vse fotografije shranjene v tej napravi.", + "always_keep_videos_hint": "S funkcijo \"Sprosti prostor\" bodo vsi videoposnetki shranjeni v tej napravi.", "anti_clockwise": "V nasprotni smeri urinega kazalca", "api_key": "API ključ", "api_key_description": "Ta vrednost bo prikazana samo enkrat. Ne pozabite jo kopirati, preden zaprete okno.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, one {# arhiviran} two {# arhivirana} few {# arhivirani} other {# arhiviranih}}", "are_these_the_same_person": "Ali je to ista oseba?", "are_you_sure_to_do_this": "Ste prepričani, da Åželite to narediti?", + "array_field_not_fully_supported": "Polja matrike zahtevajo ročno urejanje JSON", "asset_action_delete_err_read_only": "Sredstev samo za branje ni mogoče izbrisati, preskočim", "asset_action_share_err_offline": "Ni mogoče pridobiti sredstev brez povezave, preskočim", "asset_added_to_album": "Dodano v album", "asset_adding_to_album": "Dodajanje v albumâ€Ļ", + "asset_created": "Sredstvo ustvarjeno", "asset_description_updated": "Opis sredstva je posodobljen", "asset_filename_is_offline": "Sredstvo {filename} je brez povezave", "asset_has_unassigned_faces": "Sredstvo ima nedodeljene obraze", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Postavitev", "asset_list_settings_subtitle": "Nastavitve postavitve mreÅže fotografij", "asset_list_settings_title": "MreÅža fotografij", + "asset_not_found_on_device_android": "Sredstva ni bilo mogoče najti v napravi", + "asset_not_found_on_device_ios": "Sredstva ni bilo mogoče najti v napravi. Če uporabljate iCloud, sredstvo morda ni dostopno zaradi napačne datoteke, shranjene v iCloudu", + "asset_not_found_on_icloud": "Sredstva ni bilo mogoče najti v iCloudu. Sredstvo morda ni dostopno zaradi napačne datoteke, shranjene v iCloudu", "asset_offline": "Sredstvo brez povezave", "asset_offline_description": "Tega zunanjega sredstva ni več mogoče najti na disku. Za pomoč kontaktirajte Immich skrbnika.", "asset_restored_successfully": "Sredstvo uspeÅĄno obnovljeno", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Gesli se ne ujemata", "change_password_form_reenter_new_password": "Znova vnesi novo geslo", "change_pin_code": "Spremeni PIN kodo", + "change_trigger": "Spremeni sproÅžilec", + "change_trigger_prompt": "Ali ste prepričani, da Åželite spremeniti sproÅžilec? S tem boste odstranili vsa obstoječa dejanja in filtre.", "change_your_password": "Spremenite geslo", "changed_visibility_successfully": "UspeÅĄno spremenjena vidnost", "charging": "Polnjenje", @@ -722,6 +759,18 @@ "checksum": "Kontrolna vsota", "choose_matching_people_to_merge": "Izberite ujemajoče se osebe za zdruÅžitev", "city": "Mesto", + "cleanup_confirm_description": "Immich je naÅĄel {count} sredstev (ustvarjenih pred {date}), ki so varno varnostno shranjena na streÅžniku. Ali ÅželiÅĄ odstraniti lokalne kopije iz te naprave?", + "cleanup_confirm_prompt_title": "Odstrani iz te naprave?", + "cleanup_deleted_assets": "{count} sredstev premaknjenih v koÅĄ", + "cleanup_deleting": "Premikanje v koÅĄ...", + "cleanup_found_assets": "Najdenih je bilo {count} varnostno kopiranih sredstev", + "cleanup_found_assets_with_size": "Najdenih {count} varnostno kopiranih sredstev ({size})", + "cleanup_icloud_shared_albums_excluded": "Skupni albumi iCloud so izključeni iz skeniranja", + "cleanup_no_assets_found": "Ni najdenih sredstev, ki bi ustrezala zgornjim kriterijem. Funkcija \"Sprosti prostor\" lahko odstrani samo sredstva, ki so bila varnostno kopirana na streÅžnik", + "cleanup_preview_title": "Sredstva za odstranitev ({count})", + "cleanup_step3_description": "PoiÅĄÄite varnostne kopije sredstev, ki ustrezajo vaÅĄemu datumu, in ohranite nastavitve.", + "cleanup_step4_summary": "{count} {count, plural, one {element (ustvarjen} two {elementa (ustvarjena} few {elementi (ustvarjeni} other {elementov (ustvarjenih}} pred {date}) za odstranitev iz vaÅĄe lokalne naprave. Fotografije bodo ÅĄe naprej dostopne iz aplikacije Immich.", + "cleanup_trash_hint": "Če Åželite v celoti sprostiti prostor za shranjevanje, odprite aplikacijo sistemske galerije in izpraznite koÅĄ", "clear": "Počisti", "clear_all": "Počisti vse", "clear_all_recent_searches": "Počisti vsa nedavna iskanja", @@ -733,6 +782,8 @@ "client_cert_import": "Uvozi", "client_cert_import_success_msg": "Potrdilo odjemalca je uvoÅženo", "client_cert_invalid_msg": "Neveljavna datoteka potrdila ali napačno geslo", + "client_cert_password_message": "Vnesite geslo za to potrdilo", + "client_cert_password_title": "Geslo potrdila", "client_cert_remove_msg": "Potrdilo odjemalca je odstranjeno", "client_cert_subtitle": "Podpira samo format PKCS12 (.p12, .pfx). Uvoz/odstranitev potrdila je na voljo samo pred prijavo", "client_cert_title": "Potrdilo odjemalca SSL [POSKUSNO]", @@ -743,6 +794,11 @@ "color": "Barva", "color_theme": "Barva teme", "command": "Ukaz", + "command_palette_prompt": "Hitro iskanje strani, dejanj ali ukazov", + "command_palette_to_close": "zapreti", + "command_palette_to_navigate": "vstopiti", + "command_palette_to_select": "izbrati", + "command_palette_to_show_all": "prikazati vse", "comment_deleted": "Komentar izbrisan", "comment_options": "MoÅžnosti komentiranja", "comments_and_likes": "Komentarji in vÅĄečki", @@ -787,6 +843,7 @@ "create_album": "Ustvari album", "create_album_page_untitled": "Brez naslova", "create_api_key": "Ustvari API ključ", + "create_first_workflow": "Ustvari prvi potek dela", "create_library": "Ustvari knjiÅžnico", "create_link": "Ustvari povezavo", "create_link_to_share": "Ustvari povezavo za skupno rabo", @@ -801,17 +858,25 @@ "create_tag": "Ustvari oznako", "create_tag_description": "Ustvarite novo oznako. Za ugnezdene oznake vnesite celotno pot oznake, vključno s poÅĄevnicami.", "create_user": "Ustvari uporabnika", + "create_workflow": "Ustvari potek dela", "created": "Ustvarjeno", "created_at": "Ustvarjeno", "creating_linked_albums": "Ustvarjanje povezanih albumov ...", "crop": "Obrezovanje", + "crop_aspect_ratio_fixed": "Fiksno", + "crop_aspect_ratio_free": "Poljubno", + "crop_aspect_ratio_original": "Izvirno", "curated_object_page_title": "Stvari", "current_device": "Trenutna naprava", "current_pin_code": "Trenutna PIN koda", "current_server_address": "Trenutni naslov streÅžnika", + "custom_date": "Datum po meri", "custom_locale": "Jezik po meri", "custom_locale_description": "Oblikujte datume in ÅĄtevilke glede na jezik in regijo", "custom_url": "URL po meri", + "cutoff_date_description": "Shranite fotografije iz zadnjegaâ€Ļ", + "cutoff_day": "{count, plural, one {dan} other {dni}}", + "cutoff_year": "{count, plural, one {leto} two {leti} few {leta} other {let}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "dark": "Temno", @@ -867,6 +932,7 @@ "deselect_all": "Prekliči vse", "details": "Podrobnosti", "direction": "Usmeritev", + "disable": "Onemogoči", "disabled": "Onemogočeno", "disallow_edits": "Onemogoči urejanje", "discord": "Discord", @@ -892,6 +958,7 @@ "download_include_embedded_motion_videos": "Vdelani videoposnetki", "download_include_embedded_motion_videos_description": "Videoposnetke, vdelane v fotografije gibanja, vključite kot ločeno datoteko", "download_notfound": "Prenosa ni bilo mogoče najti", + "download_original": "Prenesi izvirnik", "download_paused": "Prenos zaustavljen", "download_settings": "Prenos", "download_settings_description": "Upravljajte nastavitve, povezane s prenosom sredstev", @@ -901,6 +968,7 @@ "download_waiting_to_retry": "Čakam na ponovni poskus", "downloading": "PrenaÅĄanje", "downloading_asset_filename": "PrenaÅĄanje sredstva {filename}", + "downloading_from_icloud": "Prenos iz iClouda", "downloading_media": "PrenaÅĄanje medijev", "drop_files_to_upload": "Spustite datoteke kamor koli, da jih naloÅžite", "duplicates": "Dvojniki", @@ -929,11 +997,22 @@ "edit_tag": "Uredi oznako", "edit_title": "Uredi naslov", "edit_user": "Uredi uporabnika", + "edit_workflow": "Urejanje poteka dela", "editor": "Urejevalnik", "editor_close_without_save_prompt": "Spremembe ne bodo shranjene", "editor_close_without_save_title": "Zapri urejevalnik?", - "editor_crop_tool_h2_aspect_ratios": "Razmerja stranic", - "editor_crop_tool_h2_rotation": "Vrtenje", + "editor_confirm_reset_all_changes": "Ali ste prepričani, da Åželite ponastaviti vse spremembe?", + "editor_discard_edits_confirm": "Zavrzi urejanja", + "editor_discard_edits_prompt": "Imate neshranjene spremembe. Ste prepričani, da jih Åželite zavreči?", + "editor_discard_edits_title": "ZavrÅžem urejanja?", + "editor_edits_applied_error": "Urejanja ni bilo mogoče uporabiti", + "editor_edits_applied_success": "Spremembe so bile uspeÅĄno uporabljene", + "editor_flip_horizontal": "Obrni vodoravno", + "editor_flip_vertical": "Obrni navpično", + "editor_orientation": "Usmerjenost", + "editor_reset_all_changes": "Ponastavi spremembe", + "editor_rotate_left": "Zavrtite za 90° v levo", + "editor_rotate_right": "Zavrtite za 90° v desno", "email": "E-poÅĄta", "email_notifications": "Obvestila po e-poÅĄti", "empty_folder": "Ta mapa je prazna", @@ -952,11 +1031,14 @@ "error_change_sort_album": "Vrstnega reda albuma ni bilo mogoče spremeniti", "error_delete_face": "Napaka pri brisanju obraza iz sredstva", "error_getting_places": "Napaka pri pridobivanju mest", + "error_loading_albums": "Napaka pri nalaganju albumov", "error_loading_image": "Napaka pri nalaganju slike", "error_loading_partners": "Napaka pri nalaganju partnerjev: {error}", + "error_retrieving_asset_information": "Napaka pri pridobivanju podatkov o sredstvu", "error_saving_image": "Napaka: {error}", "error_tag_face_bounding_box": "Napaka pri označevanju obraza - ni mogoče pridobiti koordinat omejevalnega okvirja", "error_title": "Napaka - nekaj je ÅĄlo narobe", + "error_while_navigating": "Napaka pri navigaciji do sredstva", "errors": { "cannot_navigate_next_asset": "Ni mogoče krmariti do naslednjega sredstva", "cannot_navigate_previous_asset": "Ni mogoče krmariti na prejÅĄnje sredstvo", @@ -1014,6 +1096,7 @@ "unable_to_complete_oauth_login": "Prijave OAuth ni mogoče dokončati", "unable_to_connect": "Ni mogoče vzpostaviti povezave", "unable_to_copy_to_clipboard": "Ni mogoče kopirati v odloÅžiÅĄÄe, preverite, ali dostopate do strani prek https", + "unable_to_create": "Ni mogoče ustvariti poteka dela", "unable_to_create_admin_account": "Ni mogoče ustvariti skrbniÅĄkega računa", "unable_to_create_api_key": "Ni mogoče ustvariti novega API ključa", "unable_to_create_library": "Ni mogoče ustvariti knjiÅžnice", @@ -1024,6 +1107,7 @@ "unable_to_delete_exclusion_pattern": "Vzorca izključitve ni mogoče izbrisati", "unable_to_delete_shared_link": "Povezave v skupni rabi ni mogoče izbrisati", "unable_to_delete_user": "Uporabnika ni mogoče izbrisati", + "unable_to_delete_workflow": "Poteka dela ni mogoče izbrisati", "unable_to_download_files": "Ni mogoče prenesti datotek", "unable_to_edit_exclusion_pattern": "Vzorca izključitve ni mogoče urediti", "unable_to_empty_trash": "Smetnjaka ni mogoče izprazniti", @@ -1063,6 +1147,7 @@ "unable_to_scan_library": "KnjiÅžnice ni mogoče pregledati", "unable_to_set_feature_photo": "Ni mogoče nastaviti glavne fotografije", "unable_to_set_profile_picture": "Profilne slike ni mogoče nastaviti", + "unable_to_set_rating": "Ocene ni mogoče nastaviti", "unable_to_submit_job": "Naloga ni mogoče oddati", "unable_to_trash_asset": "Sredstva ni mogoče odstraniti v smetnjak", "unable_to_unlink_account": "Povezave računa ni mogoče prekiniti", @@ -1074,8 +1159,10 @@ "unable_to_update_settings": "Nastavitev ni mogoče posodobiti", "unable_to_update_timeline_display_status": "Ni mogoče posodobiti stanja prikaza časovnice", "unable_to_update_user": "Uporabnika ni mogoče posodobiti", + "unable_to_update_workflow": "Poteka dela ni mogoče posodobiti", "unable_to_upload_file": "Datoteke ni mogoče naloÅžiti" }, + "errors_text": "Napake", "exclusion_pattern": "Vzorec izključitve", "exif": "Exif", "exif_bottom_sheet_description": "Dodaj opis..", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "OSEBE", "exif_bottom_sheet_person_add_person": "Dodaj ime", "exit_slideshow": "Zapustite diaprojekcijo", + "expand": "RazÅĄiri", "expand_all": "RazÅĄiri vse", "experimental_settings_new_asset_list_subtitle": "Delo v teku", "experimental_settings_new_asset_list_title": "Omogoči eksperimentalno mreÅžo fotografij", @@ -1120,14 +1208,17 @@ "features": "Funkcije", "features_in_development": "Funkcije v razvoju", "features_setting_description": "Upravljaj funkcije aplikacije", - "file_name": "Ime datoteke", "file_name_or_extension": "Ime ali končnica datoteke", + "file_name_text": "Ime datoteke", + "file_name_with_value": "Ime datoteke: {file_name}", "file_size": "Velikost datoteke", "filename": "Ime datoteke", "filetype": "Vrsta datoteke", "filter": "Filter", + "filter_description": "Pogoji za filtriranje ciljnih sredstev", "filter_people": "Filtriraj ljudi", "filter_places": "Filtriraj kraje", + "filters": "Filtri", "find_them_fast": "Z iskanjem jih hitro poiÅĄÄite po imenu", "first": "Prvi", "fix_incorrect_match": "Popravi napačno ujemanje", @@ -1137,12 +1228,16 @@ "folders_feature_description": "Brskanje po pogledu mape za fotografije in videoposnetke v datotečnem sistemu", "forgot_pin_code_question": "Ste pozabili PIN?", "forward": "Naprej", + "free_up_space": "Sprostite prostor", + "free_up_space_description": "Varnostno kopirane fotografije in videoposnetke premaknite v koÅĄ v napravi, da sprostite prostor. VaÅĄe kopije na streÅžniku ostanejo varne.", + "free_up_space_settings_subtitle": "Sprostite prostor v napravi", "full_path": "Celotna pot: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Ta funkcija za delovanje nalaga zunanje vire iz Googla.", "general": "SploÅĄno", "geolocation_instruction_location": "Kliknite na sredstvo z GPS koordinatami, da uporabite njegovo lokacijo, ali pa izberite lokacijo neposredno na zemljevidu", "get_help": "PoiÅĄÄite pomoč", + "get_people_error": "Napaka pri pridobivanju oseb", "get_wifiname_error": "Imena Wi-Fi ni bilo mogoče dobiti. Prepričajte se, da ste podelili potrebna dovoljenja in ste povezani v omreÅžje Wi-Fi", "getting_started": "Začetek", "go_back": "Pojdi nazaj", @@ -1150,7 +1245,7 @@ "go_to_search": "Pojdi na iskanje", "gps": "GPS", "gps_missing": "Brez GPS-a", - "grant_permission": "Podeli dovoljenje", + "grant_permission": "Dodaj dovoljenje", "group_albums_by": "ZdruÅži albume po ...", "group_country": "ZdruÅži po drÅžavah", "group_no": "Brez zdruÅževanja", @@ -1175,6 +1270,7 @@ "hide_named_person": "Skrij osebo {name}", "hide_password": "Skrij geslo", "hide_person": "Skrij osebo", + "hide_schema": "Skrij shemo", "hide_text_recognition": "Skrij prepoznavanje besedila", "hide_unnamed_people": "Skrij osebe brez imen", "home_page_add_to_album_conflicts": "Dodanih {added} sredstev v album {album}. {failed} sredstev je Åže v albumu.", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "Obdelava je potekala {dateTime}", "items_count": "{count, plural, one {# predmet} two {# predmeta} few {# predmeti} other {# predmetov}}", "jobs": "Opravila", + "json_editor": "Urejevalnik JSON", + "json_error": "Napaka JSON", "keep": "ObdrÅži", + "keep_albums": "Ohrani albume", + "keep_albums_count": "Ohrani {count} {count, plural, one {album} two {albuma} few {albume} other {albumov}}", "keep_all": "ObdrÅži vse", + "keep_description": "Izberite, kaj ostane v napravi, ko sprostite prostor.", + "keep_favorites": "ObdrÅži priljubljene", + "keep_on_device": "Shrani v napravi", + "keep_on_device_hint": "Izberite elemente, ki jih Åželite shraniti v tej napravi", "keep_this_delete_others": "ObdrÅži to, izbriÅĄi ostalo", + "keeping": "Ohranjanje: {items}", "kept_this_deleted_others": "ObdrÅži to sredstvo in izbriÅĄi {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "keyboard_shortcuts": "BliÅžnjice na tipkovnici", "language": "Jezik", @@ -1343,10 +1448,28 @@ "loop_videos_description": "Omogočite samodejno ponavljanje videoposnetka v pregledovalniku podrobnosti.", "main_branch_warning": "Uporabljate razvojno različico; močno priporočamo uporabo izdajne različice!", "main_menu": "Glavni meni", + "maintenance_action_restore": "Obnavljanje baze podatkov", "maintenance_description": "Immich je bil preklopljen v vzdrÅževalni način.", "maintenance_end": "Konec vzdrÅževalnega načina", "maintenance_end_error": "VzdrÅževalnega načina ni bilo mogoče končati.", "maintenance_logged_in_as": "Trenutno prijavljen kot {user}", + "maintenance_restore_from_backup": "Obnovi iz varnostne kopije", + "maintenance_restore_library": "Obnovi svojo knjiÅžnico", + "maintenance_restore_library_confirm": "Če je to videti pravilno, nadaljujte z obnovitvijo varnostne kopije!", + "maintenance_restore_library_description": "Obnavljanje baze podatkov", + "maintenance_restore_library_folder_has_files": "{folder} ima {count, plural, one {# mapo} two {# mapi} few {# mape} other {# map}}", + "maintenance_restore_library_folder_no_files": "V mapi {folder} manjkajo datoteke!", + "maintenance_restore_library_folder_pass": "berljivo in zapisljivo", + "maintenance_restore_library_folder_read_fail": "ni berljivo", + "maintenance_restore_library_folder_write_fail": "ni zapisljivo", + "maintenance_restore_library_hint_missing_files": "Morda vam manjkajo pomembne datoteke", + "maintenance_restore_library_hint_regenerate_later": "Te lahko kasneje ponovno ustvarite v nastavitvah", + "maintenance_restore_library_hint_storage_template_missing_files": "Uporabljate predlogo za shranjevanje? Morda vam manjkajo datoteke", + "maintenance_restore_library_loading": "Nalaganje preverjanj integritete in hevristikâ€Ļ", + "maintenance_task_backup": "Ustvarjanje varnostne kopije obstoječe baze podatkovâ€Ļ", + "maintenance_task_migrations": "Izvajanje migracij baz podatkovâ€Ļ", + "maintenance_task_restore": "Obnavljanje izbrane varnostne kopijeâ€Ļ", + "maintenance_task_rollback": "Obnovitev ni uspela, vrnitev na obnovitveno točkoâ€Ļ", "maintenance_title": "Trenutno ni na voljo", "make": "Izdelava", "manage_geolocation": "Upravljanje lokacije", @@ -1408,6 +1531,8 @@ "minimize": "ZmanjÅĄaj", "minute": "minuta", "minutes": "Minute", + "mirror_horizontal": "Vodoravno", + "mirror_vertical": "Navpično", "missing": "manjka", "mobile_app": "Mobilna aplikacija", "mobile_app_download_onboarding_note": "Prenesite spremljevalno mobilno aplikacijo z uporabo naslednjih moÅžnosti", @@ -1416,11 +1541,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Več", "move": "Premakni", + "move_down": "Premakni navzdol", "move_off_locked_folder": "Premakni iz zaklenjene mape", "move_to": "Premakni v", + "move_to_device_trash": "Premakni v koÅĄ naprave", "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", + "move_up": "Premakni navzgor", "moved_to_archive": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v arhiv", "moved_to_library": "Premaknjeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} v knjiÅžnico", "moved_to_trash": "Premaknjeno v smetnjak", @@ -1430,6 +1558,7 @@ "my_albums": "Moji albumi", "name": "Ime", "name_or_nickname": "Ime ali vzdevek", + "name_required": "Ime je obvezno", "navigate": "Navigacija", "navigate_to_time": "Pomaknite se do časa", "network_requirement_photos_upload": "Uporaba mobilnih podatkov za varnostno kopiranje fotografij", @@ -1454,20 +1583,24 @@ "next": "Naslednji", "next_memory": "Naslednji spomin", "no": "Ne", + "no_actions_added": "Ni ÅĄe dodanih dejanj", + "no_albums_found": "Ni najdenih albumov", "no_albums_message": "Ustvarite album za organiziranje svojih fotografij in videoposnetkov", "no_albums_with_name_yet": "Videti je, da ÅĄe nimate nobenega albuma s tem imenom.", "no_albums_yet": "Videti je, da ÅĄe nimate nobenega albuma.", "no_archived_assets_message": "Arhivirajte fotografije in videoposnetke, da jih skrijete v pogledu fotografij", - "no_assets_message": "KLIKNITE ZA NALOÅŊITEV SVOJE PRVE FOTOGRAFIJE", + "no_assets_message": "Kliknite za nalaganje vaÅĄe prve fotografije", "no_assets_to_show": "Ni sredstev za prikaz", "no_cast_devices_found": "Naprav za predvajanje ni bilo mogoče najti", "no_checksum_local": "Kontrolna vsota ni na voljo – lokalnih sredstev ni mogoče pridobiti", "no_checksum_remote": "Kontrolna vsota ni na voljo – oddaljenega sredstva ni mogoče pridobiti", + "no_configuration_needed": "Konfiguracija ni potrebna", "no_devices": "Ni pooblaÅĄÄenih naprav", "no_duplicates_found": "Najden ni bil noben dvojnik.", "no_exif_info_available": "Podatki o exif niso na voljo", "no_explore_results_message": "NaloÅžite več fotografij, da raziÅĄÄete svojo zbirko.", "no_favorites_message": "Dodajte priljubljene, da hitreje najdete svoje najboljÅĄe slike in videoposnetke", + "no_filters_added": "Ni ÅĄe dodanih filtrov", "no_libraries_message": "Ustvarite zunanjo knjiÅžnico za ogled svojih fotografij in videoposnetkov", "no_local_assets_found": "S to kontrolno vsoto ni bilo najdenih lokalnih sredstev", "no_location_set": "Lokacija ni nastavljena", @@ -1481,15 +1614,15 @@ "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", + "none": "Nič", "not_allowed": "Ni dovoljeno", "not_available": "Ni na voljo", "not_in_any_album": "Ni v nobenem albumu", "not_selected": "Ni izbrano", - "note_apply_storage_label_to_previously_uploaded assets": "Opomba: Če Åželite oznako za shranjevanje uporabiti za predhodno naloÅžena sredstva, zaÅženite", "notes": "Opombe", "nothing_here_yet": "Tukaj ÅĄe ni ničesar", "notification_permission_dialog_content": "Če Åželite omogočiti obvestila, pojdite v Nastavitve in izberite Dovoli.", - "notification_permission_list_tile_content": "Izdaj dovoljenje za omogočanje obvestil.", + "notification_permission_list_tile_content": "Dodaj dovoljenje za poÅĄiljanje obvestil.", "notification_permission_list_tile_enable_button": "Omogoči obvestila", "notification_permission_list_tile_title": "Dovoljenje za obvestila", "notification_toggle_setting_description": "Omogoči e-poÅĄtna obvestila", @@ -1515,6 +1648,7 @@ "online": "Povezano", "only_favorites": "Samo priljubljene", "open": "Odpri", + "open_calendar": "Odpri koledar", "open_in_map_view": "Odpri v pogledu zemljevida", "open_in_openstreetmap": "Odpri v OpenStreetMap", "open_the_search_filters": "Odpri iskalne filtre", @@ -1563,6 +1697,7 @@ "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", + "people_selected": "{count, plural, one {izbrana # oseba} two {izbrani # osebi} few {izbrane # osebe} other {izbranih # oseb}}", "people_sidebar_description": "PrikaÅžite povezavo do Ljudje v stranski vrstici", "permanent_deletion_warning": "Opozorilo o trajnem izbrisu", "permanent_deletion_warning_setting_description": "PokaÅži opozorilo pri trajnem brisanju sredstev", @@ -1577,8 +1712,8 @@ "permission_onboarding_continue_anyway": "Vseeno nadaljuj", "permission_onboarding_get_started": "Začnimo", "permission_onboarding_go_to_settings": "Pojdite na nastavitve", - "permission_onboarding_permission_denied": "Dovoljenje zavrnjeno. Če Åželite uporabljati Immich, v nastavitvah podelite dovoljenja za fotografije in videoposnetke.", - "permission_onboarding_permission_granted": "Dovoljenje je izdano! Vse je pripravljeno.", + "permission_onboarding_permission_denied": "Dovoljenje zavrnjeno. Če Åželite uporabljati Immich, v nastavitvah dodajte dovoljenja za fotografije in videoposnetke.", + "permission_onboarding_permission_granted": "Dovoljenje ste dodali! Vse je pripravljeno.", "permission_onboarding_permission_limited": "Dovoljenje je omejeno. Če Åželite Immichu dovoliti varnostno kopiranje in upravljanje vaÅĄe celotne zbirke galerij, v nastavitvah podelite dovoljenja za fotografije in videoposnetke.", "permission_onboarding_request": "Immich potrebuje dovoljenje za ogled vaÅĄih fotografij in videoposnetkov.", "person": "Oseba", @@ -1587,11 +1722,14 @@ "person_age_years": "{years, plural, two {# leti} few {# leta} other {# let}} star/a", "person_birthdate": "Rojen dne {date}", "person_hidden": "{name}{hidden, select, true { (skrita)} other {}}", + "person_recognized": "Oseba prepoznana", + "person_selected": "Oseba izbrana", "photo_shared_all_users": "Videti je, da ste svoje fotografije delili z vsemi uporabniki ali pa nimate nobenega uporabnika, s katerim bi jih delili.", "photos": "Slike", "photos_and_videos": "Fotografije & videi", "photos_count": "{count, plural, one {{count, number} slika} two {{count, number} sliki} few {{count, number} slike} other {{count, number} slik}}", "photos_from_previous_years": "Fotografije iz prejÅĄnjih let", + "photos_only": "Samo fotografije", "pick_a_location": "Izberi lokacijo", "pick_custom_range": "Obseg po meri", "pick_date_range": "Izberi časovno obdobje", @@ -1667,10 +1805,12 @@ "purchase_settings_server_activated": "Ključ izdelka streÅžnika upravlja skrbnik", "query_asset_id": "ID sredstva poizvedbe", "queue_status": "Čakalna vrsta {count}/{total}", + "rate_asset": "Oceni sredstvo", "rating": "Ocena z zvezdicami", "rating_clear": "Počisti oceno", "rating_count": "{count, plural, one {# zvezdica} two {# zvezdici} few {# zvezdice} other {# zvezdic}}", "rating_description": "PrikaÅžite oceno EXIF v informacijski ploÅĄÄi", + "rating_set": "Ocena nastavljena na {rating, plural, one {# zvezdo} two {# zvezdi} few {# zvezde} other {# zvezd}}", "reaction_options": "MoÅžnosti reakcije", "read_changelog": "Preberi dnevnik sprememb", "readonly_mode_disabled": "Način samo za branje je onemogočen", @@ -1681,7 +1821,7 @@ "reassigned_assets_to_new_person": "Ponovno dodeljeno {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}} za novo osebo", "reassing_hint": "Dodeli izbrana sredstva obstoječi osebi", "recent": "Nedavno", - "recent-albums": "Zadnji albumi", + "recent_albums": "Zadnji albumi", "recent_searches": "Nedavna iskanja", "recently_added": "Nedavno dodano", "recently_added_page_title": "Nedavno dodano", @@ -1770,9 +1910,11 @@ "saved_settings": "Shranjene nastavitve", "say_something": "Reci kaj", "scaffold_body_error_occurred": "PriÅĄlo je do napake", + "scan": "Skeniraj", "scan_all_libraries": "Preglej vse knjiÅžnice", "scan_library": "Pregled", "scan_settings": "Nastavitve pregleda", + "scanning": "Skeniranje", "scanning_for_album": "Iskanje albuma...", "search": "Iskanje", "search_albums": "Iskanje albumov", @@ -1802,6 +1944,7 @@ "search_filter_media_type_title": "Izberi vrsto medija", "search_filter_ocr": "Iskanje po optičnem prepoznavanju znakov (OCR)", "search_filter_people_title": "Izberi osebe", + "search_filter_star_rating": "Ocena z zvezdicami", "search_for": "PoiÅĄÄi za", "search_for_existing_person": "Iskanje obstoječe osebe", "search_no_more_result": "Ni več rezultatov", @@ -1836,17 +1979,23 @@ "second": "Sekunda", "see_all_people": "Oglejte si vse ljudi", "select": "Izberi", + "select_album": "Izberi album", "select_album_cover": "Izberi naslovnico albuma", + "select_albums": "Izberi albume", "select_all": "Izberi vse", "select_all_duplicates": "Izberi vse dvojnike", "select_all_in": "Izberi vse v {group}", "select_avatar_color": "Izberi barvo avatarja", + "select_count": "{count, plural, one {# izbran} two {# izbrana} few {# izbrani} other {# izbranih}}", + "select_cutoff_date": "Izberite datum zaključka", "select_face": "Izberi obraz", "select_featured_photo": "Izberi predstavljeno fotografijo", "select_from_computer": "Izberi iz računalnika", "select_keep_all": "Izberi obdrÅži vse", "select_library_owner": "Izberi lastnika knjiÅžnice", "select_new_face": "Izberi nov obraz", + "select_people": "Izberi osebe", + "select_person": "Izberi osebo", "select_person_to_tag": "Izberite osebo, ki jo Åželite označiti", "select_photos": "Izberi fotografije", "select_trash_all": "Izberi vse v smetnjak", @@ -1982,6 +2131,7 @@ "show_password": "PrikaÅži geslo", "show_person_options": "PrikaÅži moÅžnosti osebe", "show_progress_bar": "PrikaÅži vrstico napredka", + "show_schema": "PrikaÅži shemo", "show_search_options": "PrikaÅži moÅžnosti iskanja", "show_shared_links": "PokaÅži povezave v skupni rabi", "show_slideshow_transition": "PrikaÅži prehod diaprojekcije", @@ -1999,6 +2149,8 @@ "skip_to_folders": "Preskoči na mape", "skip_to_tags": "Preskoči na oznake", "slideshow": "Diaprojekcija", + "slideshow_repeat": "Ponavljanje diaprojekcije", + "slideshow_repeat_description": "Po koncu diaprojekcije se zanka vrne na začetek", "slideshow_settings": "Nastavitve diaprojekcije", "sort_albums_by": "Razvrsti albume po...", "sort_created": "Datum nastanka", @@ -2038,6 +2190,7 @@ "support": "Podpora", "support_and_feedback": "Podpora in povratne informacije", "support_third_party_description": "VaÅĄo namestitev Immich je pakirala tretja oseba. TeÅžave, ki jih imate, lahko povzroči ta paket, zato prosimo, da teÅžave najprej izpostavite njim, tako da uporabite spodnje povezave.", + "supporter": "Podpornik", "swap_merge_direction": "Zamenjaj smer zdruÅževanja", "sync": "Sinhronizacija", "sync_albums": "Sinhronizacija albumov", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "Izberi nastavitev teme aplikacije", "theme_setting_three_stage_loading_subtitle": "Tristopenjsko nalaganje lahko poveča zmogljivost nalaganja, vendar povzroči znatno večjo obremenitev omreÅžja", "theme_setting_three_stage_loading_title": "Omogoči tristopenjsko nalaganje", + "then": "Potem", "they_will_be_merged_together": "ZdruÅženi bodo skupaj", "third_party_resources": "Viri tretjih oseb", "time": "Čas", @@ -2109,6 +2263,13 @@ "trash_page_select_assets_btn": "Izberite sredstva", "trash_page_title": "Smetnjak ({count})", "trashed_items_will_be_permanently_deleted_after": "Elementi v smetnjaku bodo trajno izbrisani po {days, plural, one {# dnevu} two {# dnevih} few {# dnevih} other {# dneh}}.", + "trigger": "SproÅžilec", + "trigger_asset_uploaded": "Sredstvo je naloÅženo", + "trigger_asset_uploaded_description": "SproÅži se ob nalaganju novega sredstva", + "trigger_description": "Dogodek, ki sproÅži delovni proces", + "trigger_person_recognized": "Oseba prepoznana", + "trigger_person_recognized_description": "SproÅži se, ko je zaznana oseba", + "trigger_type": "Vrsta sproÅžilca", "troubleshoot": "Odpravljanje teÅžav", "type": "Vrsta", "unable_to_change_pin_code": "PIN kode ni mogoče spremeniti", @@ -2123,6 +2284,7 @@ "unhide_person": "PrikaÅži osebo", "unknown": "Neznano", "unknown_country": "Neznana drÅžava", + "unknown_date": "Neznan datum", "unknown_year": "Neznano leto", "unlimited": "Neomejeno", "unlink_motion_video": "Prekini povezavo videoposnetka gibanja", @@ -2139,17 +2301,19 @@ "unstack": "Razklad", "unstack_action_prompt": "{count} razloÅženih", "unstacked_assets_count": "RazloÅži {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", + "unsupported_field_type": "Nepodprta vrsta polja", "untagged": "Neoznačeno", + "untitled_workflow": "Neimenovani potek dela", "up_next": "Naslednja", "update_location_action_prompt": "Posodobi lokacijo izbranih sredstev {count} s/z:", "updated_at": "Posodobljeno", "updated_password": "Posodobljeno geslo", "upload": "NaloÅži", - "upload_action_prompt": "{count} v čakalni vrsti za nalaganje", "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_error_with_count": "Napaka pri prilaganju za {count, plural, one {# sredstvo} two {# sredstvi} few {# sredstva} other {# sredstev}}", "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.", "upload_finished": "Nalaganje končano", "upload_progress": "Preostalo {remaining, number} - Obdelano {processed, number}/{total, number}", @@ -2164,7 +2328,7 @@ "url": "URL", "usage": "Uporaba", "use_biometric": "Uporabite biometrične podatke", - "use_current_connection": "uporabi trenutno povezavo", + "use_current_connection": "Uporabi trenutno povezavo", "use_custom_date_range": "Namesto tega uporabite časovno obdobje po meri", "user": "Uporabnik", "user_has_been_deleted": "Ta uporabnik je bil izbrisan.", @@ -2185,6 +2349,7 @@ "utilities": "Pripomočki", "validate": "Potrdi", "validate_endpoint_error": "Vnesite veljaven URL", + "validation_error": "Napaka pri preverjanju", "variables": "Spremenljivke", "version": "Različica", "version_announcement_closing": "Tvoj prijatelj, Alex", @@ -2196,6 +2361,7 @@ "video_hover_setting_description": "Predvajaj sličico videoposnetka, ko se miÅĄka pomakne nad element. Tudi ko je onemogočeno, lahko predvajanje začnete tako, da miÅĄkin kazalec premaknete nad ikono za predvajanje.", "videos": "Videoposnetki", "videos_count": "{count, plural, one {# video} two {# videa} few {# videi} other {# videov}}", + "videos_only": "Samo videoposnetki", "view": "Ogled", "view_album": "Ogled albuma", "view_all": "Poglej vse", @@ -2216,6 +2382,8 @@ "viewer_stack_use_as_main_asset": "Uporabi kot glavno sredstvo", "viewer_unstack": "Razkladi", "visibility_changed": "Vidnost spremenjena za {count, plural, one {# osebo} two {# osebi} few {# osebe} other {# oseb}}", + "visual": "Vizualno", + "visual_builder": "Vizualni graditelj", "waiting": "Čakanje", "waiting_count": "Čakanje: {count}", "warning": "Opozorilo", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "DobrodoÅĄli v Immich", "width": "Å irina", "wifi_name": "Wi-Fi ime", - "workflow": "Potek dela", + "workflow_delete_prompt": "Ali ste prepričani, da Åželite izbrisati ta potek dela?", + "workflow_deleted": "Potek dela izbrisan", + "workflow_description": "Opis poteka dela", + "workflow_info": "Informacije o poteku dela", + "workflow_json": "JSON poteka dela", + "workflow_json_help": "Uredite konfiguracijo poteka dela v formatu JSON. Spremembe se bodo sinhronizirale z vizualnim graditeljem.", + "workflow_name": "Ime poteka dela", + "workflow_navigation_prompt": "Ali ste prepričani, da Åželite zapustiti stran brez shranjevanja sprememb?", + "workflow_summary": "Povzetek poteka dela", + "workflow_update_success": "Potek dela je bil uspeÅĄno posodobljen", + "workflow_updated": "Potek dela posodobljen", + "workflows": "Poteki dela", + "workflows_help_text": "Poteki dela avtomatizirajo dejanja na vaÅĄih sredstvih na podlagi sproÅžilcev in filtrov", "wrong_pin_code": "Napačna PIN koda", "year": "Leto", "years_ago": "{years, plural, one {# leto} two {# leti} few {# leta} other {# let}} nazaj", "yes": "Da", "you_dont_have_any_shared_links": "Nimate nobenih skupnih povezav", "your_wifi_name": "VaÅĄe ime Wi-Fi", + "zero_to_clear_rating": "Pritisnite 0 za brisanje ocene sredstva", "zoom_image": "Povečava slike", "zoom_to_bounds": "Povečaj do meja" } diff --git a/i18n/sq.json b/i18n/sq.json index cd521122df..ba90837839 100644 --- a/i18n/sq.json +++ b/i18n/sq.json @@ -5,8 +5,10 @@ "acknowledge": "Prano", "action": "Aksion", "action_common_update": "PÃĢrditÃĢso", + "action_description": "NjÃĢ grup veprimesh pÃĢr t'u kryer nÃĢ asetet e filtruara", "actions": "Aksione", "active": "Aktiv", + "active_count": "Aktive: {count}", "activity": "Aktivitet", "activity_changed": "Aktiviteti ÃĢshtÃĢ {enabled, select, true {aktivizuar} other {çaktivizuar}}", "add": "Shto", @@ -14,9 +16,14 @@ "add_a_location": "Shto njÃĢ vendndodhje", "add_a_name": "Shto njÃĢ emÃĢr", "add_a_title": "Shto njÃĢ titull", + "add_action": "Shto veprim", + "add_action_description": "Klikoni pÃĢr tÃĢ shtuar njÃĢ veprim pÃĢr t'u kryer", + "add_assets": "Shto asete", "add_birthday": "Shto njÃĢ ditÃĢlindje", "add_endpoint": "Shto njÃĢ endpoint", "add_exclusion_pattern": "Shto model pÃĢrjashtimi", + "add_filter": "Shto filtÃĢr", + "add_filter_description": "Klikoni pÃĢr tÃĢ shtuar njÃĢ kusht filtri", "add_location": "Shto vendndodhje", "add_more_users": "Shto mÃĢ shumÃĢ pÃĢrdorues", "add_partner": "Shto partner", @@ -27,11 +34,15 @@ "add_to_album": "Shto nÃĢ album", "add_to_album_bottom_sheet_added": "Shtuar nÃĢ {album}", "add_to_album_bottom_sheet_already_exists": "Existon nÃĢ {album}", + "add_to_album_bottom_sheet_some_local_assets": "Disa asete lokale nuk mund tÃĢ shtoheshin nÃĢ album", "add_to_album_toggle": "Aktivizo/çaktivizo zgjedhjen pÃĢr {album}", "add_to_albums": "Shto nÃĢ albume", "add_to_albums_count": "Shto nÃĢ albume ({count})", + "add_to_bottom_bar": "Shto nÃĢ", "add_to_shared_album": "Shto nÃĢ album tÃĢ hapur", + "add_upload_to_stack": "Shto ngarkimin nÃĢ stivÃĢ", "add_url": "Shto URL", + "add_workflow_step": "Shto hap workflow", "added_to_archive": "Shtuar nÃĢ arkiv", "added_to_favorites": "Shtuar tek tÃĢ preferuarat", "added_to_favorites_count": "Shtuar {count, number} nÃĢ tÃĢ preferuarat", @@ -50,9 +61,74 @@ "backup_onboarding_1_description": "kopje nÃĢ cloud ose nÃĢ njÃĢ vendndodhje tjetÃĢr fizike.", "backup_onboarding_2_description": "kopje lokale nÃĢ pajisje tÃĢ ndryshme. Kjo pÃĢrfshin skedarÃĢt kryesorÃĢ dhe njÃĢ kopje rezervÃĢ tÃĢ kÃĢtyre skedarÃĢve lokalisht.", "backup_onboarding_3_description": "kopje totale tÃĢ tÃĢ dhÃĢnave tuaja, duke pÃĢrfshirÃĢ skedarÃĢt origjinalÃĢ. Kjo pÃĢrfshin 1 kopje jashtÃĢ faqes dhe 2 kopje lokale.", - "backup_onboarding_description": "Rekomandohet njÃĢ strategji 3-2-1 pÃĢr ruajtjen e tÃĢ dhÃĢnave tuaja. Duhet tÃĢ ruani kopje tÃĢ fotove/videove tÃĢ ngarkuara, si dhe tÃĢ bazÃĢs sÃĢ tÃĢ dhÃĢnave tÃĢ Immich pÃĢr njÃĢ zgjidhje gjithÃĢpÃĢrfshirÃĢse tÃĢ ruajtjes sÃĢ tÃĢ dhÃĢnave.", + "backup_onboarding_description": "Rekomandohet njÃĢ strategji 3-2-1 pÃĢr ruajtjen e tÃĢ dhÃĢnave tuaja. Duhet tÃĢ ruani kopje tÃĢ fotove/videove tÃĢ ngarkuara, si dhe tÃĢ bazÃĢs sÃĢ tÃĢ dhÃĢnave tÃĢ Immich pÃĢr njÃĢ zgjidhje gjithÃĢpÃĢrfshirÃĢse tÃĢ ruajtjes sÃĢ tÃĢ dhÃĢnave.", "backup_onboarding_footer": "PÃĢr mÃĢ shumÃĢ informacion pÃĢr tÃĢ krijuar njÃĢ kopje rezervÃĢ tÃĢ Immich, ju lutem referouni tek dokumentimi.", "backup_onboarding_parts_title": "NjÃĢ kopje rezervÃĢ 3-2-1 ka:", - "backup_onboarding_title": "Kopje rezervÃĢ" - } + "backup_onboarding_title": "Kopje rezervÃĢ", + "backup_settings": "CilÃĢsimet e eksportimit tÃĢ databazÃĢs", + "backup_settings_description": "Menaxho cilÃĢsimet e eksportimit tÃĢ databazÃĢs.", + "cleared_jobs": "PunÃĢt u pastruan pÃĢr: {job}", + "config_set_by_file": "Konfigurimi ÃĢshtÃĢ aktualisht vendosur nga njÃĢ skedar konfigurimi", + "confirm_delete_library": "A jeni i sigurt qÃĢ dÃĢshironi tÃĢ fshini bibliotekÃĢn {library}?", + "confirm_delete_library_assets": "A jeni i sigurt qÃĢ dÃĢshironi ta fshini kÃĢtÃĢ bibliotekÃĢ? Kjo do tÃĢ fshijÃĢ {count, plural, one {# element tÃĢ pÃĢrmbajtur} other {tÃĢ gjithÃĢ # elementÃĢt e pÃĢrmbajtur}} nga Immich dhe ky veprim nuk mund tÃĢ zhbÃĢhet. SkedarÃĢt do tÃĢ mbeten nÃĢ disk.", + "confirm_email_below": "PÃĢr tÃĢ konfirmuar, shkruani \"{email}\" mÃĢ poshtÃĢ", + "confirm_reprocess_all_faces": "A jeni i sigurt qÃĢ dÃĢshironi tÃĢ rindÃĢrtoni tÃĢ gjitha fytyrat? Kjo gjithashtu do tÃĢ fshijÃĢ personat e emÃĢruar.", + "confirm_user_password_reset": "A jeni i sigurt qÃĢ dÃĢshironi tÃĢ rivendosni fjalÃĢkalimin e {user}?", + "confirm_user_pin_code_reset": "A jeni i sigurt qÃĢ dÃĢshironi tÃĢ rivendosni kodin PIN tÃĢ {user}?", + "copy_config_to_clipboard_description": "Kopjo konfigurimin aktual tÃĢ sistemit si objekt JSON nÃĢ clipboard", + "create_job": "Krijo punÃĢ", + "cron_expression_description": "Vendosni intervalin e skanimit duke pÃĢrdorur formatin Cron. PÃĢr mÃĢ shumÃĢ informacion, ju lutem shikoni p.sh. Crontab Guru", + "disable_login": "Çaktivizo hyrjen", + "duplicate_detection_job_description": "Ekzekuto mÃĢsimin makinerik mbi skedarÃĢt pÃĢr tÃĢ zbuluar imazhe tÃĢ ngjashme. Bazohet nÃĢ Smart Search", + "exclusion_pattern_description": "Modelet e pÃĢrjashtimit ju lejojnÃĢ tÃĢ injoroni skedarÃĢ dhe dosje gjatÃĢ skanimit tÃĢ bibliotekÃĢs suaj. Kjo ÃĢshtÃĢ e dobishme nÃĢse keni dosje qÃĢ pÃĢrmbajnÃĢ skedarÃĢ qÃĢ nuk dÃĢshironi tÃĢ importoni, si p.sh. skedarÃĢt e papÃĢrpunuara.", + "export_config_as_json_description": "Shkarkoni konfigurimin aktual tÃĢ sistemit si njÃĢ skedar JSON", + "external_libraries_page_description": "Faqja e bibliotekÃĢs sÃĢ jashtme pÃĢr administratorin", + "face_detection": "Zbulimi i fytyrave", + "face_detection_description": "Zbulo fytyrat nÃĢ skedarÃĢ duke pÃĢrdorur mÃĢsimin makinerik. PÃĢr videot, konsiderohet vetÃĢm miniatura. “Rifresko” (Refresh) pÃĢrpunon pÃĢrsÃĢri tÃĢ gjithÃĢ skedarÃĢt. “Rivendos” (Reset) gjithashtu fshin tÃĢ gjitha tÃĢ dhÃĢnat aktuale tÃĢ fytyrave. “Mungon” (Missing) vendos nÃĢ pritje skedarÃĢt qÃĢ ende nuk janÃĢ pÃĢrpunuar. Fytyrat e zbuluara do tÃĢ vendosen nÃĢ pritje pÃĢr Njohjen e Fytyrave pas pÃĢrfundimit tÃĢ Zbulimit tÃĢ Fytyrave, duke i grupuar ato te personat ekzistues ose tÃĢ rinj.", + "failed_job_command": "Komanda {command} dÃĢshtoi pÃĢr punÃĢn: {job}", + "force_delete_user_warning": "KUJDES: Kjo do tÃĢ heqÃĢ menjÃĢherÃĢ pÃĢrdoruesin dhe tÃĢ gjithÃĢ skedarÃĢt e tij. Ky veprim nuk mund tÃĢ zhbÃĢhet dhe skedarÃĢt nuk mund tÃĢ rikuperohen.", + "image_format": "Formati", + "image_format_description": "WebP prodhon skedarÃĢ mÃĢ tÃĢ vegjÃĢl se JPEG, por kodimi i tij ÃĢshtÃĢ mÃĢ i ngadaltÃĢ.", + "image_fullsize_description": "Imazh me madhÃĢsi tÃĢ plotÃĢ pa metadata, pÃĢrdoret kur zmadhohet", + "image_fullsize_enabled": "Aktivizo gjenerimin e imazhit me madhÃĢsi tÃĢ plotÃĢ", + "image_fullsize_quality_description": "CilÃĢsia e imazhit me madhÃĢsi tÃĢ plotÃĢ nga 1-100. Sa mÃĢ e lartÃĢ, aq mÃĢ e mirÃĢ, por krijon skedarÃĢ mÃĢ tÃĢ mÃĢdhenj.", + "image_fullsize_title": "CilÃĢsimet e imazhit me madhÃĢsi tÃĢ plotÃĢ", + "image_prefer_embedded_preview": "Prefero parapamjen e integruar", + "image_prefer_embedded_preview_setting_description": "PÃĢrdor parapamjet e integruara nÃĢ fotot te papÃĢrpunuara si hyrje pÃĢr pÃĢrpunimin e imazhit, kur janÃĢ tÃĢ disponueshme. Kjo mund tÃĢ japÃĢ ngjyra mÃĢ tÃĢ sakta pÃĢr disa imazhe, por cilÃĢsia e parapamjes varet nga kamera dhe imazhi mund tÃĢ ketÃĢ mÃĢ shumÃĢ artefakte tÃĢ kompresimit.", + "image_prefer_wide_gamut": "Prefero gamÃĢn e gjerÃĢ tÃĢ ngjyrave", + "image_preview_quality_description": "CilÃĢsia e shikimit paraprak nga 1-100. NjÃĢ cilÃĢsi mÃĢ e lartÃĢ ÃĢshtÃĢ mÃĢ e mirÃĢ, por prodhon skedarÃĢ mÃĢ tÃĢ mÃĢdhenj dhe mund tÃĢ zvogÃĢlojÃĢ reagimin e aplikacionit. Vendosja e njÃĢ vlere tÃĢ ulÃĢt mund tÃĢ ndikojÃĢ nÃĢ cilÃĢsinÃĢ e tÃĢ mÃĢsuarit automatik.", + "image_preview_title": "CilÃĢsimet e parapamjes", + "image_progressive_description": "Kodimi i imazheve JPEG bÃĢhet nÃĢ mÃĢnyrÃĢ progresive pÃĢr njÃĢ shfaqje me ngarkim gradual. Kjo nuk ka efekt nÃĢ imazhet WebP.", + "image_quality": "CilÃĢsia", + "image_resolution": "Rezolucioni", + "image_resolution_description": "Rezolucionet mÃĢ tÃĢ larta mund tÃĢ ruajnÃĢ mÃĢ shumÃĢ detaje, por kÃĢrkojnÃĢ mÃĢ shumÃĢ kohÃĢ pÃĢr t'u koduar, kanÃĢ madhÃĢsi mÃĢ tÃĢ mÃĢdha skedarÃĢsh dhe mund tÃĢ zvogÃĢlojnÃĢ reagimin e aplikacionit.", + "image_settings": "CilÃĢsimet e imazhit", + "image_settings_description": "Menaxhoni cilÃĢsinÃĢ dhe rezolucionin e imazheve tÃĢ gjeneruara", + "image_thumbnail_quality_description": "CilÃĢsia e miniaturave nga 1-100. Sa mÃĢ e lartÃĢ tÃĢ jetÃĢ aq mÃĢ mirÃĢ, por prodhon skedarÃĢ mÃĢ tÃĢ mÃĢdhenj dhe mund tÃĢ zvogÃĢlojÃĢ reagimin e aplikacionit.", + "image_thumbnail_title": "CilÃĢsimet e miniaturÃĢs", + "import_config_from_json_description": "Importo konfigurimin e sistemit duke ngarkuar njÃĢ skedar konfigurimi JSON", + "job_concurrency": "{job} paralele", + "job_created": "Puna u krijua", + "job_not_concurrency_safe": "Kjo punÃĢ nuk ÃĢshtÃĢ e sigurt pÃĢr paralelizmin.", + "job_settings": "CilÃĢsimet e punÃĢs", + "job_settings_description": "Menaxhoni konkurencÃĢn e punÃĢs", + "library_scanning": "Skanimi periodik", + "library_scanning_description": "Konfiguro skanimin periodik tÃĢ bibliotekÃĢs", + "library_scanning_enable_description": "Aktivizo skanimin periodik tÃĢ bibliotekÃĢs" + }, + "download_original": "Shkarko origjinalin", + "download_paused": "Shkarkimi u pezullua", + "download_settings": "Shkarko", + "download_started": "Shkarkimi filloi", + "download_sucess": "Shkarkimi u krye me sukses", + "download_sucess_android": "Media u shkarkua tek DCIM/Immich", + "download_waiting_to_retry": "Duke pritur pÃĢr ta provuar pÃĢrsÃĢri", + "downloading": "Duke u shkarkuar", + "downloading_asset_filename": "Duke shkarkuar asetin {filename}", + "downloading_from_icloud": "Duke shkarkuar nga iCloud", + "downloading_media": "Duke shkarkuar median", + "you_dont_have_any_shared_links": "Nuk keni asnjÃĢ link tÃĢ shpÃĢrndarÃĢ", + "your_wifi_name": "Emri i Wi-Fi tuaj", + "zoom_image": "Zmadho imazhin", + "zoom_to_bounds": "Zmadho sipas kufijve" } diff --git a/i18n/sr_Cyrl.json b/i18n/sr_Cyrl.json index d3ca352625..ad4dd8ecca 100644 --- a/i18n/sr_Cyrl.json +++ b/i18n/sr_Cyrl.json @@ -821,8 +821,6 @@ "editor": "ĐŖŅ€ĐĩĐ´ĐŊиĐē", "editor_close_without_save_prompt": "ĐŸŅ€ĐžĐŧĐĩĐŊĐĩ ĐŊĐĩŅ›Đĩ ĐąĐ¸Ņ‚Đ¸ ŅĐ°Ņ‡ŅƒĐ˛Đ°ĐŊĐĩ", "editor_close_without_save_title": "Đ—Đ°Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ŅƒŅ€ĐĩŅ’Đ¸Đ˛Đ°Ņ‡?", - "editor_crop_tool_h2_aspect_ratios": "ĐŸŅ€ĐžĐŋĐžŅ€Ņ†Đ¸Ņ˜Đĩ (Đ°ŅĐŋĐĩ҆҂ Ņ€Đ°Ņ‚Đ¸ĐžŅ)", - "editor_crop_tool_h2_rotation": "Đ ĐžŅ‚Đ°Ņ†Đ¸Ņ˜Đ°", "email": "Е-ĐŋĐžŅˆŅ‚Đ°", "email_notifications": "ОбавĐĩŅˆŅ‚ĐĩŅšĐ° Đĩ-ĐŋĐžŅˆŅ‚ĐžĐŧ", "empty_folder": "Ова ĐŧаĐŋа ҘĐĩ ĐŋŅ€Đ°ĐˇĐŊа", @@ -989,7 +987,6 @@ "feature_photo_updated": "ГĐģавĐŊа Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Đ¸Ņ˜Đ° ҘĐĩ аĐļŅƒŅ€Đ¸Ņ€Đ°ĐŊа", "features": "Đ¤ŅƒĐŊĐēŅ†Đ¸Ņ˜Đĩ (Ņ„ĐĩĐ°Ņ‚ŅƒŅ€Đĩҁ)", "features_setting_description": "ĐŖĐŋŅ€Đ°Đ˛Ņ™Đ°Ņ˜Ņ‚Đĩ Ņ„ŅƒĐŊĐēŅ†Đ¸Ņ˜Đ°Đŧа аĐŋĐģиĐēĐ°Ņ†Đ¸Ņ˜Đĩ", - "file_name": "Назив Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°", "file_name_or_extension": "ИĐŧĐĩ Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ иĐģи ĐĩĐēҁ҂ĐĩĐŊĐˇĐ¸Ņ˜Đ°", "filename": "ИĐŧĐĩ Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ", "filetype": "Đ’Ņ€ŅŅ‚Đ° Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°", @@ -1283,7 +1280,6 @@ "not_available": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐž", "not_in_any_album": "НĐĩĐŧа ĐŊи ҃ ҘĐĩĐ´ĐŊĐžĐŧ аĐģĐąŅƒĐŧ҃", "not_selected": "ĐĐ¸Ņ˜Đĩ Đ¸ĐˇĐ°ĐąŅ€Đ°ĐŊĐž", - "note_apply_storage_label_to_previously_uploaded assets": "НаĐŋĐžĐŧĐĩĐŊа: Да ĐąĐ¸ŅŅ‚Đĩ ĐŋŅ€Đ¸ĐŧĐĩĐŊиĐģи ОСĐŊаĐē҃ Са ҁĐēĐģĐ°Đ´Đ¸ŅˆŅ‚ĐĩҚĐĩ ĐŊа ĐŋŅ€ĐĩŅ‚Ņ…ĐžĐ´ĐŊĐž ҃ĐŋĐģĐžĐ°Đ´Đ¸Ņ€Đ°ĐŊĐĩ Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ, ĐŋĐžĐēŅ€ĐĩĐŊĐ¸Ņ‚Đĩ", "notes": "НаĐŋĐžĐŧĐĩĐŊĐĩ", "notification_permission_dialog_content": "Да йи ҃ĐēŅ™ŅƒŅ†Đ¸Đģи ĐŊĐžŅ‚Đ¸Ņ„Đ¸ĐēĐ°Ņ†Đ¸Ņ˜Đĩ, Đ¸Đ´Đ¸Ņ‚Đĩ ҃ ОĐŋŅ†Đ¸Ņ˜Đĩ и ОдайĐĩŅ€Đ¸Ņ‚Đĩ ДозвоĐģи.", "notification_permission_list_tile_content": "Đ”Đ°Ņ˜Ņ‚Đĩ дОСвОĐģ҃ Са ĐžĐŧĐžĐŗŅƒŅ›Đ°Đ˛Đ°ŅšĐĩ ОйавĐĩŅˆŅ‚ĐĩŅšĐ°.", @@ -1451,7 +1447,7 @@ "reassigned_assets_to_new_person": "ПоĐŊОвО дОдĐĩŅ™ĐĩĐŊĐž {count, plural, one {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēа} other {# Đ´Đ°Ņ‚ĐžŅ‚ĐĩĐēĐĩ}} ĐŊĐžĐ˛ĐžŅ˜ ĐžŅĐžĐąĐ¸", "reassing_hint": "ДодĐĩĐģĐ¸Ņ‚Đĩ Đ¸ĐˇĐ°ĐąŅ€Đ°ĐŊа ҁҀĐĩĐ´ŅŅ‚Đ˛Đ° ĐŋĐžŅŅ‚ĐžŅ˜ĐĩŅ›ĐžŅ˜ ĐžŅĐžĐąĐ¸", "recent": "ĐĄĐēĐžŅ€Đ°ŅˆŅšĐ¸", - "recent-albums": "НĐĩдавĐŊи аĐģĐąŅƒĐŧи", + "recent_albums": "НĐĩдавĐŊи аĐģĐąŅƒĐŧи", "recent_searches": "ĐĄĐēĐžŅ€Đ°ŅˆŅšĐĩ ĐŋŅ€ĐĩŅ‚Ņ€Đ°ĐŗĐĩ", "recently_added": "НĐĩдавĐŊĐž Đ´ĐžĐ´Đ°Ņ‚Đž", "recently_added_page_title": "НĐĩдавĐŊĐž Đ”ĐžĐ´Đ°Ņ‚Đž", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index ad490d491f..b59f86f37c 100644 --- a/i18n/sr_Latn.json +++ b/i18n/sr_Latn.json @@ -5,6 +5,7 @@ "acknowledge": "Potvrdi", "action": "Postupak", "action_common_update": "AÅžuriraj", + "action_description": "Skup akcija da se obave na filtriranim aktivima", "actions": "Postupci", "active": "Aktivni", "active_count": "Aktivno: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Dodaj lokaciju", "add_a_name": "Dodaj ime", "add_a_title": "Dodaj naslov", + "add_action": "Dodaj akciju", + "add_action_description": "Klikni da dodas akciju", + "add_assets": "Dodaj aktive", "add_birthday": "Dodaj rođendan", "add_endpoint": "Dodajte krajnju tačku", "add_exclusion_pattern": "Dodajte obrazac izuzimanja", + "add_filter": "Dodaj filter", + "add_filter_description": "Klikni da dodas stanje filtera", "add_location": "Dodaj lokaciju", "add_more_users": "Dodaj korisnike", "add_partner": "Dodaj partner", @@ -28,10 +34,13 @@ "add_to_album": "Dodaj u album", "add_to_album_bottom_sheet_added": "Dodato u {album}", "add_to_album_bottom_sheet_already_exists": "Već u {album}", + "add_to_album_bottom_sheet_some_local_assets": "Neki lokalni aktivi se ne mogu dodati u album", "add_to_album_toggle": "Uključi/isključi izbor za {album}", "add_to_albums": "Dodaj u albume", "add_to_albums_count": "Dodaj u albume ({count})", + "add_to_bottom_bar": "Dodaj u", "add_to_shared_album": "Dodaj u deljen album", + "add_upload_to_stack": "Dodaj fajl u snop", "add_url": "Dodaj URL", "added_to_archive": "Dodato u arhivu", "added_to_favorites": "Dodato u favorite", @@ -65,6 +74,7 @@ "confirm_reprocess_all_faces": "Da li ste sigurni da Åželite da ponovo obradite sva lica? Ovo cˁe takođe obrisati imenovane osobe.", "confirm_user_password_reset": "Da li ste sigurni da Åželite da resetujete lozinku korisnika {user}?", "confirm_user_pin_code_reset": "Da li ste sigurni da Åželite da resetujete PIN kod korisnika {user}?", + "copy_config_to_clipboard_description": "Kopirajte trenutnu konfiguraciju kao JSON objekat u klip", "create_job": "Kreirajte posao", "cron_expression": "Cron izraz (expression)", "cron_expression_description": "Podesite interval skeniranja koristecˁi cron format. Za viÅĄe informacija pogledajte npr. Crontab Guru", @@ -72,6 +82,7 @@ "disable_login": "Onemogucˁi prijavu", "duplicate_detection_job_description": "Pokrenite maÅĄinsko učenje na sredstvima da biste otkrili slične slike. Oslanja se na pametnu pretragu", "exclusion_pattern_description": "Obrasci izuzimanja vam omogucˁavaju da ignoriÅĄete datoteke i fascikle kada skenirate biblioteku. Ovo je korisno ako imate fascikle koje sadrÅže datoteke koje ne Åželite da uvezete, kao ÅĄto su RAW datoteke.", + "export_config_as_json_description": "Skini trenutnu sistemsku konfiguraciju kao JSON fajl", "face_detection": "Detekcija lica", "face_detection_description": "Otkrijte lica u datotekama pomocˁu maÅĄinskog učenja. Za video snimke se uzima u obzir samo sličica. „OsveÅži“ (ponovno) obrađuje sve datoteke. „Resetovanje“ dodatno briÅĄe sve trenutne podatke o licu. „Nedostaju“ datoteke u redu koje joÅĄ nisu obrađene. Otkrivena lica cˁe biti stavljena u red za prepoznavanje lica nakon ÅĄto se prepoznavanje lica zavrÅĄi, grupiÅĄucˁi ih u postojecˁe ili nove osobe.", "facial_recognition_job_description": "Grupa je detektovala lica i dodala ih postojecˁim osobama. Ovaj korak se pokrecˁe nakon ÅĄto je prepoznavanje lica zavrÅĄeno. „Resetuj“ (ponovno) grupiÅĄe sva lica. „Nedostaju“ lica u redovima kojima nije dodeljena osoba.", @@ -91,6 +102,7 @@ "image_preview_description": "Slika srednje veličine sa uklonjenim metapodacima, koja se koristi prilikom pregleda jednog elementa i za maÅĄinsko učenje", "image_preview_quality_description": "Kvalitet pregleda od 1-100. ViÅĄe je bolje, ali proizvodi vecˁe datoteke i moÅže smanjiti odziv aplikacije. Postavljanje niske vrednosti moÅže uticati na kvalitet maÅĄinskog učenja.", "image_preview_title": "PodeÅĄavanja pregleda", + "image_progressive": "Napredan", "image_quality": "Kvalitet", "image_resolution": "Rezolucija", "image_resolution_description": "Vecˁe rezolucije mogu da sačuvaju viÅĄe detalja, ali im je potrebno viÅĄe vremena za kodiranje, imaju vecˁe veličine datoteka i mogu da smanje odziv aplikacije.", @@ -795,8 +807,6 @@ "editor": "Urednik", "editor_close_without_save_prompt": "Promene necˁe biti sačuvane", "editor_close_without_save_title": "Zatvoriti uređivač?", - "editor_crop_tool_h2_aspect_ratios": "Proporcije (aspect ratios)", - "editor_crop_tool_h2_rotation": "Rotacija", "email": "E-poÅĄta", "email_notifications": "ObaveÅĄtenja e-poÅĄtom", "empty_folder": "Ova mapa je prazna", @@ -961,7 +971,6 @@ "feature_photo_updated": "Glavna fotografija je aÅžurirana", "features": "Funkcije (features)", "features_setting_description": "Upravljajte funkcijama aplikacije", - "file_name": "Naziv dokumenta", "file_name_or_extension": "Ime datoteke ili ekstenzija", "filename": "Ime datoteke", "filetype": "Vrsta dokumenta", @@ -1232,7 +1241,6 @@ "no_shared_albums_message": "Napravite album da biste delili fotografije i video zapise sa ljudima u vaÅĄoj mreÅži", "not_in_any_album": "Nema ni u jednom albumu", "not_selected": "Nije izabrano", - "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primenili oznaku za skladiÅĄtenje na prethodno uploadirane datoteke, pokrenite", "notes": "Napomene", "notification_permission_dialog_content": "Da bi ukljucili notifikacije, idite u Opcije i odaberite Dozvoli.", "notification_permission_list_tile_content": "Dajte dozvolu za omogucˁavanje obaveÅĄtenja.", @@ -1390,7 +1398,7 @@ "reassigned_assets_to_new_person": "Ponovo dodeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi", "reassing_hint": "Dodelite izabrana sredstva postojecˁoj osobi", "recent": "SkoraÅĄnji", - "recent-albums": "Nedavni albumi", + "recent_albums": "Nedavni albumi", "recent_searches": "SkoraÅĄnje pretrage", "recently_added": "Nedavno dodato", "recently_added_page_title": "Nedavno Dodato", diff --git a/i18n/sv.json b/i18n/sv.json index b8d33cd838..ddc6a1b336 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -5,19 +5,25 @@ "acknowledge": "Bekräfta", "action": "Åtgärd", "action_common_update": "Uppdatera", + "action_description": "En uppsättning ÃĨtgärder som ska utfÃļras pÃĨ de filtrerade tillgÃĨngarna", "actions": "Händelser", - "active": "Aktiva", + "active": "Aktiv", "active_count": "Aktiva: {count}", "activity": "Aktivitet", "activity_changed": "Aktiviteten är {enabled, select, true {aktiverad} other {inaktiverad}}", - "add": "Tillägga", + "add": "Lägg till", "add_a_description": "Lägg till en beskrivning", "add_a_location": "Lägg till en plats", "add_a_name": "Lägg till ett namn", "add_a_title": "Lägg till en titel", + "add_action": "Lägg till ÃĨtgärd", + "add_action_description": "Klicka fÃļr att lägga till en ÃĨtgärd att utfÃļra", + "add_assets": "Lägg till tillgÃĨngar", "add_birthday": "Lägg till fÃļdelsedag", "add_endpoint": "Lägg till ändpunkt", "add_exclusion_pattern": "Lägg till uteslutningsmÃļnster", + "add_filter": "Lägg till filter", + "add_filter_description": "Klicka fÃļr att lägga till ett filtervillkor", "add_location": "Lägg till plats", "add_more_users": "Lägg till fler användare", "add_partner": "Lägg till partner", @@ -32,10 +38,11 @@ "add_to_album_toggle": "Växla val fÃļr {album}", "add_to_albums": "Lägg till i album", "add_to_albums_count": "Lägg till i album ({count})", - "add_to_bottom_bar": "Lägg till", + "add_to_bottom_bar": "Lägg till i", "add_to_shared_album": "Lägg till i delat album", "add_upload_to_stack": "Lägg till uppladdning till stack", "add_url": "Lägg till URL", + "add_workflow_step": "Lägg till arbetsflÃļdessteg", "added_to_archive": "Tillagd i arkiv", "added_to_favorites": "Tillagd till favoriter", "added_to_favorites_count": "{count, number} tillagda till favoriter", @@ -92,11 +99,13 @@ "image_fullsize_title": "Inställningar fÃļr fullstora bilder", "image_prefer_embedded_preview": "FÃļredra inbäddad fÃļrhandsgranskning", "image_prefer_embedded_preview_setting_description": "Använd inbäddade fÃļrhandsvisningar i RAW-foton som indata till bildbehandling och när det är tillgängligt. Detta kan ge mer exakta färger fÃļr vissa bilder, men kvaliteten pÃĨ fÃļrhandsgranskningen är kameraberoende och bilden kan ha fler komprimeringsartefakter.", - "image_prefer_wide_gamut": "FÃļredrar brett spektrum", + "image_prefer_wide_gamut": "FÃļredra brett färgomfÃĨng", "image_prefer_wide_gamut_setting_description": "Använd Display P3 fÃļr miniatyrer. Detta bevarar livfullheten bättre hos bilder med bred färgrymd, men bilder kan se annorlunda ut pÃĨ gamla enheter med en gammal webbläsarversion. sRGB-bilder behÃĨlls som sRGB fÃļr att undvika färgskiftningar.", "image_preview_description": "Mellanstor bild med avskalad metadata, används vid visning av en enskild tillgÃĨng och fÃļr maskininlärning", "image_preview_quality_description": "FÃļrhandsgranskningskvalitet frÃĨn 1-100. HÃļgre är bättre, men ger stÃļrre filer och kan gÃļra appen mindre fÃļljsam. Att ställa in ett lÃĨgt värde kan pÃĨverka kvaliteten pÃĨ maskininlärning.", "image_preview_title": "FÃļrhandsvisningsinställningar", + "image_progressive": "Progressiv", + "image_progressive_description": "Koda JPEG-bilder progressivt fÃļr gradvis laddning. Detta pÃĨverkar inte WebP-bilder.", "image_quality": "Kvalitet", "image_resolution": "UpplÃļsning", "image_resolution_description": "HÃļgre upplÃļsningar kan bevara fler detaljer men tar längre tid att koda, har stÃļrre filstorlekar och kan minska appens fÃļljsamhet.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "Aktivera smart sÃļkning", "machine_learning_smart_search_enabled_description": "Om inaktiverat kommer bilder inte att kodas fÃļr smart sÃļkning.", "machine_learning_url_description": "Maskininlärningsserverns URL. Om det är mer än en URL tillagd sÃĨ kommer ett fÃļrsÃļk per URL att utfÃļras tills nÃĨgon av dom svarar, fÃļrsÃļken gÃļrs i kronologisk ordning. Servrar som inte svarar kommer tillfälligt ignoreras tills de är nÃĨbara igen.", + "maintenance_delete_backup": "Ta bort säkerhetskopia", + "maintenance_delete_backup_description": "Den här filen kommer att raderas oÃĨterkalleligt.", + "maintenance_delete_error": "Det gick inte att ta bort säkerhetskopian.", + "maintenance_restore_backup": "Återställ säkerhetskopia", + "maintenance_restore_backup_description": "Immich kommer att ÃĨterställas frÃĨn den valda säkerhetskopian. En ny säkerhetskopia kommer att skapas innan du fortsätter.", + "maintenance_restore_backup_different_version": "Denna säkerhetskopia skapades med en annan version av Immich!", + "maintenance_restore_backup_unknown_version": "Kunde inte fastställa säkerhetskopians verison.", + "maintenance_restore_database_backup": "Återställ databasens säkerhetskopia", + "maintenance_restore_database_backup_description": "Återställ till ett tidigare databasläge med hjälp av en säkerhetskopia", "maintenance_settings": "UnderhÃĨll", "maintenance_settings_description": "FÃļrsätt Immich i underhÃĨllsläge.", - "maintenance_start": "PÃĨbÃļrja underhÃĨllsläget", + "maintenance_start": "Växla till underhÃĨllsläge", "maintenance_start_error": "Misslyckades att starta underhÃĨllsläget.", + "maintenance_upload_backup": "Ladda upp en säkerhetskopia av databasen", + "maintenance_upload_backup_error": "Det gick inte att ladda upp säkerhetskopian. Är det en .sql/.sql.gz-fil?", "manage_concurrency": "Hantera samtidighet", "manage_concurrency_description": "Navigera till jobbsidan fÃļr att hantera jobbens samtidighet", "manage_log_settings": "Hantera logginställningar", @@ -252,7 +272,7 @@ "oauth_auto_register": "Autoregistrera", "oauth_auto_register_description": "Registrera nya användare automatiskt efter inloggning med OAuth", "oauth_button_text": "Knapptext", - "oauth_client_secret_description": "Krävs om PKCE (Proof Key for Code Exchange) inte stÃļds av OAuth-leverantÃļren", + "oauth_client_secret_description": "Krävs fÃļr konfidentiell klient, eller om PKCE (Proof Key for Code Exchange) inte stÃļds fÃļr publik klient.", "oauth_enable_description": "Logga in med OAuth", "oauth_mobile_redirect_uri": "Telefonomdirigernings-URI", "oauth_mobile_redirect_uri_override": "Telefonomdirigerings-URI Ãļverrskridning", @@ -363,7 +383,7 @@ "transcoding_hardware_acceleration": "HÃĨrdvaruacceleration", "transcoding_hardware_acceleration_description": "Experimentell: snabbare transkodning men kan minska kvaliteten vid samma bithastighet", "transcoding_hardware_decoding": "HÃĨrdvaruavkodning", - "transcoding_hardware_decoding_setting_description": "Tillämpas enbart pÃĨ NVENC, QSV och RKMPP. Aktiverar end-to-end accelerering i stället fÃļr endast kodningsacceleration. Fungerar inte med alla videor.", + "transcoding_hardware_decoding_setting_description": "Aktiverar end-to-end accelerering i stället fÃļr endast kodningsacceleration. Fungerar inte med alla videor.", "transcoding_max_b_frames": "Max B-ramar", "transcoding_max_b_frames_description": "HÃļgre värden fÃļrbättrar kompressionseffektiviteten, men saktar ner kodningen. Kan vara inkompatibel med hÃĨrdvaruacceleration pÃĨ äldre enheter. 0 avaktiverar B-frames, medan -1 anger detta värde automatiskt.", "transcoding_max_bitrate": "Max bithastighet", @@ -423,7 +443,7 @@ "version_check_enabled_description": "Aktivera versionskontroll", "version_check_implications": "Funktionen fÃļr versionskontroll är beroende av periodisk kommunikation med github.com", "version_check_settings": "Versionskontroll", - "version_check_settings_description": "Aktivera/inaktivera meddelandet om ny versionen", + "version_check_settings_description": "Aktivera/inaktivera notis om ny version", "video_conversion_job": "Omkoda videor", "video_conversion_job_description": "Koda om videor fÃļr bredare kompatibilitet med webbläsare och enheter" }, @@ -431,6 +451,9 @@ "admin_password": "Admin LÃļsenord", "administration": "Administration", "advanced": "Avancerat", + "advanced_settings_clear_image_cache": "Rensa bild-cache", + "advanced_settings_clear_image_cache_error": "Misslyckades med att rensa bild-cachen", + "advanced_settings_clear_image_cache_success": "{size} har rensats", "advanced_settings_enable_alternate_media_filter_subtitle": "Använd det här alternativet fÃļr att filtrera media under synkronisering baserat pÃĨ alternativa kriterier. Prova detta endast om du har problem med att appen inte hittar alla album.", "advanced_settings_enable_alternate_media_filter_title": "[EXPERIMENTELLT] Använd alternativ enhetsalbum-synkroniseringsfilter", "advanced_settings_log_level_title": "LoggnivÃĨ: {level}", @@ -467,10 +490,12 @@ "album_remove_user": "Ta bort användare?", "album_remove_user_confirmation": "Är du säker pÃĨ att du vill ta bort {user}?", "album_search_not_found": "Inga album hittades som matchade din sÃļkning", + "album_selected": "Album valt", "album_share_no_users": "Det verkar som att du har delat det här albumet med alla användare eller sÃĨ har du inte nÃĨgon användare att dela med.", "album_summary": "Albumsammanfattning", "album_updated": "Albumet uppdaterat", "album_updated_setting_description": "FÃĨ ett e-postmeddelande när ett delat album har nya tillgÃĨngar", + "album_upload_assets": "Ladda upp material frÃĨn din dator och lägg till i album", "album_user_left": "Lämnade {album}", "album_user_removed": "Tog bort {user}", "album_viewer_appbar_delete_confirm": "Är du säker pÃĨ att du vill ta bort albumet frÃĨn ditt konto?", @@ -488,9 +513,11 @@ "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.", "albums_on_device_count": "Album pÃĨ enheten ({count})", + "albums_selected": "{count, plural, one {# album valt} other {# album valda}}", "all": "Allt", "all_albums": "Alla album", "all_people": "Alla personer", + "all_photos": "Alla foton", "all_videos": "Alla videor", "allow_dark_mode": "TillÃĨt mÃļrkt läge", "allow_edits": "TillÃĨt redigeringar", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "TillÃĨt en offentlig användare att ladda upp", "allowed": "TillÃĨten", "alt_text_qr_code": "QR-kod", + "always_keep": "BehÃĨll alltid", + "always_keep_photos_hint": "FrigÃļr utrymme behÃĨller alla foton pÃĨ den här enheten.", + "always_keep_videos_hint": "FrigÃļr utrymme behÃĨller alla videor pÃĨ den här enheten.", "anti_clockwise": "Moturs", "api_key": "API Nyckel", "api_key_description": "Detta värde kommer bara att visas en gÃĨng. Se till att kopiera det innan du stänger fÃļnstret.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {Arkiverade #}}", "are_these_the_same_person": "Är det samma person?", "are_you_sure_to_do_this": "Är du säker pÃĨ att du vill gÃļra det här?", + "array_field_not_fully_supported": "Arrayfält kräver manuell JSON-redigering", "asset_action_delete_err_read_only": "Kan inte ta bort skrivskyddade objekt, hoppar Ãļver", "asset_action_share_err_offline": "Kan inte hämta offline-objekt, hoppar Ãļver", "asset_added_to_album": "Lades till i album", "asset_adding_to_album": "Lägger till i album...â€Ļ", + "asset_created": "TillgÃĨng skapad", "asset_description_updated": "TillgÃĨngens beskrivning har uppdaterats", "asset_filename_is_offline": "TillgÃĨngen {filename} är offline", "asset_has_unassigned_faces": "TillgÃĨngen har otilldelade ansikten", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Layoutinställningar fÃļr bildrutnät", "asset_list_settings_title": "Bildrutnät", + "asset_not_found_on_device_android": "TillgÃĨngar hittades inte pÃĨ enheten", + "asset_not_found_on_device_ios": "TillgÃĨngar hittades inte pÃĨ enheten. Om du använder iCloud kan tillgÃĨngen vara oÃĨtkomlig pÃĨ grund av en felaktig fil som lagrats pÃĨ iCloud", + "asset_not_found_on_icloud": "TillgÃĨngar hittades inte pÃĨ iCloud. TillgÃĨngen kan vara oÃĨtkomlig pÃĨ grund av en felaktig fil som lagras pÃĨ iCloud", "asset_offline": "TillgÃĨng offline", "asset_offline_description": "Denna externa tillgÃĨng finns inte längre pÃĨ disken. Kontakta din Immich-administratÃļr fÃļr hjälp.", "asset_restored_successfully": "Objekt ÃĨterställt", @@ -591,7 +626,7 @@ "backup_album_selection_page_select_albums": "Välj album", "backup_album_selection_page_selection_info": "Info om valda objekt", "backup_album_selection_page_total_assets": "Antal unika objekt", - "backup_albums_sync": "Säkerhetskopiera album synkronisering", + "backup_albums_sync": "Backup-albumsynkronisering", "backup_all": "Allt", "backup_background_service_backup_failed_message": "Säkerhetskopiering av foton och videor misslyckades. FÃļrsÃļker igenâ€Ļ", "backup_background_service_complete_notification": "Säkerhetskopiering av tillgÃĨngar klar", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "LÃļsenorden matchar inte", "change_password_form_reenter_new_password": "Ange Nytt LÃļsenord Igen", "change_pin_code": "Ändra PIN-kod", + "change_trigger": "Ändra utlÃļsare", + "change_trigger_prompt": "Är du säker pÃĨ att du vill ändra utlÃļsaren? Detta tar bort alla befintliga ÃĨtgärder och filter.", "change_your_password": "Ändra ditt lÃļsenord", "changed_visibility_successfully": "Synligheten har ändrats", "charging": "Laddar", @@ -722,6 +759,18 @@ "checksum": "Checksumma", "choose_matching_people_to_merge": "Välj matchande personer att slÃĨ samman", "city": "Stad", + "cleanup_confirm_description": "Immich hittade {count} material (skapade fÃļre {date} som säkerhetskopierats säkert till servern. Ta bort de lokala kopiorna frÃĨn den här enheten?", + "cleanup_confirm_prompt_title": "Ta bort frÃĨn den här enheten?", + "cleanup_deleted_assets": "Flyttade {count} material till enhetens papperskorg", + "cleanup_deleting": "Flyttar till papperskorg...", + "cleanup_found_assets": "Hittade {count} säkerhetskopierade material", + "cleanup_found_assets_with_size": "Hittade {count} säkerhetskopierade tillgÃĨngar ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud delade album exkluderas frÃĨn skanningen", + "cleanup_no_assets_found": "Inga tillgÃĨngar hittades som matchar kriterierna ovan. FrigÃļr utrymme kan bara ta bort tillgÃĨngar som har säkerhetskopierats till servern", + "cleanup_preview_title": "Material att ta bort {count}", + "cleanup_step3_description": "Skanna efter säkerhetskopierade tillgÃĨngar som matchar ditt datum och behÃĨll inställningarna.", + "cleanup_step4_summary": "{count} tillgÃĨngar (skapade fÃļre {date}) att tas bort frÃĨn din lokala enhet. Foton kommer att fÃļrbli tillgängliga frÃĨn Immich-appen.", + "cleanup_trash_hint": "FÃļr att helt frigÃļra lagringsutrymme, Ãļppna systemgalleriappen och tÃļm papperskorgen", "clear": "Rensa", "clear_all": "Rensa allt", "clear_all_recent_searches": "Rensa alla senaste sÃļkningar", @@ -733,6 +782,8 @@ "client_cert_import": "Importera", "client_cert_import_success_msg": "Klientcertifikatet är importerat", "client_cert_invalid_msg": "Felaktig certifikatfil eller fel lÃļsenord", + "client_cert_password_message": "Ange lÃļsenordet fÃļr detta certifikat", + "client_cert_password_title": "CertifikatlÃļsenord", "client_cert_remove_msg": "Klientcertifikatet är borttaget", "client_cert_subtitle": "StÃļdjer endast formatet PKCS12 (.p12, .pfx). import/borttagning av certifikat är tillgängligt endast fÃļre inloggning", "client_cert_title": "SSL klientcertifikat [EXPERIMENTELLT]", @@ -787,6 +838,7 @@ "create_album": "Skapa album", "create_album_page_untitled": "NamnlÃļs", "create_api_key": "Skapa API-nyckel", + "create_first_workflow": "Skapa fÃļrsta arbetsflÃļdet", "create_library": "Skapa bibliotek", "create_link": "Skapa länk", "create_link_to_share": "Skapa länk att dela", @@ -801,17 +853,25 @@ "create_tag": "Skapa tagg", "create_tag_description": "Skapa en ny tagg. FÃļr kapslade taggar anger du hela sÃļkvägen fÃļr taggen inklusive snedstreck.", "create_user": "Skapa användare", + "create_workflow": "Skapa arbetsflÃļde", "created": "Skapad", "created_at": "Skapad", "creating_linked_albums": "Skapar länkade album...", "crop": "Beskär", + "crop_aspect_ratio_fixed": "Fixat", + "crop_aspect_ratio_free": "Fritt", + "crop_aspect_ratio_original": "Original", "curated_object_page_title": "Objekt", "current_device": "Aktuell enhet", "current_pin_code": "Nuvarande PIN-kod", "current_server_address": "Aktuell server-adress", + "custom_date": "Anpassat datum", "custom_locale": "Anpassad plats", "custom_locale_description": "Formatera datum och siffror baserat pÃĨ sprÃĨket och regionen", "custom_url": "Anpassad URL", + "cutoff_date_description": "BehÃĨll bilder frÃĨnâ€Ļ", + "cutoff_day": "{count, plural, one {dag} other {dagar}}", + "cutoff_year": "{count, plural, one {ÃĨr} other {ÃĨr}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "MÃļrk", @@ -867,6 +927,7 @@ "deselect_all": "Avmarkera alla", "details": "Detaljer", "direction": "Riktning", + "disable": "inaktivera", "disabled": "Inaktiverad", "disallow_edits": "TillÃĨt inte redigeringar", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "Inbäddade videor", "download_include_embedded_motion_videos_description": "Inkludera videor inbäddade i rÃļrliga bilder som en separat fil", "download_notfound": "Nedladdning kan inte hittas", + "download_original": "Ladda ner ursprunglig fil", "download_paused": "Nedladdning pausad", "download_settings": "Ladda ner", "download_settings_description": "Hantera inställningar relaterade till nedladdning av objekt", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Väntar pÃĨ omfÃļrsÃļk", "downloading": "Laddar ner", "downloading_asset_filename": "Laddar ned objekt {filename}", + "downloading_from_icloud": "Laddar ner frÃĨn iCloud", "downloading_media": "Laddar ner media", "drop_files_to_upload": "Släpp filer var som helst fÃļr att ladda upp", "duplicates": "Dubletter", @@ -929,11 +992,22 @@ "edit_tag": "Redigera tagg", "edit_title": "Redigera titel", "edit_user": "Redigera användare", + "edit_workflow": "Redigera arbetsflÃļde", "editor": "Redigerare", "editor_close_without_save_prompt": "Ändringarna kommer inte att sparas", "editor_close_without_save_title": "Stäng redigeraren?", - "editor_crop_tool_h2_aspect_ratios": "BildfÃļrhÃĨllande", - "editor_crop_tool_h2_rotation": "Vridning", + "editor_confirm_reset_all_changes": "Är du säker pÃĨ att du vill ÃĨterställa alla ändringar?", + "editor_discard_edits_confirm": "Ignorera redigeringar", + "editor_discard_edits_prompt": "Du har redigeringar som inte har sparats. Är du säker pÃĨ att du vill slänga dem?", + "editor_discard_edits_title": "Släng redigeringar?", + "editor_edits_applied_error": "Misslyckades att verkställa redigeringar", + "editor_edits_applied_success": "Redigeringarna har tillämpats framgÃĨngsrikt", + "editor_flip_horizontal": "Vänd horisontellt", + "editor_flip_vertical": "Vänd vertikalt", + "editor_orientation": "Orientering", + "editor_reset_all_changes": "Återställ ändringar", + "editor_rotate_left": "Rotera 90° moturs", + "editor_rotate_right": "Rotera 90° medurs", "email": "Epost", "email_notifications": "E-postaviseringar", "empty_folder": "Mappen är tom", @@ -952,11 +1026,14 @@ "error_change_sort_album": "Kunde inte ändra sorteringsordning fÃļr album", "error_delete_face": "Fel uppstod när ansikte skulle tas bort frÃĨn objektet", "error_getting_places": "Det gick inte att hämta platser", + "error_loading_albums": "Fel vid laddning av album", "error_loading_image": "Fel vid bildladdning", "error_loading_partners": "Fel vid inläsning av partner: {error}", + "error_retrieving_asset_information": "Fel vid hämtning av tillgÃĨngsinformation", "error_saving_image": "Fel: {error}", "error_tag_face_bounding_box": "Fel vid taggning av ansikte – kan inte hämta koordinater fÃļr begränsningsruta", "error_title": "Fel – nÃĨgot gick fel", + "error_while_navigating": "Fel vid navigering till objektet", "errors": { "cannot_navigate_next_asset": "Det gÃĨr inte att navigera till nästa objekt", "cannot_navigate_previous_asset": "Det gÃĨr inte att navigera till fÃļregÃĨende objekt", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "Det gick inte att slutfÃļra OAuth-inloggning", "unable_to_connect": "Det gÃĨr inte att ansluta", "unable_to_copy_to_clipboard": "Kan inte kopiera till urklipp, se till att du kommer ÃĨt sidan via https", + "unable_to_create": "Det gick inte att skapa arbetsflÃļde", "unable_to_create_admin_account": "Det gick inte att skapa ett administratÃļrskonto", "unable_to_create_api_key": "Det gick inte att skapa en ny API-nyckel", "unable_to_create_library": "Kunde inte skapa bibliotek", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Det gick inte att ta bort uteslutningsmÃļnster", "unable_to_delete_shared_link": "Det gick inte att ta bort delad länk", "unable_to_delete_user": "Kunde inte ta bort användare", + "unable_to_delete_workflow": "Det gick inte att ta bort arbetsflÃļdet", "unable_to_download_files": "Det gÃĨr inte att ladda ner filer", "unable_to_edit_exclusion_pattern": "Det gick inte att redigera uteslutningsmÃļnster", "unable_to_empty_trash": "Kunde inte tÃļmma papperskorgen", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "Det gÃĨr inte att skanna biblioteket", "unable_to_set_feature_photo": "Det gÃĨr inte att ställa in funktionsfoto", "unable_to_set_profile_picture": "Det gÃĨr inte att ställa in profilbilden", + "unable_to_set_rating": "Det gick inte att sätta betyg", "unable_to_submit_job": "Det gÃĨr inte att skicka jobbet", "unable_to_trash_asset": "Det gÃĨr inte att slänga resursen", "unable_to_unlink_account": "Det gÃĨr inte att ta bort länken till kontot", @@ -1074,10 +1154,12 @@ "unable_to_update_settings": "Kunde inte uppdatera inställningar", "unable_to_update_timeline_display_status": "Det gÃĨr inte att uppdatera visningsstatus fÃļr tidslinjen", "unable_to_update_user": "Kunde inte uppdatera användare", + "unable_to_update_workflow": "Det gick inte att uppdatera arbetsflÃļdet", "unable_to_upload_file": "Det gÃĨr inte att ladda upp filen" }, + "errors_text": "Fel", "exclusion_pattern": "ExkluderingsmÃļnster", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "Lägg till beskrivning...", "exif_bottom_sheet_description_error": "Fel vid uppdatering av beskrivningen", "exif_bottom_sheet_details": "DETALJER", @@ -1100,7 +1182,7 @@ "export_as_json": "Exportera som JSON", "export_database": "Exportera databas", "export_database_description": "Exportera SQLite-databasen", - "extension": "Tillägg", + "extension": "FÃļrlängning", "external": "Externt", "external_libraries": "Externa Bibliotek", "external_network": "Externt nätverk", @@ -1120,14 +1202,17 @@ "features": "Funktioner", "features_in_development": "Funktioner i utveckling", "features_setting_description": "Hantera appens funktioner", - "file_name": "Filnamn", "file_name_or_extension": "Filnamn eller -tillägg", + "file_name_text": "Filnamn", + "file_name_with_value": "Filnamn: {file_name}", "file_size": "Filstorlek", "filename": "Filnamn", "filetype": "Filtyp", "filter": "Filter", + "filter_description": "Villkor fÃļr att filtrera mÃĨltillgÃĨngarna", "filter_people": "Filtrera personer", "filter_places": "Filtrera platser", + "filters": "Filter", "find_them_fast": "Hitta dem snabbt efter namn med sÃļk", "first": "FÃļrst", "fix_incorrect_match": "Fixa inkorrekt matchning", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Bläddra i mappvyn fÃļr foton och videoklipp i filsystemet", "forgot_pin_code_question": "GlÃļmt din pinkod?", "forward": "FramÃĨt", + "free_up_space": "FrigÃļr utrymme", + "free_up_space_description": "Flytta säkerhetskopierade foton och videor till din enhets papperskorg fÃļr att frigÃļra utrymme. Dina kopior pÃĨ servern fÃļrblir säkra.", + "free_up_space_settings_subtitle": "FrigÃļr lagringsutrymme pÃĨ enheten", "full_path": "Fullständig sÃļkväg: {path}", "gcast_enabled": "Google-Cast", "gcast_enabled_description": "Denna funktion läser in externa resurser frÃĨn Google fÃļr att fungera.", "general": "Allmänt", "geolocation_instruction_location": "Klicka pÃĨ en tillgÃĨng med GPS-koordinater fÃļr att använda dess plats, eller välj en plats direkt frÃĨn kartan", "get_help": "FÃĨ hjälp", + "get_people_error": "Fel vid hämtning av personer", "get_wifiname_error": "Kunde inte hämta Wi-Fi-namn. Säkerställ att du tillÃĨtit nÃļdvändiga rättigheter och är ansluten till ett Wi-Fi-nätverk", "getting_started": "Komma igÃĨng", "go_back": "GÃĨ tillbaka", @@ -1175,6 +1264,7 @@ "hide_named_person": "GÃļm personen {name}", "hide_password": "DÃļlj lÃļsenord", "hide_person": "DÃļlj person", + "hide_schema": "GÃļm schema", "hide_text_recognition": "DÃļlj textigenkänning", "hide_unnamed_people": "GÃļm personer utan namn", "home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.", @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "Bearbetningen kÃļrdes {dateTime}", "items_count": "{count, plural, one {# objekt} other {# objekt}}", "jobs": "Jobb", + "json_editor": "JSON-redigerare", + "json_error": "JSON-fel", "keep": "BehÃĨll", + "keep_albums": "BehÃĨll album", + "keep_albums_count": "BehÃĨller {count} {count, plural, one {album} other {album}}", "keep_all": "BehÃĨll alla", + "keep_description": "Välj vad som stannar kvar pÃĨ din enhet när du frigÃļr utrymme.", + "keep_favorites": "BehÃĨll favoriter", + "keep_on_device": "BehÃĨll pÃĨ enhet", + "keep_on_device_hint": "Välj objekt som ska behÃĨllas pÃĨ denna enhet", "keep_this_delete_others": "BehÃĨll denna, radera Ãļvriga", + "keeping": "BehÃĨller: {items}", "kept_this_deleted_others": "BehÃĨll denna tillgÃĨng och borttagna {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Kortkommandon", "language": "SprÃĨk", @@ -1343,10 +1442,28 @@ "loop_videos_description": "Aktivera fÃļr att automatiskt loopa en video i detaljvisaren.", "main_branch_warning": "Du använder en utvecklingsversion. Vi rekommenderar starkt att du använder en utgiven version!", "main_menu": "Huvudmeny", + "maintenance_action_restore": "Återställer databasen", "maintenance_description": "Immich har fÃļrsatts i underhÃĨllsläge.", "maintenance_end": "Avsluta underhÃĨllsläge", "maintenance_end_error": "Misslyckades att avsluta underhÃĨllsläge.", "maintenance_logged_in_as": "FÃļr närvarande inloggad som {user}", + "maintenance_restore_from_backup": "Återställ frÃĨn säkerhetskopia", + "maintenance_restore_library": "Återställ ditt bibliotek", + "maintenance_restore_library_confirm": "Om detta ser bra ut, fortsätt med att ÃĨterställa säkerhetskopian!", + "maintenance_restore_library_description": "Återställer databasen", + "maintenance_restore_library_folder_has_files": "{folder} har {count} mapp(ar)", + "maintenance_restore_library_folder_no_files": "{folder} saknar filer!", + "maintenance_restore_library_folder_pass": "läsbar och skrivbar", + "maintenance_restore_library_folder_read_fail": "inte läsbar", + "maintenance_restore_library_folder_write_fail": "inte skrivbar", + "maintenance_restore_library_hint_missing_files": "Du kanske saknar viktiga filer", + "maintenance_restore_library_hint_regenerate_later": "Du kan ÃĨterställa dessa senare i inställningarna", + "maintenance_restore_library_hint_storage_template_missing_files": "Använder du en lagringsmall? Du kanske saknar filer", + "maintenance_restore_library_loading": "Laddar integritetskontroller och heuristikâ€Ļ", + "maintenance_task_backup": "Skapar en säkerhetskopia av den befintliga databasenâ€Ļ", + "maintenance_task_migrations": "KÃļr databasmigreringarâ€Ļ", + "maintenance_task_restore": "Återställer den valda säkerhetskopianâ€Ļ", + "maintenance_task_rollback": "Återställningen misslyckades, ÃĨtergÃĨr till ÃĨterställningspunktâ€Ļ", "maintenance_title": "Tillfälligt otillgänglig", "make": "Tillverkare", "manage_geolocation": "Hantera plats", @@ -1408,6 +1525,8 @@ "minimize": "Minimera", "minute": "Minut", "minutes": "Minuter", + "mirror_horizontal": "Horisontell", + "mirror_vertical": "Vertikallt", "missing": "Saknade", "mobile_app": "Mobilapp", "mobile_app_download_onboarding_note": "Ladda ner den medfÃļljande mobilappen med fÃļljande alternativ", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "MMMM y", "more": "Mer", "move": "Flytta", + "move_down": "Flytta nedÃĨt", "move_off_locked_folder": "Flytta frÃĨn lÃĨst mapp", "move_to": "Flytta till", + "move_to_device_trash": "Flytta till enhetens papperskorg", "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", + "move_up": "Flytta uppÃĨt", "moved_to_archive": "Flyttade {count, plural, one {# resurs} other {# assets}} till arkivet", "moved_to_library": "\"Flyttade {count, plural, one {# asset} other {# assets}} till biblioteket.\"", "moved_to_trash": "Flyttad till papperskorgen", @@ -1430,6 +1552,7 @@ "my_albums": "Mina album", "name": "Namn", "name_or_nickname": "Namn eller smeknamn", + "name_required": "Namn krävs", "navigate": "Navigera", "navigate_to_time": "Navigera till tid", "network_requirement_photos_upload": "Använd mobildata fÃļr att säkerhetskopiera foton", @@ -1454,20 +1577,24 @@ "next": "Nästa", "next_memory": "Nästa minne", "no": "Nej", + "no_actions_added": "Inga ÃĨtgärder tillagda än", + "no_albums_found": "Inga album hittades", "no_albums_message": "Skapa ett album fÃļr att organisera dina foton och videor", "no_albums_with_name_yet": "Du verkar inte ha nÃĨgra album med det här namnet ännu.", "no_albums_yet": "Det ser ut som att du inte har nÃĨgra album ännu.", "no_archived_assets_message": "Arkivera bilder och videor fÃļr att dÃļlja dem frÃĨn bild-vyn", - "no_assets_message": "KLICKA FÖR ATT LADDA UPP DIN FÖRSTA BILD", + "no_assets_message": "Kicka fÃļr att ladda upp din fÃļrsta bild", "no_assets_to_show": "Inga objekt att visa", "no_cast_devices_found": "Inga Cast-enheter hittades", "no_checksum_local": "Ingen kontrollsumma tillgänglig - kan inte hämta lokala tillgÃĨngar", "no_checksum_remote": "Ingen kontrollsumma tillgänglig - kan inte hämta fjärrtillgÃĨng", + "no_configuration_needed": "Ingen konfiguration behÃļvs", "no_devices": "Inga auktoriserade enheter", "no_duplicates_found": "Inga dubbletter hittades.", - "no_exif_info_available": "EXIF-information ej tillgänglig", + "no_exif_info_available": "Exif-information ej tillgänglig", "no_explore_results_message": "Ladda upp fler bilder fÃļr att utforska din samling.", "no_favorites_message": "Lägg till favoriter fÃļr att snabbt hitta dina bästa bilder och videor", + "no_filters_added": "Inga filter tillagda än", "no_libraries_message": "Skapa ett externt bibliotek fÃļr att se dina bilder och videor", "no_local_assets_found": "Inga lokala tillgÃĨngar hittades med denna kontrollsumma", "no_location_set": "Ingen plats satt", @@ -1481,11 +1608,11 @@ "no_results_description": "PrÃļva en synonym eller ett annat mer allmänt sÃļkord", "no_shared_albums_message": "Skapa ett album fÃļr att dela bilder och videor med andra personer", "no_uploads_in_progress": "Inga uppladdningar pÃĨgÃĨr", + "none": "Inga", "not_allowed": "Inte tillÃĨten", "not_available": "N/A", "not_in_any_album": "Inte i nÃĨgot album", "not_selected": "Ej vald", - "note_apply_storage_label_to_previously_uploaded assets": "Obs: Om du vill använda lagringsetiketten pÃĨ tidigare uppladdade tillgÃĨngar kÃļr du", "notes": "Notera", "nothing_here_yet": "Inget här ännu", "notification_permission_dialog_content": "FÃļr att aktivera notiser, gÃĨ till Inställningar och välj tillÃĨt.", @@ -1563,6 +1690,7 @@ "people": "Personer", "people_edits_count": "Redigerad {count, plural, one {# person} other {# personer}}", "people_feature_description": "Visar foton och videor grupperade efter personer", + "people_selected": "{count, plural, one {# person vald} other {# personer valda}}", "people_sidebar_description": "Visa en länk till Personer i sidopanelen", "permanent_deletion_warning": "Varning om permanent radering", "permanent_deletion_warning_setting_description": "Visa en varning när tillgÃĨngar raderas permanent", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# ÃĨr}} gammal", "person_birthdate": "FÃļdd {date}", "person_hidden": "{name}{hidden, select, true { (dold)} other {}}", + "person_recognized": "Person igenkänd", + "person_selected": "Person vald", "photo_shared_all_users": "Du har antingen delat dina foton med alla användare eller sÃĨ har du inga användare att dela dem med.", "photos": "Foton", "photos_and_videos": "Foton & videor", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Foton}}", "photos_from_previous_years": "Foton frÃĨn tidigare ÃĨr", + "photos_only": "Foton endast", "pick_a_location": "Välj en plats", "pick_custom_range": "Anpassat intervall", "pick_date_range": "Välj ett datumintervall", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Produktnyckeln fÃļr servern hanteras av administratÃļren", "query_asset_id": "FrÃĨga om objekts-ID", "queue_status": "KÃļande {count}/{total}", + "rate_asset": "Betygsätt materialet", "rating": "Antal stjärnor", "rating_clear": "Ta bort betyg", "rating_count": "{count, plural, one {# stjärna} other {# stjärnor}}", "rating_description": "Visa EXIF betyget i informationspanelen", + "rating_set": "Rating set to {rating, plural, one {# stjärna} other {# stjärnor}}", "reaction_options": "Alternativ fÃļr reaktion", "read_changelog": "Läs ändringslogg", "readonly_mode_disabled": "Skrivskyddat läge inaktiverat", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "Tilldelade om {count, plural, one {# objekt} other {# objekt}} till en ny persson", "reassing_hint": "Tilldela valda tillgÃĨngar till en befintlig person", "recent": "Nyligen", - "recent-albums": "Senaste album", + "recent_albums": "Senaste album", "recent_searches": "Senaste sÃļkningar", "recently_added": "Nyligen tillagda", "recently_added_page_title": "Nyligen tillagda", @@ -1770,9 +1903,11 @@ "saved_settings": "Sparade inställningar", "say_something": "Säg nÃĨgot", "scaffold_body_error_occurred": "Fel uppstod", + "scan": "Skanna", "scan_all_libraries": "Skanna alla bibliotek", "scan_library": "Skanna", "scan_settings": "Skanningsinställningar", + "scanning": "Skannar", "scanning_for_album": "SÃļker efter album...", "search": "SÃļk", "search_albums": "SÃļk album", @@ -1781,7 +1916,7 @@ "search_by_description_example": "Vandringsdag i Sapa", "search_by_filename": "SÃļk efter filnamn eller filändelse", "search_by_filename_example": "t.ex. IMG_1234.JPG eller PNG", - "search_by_ocr": "SÃļk efter OCR", + "search_by_ocr": "SÃļk text i bild", "search_by_ocr_example": "Latte", "search_camera_lens_model": "SÃļk kameraobjektiv...", "search_camera_make": "SÃļk efter kameratillverkare...", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Välj mediatyp", "search_filter_ocr": "SÃļk efter OCR", "search_filter_people_title": "Välj personer", + "search_filter_star_rating": "Stjärnbetyg", "search_for": "SÃļk efter", "search_for_existing_person": "SÃļk efter befintlig person", "search_no_more_result": "Inga fler resultat", @@ -1836,17 +1972,23 @@ "second": "Sekund", "see_all_people": "Se alla personer", "select": "Välj", + "select_album": "Välj album", "select_album_cover": "Välj albumomslag", + "select_albums": "Välj albums", "select_all": "Välj alla", "select_all_duplicates": "Välj alla dubletter", "select_all_in": "Markera alla i {group}", "select_avatar_color": "Välj färg fÃļr avatar", - "select_face": "Välj person", + "select_count": "{count, plural, one {Välj #} other {Välj #}}", + "select_cutoff_date": "Välj slutdatum", + "select_face": "Välj ansikte", "select_featured_photo": "Välj utvald bild", "select_from_computer": "Välj frÃĨn datorn", "select_keep_all": "Spara alla", "select_library_owner": "Välj biblioteksägare", "select_new_face": "Välj nytt ansikte", + "select_people": "Välj personer", + "select_person": "Välj person", "select_person_to_tag": "Välj en person att tagga", "select_photos": "Välj foton", "select_trash_all": "Släng alla", @@ -1982,6 +2124,7 @@ "show_password": "Visa lÃļsenord", "show_person_options": "Visa alternativ fÃļr person", "show_progress_bar": "Visa fÃļrloppsindikator", + "show_schema": "Visa schema", "show_search_options": "Visa sÃļkalternativ", "show_shared_links": "Visa delade länkar", "show_slideshow_transition": "Visa bildspelsÃļvergÃĨng", @@ -1999,6 +2142,8 @@ "skip_to_folders": "Hoppa till mapp", "skip_to_tags": "Hoppa till taggar", "slideshow": "Bildspel", + "slideshow_repeat": "Upprepa bildspel", + "slideshow_repeat_description": "GÃĨ tillbaka till bÃļrjan när bildspelet slutar", "slideshow_settings": "Bildspelsinställningar", "sort_albums_by": "Sortera album efter...", "sort_created": "Skapat datum", @@ -2034,7 +2179,7 @@ "submit": "Skicka", "success": "FramgÃĨng", "suggestions": "FÃļrslag", - "sunrise_on_the_beach": "SoluppgÃĨng pÃĨ stranden", + "sunrise_on_the_beach": "Exempel: SoluppgÃĨng pÃĨ stranden", "support": "Support", "support_and_feedback": "Support och Feedback", "support_third_party_description": "Din Immich-installation paketerades av en tredje part. Problem som du upplever kan orsakas av det paketet, sÃĨ vänligen ta upp problem med dem i fÃļrsta hand med hjälp av länkarna nedan.", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Välj inställning fÃļr appens tema", "theme_setting_three_stage_loading_subtitle": "Trestegsladdning kan Ãļka prestandan, men kan ocksÃĨ leda till signifikant hÃļgre nätverksbelastning", "theme_setting_three_stage_loading_title": "Aktivera trestegsladdning", + "then": "Sedan", "they_will_be_merged_together": "De kommer att slÃĨs samman", "third_party_resources": "Tredjepartsresurser", "time": "Tid", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Välj objekt", "trash_page_title": "Papperskorg ({count})", "trashed_items_will_be_permanently_deleted_after": "Objekt i papperskorgen raderas permanent efter {days, plural, one {# dag} other {# dagar}}.", + "trigger": "UtlÃļsare", + "trigger_asset_uploaded": "TillgÃĨng uppladdad", + "trigger_asset_uploaded_description": "UtlÃļses när en ny tillgÃĨng laddas upp", + "trigger_description": "Ett evenemang som sätter igÃĨng arbetsflÃļdet", + "trigger_person_recognized": "Person igenkänd", + "trigger_person_recognized_description": "UtlÃļses när en person upptäcks", + "trigger_type": "UtlÃļsningstyp", "troubleshoot": "FelsÃļk", "type": "Typ", "unable_to_change_pin_code": "Kunde inte ändra pinkod", @@ -2123,6 +2276,7 @@ "unhide_person": "Visa person", "unknown": "Okänd", "unknown_country": "Okänt Land", + "unknown_date": "Okänt datum", "unknown_year": "Okänt ÃĨr", "unlimited": "Obegränsat", "unlink_motion_video": "Ta bort länken till rÃļrlig video", @@ -2139,17 +2293,19 @@ "unstack": "Stapla Av", "unstack_action_prompt": "{count} ostaplade", "unstacked_assets_count": "Avstaplade {count, plural, one {# asset} other {# assets}}", + "unsupported_field_type": "Fälttyp som inte stÃļds", "untagged": "Otaggad", + "untitled_workflow": "NamnlÃļst arbetsflÃļde", "up_next": "Kommande", "update_location_action_prompt": "Uppdatera platsen fÃļr {count} valda tillgÃĨngar med:", "updated_at": "Uppdaterat", "updated_password": "LÃļsenordet har uppdaterats", "upload": "Ladda upp", - "upload_action_prompt": "{count} i kÃļ fÃļr uppladdning", "upload_concurrency": "Uppladdning samtidighet", "upload_details": "Uppladdningsdetaljer", "upload_dialog_info": "Vill du säkerhetskopiera de valda objekten till servern?", "upload_dialog_title": "Ladda Upp Objekt", + "upload_error_with_count": "Uppladdningsfel fÃļr {count, plural, one {# asset} other {# assets}}", "upload_errors": "Uppladdning klar med {count, plural, one {# fel} other {# fel}}, ladda om sidan fÃļr att se nya objekt.", "upload_finished": "Uppladdningen är klar", "upload_progress": "ÅterstÃĨende {remaining, number} - Bearbetade {processed, number}/{total, number}", @@ -2185,6 +2341,7 @@ "utilities": "Verktyg", "validate": "Validera", "validate_endpoint_error": "Ange en giltig URL", + "validation_error": "Valideringsskräck", "variables": "Variabler", "version": "Version", "version_announcement_closing": "Din vän, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Spela upp videotumnagel när muspekaren är Ãļver den. Även när den är deaktiverad kan uppspelning startas när muspekaren är Ãļver play-ikonen.", "videos": "Videor", "videos_count": "{count, plural, one {# Video} other {# Videor}}", + "videos_only": "Videor endast", "view": "Visa", "view_album": "Visa Album", "view_all": "Visa alla", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Använd som Huvudobjekt", "viewer_unstack": "Stapla Av", "visibility_changed": "Synlighet ändrad fÃļr {count, plural, one {# person} other {# personer}}", + "visual": "Visuellt", + "visual_builder": "Visuell byggare", "waiting": "Väntar", "waiting_count": "Väntande: {count}", "warning": "Varning", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Välkommen till Immich", "width": "Bredd", "wifi_name": "Wi-Fi-namn", - "workflow": "ArbetsflÃļde", + "workflow_delete_prompt": "Är du säker pÃĨ att du vill ta bort det här arbetsflÃļdet?", + "workflow_deleted": "ArbetsflÃļdet raderat", + "workflow_description": "Beskrivning av arbetsflÃļdet", + "workflow_info": "ArbetsflÃļdesinformation", + "workflow_json": "ArbetsflÃļdes-JSON", + "workflow_json_help": "Redigera arbetsflÃļdeskonfigurationen i JSON-format. Ändringarna synkroniseras med den visuella verktygsbyggaren.", + "workflow_name": "ArbetsflÃļdesnamn", + "workflow_navigation_prompt": "Är du säker pÃĨ att du vill avsluta utan att spara dina ändringar?", + "workflow_summary": "Sammanfattning av arbetsflÃļde", + "workflow_update_success": "ArbetsflÃļdet har uppdaterats", + "workflow_updated": "ArbetsflÃļdet uppdaterades", + "workflows": "ArbetsflÃļden", + "workflows_help_text": "ArbetsflÃļden automatiserar ÃĨtgärder pÃĨ dina resurser baserat pÃĨ utlÃļsare och filter", "wrong_pin_code": "Fel pinkod", "year": "År", "years_ago": "{years, plural, one {# ÃĨr} other {# ÃĨr}} sedan", "yes": "Ja", "you_dont_have_any_shared_links": "Du har inga delade länkar", "your_wifi_name": "Ditt Wi-Fi-namn", + "zero_to_clear_rating": "Tryck 0 fÃļr att rensa betygsättningen", "zoom_image": "Zooma bild", "zoom_to_bounds": "Zooma till gränser" } diff --git a/i18n/ta.json b/i18n/ta.json index a686df7326..90a7ce6664 100644 --- a/i18n/ta.json +++ b/i18n/ta.json @@ -5,8 +5,10 @@ "acknowledge": "āŽ’āŽĒā¯āŽĒā¯āŽ•ā¯āŽ•ā¯ŠāŽŗā¯āŽ•āŽŋāŽąā¯‡āŽŠā¯", "action": "āŽšā¯†āŽ¯āŽ˛ā¯", "action_common_update": "āŽŽā¯‡āŽŽā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯", + "action_description": "āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽšā¯†āŽ¯ā¯āŽ¯ āŽĩā¯‡āŽŖā¯āŽŸāŽŋāŽ¯ āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗāŽŋāŽŠā¯ āŽ¤ā¯ŠāŽ•ā¯āŽĒā¯āŽĒ❁", "actions": "āŽšā¯†āŽ¯āŽ˛ā¯āŽ•āŽŗā¯", "active": "āŽšā¯†āŽ¯āŽ˛ā¯āŽĒāŽžāŽŸā¯āŽŸāŽŋāŽ˛ā¯", + "active_count": "āŽšā¯†āŽ¯āŽ˛āŽŋāŽ˛ā¯: {count}", "activity": "āŽšā¯†āŽ¯āŽ˛ā¯āŽĒāŽžāŽŸā¯āŽ•āŽŗā¯", "activity_changed": "āŽšā¯†āŽ¯āŽ˛ā¯āŽĒāŽžāŽŸā¯ {enabled, select, true {āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯} other {āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯}}", "add": "āŽšā¯‡āŽ°ā¯", @@ -14,9 +16,14 @@ "add_a_location": "āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_a_name": "āŽĒā¯†āŽ¯āŽ°ā¯ˆ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_a_title": "āŽ¤āŽ˛ā¯ˆāŽĒā¯āŽĒ❁ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "add_action": "āŽšā¯†āŽ¯āŽ˛ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯", + "add_action_description": "āŽšā¯†āŽ¯ā¯āŽ¯ āŽĩā¯‡āŽŖā¯āŽŸāŽŋāŽ¯ āŽšā¯†āŽ¯āŽ˛ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ• āŽ•āŽŋāŽŗāŽŋāŽ•ā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", + "add_assets": "āŽŠāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_birthday": "āŽĒāŽŋāŽąāŽ¨ā¯āŽ¤āŽ¨āŽžāŽŗā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_endpoint": "āŽšā¯‡āŽĩ❈ āŽ¨āŽŋāŽ°āŽ˛ā¯ˆ āŽšā¯‡āŽ°ā¯", "add_exclusion_pattern": "āŽĩāŽŋāŽ˛āŽ•ā¯āŽ•ā¯ āŽĩāŽŸāŽŋāŽĩāŽ¤ā¯āŽ¤ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "add_filter": "āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽŋāŽ¯ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "add_filter_description": "āŽĩāŽŸāŽŋāŽ•āŽŸā¯āŽŸāŽŋ āŽ¨āŽŋāŽĒāŽ¨ā¯āŽ¤āŽŠā¯ˆāŽ¯ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ• āŽ•āŽŋāŽŗāŽŋāŽ•ā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĩā¯āŽŽā¯", "add_location": "āŽ‡āŽŸāŽ¤ā¯āŽ¤ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_more_users": "āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽĒāŽ¯āŽŠāŽ°ā¯āŽ•āŽŗā¯ˆ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "add_partner": "āŽ¤ā¯āŽŖā¯ˆāŽ¯ā¯ˆ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", @@ -35,6 +42,7 @@ "add_to_shared_album": "āŽĒāŽ•āŽŋāŽ°āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ†āŽ˛ā¯āŽĒāŽŽāŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•", "add_upload_to_stack": "āŽ…āŽŸā¯āŽ•ā¯āŽ•āŽŋāŽ˛ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽ¤ā¯āŽ¤ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯", "add_url": "URL āŽāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", + "add_workflow_step": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁āŽĒā¯ āŽĒāŽŸāŽŋāŽ¯ā¯ˆāŽšā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "added_to_archive": "āŽ•āŽžāŽĒā¯āŽĒāŽ•āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "added_to_favorites": "āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ (āŽĒ❇āŽĩāŽ°āŽŋāŽŸā¯āŽ¸ā¯) āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "added_to_favorites_count": "āŽĩāŽŋāŽ°ā¯āŽĒā¯āŽĒāŽ™ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ {count, number} āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -70,7 +78,7 @@ "copy_config_to_clipboard_description": "āŽ¤āŽąā¯āŽĒā¯‹āŽ¤ā¯ˆāŽ¯ āŽ•āŽŖāŽŋāŽŠāŽŋ āŽ‰āŽŗā¯āŽŗāŽŽā¯ˆāŽĩ❈ JSON āŽĒā¯ŠāŽ°ā¯āŽŗāŽžāŽ• āŽ•āŽŋāŽŗāŽŋāŽĒā¯āŽĒā¯‹āŽ°ā¯āŽŸā¯āŽ•ā¯āŽ•ā¯ āŽ¨āŽ•āŽ˛ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "create_job": "āŽĩā¯‡āŽ˛ā¯ˆāŽ¯ā¯ˆ āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•ā¯", "cron_expression": "āŽ•ā¯āŽ°ā¯‹āŽŠā¯ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒāŽžāŽŸā¯", - "cron_expression_description": "CRON āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋ āŽšā¯āŽ•ā¯‡āŽŠāŽŋāŽ™ā¯ āŽ‡āŽŸā¯ˆāŽĩā¯†āŽŗāŽŋāŽ¯ā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯. āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯āŽ•ā¯āŽ•ā¯ āŽŽ.āŽ•āŽž. āŽ•ā¯āŽ°ā¯‹āŽŠā¯āŽŸāŽžāŽĒā¯ āŽ•ā¯āŽ°ā¯ ", + "cron_expression_description": "āŽ•ā¯āŽ°ā¯‹āŽŠā¯ āŽĩāŽŸāŽŋāŽĩāŽŽā¯ˆāŽĒā¯āŽĒ❈āŽĒā¯ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤āŽŋ āŽšā¯āŽ•ā¯‡āŽŠāŽŋāŽ™ā¯ āŽ‡āŽŸā¯ˆāŽĩā¯†āŽŗāŽŋāŽ¯ā¯ˆ āŽ…āŽŽā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯. āŽŽā¯‡āŽ˛ā¯āŽŽā¯ āŽ¤āŽ•āŽĩāŽ˛ā¯āŽ•ā¯āŽ•ā¯ āŽŽ.āŽ•āŽž. āŽ•ā¯āŽ°ā¯‹āŽŠā¯āŽŸāŽžāŽĒā¯ āŽ•ā¯āŽ°ā¯ ", "cron_expression_presets": "āŽ•ā¯āŽ°ā¯‹āŽŠā¯ āŽĩā¯†āŽŗāŽŋāŽĒā¯āŽĒāŽžāŽŸā¯ āŽŽā¯āŽŠā¯āŽŠāŽŽā¯ˆāŽĩā¯āŽ•āŽŗā¯", "disable_login": "āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩ❈ āŽŽā¯āŽŸāŽ•ā¯āŽ•ā¯", "duplicate_detection_job_description": "āŽ’āŽ¤ā¯āŽ¤ āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ˆāŽ•ā¯ āŽ•āŽŖā¯āŽŸāŽąāŽŋāŽ¯, āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗāŽŋāŽ˛ā¯ āŽ‡āŽ¯āŽ¨ā¯āŽ¤āŽŋāŽ°āŽ•ā¯ āŽ•āŽąā¯āŽąāŽ˛ā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯. āŽ¸ā¯āŽŽāŽžāŽ°ā¯āŽŸā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯ˆ āŽ¨āŽŽā¯āŽĒāŽŋāŽ¯ā¯āŽŗā¯āŽŗāŽ¤ā¯", @@ -112,6 +120,7 @@ "job_settings_description": "āŽĩā¯‡āŽ˛ā¯ˆ āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❈ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "jobs_delayed": "{jobCount, plural, other {# āŽ¤āŽžāŽŽāŽ¤āŽŽāŽžāŽŠāŽ¤ā¯}}", "jobs_failed": "{jobCount, plural, other {# āŽ¤ā¯‹āŽ˛ā¯āŽĩāŽŋāŽ¯ā¯āŽąā¯āŽąāŽ¤ā¯}}", + "jobs_over_time": "āŽ•āŽžāŽ˛āŽĒā¯āŽĒā¯‹āŽ•ā¯āŽ•āŽŋāŽ˛ā¯ āŽĩā¯‡āŽ˛ā¯ˆāŽ•āŽŗā¯", "library_created": "āŽ‰āŽ°ā¯āŽĩāŽžāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ¨ā¯‚āŽ˛āŽ•āŽŽā¯: {library}", "library_deleted": "āŽĒā¯āŽ•ā¯ˆāŽĒā¯āŽĒāŽŸ āŽ¨ā¯‚āŽ˛āŽ•āŽŽā¯ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "library_details": "āŽ¨ā¯‚āŽ˛āŽ• āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯", @@ -179,6 +188,7 @@ "machine_learning_smart_search_enabled": "āŽ¸ā¯āŽŽāŽžāŽ°ā¯āŽŸā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•ā¯", "machine_learning_smart_search_enabled_description": "āŽŽā¯āŽŸāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŋāŽ°ā¯āŽ¨ā¯āŽ¤āŽžāŽ˛ā¯, āŽ¸ā¯āŽŽāŽžāŽ°ā¯āŽŸā¯ āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽ•ā¯āŽ•āŽžāŽ• āŽĒāŽŸāŽ™ā¯āŽ•āŽŗā¯ āŽ•ā¯āŽąāŽŋāŽ¯āŽžāŽ•ā¯āŽ•āŽŽā¯ āŽšā¯†āŽ¯ā¯āŽ¯āŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯.", "machine_learning_url_description": "āŽ‡āŽ¯āŽ¨ā¯āŽ¤āŽŋāŽ° āŽ•āŽąā¯āŽąāŽ˛ā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽŠā¯ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ. āŽ’āŽŠā¯āŽąā¯āŽ•ā¯āŽ•ā¯ āŽŽā¯‡āŽąā¯āŽĒāŽŸā¯āŽŸ āŽŽā¯āŽ•āŽĩāŽ°āŽŋ āŽĩāŽ´āŽ™ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽžāŽ˛ā¯, āŽ’āŽĩā¯āŽĩā¯ŠāŽ°ā¯ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽŽā¯āŽŽā¯ āŽ’āŽĩā¯āŽĩā¯ŠāŽŠā¯āŽąāŽžāŽ• āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽĒāŽ¤āŽŋāŽ˛āŽŗāŽŋāŽ•ā¯āŽ•ā¯āŽŽā¯ āŽĩāŽ°ā¯ˆ, āŽŽā¯āŽ¤āŽ˛āŽŋāŽ˛ā¯ āŽ‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯ āŽ•āŽŸā¯ˆāŽšāŽŋ āŽĩāŽ°ā¯ˆ āŽŽā¯āŽ¯āŽąā¯āŽšāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯. āŽĒāŽ¤āŽŋāŽ˛āŽŗāŽŋāŽ•ā¯āŽ•āŽžāŽ¤ āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ™ā¯āŽ•āŽŗā¯ āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ†āŽŠā¯āŽ˛ā¯ˆāŽŠāŽŋāŽ˛ā¯ āŽĩāŽ°ā¯āŽŽā¯ āŽĩāŽ°ā¯ˆ āŽ¤āŽąā¯āŽ•āŽžāŽ˛āŽŋāŽ•āŽŽāŽžāŽ•āŽĒā¯ āŽĒā¯āŽąāŽ•ā¯āŽ•āŽŖāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŽā¯.", + "maintenance_delete_backup": "āŽ•āŽžāŽĒā¯āŽĒā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽ¨ā¯€āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "maintenance_settings": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁", "maintenance_settings_description": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšā¯ˆ āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽŽā¯āŽąā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽĩā¯ˆāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", "maintenance_start": "āŽĒāŽ°āŽžāŽŽāŽ°āŽŋāŽĒā¯āŽĒ❁ āŽĒāŽ¯āŽŠā¯āŽŽā¯āŽąā¯ˆāŽ¯ā¯ˆāŽ¤ā¯ āŽ¤ā¯ŠāŽŸāŽ™ā¯āŽ•ā¯", @@ -274,6 +284,7 @@ "password_settings_description": "āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯ āŽ‰āŽŗā¯āŽ¨ā¯āŽ´ā¯ˆāŽĩ❁ āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "paths_validated_successfully": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽĒāŽžāŽ¤ā¯ˆāŽ•āŽŗā¯āŽŽā¯ āŽĩā¯†āŽąā¯āŽąāŽŋāŽ•āŽ°āŽŽāŽžāŽ• āŽšāŽ°āŽŋāŽĒāŽžāŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽŠ", "person_cleanup_job": "āŽ¨āŽĒāŽ°ā¯ āŽ¤ā¯‚āŽ¯ā¯āŽŽā¯ˆāŽĒā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤ā¯āŽ¤āŽ˛ā¯", + "queue_details": "āŽĩāŽ°āŽŋāŽšā¯ˆ āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯", "quota_size_gib": "āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯€āŽŸā¯ āŽ…āŽŗāŽĩ❁ (GiB)", "refreshing_all_libraries": "āŽ…āŽŠā¯ˆāŽ¤ā¯āŽ¤ā¯ āŽ¨ā¯‚āŽ˛āŽ•āŽ™ā¯āŽ•āŽŗā¯ˆāŽ¯ā¯āŽŽā¯ āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽŋāŽąāŽ¤ā¯", "registration": "āŽ¨āŽŋāŽ°ā¯āŽĩāŽžāŽ• āŽĒāŽ¤āŽŋāŽĩ❁", @@ -924,8 +935,6 @@ "editor": "āŽ¤āŽŋāŽ°ā¯āŽ¤ā¯āŽ¤āŽŋ", "editor_close_without_save_prompt": "āŽŽāŽžāŽąā¯āŽąāŽ™ā¯āŽ•āŽŗā¯ āŽšā¯‡āŽŽāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽžāŽ¤ā¯", "editor_close_without_save_title": "āŽŽā¯‚āŽŸā¯ āŽ†āŽšāŽŋāŽ°āŽŋāŽ¯āŽ°ā¯?", - "editor_crop_tool_h2_aspect_ratios": "āŽ…āŽŽā¯āŽš āŽĩāŽŋāŽ•āŽŋāŽ¤āŽ™ā¯āŽ•āŽŗā¯", - "editor_crop_tool_h2_rotation": "āŽšā¯āŽ´āŽąā¯āŽšāŽŋ", "email": "āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯", "email_notifications": "āŽŽāŽŋāŽŠā¯āŽŠāŽžā¯āŽšāŽ˛ā¯ āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "empty_folder": "āŽ‡āŽ¨ā¯āŽ¤ āŽ•ā¯‹āŽĒā¯āŽĒā¯āŽąā¯ˆ āŽ•āŽžāŽ˛āŽŋāŽ¯āŽžāŽ• āŽ‰āŽŗā¯āŽŗāŽ¤ā¯", @@ -1111,7 +1120,6 @@ "features": "āŽ¨āŽąā¯āŽĒā¯ŠāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯", "features_in_development": "āŽĩāŽŗāŽ°ā¯āŽšā¯āŽšāŽŋāŽ¯āŽŋāŽ˛ā¯ āŽ¨āŽąā¯āŽĒā¯ŠāŽ°ā¯āŽ¤ā¯āŽ¤āŽ™ā¯āŽ•āŽŗā¯", "features_setting_description": "āŽĒāŽ¯āŽŠā¯āŽĒāŽžāŽŸā¯āŽŸā¯ āŽ…āŽŽā¯āŽšāŽ™ā¯āŽ•āŽŗā¯ˆ āŽ¨āŽŋāŽ°ā¯āŽĩāŽ•āŽŋāŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", - "file_name": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯", "file_name_or_extension": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽĒā¯†āŽ¯āŽ°ā¯ āŽ…āŽ˛ā¯āŽ˛āŽ¤ā¯ āŽ¨ā¯€āŽŸā¯āŽŸāŽŋāŽĒā¯āŽĒ❁", "file_size": "āŽ•ā¯‹āŽĒā¯āŽĒ❁ āŽ…āŽŗāŽĩ❁", "filename": "āŽ•ā¯‹āŽĒā¯āŽĒ❁āŽĒā¯āŽĒā¯†āŽ¯āŽ°ā¯", @@ -1474,7 +1482,6 @@ "not_available": "āŽ‡āŽ¤āŽąā¯āŽ•āŽŋāŽ˛ā¯āŽ˛ā¯ˆ", "not_in_any_album": "āŽŽāŽ¨ā¯āŽ¤ āŽ†āŽ˛ā¯āŽĒāŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "not_selected": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸāŽĩāŽŋāŽ˛ā¯āŽ˛ā¯ˆ", - "note_apply_storage_label_to_previously_uploaded assets": "āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒ❁: āŽŽā¯āŽŠā¯āŽŠāŽ°ā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯ āŽšā¯‡āŽŽāŽŋāŽĒā¯āŽĒāŽ• āŽ˛ā¯‡āŽĒāŽŋāŽŗā¯ˆ āŽĒāŽ¯āŽŠā¯āŽĒāŽŸā¯āŽ¤ā¯āŽ¤, āŽ‡āŽ¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯", "notes": "āŽ•ā¯āŽąāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯", "nothing_here_yet": "āŽ‡āŽŠā¯āŽŠā¯āŽŽā¯ āŽ‡āŽ™ā¯āŽ•ā¯‡ āŽŽāŽ¤ā¯āŽĩā¯āŽŽā¯ āŽ‡āŽ˛ā¯āŽ˛ā¯ˆ", "notification_permission_dialog_content": "āŽ…āŽąāŽŋāŽĩāŽŋāŽĒā¯āŽĒā¯āŽ•āŽŗā¯ˆ āŽ‡āŽ¯āŽ•ā¯āŽ•, āŽ…āŽŽā¯ˆāŽĒā¯āŽĒā¯āŽ•āŽŗā¯āŽ•ā¯āŽ•ā¯āŽšā¯ āŽšā¯†āŽŠā¯āŽąā¯ āŽ‡āŽšā¯ˆāŽĩ❁ āŽŽāŽŠā¯āŽĒāŽ¤ā¯ˆāŽ¤ā¯ āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĩā¯āŽŽā¯.", @@ -1670,7 +1677,7 @@ "reassigned_assets_to_new_person": "āŽĒā¯āŽ¤āŽŋāŽ¯ āŽ¨āŽĒāŽ°ā¯āŽ•ā¯āŽ•ā¯ {count, plural, one {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯} other {# āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•āŽŗā¯}} āŽŽā¯€āŽŖā¯āŽŸā¯āŽŽā¯ āŽ’āŽ¤ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "reassing_hint": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯āŽ•ā¯āŽ•āŽŗā¯ˆ āŽāŽąā¯āŽ•āŽŠāŽĩ❇ āŽ‡āŽ°ā¯āŽ•ā¯āŽ•ā¯āŽŽā¯ āŽ¨āŽĒāŽ°ā¯āŽ•ā¯āŽ•ā¯ āŽ’āŽ¤ā¯āŽ•ā¯āŽ•ā¯āŽ™ā¯āŽ•āŽŗā¯", "recent": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛", - "recent-albums": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", + "recent_albums": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛ āŽ†āŽ˛ā¯āŽĒāŽ™ā¯āŽ•āŽŗā¯", "recent_searches": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛ āŽ¤ā¯‡āŽŸāŽ˛ā¯āŽ•āŽŗā¯", "recently_added": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "recently_added_page_title": "āŽ…āŽŖā¯āŽŽā¯ˆāŽ•ā¯ āŽ•āŽžāŽ˛āŽ¤ā¯āŽ¤āŽŋāŽ˛ā¯ āŽšā¯‡āŽ°ā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", @@ -2134,7 +2141,6 @@ "updated_at": "āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "updated_password": "āŽĒā¯āŽ¤ā¯āŽĒā¯āŽĒāŽŋāŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽ•āŽŸāŽĩā¯āŽšā¯āŽšā¯ŠāŽ˛ā¯", "upload": "āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąā¯āŽŽā¯", - "upload_action_prompt": "{count} āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąā¯āŽĩāŽ¤āŽąā¯āŽ•ā¯ āŽĩāŽ°āŽŋāŽšā¯ˆāŽ¯āŽŋāŽ˛ā¯ āŽ¨āŽŋāŽąā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸāŽ¤ā¯", "upload_concurrency": "āŽ’āŽ¤ā¯āŽ¤āŽŋāŽšā¯ˆāŽĩ❈āŽĒā¯ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯", "upload_details": "āŽĩāŽŋāŽĩāŽ°āŽ™ā¯āŽ•āŽŗā¯ˆ āŽĒāŽ¤āŽŋāŽĩā¯‡āŽąā¯āŽąāŽĩā¯āŽŽā¯", "upload_dialog_info": "āŽ¤ā¯‡āŽ°ā¯āŽ¨ā¯āŽ¤ā¯†āŽŸā¯āŽ•ā¯āŽ•āŽĒā¯āŽĒāŽŸā¯āŽŸ āŽšā¯ŠāŽ¤ā¯āŽ¤ā¯ (āŽ•āŽŗā¯ˆ) āŽšā¯‡āŽĩā¯ˆāŽ¯āŽ•āŽ¤ā¯āŽ¤āŽŋāŽąā¯āŽ•ā¯ āŽ•āŽžāŽĒā¯āŽĒ❁āŽĒā¯ āŽĒāŽŋāŽ°āŽ¤āŽŋ āŽŽāŽŸā¯āŽ•ā¯āŽ• āŽĩāŽŋāŽ°ā¯āŽŽā¯āŽĒā¯āŽ•āŽŋāŽąā¯€āŽ°ā¯āŽ•āŽŗāŽž?", @@ -2211,7 +2217,6 @@ "welcome": "āŽĩāŽ°āŽĩā¯‡āŽąā¯āŽ•āŽŋāŽąā¯‹āŽŽā¯", "welcome_to_immich": "āŽ‡āŽŽā¯āŽŽāŽŋāŽšā¯āŽšāŽŋāŽąā¯āŽ•ā¯ āŽĩāŽ°ā¯āŽ•", "wifi_name": "āŽĩā¯ˆāŽƒāŽĒ❈ āŽĒā¯†āŽ¯āŽ°ā¯", - "workflow": "āŽĒāŽŖāŽŋāŽĒā¯āŽĒāŽžāŽ¯ā¯āŽĩ❁", "wrong_pin_code": "āŽ¤āŽĩāŽąāŽžāŽŠ āŽĒāŽŋāŽŠā¯ āŽ•ā¯āŽąāŽŋāŽ¯ā¯€āŽŸā¯", "year": "āŽ†āŽŖā¯āŽŸā¯", "years_ago": "{years, plural, one {# āŽ†āŽŖā¯āŽŸā¯} other {# āŽ†āŽŖā¯āŽŸā¯āŽ•āŽŗā¯}} āŽŽā¯āŽŠā¯āŽĒ❁", diff --git a/i18n/te.json b/i18n/te.json index c146609e13..275b2deb42 100644 --- a/i18n/te.json +++ b/i18n/te.json @@ -588,8 +588,6 @@ "editor": "ā°Žā°Ąā°ŋā°Ÿā°°āą", "editor_close_without_save_prompt": "ā°Žā°žā°°āąā°Ēāąā°˛āą ā°¸āą‡ā°ĩāą ā°šāą‡ā°¯ā°Ŧā°Ąā°ĩāą", "editor_close_without_save_title": "ā°Žā°Ąā°ŋā°Ÿā°°āąâ€Œā°¨āą ā°Žāą‚ā°¸ā°ŋā°ĩāą‡ā°¯ā°žā°˛ā°ž?", - "editor_crop_tool_h2_aspect_ratios": "ā°•ā°žā°°ā°• ā°¨ā°ŋā°ˇāąā°Ēā°¤āąā°¤āąā°˛āą", - "editor_crop_tool_h2_rotation": "ā°­āąā°°ā°Žā°Ŗā°‚", "email": "ā°‡-ā°Žāą†ā°¯ā°ŋā°˛āą", "empty_trash": "ā°šāą†ā°¤āąā°¤ā°¨āą ā°–ā°žā°ŗāą€ ā°šāą‡ā°¯ā°ŋ", "empty_trash_confirmation": "ā°Žāą€ā°°āą ā°–ā°šāąā°šā°ŋā°¤ā°‚ā°—ā°ž ā°Ÿāąā°°ā°žā°ˇāąâ€Œā°¨āą ā°–ā°žā°ŗāą€ ā°šāą‡ā°¯ā°žā°˛ā°¨āąā°•āąā°‚ā°Ÿāąā°¨āąā°¨ā°žā°°ā°ž? ā°‡ā°Ļā°ŋ ā°Ÿāąā°°ā°žā°ˇāąâ€Œā°˛āą‹ā°¨ā°ŋ ā°…ā°¨āąā°¨ā°ŋ ā°†ā°¸āąā°¤āąā°˛ā°¨āą ā°‡ā°Žāąā°Žā°ŋā°šāą ā°¨āąā°‚ā°Ąā°ŋ ā°ļā°žā°ļāąā°ĩā°¤ā°‚ā°—ā°ž ā°¤āąŠā°˛ā°—ā°ŋā°¸āąā°¤āąā°‚ā°Ļā°ŋ.\nā°Žāą€ā°°āą Ⰸ ā°šā°°āąā°¯ā°¨āą ā°°ā°Ļāąā°Ļāą ā°šāą‡ā°¯ā°˛āą‡ā°°āą!", @@ -733,7 +731,6 @@ "feature_photo_updated": "ā°Ģāą€ā°šā°°āą ā°Ģāą‹ā°Ÿāą‹ ā°¨ā°ĩāą€ā°•ā°°ā°ŋā°‚ā°šā°Ŧā°Ąā°ŋā°‚ā°Ļā°ŋ", "features": "ā°˛ā°•āąā°ˇā°Ŗā°žā°˛āą", "features_setting_description": "ā°¯ā°žā°Ēāą ā°Ģāą€ā°šā°°āąâ€Œā°˛ā°¨āą ā°¨ā°ŋā°°āąā°ĩā°šā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", - "file_name": "ā°Ģāąˆā°˛āą ā°Ēāą‡ā°°āą", "file_name_or_extension": "ā°Ģāąˆā°˛āą ā°Ēāą‡ā°°āą ā°˛āą‡ā°Ļā°ž ā°ĒāąŠā°Ąā°ŋā°—ā°ŋā°‚ā°Ēāą", "filename": "ā°Ģāąˆā°˛āąā°Ēāą‡ā°°āą", "filetype": "ā°Ģāąˆā°˛āą ā°°ā°•ā°‚", @@ -898,7 +895,6 @@ "no_results_description": "ā°Ēā°°āąā°¯ā°žā°¯ā°Ēā°Ļā°‚ ā°˛āą‡ā°Ļā°ž ā°Žā°°ā°ŋā°‚ā°¤ ā°¸ā°žā°§ā°žā°°ā°Ŗ ā°•āą€ā°ĩā°°āąā°Ąāąâ€Œā°¨ā°ŋ ā°Ēāąā°°ā°¯ā°¤āąā°¨ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "no_shared_albums_message": "ā°Žāą€ ā°¨āą†ā°Ÿāąâ€Œā°ĩā°°āąā°•āąâ€Œā°˛āą‹ā°¨ā°ŋ ā°ĩāąā°¯ā°•āąā°¤āąā°˛ā°¤āą‹ ā°Ģāą‹ā°Ÿāą‹ā°˛āą ā°Žā°°ā°ŋā°¯āą ā°ĩāą€ā°Ąā°ŋā°¯āą‹ā°˛ā°¨āą ā°­ā°žā°—ā°¸āąā°ĩā°žā°Žāąā°¯ā°‚ ā°šāą‡ā°¯ā°Ąā°žā°¨ā°ŋā°•ā°ŋ ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°¨āą ā°¸āąƒā°ˇāąā°Ÿā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "not_in_any_album": "ā° ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°˛āą‹ā°¨āą‚ ā°˛āą‡ā°Ļāą", - "note_apply_storage_label_to_previously_uploaded assets": "ā°—ā°Žā°¨ā°ŋā°•: ā°—ā°¤ā°‚ā°˛āą‹ ā°…ā°Ēāąâ€Œā°˛āą‹ā°Ąāą ā°šāą‡ā°¸ā°ŋā°¨ ā°†ā°¸āąā°¤āąā°˛ā°•āą ā°¨ā°ŋā°˛āąā°ĩ ā°˛āą‡ā°Ŧāąā°˛āąâ€Œā°¨āą ā°ĩā°°āąā°¤ā°ŋā°‚ā°Ēā°œāą‡ā°¯ā°Ąā°žā°¨ā°ŋā°•ā°ŋ,", "notes": "ā°—ā°Žā°¨ā°ŋā°•ā°˛āą", "notification_toggle_setting_description": "ā°‡ā°Žāą†ā°¯ā°ŋā°˛āą ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°˛ā°¨āą ā°Ēāąā°°ā°žā°°ā°‚ā°­ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "notifications": "ā°¨āą‹ā°Ÿā°ŋā°Ģā°ŋā°•āą‡ā°ˇā°¨āąâ€Œā°˛āą", @@ -1024,7 +1020,7 @@ "reassign": "ā°¤ā°ŋā°°ā°ŋā°—ā°ŋ ā°•āą‡ā°Ÿā°žā°¯ā°ŋā°‚ā°šāą", "reassing_hint": "ā°Žā°‚ā°šāąā°•āąā°¨āąā°¨ ā°†ā°¸āąā°¤āąā°˛ā°¨āą ā°‡ā°Ēāąā°ĒⰟā°ŋā°•āą‡ ā°‰ā°¨āąā°¨ ā°ĩāąā°¯ā°•āąā°¤ā°ŋā°•ā°ŋ ā°•āą‡ā°Ÿā°žā°¯ā°ŋā°‚ā°šā°‚ā°Ąā°ŋ", "recent": "ā°‡ā°Ÿāą€ā°ĩā°˛ā°ŋ", - "recent-albums": "ā°‡ā°Ÿāą€ā°ĩā°˛ā°ŋ ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°˛āą", + "recent_albums": "ā°‡ā°Ÿāą€ā°ĩā°˛ā°ŋ ā°†ā°˛āąā°Ŧā°Žāąâ€Œā°˛āą", "recent_searches": "ā°‡ā°Ÿāą€ā°ĩā°˛ā°ŋ ā°ļāą‹ā°§ā°¨ā°˛āą", "refresh": "ā°°ā°ŋā°Ģāąā°°āą†ā°ˇāą ā°šāą‡ā°¯ā°ŋ", "refresh_encoded_videos": "ā°Žā°¨āąâ€Œā°•āą‹ā°Ąāą ā°šāą‡ā°¸ā°ŋā°¨ ā°ĩāą€ā°Ąā°ŋā°¯āą‹ā°˛ā°¨āą ā°°ā°ŋā°Ģāąā°°āą†ā°ˇāą ā°šāą‡ā°¯ā°‚ā°Ąā°ŋ", diff --git a/i18n/th.json b/i18n/th.json index c960fd8cb9..ee53ff2c9f 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -6,36 +6,41 @@ "action": "ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", "action_common_update": "ā¸­ā¸ąā¸›āš€ā¸”ā¸•", "actions": "ā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", - "active": "āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸­ā¸ĸā¸šāšˆ", + "active": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸—ā¸ŗā¸‡ā¸˛ā¸™", + "active_count": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸—ā¸ŗā¸‡ā¸˛ā¸™: {count}", "activity": "ā¸ā¸´ā¸ˆā¸ā¸Ŗā¸Ŗā¸Ą", "activity_changed": "ā¸ā¸´ā¸ˆā¸ā¸Ŗā¸Ŗā¸Ą{enabled, select, true {āš€ā¸›ā¸´ā¸”} other {⏛⏴⏔}}⏭ā¸ĸā¸šāšˆ", "add": "āš€ā¸žā¸´āšˆā¸Ą", - "add_a_description": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔", + "add_a_description": "āš€ā¸žā¸´āšˆā¸Ąā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", "add_a_location": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡", "add_a_name": "āš€ā¸žā¸´āšˆā¸Ąā¸Šā¸ˇāšˆā¸­", "add_a_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ģā¸ąā¸§ā¸‚āš‰ā¸­", + "add_action": "āš€ā¸žā¸´āšˆā¸Ąā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗ", + "add_assets": "āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­", "add_birthday": "āš€ā¸žā¸´āšˆā¸Ąā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”", "add_endpoint": "āš€ā¸žā¸´āšˆā¸Ąā¸›ā¸Ĩ⏞ā¸ĸ⏗⏞⏇", "add_exclusion_pattern": "āš€ā¸žā¸´āšˆā¸Ąā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", + "add_filter": "āš€ā¸žā¸´āšˆā¸Ąā¸•ā¸ąā¸§ā¸ā¸Ŗā¸­ā¸‡", "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": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡â€Ļ", + "add_to_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "add_to_album_bottom_sheet_added": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ {album} āšā¸Ĩāš‰ā¸§", "add_to_album_bottom_sheet_already_exists": "⏭ā¸ĸā¸šāšˆāšƒā¸™ {album} ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "add_to_album_bottom_sheet_some_local_assets": "āš„ā¸Ÿā¸ĨāšŒā¸šā¸˛ā¸‡ā¸Ēāšˆā¸§ā¸™āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš„ā¸”āš‰", "add_to_albums": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛āšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "add_to_albums_count": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą ({count})", - "add_to_shared_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒā¸ā¸ąā¸™", + "add_to_bottom_bar": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡", + "add_to_shared_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒ", "add_upload_to_stack": "āš€ā¸žā¸´āšˆā¸Ąā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš€ā¸‚āš‰ā¸˛ stack", "add_url": "āš€ā¸žā¸´āšˆā¸Ą URL", "added_to_archive": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "added_to_favorites": "āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", - "added_to_favorites_count": "{count, number} ā¸Ŗā¸šā¸›ā¸–ā¸šā¸āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", + "added_to_favorites_count": "āš€ā¸žā¸´āšˆā¸Ą {count, number} ā¸Ŗā¸šā¸›āš€ā¸‚āš‰ā¸˛ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”āšā¸Ĩāš‰ā¸§", "admin": { "add_exclusion_pattern_description": "āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›āšā¸šā¸šā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™ ā¸Ŗā¸­ā¸‡ā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ *, ** āšā¸Ĩ⏰ ? ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™āš„ā¸”āš€ā¸Ŗāš‡ā¸ā¸—ā¸­ā¸Ŗā¸ĩ⏗ā¸ĩāšˆā¸Šā¸ˇāšˆā¸­ā¸§āšˆā¸˛ \"Raw\" āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"**/Raw/**\" ā¸–āš‰ā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸—ā¸ĩāšˆā¸Ĩā¸‡ā¸—āš‰ā¸˛ā¸ĸā¸”āš‰ā¸§ā¸ĸ \".tif\" āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"**/*.tif\" ā¸–āš‰ā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸°āš€ā¸§āš‰ā¸™ā¸žā¸˛ā¸˜ā¸—ā¸ĩāšˆāš€ā¸Ŗā¸´āšˆā¸Ąā¸ˆā¸˛ā¸āš„ā¸”āš€ā¸Ŗā¸ā¸—ā¸­ā¸Ŗā¸ĩā¸šā¸™ā¸Ēā¸¸ā¸”āšƒā¸Ģāš‰āšƒā¸Šāš‰ \"/ā¸žā¸˛ā¸˜/⏗ā¸ĩāšˆā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗ/ā¸Ĩā¸°āš€ā¸§āš‰ā¸™/**\"", "admin_user": "ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ", @@ -49,7 +54,7 @@ "backup_database_enable_description": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_keep_last_amount": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸•āš‰ā¸­ā¸‡āš€ā¸āš‡ā¸šāš„ā¸§āš‰", "backup_onboarding_1_description": "ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸™ā¸­ā¸ā¸Ē⏖⏞⏙⏗ā¸ĩāšˆā¸šā¸™ā¸„ā¸Ĩā¸˛ā¸§ā¸”āšŒā¸Ģ⏪⏎⏭⏗ā¸ĩāšˆā¸•ā¸ąāš‰ā¸‡ā¸­ā¸ˇāšˆā¸™", - "backup_onboarding_2_description": "ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸•āšˆā¸˛ā¸‡ā¸ā¸ąā¸™ ⏋ā¸ļāšˆā¸‡ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡āš„ā¸Ÿā¸ĨāšŒā¸Ģā¸Ĩā¸ąā¸āšā¸Ĩā¸°āš„ā¸Ÿā¸ĨāšŒā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡", + "backup_onboarding_2_description": "ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸—ā¸ĩāšˆā¸•āšˆā¸˛ā¸‡ā¸ā¸ąā¸™ ⏋ā¸ļāšˆā¸‡ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡āš„ā¸Ÿā¸ĨāšŒā¸Ģā¸Ĩā¸ąā¸āšā¸Ĩā¸°āš„ā¸Ÿā¸ĨāšŒā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡", "backup_onboarding_3_description": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸Šā¸¸ā¸”ā¸‚ā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡āš„ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸´ā¸Ą ⏋ā¸ļāšˆā¸‡ā¸Ŗā¸§ā¸Ąā¸–ā¸ļ⏇ 1 ā¸Šā¸¸ā¸”ā¸—ā¸ĩāšˆā¸•ā¸ąāš‰ā¸‡ā¸­ā¸ĸā¸šāšˆā¸„ā¸™ā¸Ĩā¸°ā¸–ā¸´āšˆā¸™ āšā¸Ĩ⏰ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ 2 ā¸Šā¸¸ā¸”", "backup_onboarding_description": "āšā¸™ā¸°ā¸™ā¸ŗāšƒā¸Ģāš‰āšƒā¸Šāš‰ ⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸šā¸š 3-2-1āš€ā¸žā¸ˇāšˆā¸­ā¸›ā¸ā¸›āš‰ā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ ā¸„ā¸§ā¸Ŗāš€ā¸āš‡ā¸šā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸‚ā¸­ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž/⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩā¸°ā¸ā¸˛ā¸™ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏂⏭⏇ Immich āš€ā¸žā¸ˇāšˆā¸­ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš„ā¸”āš‰ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸—ā¸ąāšˆā¸§ā¸–ā¸ļ⏇", "backup_onboarding_footer": "ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ąā¸—ā¸ĩāšˆāš€ā¸ā¸ĩāšˆā¸ĸā¸§ā¸ā¸ąā¸šā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ⏂⏭⏇ Immich āš‚ā¸›ā¸Ŗā¸”ā¸”ā¸šā¸—ā¸ĩāšˆ documentation", @@ -60,7 +65,7 @@ "cleared_jobs": "āš€ā¸„ā¸Ĩā¸ĩā¸ĸā¸ŖāšŒā¸‡ā¸˛ā¸™ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸š: {job}", "config_set_by_file": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸„ā¸­ā¸™ā¸Ÿā¸´ā¸ā¸ā¸ŗā¸Ĩā¸ąā¸‡ā¸–ā¸šā¸ā¸ā¸ŗā¸Ģā¸™ā¸”āš‚ā¸”ā¸ĸāš„ā¸Ÿā¸ĨāšŒā¸„ā¸­ā¸™ā¸Ÿā¸´ā¸", "confirm_delete_library": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁ā¸Ĩā¸šā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž {library} ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", - "confirm_delete_library_assets": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁ā¸Ĩā¸šā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ? ā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” {count, plural, one {# ā¸Ēā¸ˇāšˆā¸­} other {all # ā¸Ēā¸ˇāšˆā¸­}} ā¸Ēā¸ĩāšˆā¸­āšƒā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩ⏚⏭⏭⏁⏈⏞⏁ Immich āš‚ā¸”ā¸ĸ⏖⏞⏧⏪ āš„ā¸Ÿā¸ĨāšŒā¸ˆā¸°ā¸ĸā¸ąā¸‡ā¸„ā¸‡ā¸­ā¸ĸā¸šāšˆā¸šā¸™ā¸”ā¸´ā¸Ēā¸āšŒ", + "confirm_delete_library_assets": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ĩā¸šā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ? ā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” {count, plural, one {# ā¸Ēā¸ˇāšˆā¸­} other {all # ā¸Ēā¸ˇāšˆā¸­}} ā¸Ēā¸ĩāšˆā¸­āšƒā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩ⏚⏭⏭⏁⏈⏞⏁ Immich āš‚ā¸”ā¸ĸ⏖⏞⏧⏪ āš„ā¸Ÿā¸ĨāšŒā¸ˆā¸°ā¸ĸā¸ąā¸‡ā¸„ā¸‡ā¸­ā¸ĸā¸šāšˆā¸šā¸™ā¸”ā¸´ā¸Ēā¸āšŒ", "confirm_email_below": "āš‚ā¸›ā¸Ŗā¸”ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ āš‚ā¸”ā¸ĸā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ąā¸žāšŒ \"{email}\" ā¸‚āš‰ā¸˛ā¸‡ā¸Ĩāšˆā¸˛ā¸‡", "confirm_reprocess_all_faces": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸Ģā¸Ąāšˆ? ā¸Šā¸ˇāšˆā¸­ā¸„ā¸™ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šāš„ā¸›ā¸”āš‰ā¸§ā¸ĸ", "confirm_user_password_reset": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸Ŗā¸ĩāš€ā¸‹āš‡ā¸•ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ {user} ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", @@ -72,6 +77,7 @@ "disable_login": "⏛⏴⏔⏁⏞⏪ā¸Ĩāš‡ā¸­ā¸ā¸­ā¸´ā¸™", "duplicate_detection_job_description": "āšƒā¸Šāš‰ machine learning ā¸ā¸ąā¸šā¸Ēā¸ĩāšˆā¸­āš€ā¸žā¸ˇāšˆā¸­ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸—ā¸ĩāšˆā¸„ā¸Ĩāš‰ā¸˛ā¸ĸā¸ā¸ąā¸™ āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸ąā¸ˆā¸‰ā¸Ŗā¸´ā¸ĸ⏰", "exclusion_pattern_description": "ā¸‚āš‰ā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸°āš€ā¸§āš‰ā¸™āš„ā¸Ÿā¸ĨāšŒāšā¸Ĩā¸°āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸‚ā¸“ā¸°ā¸Ēāšā¸ā¸™ā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ ā¸Ąā¸ĩā¸›ā¸Ŗā¸°āš‚ā¸ĸā¸Šā¸™āšŒāš€ā¸Ąā¸ˇāšˆā¸­āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ąā¸ĩāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸­ā¸ĸā¸˛ā¸ā¸™ā¸ŗāš€ā¸‚āš‰ā¸˛ āš€ā¸Šāšˆā¸™āš„ā¸Ÿā¸ĨāšŒ RAW", + "external_libraries_page_description": "ā¸Ģā¸™āš‰ā¸˛ā¸•āšˆā¸˛ā¸‡ā¸„ā¸Ĩā¸ąā¸‡āšā¸­ā¸”ā¸Ąā¸´ā¸™ā¸ ā¸˛ā¸ĸ⏙⏭⏁", "face_detection": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛", "face_detection_description": "ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛āšƒā¸™ā¸Ēā¸ĩāšˆā¸­āš‚ā¸”ā¸ĸāšƒā¸Šāš‰ machine learning ⏧⏴⏔ā¸ĩāš‚ā¸­ā¸ˆā¸°āšƒā¸Šāš‰ā¸ ā¸˛ā¸žā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸ˆā¸˛ā¸ā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ \"ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”\" ā¸ˆā¸°ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” \"⏂⏞⏔ā¸Ģ⏞ā¸ĸ\" ā¸ˆā¸°ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸Ēā¸ĩāšˆā¸­ā¸—ā¸ĩāšˆā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩ āšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸–ā¸šā¸ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšā¸Ĩāš‰ā¸§ā¸ˆā¸°ā¸–ā¸šā¸āš€ā¸‚āš‰ā¸˛ā¸„ā¸´ā¸§ā¸›ā¸Ŗā¸°ā¸Ąā¸§ā¸Ĩ⏜ā¸Ĩā¸ā¸˛ā¸Ŗā¸ˆā¸”ā¸ˆā¸ŗāšƒā¸šā¸Ģā¸™āš‰ā¸˛ āš€ā¸žā¸´āšˆā¸Ąāš€ā¸‚āš‰ā¸˛āš„ā¸›āšƒā¸™ā¸ā¸Ĩā¸¸āšˆā¸Ąā¸—ā¸ĩāšˆā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§ā¸Ģā¸Ŗā¸ˇā¸­ā¸„ā¸™āšƒā¸Ģā¸Ąāšˆ", "facial_recognition_job_description": "ā¸™ā¸ŗāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāš„ā¸”āš‰āš„ā¸›ā¸ˆā¸ąā¸šā¸ā¸Ĩā¸¸āšˆā¸Ąā¸•ā¸˛ā¸Ąā¸œā¸šāš‰ā¸„ā¸™ ā¸‚ā¸ąāš‰ā¸™ā¸•ā¸­ā¸™ā¸™ā¸ĩāš‰ā¸—ā¸ŗā¸‡ā¸˛ā¸™ā¸Ģā¸Ĩā¸ąā¸‡ā¸ˆā¸˛ā¸ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ \"ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”\" ⏈⏰⏈⏺⏁ā¸Ĩā¸¸āšˆā¸Ąāšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸Ģā¸Ąāšˆ \"⏂⏞⏔ā¸Ģ⏞ā¸ĸ\" ā¸ˆā¸°ā¸ˆā¸ąā¸”ā¸„ā¸´ā¸§āšƒā¸šā¸Ģā¸™āš‰ā¸˛ā¸—ā¸ĩāšˆā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ŗā¸°ā¸šā¸¸ā¸„ā¸™", @@ -126,6 +132,10 @@ "logging_level_description": "āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ āšƒā¸Šāš‰ā¸Ŗā¸°ā¸”ā¸ąā¸šā¸ā¸˛ā¸Ŗā¸šā¸ąā¸™ā¸—ā¸ļā¸ā¸­ā¸°āš„ā¸Ŗ", "logging_settings": "ā¸ā¸˛ā¸Ŗā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "machine_learning_availability_checks_description": "ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šāšā¸Ĩā¸°āš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸Šāš‰āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ machine learning āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", + "machine_learning_availability_checks_interval": "⏪⏰ā¸ĸā¸°āš€ā¸§ā¸Ĩā¸˛ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚", + "machine_learning_availability_checks_interval_description": "⏪⏰ā¸ĸā¸°āš€ā¸§ā¸Ĩā¸˛āš€ā¸›āš‡ā¸™ā¸Ąā¸´ā¸Ĩā¸Ĩ⏴⏧⏴⏙⏞⏗ā¸ĩ⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸„ā¸§ā¸˛ā¸Ąā¸žā¸Ŗāš‰ā¸­ā¸Ąāšā¸•āšˆā¸Ĩā¸°ā¸„ā¸Ŗā¸ąāš‰ā¸‡", + "machine_learning_availability_checks_timeout": "⏄⏺⏂⏭ā¸Ģā¸Ąā¸”āš€ā¸§ā¸Ĩ⏞", + "machine_learning_availability_checks_timeout_description": "ā¸ˆā¸ŗā¸™ā¸§ā¸™ā¸Ąā¸´ā¸Ĩā¸Ĩ⏴⏧⏴⏙⏞⏗ā¸ĩ⏗ā¸ĩāšˆā¸ˆā¸°ā¸™ā¸ąā¸šā¸§āšˆā¸˛ā¸Ģā¸Ąā¸”āš€ā¸§ā¸Ĩ⏞ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸„ā¸§ā¸˛ā¸Ąā¸žā¸Ŗāš‰ā¸­ā¸Ą", "machine_learning_clip_model": "āš‚ā¸Ąāš€ā¸”ā¸Ĩ Clip", "machine_learning_clip_model_description": "ā¸Šā¸ˇāšˆā¸­ā¸‚ā¸­ā¸‡āš‚ā¸Ąāš€ā¸”ā¸Ĩ CLIP ⏗ā¸ĩāšˆā¸Ŗā¸°ā¸šā¸¸ā¸•ā¸Ŗā¸‡ā¸™ā¸ĩāš‰ āš‚ā¸›ā¸Ŗā¸”ā¸—ā¸Ŗā¸˛ā¸šā¸§āšˆā¸˛ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸•āš‰ā¸­ā¸‡ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸‡ā¸˛ā¸™ 'ā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸ąā¸ˆā¸‰ā¸Ŗā¸´ā¸ĸ⏰' āšƒā¸Ģā¸Ąāšˆā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸—ā¸¸ā¸ā¸Ŗā¸šā¸›āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āš‚ā¸Ąāš€ā¸”ā¸Ĩ", "machine_learning_duplicate_detection": "ā¸•ā¸Ŗā¸§ā¸ˆā¸ˆā¸ąā¸šā¸ā¸˛ā¸Ŗā¸‹āš‰ā¸ŗā¸ā¸ąā¸™", @@ -164,6 +174,8 @@ "machine_learning_smart_search_enabled": "āš€ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸ąā¸ˆā¸‰ā¸Ŗā¸´ā¸ĸ⏰", "machine_learning_smart_search_enabled_description": "ā¸Ģā¸˛ā¸ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ ā¸ ā¸˛ā¸žā¸ˆā¸°āš„ā¸Ąāšˆā¸–ā¸šā¸āšƒā¸Šāš‰ā¸Ēāšā¸˛ā¸Ģā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģā¸˛ā¸­ā¸ąā¸ˆā¸‰ā¸Ŗā¸´ā¸ĸ⏰", "machine_learning_url_description": "URL ā¸‚ā¸­ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ machine learning ⏁⏪⏓ā¸ĩā¸Ąā¸ĩ URL ā¸Ąā¸˛ā¸ā¸ā¸§āšˆā¸˛ā¸Ģ⏙ā¸ļāšˆā¸‡ URL ā¸ˆā¸°ā¸—ā¸ŗā¸ā¸˛ā¸Ŗā¸—ā¸”ā¸Ĩ⏭⏇ā¸Ēāšˆā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸Ŗā¸ĩā¸ĸā¸‡āš„ā¸›ā¸—ā¸ĩā¸Ĩā¸°ā¸­ā¸ąā¸™ā¸•ā¸˛ā¸Ąā¸Ĩā¸ŗā¸”ā¸ąā¸šā¸ˆā¸™ā¸ā¸§āšˆā¸˛ā¸ˆā¸°ā¸žā¸š URL ⏗ā¸ĩāšˆā¸•ā¸­ā¸šā¸Ē⏙⏭⏇ āšā¸Ĩā¸°ā¸ˆā¸°āš€ā¸Ĩ⏴⏁ā¸Ēāšˆā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸Šā¸ąāšˆā¸§ā¸„ā¸Ŗā¸˛ā¸§āšƒā¸™ā¸Ēāšˆā¸§ā¸™ā¸‚ā¸­ā¸‡ URL ⏗ā¸ĩāšˆāš„ā¸Ąāšˆā¸•ā¸­ā¸šā¸Ē⏙⏭⏇", + "maintenance_delete_backup": "ā¸Ĩ⏚⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", + "maintenance_delete_backup_description": "āš„ā¸Ÿā¸ĨāšŒā¸™ā¸ĩāš‰ā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šāšā¸Ĩā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šāš„ā¸”āš‰", "manage_concurrency": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸—ā¸ŗā¸‡ā¸˛ā¸™ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸™", "manage_log_settings": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ˆā¸”ā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "map_dark_style": "āšā¸šā¸šā¸Ąā¸ˇā¸”", @@ -188,9 +200,14 @@ "metadata_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ Metadata", "metadata_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ Metadata", "migration_job": "ā¸ā¸˛ā¸Ŗāš‚ā¸ĸ⏁ā¸ĸāš‰ā¸˛ā¸ĸ", - "migration_job_description": "ā¸ĸāš‰ā¸˛ā¸ĸā¸ ā¸˛ā¸žā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸Ēā¸ˇāšˆā¸­āšā¸Ĩā¸°āšƒā¸šā¸Ģā¸™āš‰ā¸˛āš„ā¸›ā¸ĸā¸ąā¸‡āš‚ā¸„ā¸Ŗā¸‡ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", + "migration_job_description": "ā¸ĸāš‰ā¸˛ā¸ĸā¸ ā¸˛ā¸žā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸Ēā¸ˇāšˆā¸­āšā¸Ĩā¸°āšƒā¸šā¸Ģā¸™āš‰ā¸˛āš„ā¸›ā¸ĸā¸ąā¸‡āš‚ā¸„ā¸Ŗā¸‡ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "nightly_tasks_cluster_new_faces_setting": "⏄ā¸Ĩā¸ąā¸Ēāš€ā¸•ā¸­ā¸ŖāšŒāšƒā¸šā¸Ģā¸™āš‰ā¸˛āšƒā¸Ģā¸Ąāšˆ", "nightly_tasks_generate_memories_setting": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆā¸ŗ", + "nightly_tasks_generate_memories_setting_description": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸„ā¸§ā¸˛ā¸Ąā¸—ā¸Ŗā¸‡ā¸ˆā¸ŗāšƒā¸Ģā¸Ąāšˆā¸ˆā¸˛ā¸ā¸Ēā¸ˇāšˆā¸­", + "nightly_tasks_missing_thumbnails_setting": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸ ā¸˛ā¸žā¸‚ā¸™ā¸˛ā¸”ā¸ĸāšˆā¸­ā¸—ā¸ĩāšˆā¸‚ā¸˛ā¸”ā¸Ģ⏞ā¸ĸāš„ā¸›", + "nightly_tasks_missing_thumbnails_setting_description": "āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸Ąā¸ĩā¸ ā¸˛ā¸žā¸‚ā¸™ā¸˛ā¸”ā¸ĸāšˆā¸­āš„ā¸›ā¸ĸā¸ąā¸‡ā¸„ā¸´ā¸§āš€ā¸žā¸ˇāšˆā¸­ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸ ā¸˛ā¸žā¸‚ā¸™ā¸˛ā¸”ā¸ĸāšˆā¸­", + "nightly_tasks_start_time_setting": "āš€ā¸§ā¸Ĩā¸˛āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", + "nightly_tasks_start_time_setting_description": "āš€ā¸§ā¸Ĩ⏞⏗ā¸ĩāšˆāš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸ˆā¸°āš€ā¸Ŗā¸´āšˆā¸Ąā¸‡ā¸˛ā¸™ā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸„ā¸ˇā¸™", "no_paths_added": "āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸žā¸´āšˆā¸Ąā¸žā¸˛ā¸˜", "no_pattern_added": "āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸šā¸›āšā¸šā¸š", "note_apply_storage_label_previous_assets": "ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ Storage Label ā¸ā¸ąā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰ āšƒā¸Ģāš‰ā¸Ŗā¸ąā¸™ā¸„ā¸ŗā¸Ēā¸ąāšˆā¸‡ā¸™ā¸ĩāš‰", @@ -322,7 +339,7 @@ "transcoding_max_b_frames": "B-frames ā¸Ēā¸šā¸‡ā¸Ē⏏⏔", "transcoding_max_b_frames_description": "ā¸„āšˆā¸˛ā¸—ā¸ĩāšˆā¸Ēā¸šā¸‡ā¸‚ā¸ļāš‰ā¸™ā¸ˆā¸°ā¸Šāšˆā¸§ā¸ĸāš€ā¸žā¸´āšˆā¸Ąā¸›ā¸Ŗā¸°ā¸Ēā¸´ā¸—ā¸˜ā¸´ā¸ ā¸˛ā¸žāšƒā¸™ā¸ā¸˛ā¸Ŗā¸šā¸ĩā¸šā¸­ā¸ąā¸” āšā¸•āšˆā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰ā¸ā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸Ŗā¸Ģā¸ąā¸Ēā¸Šāš‰ā¸˛ā¸Ĩ⏇ ā¸­ā¸˛ā¸ˆāš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸Ŗāšˆā¸§ā¸Ąā¸ā¸ąā¸šā¸ā¸˛ā¸Ŗāš€ā¸Ŗāšˆā¸‡ā¸„ā¸§ā¸˛ā¸Ąāš€ā¸Ŗāš‡ā¸§ā¸Žā¸˛ā¸ŖāšŒā¸”āšā¸§ā¸ŖāšŒā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒāš€ā¸āšˆā¸˛āš„ā¸”āš‰ ā¸„āšˆā¸˛ā¸—ā¸ĩāšˆāš€ā¸›āš‡ā¸™ 0 ā¸ˆā¸°ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ B-frame āšƒā¸™ā¸‚ā¸“ā¸°ā¸—ā¸ĩāšˆā¸„āšˆā¸˛ -1 ā¸ˆā¸°ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", "transcoding_max_bitrate": "bitrate ā¸Ēā¸šā¸‡ā¸Ē⏏⏔", - "transcoding_max_bitrate_description": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ bitrate ā¸Ēā¸šā¸‡ā¸Ēā¸¸ā¸”ā¸ˆā¸°ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸„ā¸˛ā¸”āš€ā¸”ā¸˛ā¸‚ā¸™ā¸˛ā¸”āš„ā¸Ÿā¸ĨāšŒāš„ā¸”āš‰ā¸Ąā¸˛ā¸ā¸‚ā¸ļāš‰ā¸™āš‚ā¸”ā¸ĸāš„ā¸Ąāšˆā¸ā¸Ŗā¸°ā¸—ā¸šā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸ž ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸Ąā¸Šā¸ąā¸” 720p ā¸„āšˆā¸˛ā¸—ā¸ąāšˆā¸§āš„ā¸›ā¸„ā¸ˇā¸­ 2600 kbit/s ā¸Ēāšā¸˛ā¸Ģā¸Ŗā¸ąā¸š VP9 ā¸Ģ⏪⏎⏭ HEVC, 4500 kbit/s ā¸Ēāšā¸˛ā¸Ģā¸Ŗā¸ąā¸š H.264 ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āš€ā¸Ąā¸ĩāšˆā¸­ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āš€ā¸›āš‡ā¸™ 0", + "transcoding_max_bitrate_description": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ bitrate ā¸Ēā¸šā¸‡ā¸Ēā¸¸ā¸”ā¸ˆā¸°ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸„ā¸˛ā¸”āš€ā¸”ā¸˛ā¸‚ā¸™ā¸˛ā¸”āš„ā¸Ÿā¸ĨāšŒāš„ā¸”āš‰ā¸‡āšˆā¸˛ā¸ĸ⏂ā¸ļāš‰ā¸™āš‚ā¸”ā¸ĸā¸ā¸Ŗā¸°ā¸—ā¸šā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žāš€ā¸Ĩāš‡ā¸ā¸™āš‰ā¸­ā¸ĸ ā¸„āšˆā¸˛ā¸—ā¸ąāšˆā¸§āš„ā¸›ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸Ąā¸Šā¸ąā¸” 720p ⏄⏎⏭ 2600 kbit/s ā¸Ēāšā¸˛ā¸Ģā¸Ŗā¸ąā¸š VP9 ā¸Ģ⏪⏎⏭ HEVC, ā¸Ģ⏪⏎⏭ 4500 kbit/s ā¸Ēāšā¸˛ā¸Ģā¸Ŗā¸ąā¸š H.264 ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āš€ā¸Ąā¸ĩāšˆā¸­ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āš€ā¸›āš‡ā¸™ 0 ā¸Ģā¸˛ā¸āš„ā¸Ąāšˆā¸Ŗā¸°ā¸šā¸¸ā¸Ģā¸™āšˆā¸§ā¸ĸā¸Ŗā¸°ā¸šā¸šā¸ˆā¸°ā¸–ā¸ˇā¸­ā¸§āšˆā¸˛āšƒā¸Šāš‰ā¸Ģā¸™āšˆā¸§ā¸ĸ k (kbit/s) ā¸”ā¸ąā¸‡ā¸™ā¸ąāš‰ā¸™ā¸„āšˆā¸˛ 5000, 5000k āšā¸Ĩ⏰ 5M (Mbit/s) ā¸–ā¸ˇā¸­ā¸§āšˆā¸˛āš€ā¸—ā¸ĩā¸ĸā¸šāš€ā¸—āšˆā¸˛ā¸ā¸ąā¸™", "transcoding_max_keyframe_interval": "ā¸Šāšˆā¸§ā¸‡āš€ā¸§ā¸Ĩ⏞ā¸Ēā¸šā¸‡ā¸Ē⏏⏔⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡ā¸ā¸Ŗā¸˛ā¸Ÿā¸ŸāšŒāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "transcoding_max_keyframe_interval_description": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ŗā¸°ā¸ĸ⏰ā¸Ģāšˆā¸˛ā¸‡ā¸Ēā¸šā¸‡ā¸Ē⏏⏔⏪⏰ā¸Ģā¸§āšˆā¸˛ā¸‡ā¸„ā¸ĩā¸ĸāšŒāš€ā¸Ÿā¸Ŗā¸Ą (keyframes) ā¸„āšˆā¸˛ā¸—ā¸ĩāšˆā¸•āšˆā¸ŗā¸Ĩā¸‡ā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰ā¸›ā¸Ŗā¸°ā¸Ēā¸´ā¸—ā¸˜ā¸´ā¸ ā¸˛ā¸žā¸ā¸˛ā¸Ŗā¸šā¸ĩā¸šā¸­ā¸ąā¸”āšā¸ĸāšˆā¸Ĩ⏇ āšā¸•āšˆā¸ˆā¸°ā¸Šāšˆā¸§ā¸ĸā¸›ā¸Ŗā¸ąā¸šā¸›ā¸Ŗā¸¸ā¸‡āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģā¸˛ā¸ ā¸˛ā¸ž (seek times) āšā¸Ĩā¸°ā¸­ā¸˛ā¸ˆā¸Šāšˆā¸§ā¸ĸā¸›ā¸Ŗā¸ąā¸šā¸›ā¸Ŗā¸¸ā¸‡ā¸„ā¸¸ā¸“ā¸ ā¸˛ā¸žāšƒā¸™ā¸‰ā¸˛ā¸ā¸—ā¸ĩāšˆā¸Ąā¸ĩā¸ā¸˛ā¸Ŗāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģā¸§āš€ā¸Ŗāš‡ā¸§ ā¸„āšˆā¸˛ 0 ā¸ˆā¸°ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", "transcoding_optimal_description": "⏧ā¸ĩā¸”ā¸´āš‚ā¸­ā¸Ąā¸ĩā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸Ąā¸Šā¸ąā¸”ā¸Ēā¸šā¸‡ā¸ā¸§āšˆā¸˛āš€ā¸›āš‰ā¸˛ā¸Ģā¸Ąā¸˛ā¸ĸā¸Ģ⏪⏎⏭⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸Ŗā¸šā¸›āšā¸šā¸šā¸—ā¸ĩāšˆā¸Ŗā¸ąā¸šāš„ā¸Ąāšˆāš„ā¸”āš‰", @@ -390,7 +407,7 @@ "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_sync_remote_deletions_subtitle": "⏚ā¸Ģā¸Ŗā¸ˇā¸­ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸Ÿā¸ĨāšŒā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸œāšˆā¸˛ā¸™āš€ā¸§āš‡ā¸š", + "advanced_settings_sync_remote_deletions_subtitle": "ā¸Ĩ⏚ā¸Ģā¸Ŗā¸ˇā¸­ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸Ÿā¸ĨāšŒā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰āš‚ā¸”ā¸ĸā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸œāšˆā¸˛ā¸™āš€ā¸§āš‡ā¸š", "advanced_settings_sync_remote_deletions_title": "ā¸‹ā¸´ā¸‡ā¸āšŒā¸ā¸˛ā¸Ŗā¸Ĩ⏚⏈⏞⏁⏪⏰ā¸ĸā¸°āš„ā¸ā¸Ĩ [⏄⏏⏓ā¸Ēā¸Ąā¸šā¸ąā¸•ā¸´ā¸—ā¸”ā¸Ĩ⏭⏇]", "advanced_settings_tile_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ā¸‚ā¸ąāš‰ā¸™ā¸Ēā¸šā¸‡", "advanced_settings_troubleshooting_subtitle": "āš€ā¸›ā¸´ā¸”ā¸Ÿā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ąāš€ā¸žā¸ˇāšˆā¸­āšā¸āš‰āš„ā¸‚ā¸›ā¸ąā¸ā¸Ģ⏞", @@ -398,11 +415,13 @@ "age_months": "⏭⏞ā¸ĸ⏏ {months, plural, one {# āš€ā¸”ā¸ˇā¸­ā¸™} other {# āš€ā¸”ā¸ˇā¸­ā¸™}}", "age_year_months": "⏭⏞ā¸ĸ⏏ 1 ⏛ā¸ĩ {months, plural, one {# āš€ā¸”ā¸ˇā¸­ā¸™} other {# āš€ā¸”ā¸ˇā¸­ā¸™}}", "age_years": "{years, plural, other {⏭⏞ā¸ĸ⏏ #}}", + "album": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "album_added": "āš€ā¸žā¸´āšˆā¸Ąā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_added_notification_setting_description": "āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩāš€ā¸Ąā¸ˇāšˆā¸­ā¸„ā¸¸ā¸“ā¸–ā¸šā¸āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›āšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒā¸ā¸ąā¸™", "album_cover_updated": "ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸Ģā¸™āš‰ā¸˛ā¸›ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_delete_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą {album} ⏙ā¸ĩāš‰ ?", "album_delete_confirmation_description": "ā¸Ģā¸˛ā¸āšā¸Šā¸ŖāšŒā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰ ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸Ŗā¸˛ā¸ĸā¸­ā¸ˇāšˆā¸™ā¸ˆā¸°āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš„ā¸”āš‰ā¸­ā¸ĩ⏁", + "album_deleted": "ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_info_card_backup_album_excluded": "ā¸–ā¸šā¸ā¸ĸā¸āš€ā¸§āš‰ā¸™", "album_info_card_backup_album_included": "ā¸Ŗā¸§ā¸Ą", "album_info_updated": "ā¸­ā¸ąā¸›āš€ā¸”ā¸—ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", @@ -412,9 +431,13 @@ "album_options": "ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "album_remove_user": "ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ ?", "album_remove_user_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ {user} ?", + "album_search_not_found": "āš„ā¸Ąāšˆā¸žā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆā¸•ā¸Ŗā¸‡ā¸•ā¸˛ā¸Ąā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞⏂⏭⏇⏄⏏⏓", + "album_selected": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_share_no_users": "ā¸”ā¸šāš€ā¸Ģā¸Ąā¸ˇā¸­ā¸™ā¸§āšˆā¸˛ā¸„ā¸¸ā¸“āš„ā¸”āš‰āšā¸Šā¸ŖāšŒā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰ā¸ā¸ąā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšā¸Ĩāš‰ā¸§", + "album_summary": "ā¸Ēā¸Ŗā¸¸ā¸›ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "album_updated": "ā¸­ā¸ąā¸›āš€ā¸”ā¸—ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "album_updated_setting_description": "āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩāš€ā¸Ąā¸ˇāšˆā¸­ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒā¸ā¸ąā¸™ā¸Ąā¸ĩā¸Ēā¸ˇāšˆā¸­āšƒā¸Ģā¸Ąāšˆ", + "album_upload_assets": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­ā¸ˆā¸˛ā¸ā¸„ā¸­ā¸Ąā¸žā¸´ā¸§āš€ā¸•ā¸­ā¸ŖāšŒāš€ā¸žā¸ˇāšˆā¸­āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "album_user_left": "⏭⏭⏁⏈⏞⏁ {album}", "album_user_removed": "ā¸Ĩā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ {user} āšā¸Ĩāš‰ā¸§", "album_viewer_appbar_delete_confirm": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰ā¸ˆā¸˛ā¸ā¸šā¸ąā¸ā¸Šā¸ĩ⏄⏏⏓ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ", @@ -431,15 +454,22 @@ "albums_default_sort_order": "ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™", "albums_default_sort_order_description": "ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸Ŗā¸ĩā¸ĸā¸‡āšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•āš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āš€ā¸Ąā¸ˇāšˆā¸­ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸Ģā¸Ąāšˆ", "albums_feature_description": "⏁ā¸Ĩā¸¸āšˆā¸Ąā¸‚ā¸­ā¸‡āšā¸­ā¸Ēāš€ā¸‹āš‡ā¸•ā¸—ā¸ĩāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ēāšˆā¸‡āšƒā¸Ģāš‰ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸­ā¸ˇāšˆā¸™āš„ā¸”āš‰", + "albums_on_device_count": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ ({count})", + "albums_selected": "{count, plural, one {āš€ā¸Ĩ⏎⏭⏁ # ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą} other {āš€ā¸Ĩ⏎⏭⏁ # ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą}}", "all": "ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "all_albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "all_people": "⏗⏏⏁⏄⏙", + "all_photos": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "all_videos": "⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "allow_dark_mode": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āš‚ā¸Ģā¸Ąā¸”ā¸Ąā¸ˇā¸”", "allow_edits": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸Ģāš‰āšā¸āš‰āš„ā¸‚āš„ā¸”āš‰", "allow_public_user_to_download": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸Ģāš‰ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸Ēā¸˛ā¸˜ā¸˛ā¸Ŗā¸“ā¸°ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš„ā¸”āš‰", "allow_public_user_to_upload": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•āšƒā¸Ģāš‰ā¸œā¸šāš‰āšƒā¸Šāš‰ā¸Ēā¸˛ā¸˜ā¸˛ā¸Ŗā¸“ā¸°ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš„ā¸”āš‰", + "allowed": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", "alt_text_qr_code": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž QR code", + "always_keep": "āš€ā¸āš‡ā¸šāš€ā¸Ēā¸Ąā¸­", + "always_keep_photos_hint": "\"āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡\" ā¸ˆā¸°āš€ā¸āš‡ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", + "always_keep_videos_hint": "\"āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡\" ā¸ˆā¸°āš€ā¸āš‡ā¸šā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "anti_clockwise": "ā¸—ā¸§ā¸™āš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", "api_key": "API key", "api_key_description": "ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰ā¸ˆā¸°āšā¸Ēā¸”ā¸‡āš€ā¸žā¸ĩā¸ĸā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸”ā¸ĩā¸ĸ⏧ āš‚ā¸›ā¸Ŗā¸”ā¸„ā¸ąā¸”ā¸Ĩā¸­ā¸ā¸āšˆā¸­ā¸™ā¸›ā¸´ā¸”ā¸Ģā¸™āš‰ā¸˛ā¸•āšˆā¸˛ā¸‡", @@ -448,9 +478,13 @@ "app_bar_signout_dialog_content": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸­ā¸ĸ⏞⏁⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", "app_bar_signout_dialog_ok": "āšƒā¸Šāšˆ", "app_bar_signout_dialog_title": "⏭⏭⏁⏈⏞⏁⏪⏰⏚⏚", + "app_download_links": "ā¸Ĩā¸´ā¸‡ā¸āšŒā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āšā¸­ā¸›", "app_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸­ā¸›", + "app_stores": "ā¸Ŗāš‰ā¸˛ā¸™ā¸„āš‰ā¸˛āšā¸­ā¸›", + "app_update_available": "ā¸Ąā¸ĩā¸­ā¸ąā¸›āš€ā¸”ā¸•āšā¸­ā¸›", "appears_in": "⏭ā¸ĸā¸šāšˆāšƒā¸™", "archive": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", + "archive_action_prompt": "āš€ā¸žā¸´āšˆā¸Ą {count} ⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš„ā¸›ā¸ĸā¸ąā¸‡āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩāš‰ā¸§", "archive_or_unarchive_photo": "āš€ā¸āš‡ā¸š/āš„ā¸Ąāšˆāš€ā¸āš‡ā¸šā¸ ā¸˛ā¸žā¸–ā¸˛ā¸§ā¸Ŗ", "archive_page_no_archived_assets": "āš„ā¸Ąāšˆā¸žā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗāšƒā¸™ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "archive_page_title": "āš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ ({count})", @@ -464,6 +498,7 @@ "asset_action_share_err_offline": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸”ā¸ļā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ ⏁⏺ā¸Ĩā¸ąā¸‡ā¸‚āš‰ā¸˛ā¸Ą", "asset_added_to_album": "āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšā¸Ĩāš‰ā¸§", "asset_adding_to_album": "⏁⏺ā¸Ĩā¸ąā¸‡āš€ā¸žā¸´āšˆā¸Ąāš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąâ€Ļ", + "asset_created": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸Ēā¸ˇāšˆā¸­āšā¸Ĩāš‰ā¸§", "asset_description_updated": "ā¸­ā¸ąā¸›āš€ā¸”ā¸•ā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "asset_filename_is_offline": "ā¸Ēā¸ˇāšˆā¸­ {filename} ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒā¸­ā¸ĸā¸šāšˆ", "asset_has_unassigned_faces": "ā¸Ēā¸ˇāšˆā¸­āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ŗā¸°ā¸šā¸¸āšƒā¸šā¸Ģā¸™āš‰ā¸˛", @@ -476,28 +511,35 @@ "asset_list_layout_sub_title": "ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”ā¸§ā¸˛ā¸‡", "asset_list_settings_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”ā¸§ā¸˛ā¸‡ā¸•ā¸˛ā¸Ŗā¸˛ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "asset_list_settings_title": "ā¸•ā¸˛ā¸Ŗā¸˛ā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", + "asset_not_found_on_device_android": "āš„ā¸Ąāšˆā¸žā¸šā¸Ēā¸ˇāšˆā¸­ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ", + "asset_not_found_on_device_ios": "āš„ā¸Ąāšˆā¸žā¸šā¸Ēā¸ˇāšˆā¸­ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ ā¸Ģā¸˛ā¸ā¸„ā¸¸ā¸“āšƒā¸Šāš‰ iCloud ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸˛ā¸ˆā¸ˆā¸°āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸ˆā¸˛ā¸ iCloud āš€ā¸āš‡ā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸”ā¸ĩāš„ā¸§āš‰", + "asset_not_found_on_icloud": "āš„ā¸Ąāšˆā¸žā¸šā¸Ēā¸ˇāšˆā¸­ā¸šā¸™ iCloud ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸˛ā¸ˆā¸ˆā¸°āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸™ā¸ˇāšˆā¸­ā¸‡ā¸ˆā¸˛ā¸ iCloud āš€ā¸āš‡ā¸šāš„ā¸Ÿā¸ĨāšŒā¸—ā¸ĩāšˆāš„ā¸Ąāšˆā¸”ā¸ĩāš„ā¸§āš‰", "asset_offline": "ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "asset_offline_description": "āš„ā¸Ąāšˆā¸žā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏠⏞ā¸ĸ⏙⏭⏁⏙ā¸ĩāš‰āšƒā¸™ā¸”ā¸´ā¸Ēā¸āšŒā¸­ā¸ĩā¸ā¸•āšˆā¸­āš„ā¸› āš‚ā¸›ā¸Ŗā¸”ā¸•ā¸´ā¸”ā¸•āšˆā¸­ā¸œā¸šāš‰ā¸”ā¸šāšā¸Ĩ⏪⏰⏚⏚ Immich ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸‚ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸Šāšˆā¸§ā¸ĸāš€ā¸Ģā¸Ĩ⏎⏭", "asset_restored_successfully": "ā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "asset_skipped": "ā¸‚āš‰ā¸˛ā¸Ąāšā¸Ĩāš‰ā¸§", "asset_skipped_in_trash": "āšƒā¸™ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰", + "asset_trashed": "ā¸ĸāš‰ā¸˛ā¸ĸā¸Ēā¸ˇāšˆā¸­āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°āšā¸Ĩāš‰ā¸§", + "asset_troubleshoot": "āšā¸āš‰ā¸›ā¸ąā¸ā¸Ģ⏞ā¸Ēā¸ˇāšˆā¸­", "asset_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "asset_uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔â€Ļ", "asset_viewer_settings_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāšā¸Ēā¸”ā¸‡āšā¸ā¸Ĩāš€ā¸Ĩ⏭⏪ā¸ĩ", "asset_viewer_settings_title": "ā¸•ā¸ąā¸§ā¸”ā¸šā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "assets": "ā¸Ēā¸ˇāšˆā¸­", - "assets_added_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one{# ā¸Ēā¸ˇāšˆā¸­} other {# ā¸Ēā¸ˇāšˆā¸­}} āšā¸Ĩāš‰ā¸§", + "assets_added_count": "āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ {count, plural, one{# ⏪⏞ā¸ĸ⏁⏞⏪} other {# ⏪⏞ā¸ĸ⏁⏞⏪}}āšā¸Ĩāš‰ā¸§", "assets_added_to_album_count": "āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {# asset} other {# assets}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", + "assets_added_to_albums_count": "āš€ā¸žā¸´āšˆā¸Ąā¸Ēā¸ˇāšˆā¸­ {assetTotal, plural, one {# ⏪⏞ā¸ĸ⏁⏞⏪} other {# ⏪⏞ā¸ĸ⏁⏞⏪}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą {albumTotal, plural, one {# ⏪⏞ā¸ĸ⏁⏞⏪} other {# ⏪⏞ā¸ĸ⏁⏞⏪}}āšā¸Ĩāš‰ā¸§", "assets_cannot_be_added_to_album_count": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ą {count, plural, one {ā¸Ēā¸ˇāšˆā¸­} other {ā¸Ēā¸ˇāšˆā¸­}} āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", - "assets_count": "{count, plural, one { ā¸Ēā¸ˇāšˆā¸­} other { ā¸Ēā¸ˇāšˆā¸­}}", + "assets_cannot_be_added_to_albums": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸žā¸´āšˆā¸Ą{count, plural, one {ā¸Ēā¸ˇāšˆā¸­} other {ā¸Ēā¸ˇāšˆā¸­}}āš„ā¸›ā¸ĸā¸ąā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸” āš† āš„ā¸”āš‰", + "assets_count": "ā¸Ēā¸ˇāšˆā¸­ {count, plural, one {# ⏪⏞ā¸ĸ⏁⏞⏪} other {# ⏪⏞ā¸ĸ⏁⏞⏪}}", "assets_deleted_permanently": "{count} ā¸Ēā¸ˇāšˆā¸­ā¸–ā¸šā¸ā¸Ĩ⏚⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", - "assets_deleted_permanently_from_server": "ā¸Ĩ⏚ {count} ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ Immich ⏭ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗ", + "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_removed_permanently_from_device": "ā¸Ĩ⏚ā¸Ēā¸ˇāšˆā¸­ {count} ⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸–ā¸˛ā¸§ā¸Ŗāšā¸Ĩāš‰ā¸§", "assets_restore_confirmation": "ā¸„ā¸¸ā¸“āšā¸™āšˆāšƒā¸ˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆā¸§āšˆā¸˛ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ā¸šāš‰ā¸„ā¸ˇā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸—ā¸´āš‰ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”? ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸™ā¸ĩāš‰āš„ā¸”āš‰! āš‚ā¸›ā¸Ŗā¸”ā¸—ā¸Ŗā¸˛ā¸šā¸§āšˆā¸˛ā¸Ēā¸ˇāšˆā¸­ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒāšƒā¸”āš† āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ā¸šāš‰ā¸„ā¸ˇā¸™āš„ā¸”āš‰ā¸”āš‰ā¸§ā¸ĸ⏧⏴⏘ā¸ĩ⏙ā¸ĩāš‰", "assets_restored_count": "{count, plural, one {# asset} other {# assets}} ā¸„ā¸ˇā¸™ā¸„āšˆā¸˛", "assets_restored_successfully": "ā¸ā¸šāš‰ā¸„ā¸ˇā¸™ {count} ā¸Ēā¸ˇāšˆā¸­ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", @@ -505,14 +547,17 @@ "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}} ⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸­ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", + "assets_were_part_of_albums_count": "{count, plural, one {ā¸Ēā¸ˇāšˆā¸­} other {ā¸Ēā¸ˇāšˆā¸­}}āš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸Ģ⏙ā¸ļāšˆā¸‡ā¸‚ā¸­ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸­ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "authorized_devices": "ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸—ā¸ĩāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", "automatic_endpoint_switching_subtitle": "āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸”āš‰ā¸§ā¸ĸ LAN ⏠⏞ā¸ĸāšƒā¸™ā¸§ā¸‡ Wi-Fi ⏗ā¸ĩāšˆā¸Ŗā¸°ā¸šā¸¸āš„ā¸§āš‰ āšā¸Ĩā¸°āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸”āš‰ā¸§ā¸ĸ⏧⏴⏘ā¸ĩā¸­ā¸ˇāšˆā¸™āš€ā¸Ąā¸ˇāšˆā¸­ā¸­ā¸ĸā¸šāšˆā¸™ā¸­ā¸ Wi-Fi ⏗ā¸ĩāšˆā¸Ŗā¸°ā¸šā¸¸āš„ā¸§āš‰", "automatic_endpoint_switching_title": "ā¸Ēā¸Ĩā¸ąā¸š URL ā¸­ā¸ąā¸•āš‚ā¸™ā¸Ąā¸ąā¸•ā¸´", "autoplay_slideshow": "āš€ā¸Ĩāšˆā¸™ā¸Ēāš„ā¸Ĩā¸”āšŒāš‚ā¸Šā¸§āšŒ", "back": "⏁ā¸Ĩā¸ąā¸š", "back_close_deselect": "ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸š, ⏛⏴⏔, ā¸Ģ⏪⏎⏭ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩ⏎⏭⏁", + "background_backup_running_error": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸—ā¸ŗā¸‡ā¸˛ā¸™ā¸­ā¸ĸā¸šāšˆ āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ŗā¸´āšˆā¸Ąā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸”āš‰ā¸§ā¸ĸā¸•ā¸™āš€ā¸­ā¸‡āš„ā¸”āš‰", "background_location_permission": "ā¸ā¸˛ā¸Ŗā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•ā¸Ŗā¸°ā¸šā¸¸ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", "background_location_permission_content": "āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ēā¸Ĩā¸ąā¸šā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸‚ā¸“ā¸°ā¸—ā¸ĩāšˆā¸Ŗā¸ąā¸™āšƒā¸™ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡ Immich ā¸•āš‰ā¸­ā¸‡ā¸Ŗā¸šāš‰ā¸•ā¸ŗāšā¸Ģā¸™āšˆā¸‡ā¸—ā¸ĩāšˆāšā¸Ąāšˆā¸ĸ⏺⏕ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩ⏞ āš€ā¸žā¸ˇāšˆā¸­ā¸ˆā¸°ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­āšˆā¸˛ā¸™ā¸Šā¸ˇāšˆā¸­ Wi-Fi", + "background_options": "ā¸•ā¸ąā¸§āš€ā¸Ĩā¸ˇā¸­ā¸ā¸ā¸˛ā¸Ŗā¸—ā¸ŗā¸‡ā¸˛ā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", "backup": "ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_album_selection_page_albums_device": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸šā¸™āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ ({count})", "backup_album_selection_page_albums_tap": "ā¸ā¸”āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ą ⏁⏔ā¸Ēā¸­ā¸‡ā¸„ā¸Ŗā¸ąāš‰ā¸‡āš€ā¸žā¸ˇāšˆā¸­ā¸ĸā¸āš€ā¸§āš‰ā¸™", @@ -574,8 +619,11 @@ "backup_manual_in_progress": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸­ā¸ĸā¸šāšˆ āš‚ā¸›ā¸Ŗā¸”ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆāšƒā¸™ā¸Ēā¸ąā¸ā¸žā¸ąā¸", "backup_manual_success": "ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "backup_manual_title": "ā¸Ēā¸–ā¸˛ā¸™ā¸°ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔", + "backup_options": "ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_options_page_title": "ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩ", "backup_setting_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”āšƒā¸™ā¸‰ā¸˛ā¸ā¸Ģā¸™āš‰ā¸˛ āšā¸Ĩā¸°ā¸žā¸ˇāš‰ā¸™ā¸Ģā¸Ĩā¸ąā¸‡", + "backup_settings_subtitle": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", + "backup_upload_details_page_more_details": "āšā¸•ā¸°āš€ā¸žā¸ˇāšˆā¸­ā¸”ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāš€ā¸žā¸´āšˆā¸Ąāš€ā¸•ā¸´ā¸Ą", "backward": "⏁ā¸Ĩā¸ąā¸šā¸Ģā¸Ĩā¸ąā¸‡", "biometric_auth_enabled": "ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸–ā¸šā¸āš€ā¸›ā¸´ā¸”", "biometric_locked_out": "ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒāš€ā¸žā¸ˇāšˆā¸­ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸•ā¸ąā¸§ā¸šā¸¸ā¸„ā¸„ā¸Ĩā¸–ā¸šā¸ā¸Ĩāš‡ā¸­ā¸„", @@ -611,6 +659,7 @@ "cancel": "ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "cancel_search": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", "canceled": "ā¸ĸā¸āš€ā¸Ĩ⏴⏁", + "canceling": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "cannot_merge_people": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ŗā¸§ā¸Ąā¸ā¸Ĩā¸¸āšˆā¸Ąā¸„ā¸™āš„ā¸”āš‰", "cannot_undo_this_action": "⏁⏞⏪⏁⏪⏰⏗⏺⏙ā¸ĩāš‰āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸ĸāš‰ā¸­ā¸™ā¸ā¸Ĩā¸ąā¸šāš„ā¸”āš‰!", "cannot_update_the_description": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸Ŗā¸˛ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”āš„ā¸”āš‰", @@ -627,18 +676,31 @@ "change_password_description": "ā¸ā¸˛ā¸Ŗāš€ā¸‚āš‰ā¸˛ā¸Ēā¸šāšˆā¸Ŗā¸°ā¸šā¸šā¸„ā¸Ŗā¸ąāš‰ā¸‡āšā¸Ŗā¸ ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸ˆā¸•āš‰ā¸­ā¸‡āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“āš€ā¸žā¸ˇāšˆā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸›ā¸Ĩā¸­ā¸”ā¸ ā¸ąā¸ĸ āš‚ā¸›ā¸Ŗā¸”ā¸›āš‰ā¸­ā¸™ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡", "change_password_form_confirm_password": "ā¸ĸ⏎⏙ā¸ĸā¸ąā¸™ā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", "change_password_form_description": "ā¸Ēā¸§ā¸ąā¸Ē⏔ā¸ĩ {name},\n\nā¸„ā¸Ŗā¸ąāš‰ā¸‡ā¸™ā¸ĩāš‰ā¸­ā¸˛ā¸ˆā¸ˆā¸°āš€ā¸›āš‡ā¸™ā¸„ā¸Ŗā¸ąāš‰ā¸‡āšā¸Ŗā¸ā¸—ā¸ĩāšˆā¸„ā¸¸ā¸“āš€ā¸‚āš‰ā¸˛ā¸Ēā¸šāšˆā¸Ŗā¸°ā¸šā¸š ā¸Ģā¸Ŗā¸ˇā¸­ā¸Ąā¸ĩā¸„ā¸ŗā¸‚ā¸­āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸I ā¸ā¸Ŗā¸¸ā¸“ā¸˛āš€ā¸žā¸´āšˆā¸Ąā¸Ŗā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆā¸‚āš‰ā¸˛ā¸‡ā¸Ĩāšˆā¸˛ā¸‡", + "change_password_form_log_out": "ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸­ā¸ˇāšˆā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "change_password_form_log_out_description": "āšā¸™ā¸°ā¸™ā¸ŗāšƒā¸Ģāš‰ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸­ā¸ˇāšˆā¸™ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸”āš‰ā¸§ā¸ĸ", "change_password_form_new_password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆ", "change_password_form_password_mismatch": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš„ā¸Ąāšˆā¸•ā¸Ŗā¸‡ā¸ā¸ąā¸™", "change_password_form_reenter_new_password": "⏁⏪⏭⏁⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āšƒā¸Ģā¸Ąāšˆ", "change_pin_code": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "change_your_password": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", "changed_visibility_successfully": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™ā¸ā¸˛ā¸Ŗā¸Ąā¸­ā¸‡āš€ā¸Ģāš‡ā¸™āš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§", + "charging": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸Šā¸˛ā¸ŖāšŒā¸ˆ", + "charging_requirement_mobile_backup": "⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸ˆā¸°ā¸—ā¸ŗā¸‡ā¸˛ā¸™āš€ā¸‰ā¸žā¸˛ā¸°āš€ā¸Ąā¸ˇāšˆā¸­ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸ā¸ŗā¸Ĩā¸ąā¸‡ā¸Šā¸˛ā¸ŖāšŒā¸ˆā¸­ā¸ĸā¸šāšˆ", "check_corrupt_asset_backup": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚ā¸Ē⏺⏪⏭⏇ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸œā¸´ā¸”ā¸›ā¸ā¸•ā¸´", "check_corrupt_asset_backup_button": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚", "check_corrupt_asset_backup_description": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šāš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ Wi-Fi āšā¸Ĩ⏰ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”ā¸–ā¸šā¸ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩāš‰ā¸§āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™ ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸­ā¸˛ā¸ˆāšƒā¸Šāš‰āš€ā¸§ā¸Ĩ⏞ā¸Ģā¸Ĩ⏞ā¸ĸ⏙⏞⏗ā¸ĩ", "check_logs": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "choose_matching_people_to_merge": "āš€ā¸Ĩ⏎⏭⏁⏄⏙⏗ā¸ĩāšˆā¸•ā¸Ŗā¸‡ā¸ā¸ąā¸™āš€ā¸žā¸ˇāšˆā¸­ā¸Ŗā¸§ā¸Ąāš€ā¸‚āš‰ā¸˛ā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™", "city": "āš€ā¸Ąā¸ˇā¸­ā¸‡", + "cleanup_confirm_description": "Immich ā¸žā¸šā¸Ēā¸ˇāšˆā¸­ {count} ⏪⏞ā¸ĸ⏁⏞⏪ (ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸‚ā¸ļāš‰ā¸™ā¸āšˆā¸­ā¸™ {date}) ⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸šā¸™āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸›ā¸Ĩā¸­ā¸”ā¸ ā¸ąā¸ĸāšā¸Ĩāš‰ā¸§ ā¸Ĩ⏚ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸•āš‰ā¸™ā¸—ā¸˛ā¸‡ā¸ˆā¸˛ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", + "cleanup_confirm_prompt_title": "ā¸Ĩā¸šā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", + "cleanup_deleted_assets": "ā¸ĸāš‰ā¸˛ā¸ĸā¸Ēā¸ˇāšˆā¸­ {count} ⏪⏞ā¸ĸā¸ā¸˛ā¸Ŗāš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°ā¸‚ā¸­ā¸‡ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒāšā¸Ĩāš‰ā¸§", + "cleanup_deleting": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸ĸāš‰ā¸˛ā¸ĸāš„ā¸›ā¸–ā¸ąā¸‡ā¸‚ā¸ĸ⏰...", + "cleanup_found_assets": "ā¸žā¸šā¸Ēā¸ˇāšˆā¸­ {count} ⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩāš‰ā¸§", + "cleanup_found_assets_with_size": "ā¸žā¸šā¸Ēā¸ˇāšˆā¸­ {count} ⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩāš‰ā¸§ ({size})", + "cleanup_icloud_shared_albums_excluded": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸—ā¸ĩāšˆāšā¸Šā¸ŖāšŒā¸šā¸™ iCloud āš„ā¸Ąāšˆā¸™ā¸ąā¸šā¸Ŗā¸§ā¸Ąāšƒā¸™ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", + "cleanup_no_assets_found": "āš„ā¸Ąāšˆā¸žā¸šā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸•ā¸Ŗā¸‡ā¸•ā¸˛ā¸Ąāš€ā¸‡ā¸ˇāšˆā¸­ā¸™āš„ā¸‚ā¸”āš‰ā¸˛ā¸™ā¸šā¸™ \"āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡\" ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ĩā¸šāš„ā¸”āš‰āš€ā¸‰ā¸žā¸˛ā¸°ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸šā¸™āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒāš€ā¸Ŗā¸ĩā¸ĸā¸šā¸Ŗāš‰ā¸­ā¸ĸāšā¸Ĩāš‰ā¸§āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", + "cleanup_preview_title": "ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆā¸ˆā¸°ā¸Ĩ⏚ ({count})", "clear": "ā¸Ĩāš‰ā¸˛ā¸‡", "clear_all": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "clear_all_recent_searches": "ā¸Ĩāš‰ā¸˛ā¸‡ā¸›ā¸Ŗā¸°ā¸§ā¸ąā¸•ā¸´ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞", @@ -781,8 +843,9 @@ "display_original_photos_setting_description": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸Ēā¸”ā¸‡ā¸œā¸Ĩā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸•āš‰ā¸™ā¸‰ā¸šā¸ąā¸š āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸™ā¸ĩāš‰ā¸­ā¸˛ā¸ˆā¸ˆā¸°ā¸—ā¸ŗāšƒā¸Ģāš‰ā¸ā¸˛ā¸Ŗāšā¸Ēā¸”ā¸‡ā¸ ā¸˛ā¸žāš„ā¸”āš‰ā¸Šāš‰ā¸˛ā¸Ĩ⏇", "do_not_show_again": "āš„ā¸Ąāšˆāšā¸Ēā¸”ā¸‡ā¸‚āš‰ā¸­ā¸„ā¸§ā¸˛ā¸Ąā¸™ā¸ĩāš‰ā¸­ā¸ĩ⏁", "documentation": "āš€ā¸­ā¸ā¸Ē⏞⏪", - "done": "ā¸”ā¸ŗāš€ā¸™ā¸´ā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", + "done": "āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™", "download": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_action_prompt": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {count} ā¸Šā¸´āš‰ā¸™", "download_canceled": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸ĸā¸āš€ā¸Ĩ⏴⏁", "download_complete": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™", "download_enqueue": "ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔⏭ā¸ĸā¸šāšˆāšƒā¸™ā¸„ā¸´ā¸§", @@ -792,6 +855,7 @@ "download_include_embedded_motion_videos": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģ⏧", "download_include_embedded_motion_videos_description": "ā¸Ŗā¸§ā¸Ąā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸ā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸ ā¸˛ā¸žāš€ā¸„ā¸Ĩā¸ˇāšˆā¸­ā¸™āš„ā¸Ģā¸§āš€ā¸Ąā¸ˇāšˆā¸­ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "download_notfound": "āš„ā¸Ąāšˆā¸žā¸šā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", + "download_original": "ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸•ā¸ąā¸§ā¸•ā¸ąāš‰ā¸‡ā¸•āš‰ā¸™", "download_paused": "ā¸Ģā¸ĸā¸¸ā¸”ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸Šā¸ąāšˆā¸§ā¸„ā¸Ŗā¸˛ā¸§", "download_settings": "ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "download_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", @@ -801,6 +865,7 @@ "download_waiting_to_retry": "⏪⏭ā¸Ĩā¸­ā¸‡āšƒā¸Ģā¸Ąāšˆ", "downloading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔", "downloading_asset_filename": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ {filename}", + "downloading_from_icloud": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩā¸”ā¸ˆā¸˛ā¸āš„ā¸­ā¸„ā¸Ĩ⏞⏧", "downloading_media": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸”ā¸˛ā¸§ā¸™āšŒāš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", "drop_files_to_upload": "ā¸§ā¸˛ā¸‡āš„ā¸Ÿā¸ĨāšŒāšƒā¸™ā¸Šāšˆā¸­ā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "duplicates": "⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸‹āš‰ā¸ŗā¸ā¸ąā¸™", @@ -809,6 +874,7 @@ "edit": "āšā¸āš‰āš„ā¸‚", "edit_album": "āšā¸āš‰āš„ā¸‚ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "edit_avatar": "āšā¸āš‰āš„ā¸‚ā¸•ā¸ąā¸§ā¸Ĩ⏰⏄⏪", + "edit_birthday": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™āš€ā¸ā¸´ā¸”", "edit_date": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆ", "edit_date_and_time": "āšā¸āš‰āš„ā¸‚ā¸§ā¸ąā¸™ā¸—ā¸ĩāšˆāšā¸Ĩā¸°āš€ā¸§ā¸Ĩ⏞", "edit_description": "āšā¸āš‰āš„ā¸‚ā¸„ā¸ŗā¸­ā¸˜ā¸´ā¸šā¸˛ā¸ĸ", @@ -818,17 +884,29 @@ "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_workflow": "āšā¸āš‰āš„ā¸‚ā¸ā¸Ŗā¸°ā¸šā¸§ā¸™ā¸ā¸˛ā¸Ŗā¸‡ā¸˛ā¸™", "editor": "ā¸œā¸šāš‰āšā¸āš‰āš„ā¸‚", "editor_close_without_save_prompt": "ā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩ⏇⏙ā¸ĩāš‰ā¸ˆā¸°āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸ā¸˛ā¸Ŗā¸šā¸ąā¸™ā¸—ā¸ļ⏁", "editor_close_without_save_title": "ā¸›ā¸´ā¸”āš‚ā¸›ā¸Ŗāšā¸ā¸Ŗā¸Ąāšā¸āš‰āš„ā¸‚?", - "editor_crop_tool_h2_aspect_ratios": "ā¸­ā¸ąā¸•ā¸Ŗā¸˛ā¸Ēāšˆā¸§ā¸™ā¸ ā¸˛ā¸ž", - "editor_crop_tool_h2_rotation": "⏁⏞⏪ā¸Ģā¸Ąā¸¸ā¸™", + "editor_confirm_reset_all_changes": "āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸ˆā¸°ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "editor_discard_edits_confirm": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚", + "editor_discard_edits_prompt": "ā¸„ā¸¸ā¸“ā¸Ąā¸ĩā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚ā¸—ā¸ĩāšˆā¸ĸā¸ąā¸‡āš„ā¸Ąāšˆāš„ā¸”āš‰ā¸šā¸ąā¸™ā¸—ā¸ļ⏁ āšā¸™āšˆāšƒā¸ˆā¸§āšˆā¸˛ā¸ˆā¸°ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "editor_discard_edits_title": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚", + "editor_edits_applied_error": "ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚ā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", + "editor_edits_applied_success": "ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚ā¸–ā¸šā¸ā¸šā¸ąā¸™ā¸—ā¸ļ⏁ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆāšā¸Ĩāš‰ā¸§", + "editor_flip_horizontal": "⏁ā¸Ĩā¸ąā¸šā¸”āš‰ā¸˛ā¸™ā¸—ā¸˛ā¸‡āšā¸™ā¸§ā¸™ā¸­ā¸™", + "editor_flip_vertical": "⏁ā¸Ĩā¸ąā¸šā¸”āš‰ā¸˛ā¸™ā¸—ā¸˛ā¸‡āšā¸™ā¸§ā¸•ā¸ąāš‰ā¸‡", + "editor_orientation": "ā¸ā¸˛ā¸Ŗā¸§ā¸˛ā¸‡āšā¸™ā¸§", + "editor_reset_all_changes": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāšā¸āš‰āš„ā¸‚", + "editor_rotate_left": "ā¸Ģā¸Ąā¸¸ā¸™ 90 ā¸­ā¸‡ā¸¨ā¸˛ā¸—ā¸§ā¸™āš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", + "editor_rotate_right": "ā¸Ģā¸Ąā¸¸ā¸™ 90 ā¸­ā¸‡ā¸¨ā¸˛ā¸•ā¸˛ā¸Ąāš€ā¸‚āš‡ā¸Ąā¸™ā¸˛ā¸Ŧ⏴⏁⏞", "email": "⏭ā¸ĩāš€ā¸Ąā¸Ĩ", "email_notifications": "āšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ā¸œāšˆā¸˛ā¸™ā¸­ā¸ĩāš€ā¸Ąā¸Ĩ", "empty_folder": "āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸™ā¸ĩāš‰ā¸§āšˆā¸˛ā¸‡āš€ā¸›ā¸Ĩāšˆā¸˛", @@ -986,7 +1064,7 @@ "export_as_json": "ā¸Ēāšˆā¸‡ā¸­ā¸­ā¸āš€ā¸›āš‡ā¸™āš„ā¸Ÿā¸ĨāšŒ JSON", "extension": "ā¸Ēāšˆā¸§ā¸™ā¸•āšˆā¸­ā¸‚ā¸ĸ⏞ā¸ĸ", "external": "⏠⏞ā¸ĸ⏙⏭⏁", - "external_libraries": "⏠⏞ā¸ĸ⏙⏭⏁⏄ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸ž", + "external_libraries": "⏄ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žā¸ ā¸˛ā¸ĸ⏙⏭⏁", "external_network": "ā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸ ā¸˛ā¸ĸ⏙⏭⏁", "external_network_sheet_info": "āš€ā¸Ąā¸ˇāšˆā¸­āš„ā¸Ąāšˆāš„ā¸”āš‰āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ Wi-Fi ⏗ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āš„ā¸§āš‰ āšā¸­ā¸žā¸ˆā¸°āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸œāšˆā¸˛ā¸™ URL ā¸”āš‰ā¸˛ā¸™ā¸Ĩāšˆā¸˛ā¸‡ā¸•ā¸˛ā¸Ąā¸Ĩā¸ŗā¸”ā¸ąā¸š", "face_unassigned": "āš„ā¸Ąāšˆā¸ā¸ŗā¸Ģā¸™ā¸”ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸ", @@ -1001,7 +1079,6 @@ "feature_photo_updated": "ā¸­ā¸ąā¸žāš€ā¸”ā¸—ā¸ ā¸˛ā¸žāš€ā¸”āšˆā¸™āšā¸Ĩāš‰ā¸§", "features": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒ", "features_setting_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸Ÿā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒāšā¸­ā¸›", - "file_name": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "file_name_or_extension": "ā¸™ā¸˛ā¸Ąā¸Ē⏁⏏ā¸Ĩā¸Ģā¸Ŗā¸ˇā¸­ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filename": "ā¸Šā¸ˇāšˆā¸­āš„ā¸Ÿā¸ĨāšŒ", "filetype": "ā¸Šā¸™ā¸´ā¸”āš„ā¸Ÿā¸ĨāšŒ", @@ -1015,6 +1092,9 @@ "folders": "āš‚ā¸Ÿā¸ĨāšŒāš€ā¸”ā¸­ā¸ŖāšŒ", "folders_feature_description": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸ĩā¸ĸā¸ā¸”ā¸šā¸Ąā¸¸ā¸Ąā¸Ąā¸­ā¸‡āš‚ā¸Ÿā¸Ĩāš€ā¸”ā¸­ā¸ŖāšŒā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­āšƒā¸™ā¸Ŗā¸°ā¸šā¸šāš„ā¸Ÿā¸ĨāšŒ", "forward": "āš„ā¸›ā¸‚āš‰ā¸˛ā¸‡ā¸Ģā¸™āš‰ā¸˛", + "free_up_space": "āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡", + "free_up_space_description": "āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡āš‚ā¸”ā¸ĸ⏁⏞⏪ā¸ĸāš‰ā¸˛ā¸ĸā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩāš‰ā¸§āš„ā¸›ā¸ĸā¸ąā¸‡ā¸–ā¸ąā¸‡ā¸‚ā¸ĸā¸°ā¸‚ā¸­ā¸‡ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ ā¸Ēā¸ŗāš€ā¸™ā¸˛ā¸—ā¸ĩāšˆā¸­ā¸ĸā¸šāšˆā¸šā¸™āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸ĸā¸ąā¸‡ā¸„ā¸‡ā¸­ā¸ĸā¸šāšˆā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸›ā¸Ĩā¸­ā¸”ā¸ ā¸ąā¸ĸ", + "free_up_space_settings_subtitle": "āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ", "gcast_enabled": "Google Cast", "gcast_enabled_description": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡āš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪⏈⏞⏁ Google āš€ā¸žā¸ˇāšˆā¸­ā¸—ā¸ŗā¸‡ā¸˛ā¸™", "general": "ā¸—ā¸ąāšˆā¸§āš„ā¸›", @@ -1114,6 +1194,8 @@ "jobs": "⏇⏞⏙", "keep": "āš€ā¸āš‡ā¸š", "keep_all": "āš€ā¸āš‡ā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "keep_description": "āš€ā¸Ĩ⏎⏭⏁ā¸Ēā¸´āšˆā¸‡ā¸—ā¸ĩāšˆā¸ˆā¸°āš€ā¸āš‡ā¸šāš„ā¸§āš‰ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸‚ā¸“ā¸°āš€ā¸žā¸´āšˆā¸Ąā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆā¸§āšˆā¸˛ā¸‡", + "keep_on_device_hint": "āš€ā¸Ĩ⏎⏭⏁⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸ˆā¸°āš€ā¸āš‡ā¸šāš„ā¸§āš‰ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "keep_this_delete_others": "āš€ā¸āš‡ā¸šā¸Ēā¸´āšˆā¸‡ā¸™ā¸ĩāš‰āš„ā¸§āš‰ ā¸Ĩā¸šā¸­ā¸ąā¸™ā¸­ā¸ˇāšˆā¸™ā¸­ā¸­ā¸", "kept_this_deleted_others": "āš€ā¸āš‡ā¸šāš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏙ā¸ĩāš‰āšā¸Ĩ⏰ā¸Ĩ⏚ {count, plural, one {# Asset} other {# Asset}}", "keyboard_shortcuts": "ā¸›ā¸¸āšˆā¸Ąā¸žā¸´ā¸Ąā¸žāšŒā¸Ĩā¸ąā¸”", @@ -1187,7 +1269,7 @@ "login_password_changed_error": "āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”ā¸‚ā¸“ā¸°āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™", "login_password_changed_success": "āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ", "logout_all_device_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸—ā¸¸ā¸ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒ āšƒā¸Šāšˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ ?", - "logout_this_device_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šāšƒā¸Šāšˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ ?", + "logout_this_device_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸°ā¸šā¸šā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰ā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "longitude": "ā¸Ĩā¸­ā¸‡ā¸ˆā¸´ā¸ˆā¸šā¸”", "look": "ā¸”ā¸š", "loop_videos": "⏧⏙⏧⏴⏔ā¸ĩāš‚ā¸­", @@ -1289,7 +1371,6 @@ "no_results_description": "ā¸Ĩā¸­ā¸‡āšƒā¸Šāš‰ā¸„ā¸ŗā¸žāš‰ā¸­ā¸‡ā¸Ģ⏪⏎⏭⏄⏺ā¸Ģā¸Ĩā¸ąā¸ā¸—ā¸ĩāšˆā¸ā¸§āš‰ā¸˛ā¸‡ā¸ā¸§āšˆā¸˛ā¸™ā¸ĩāš‰", "no_shared_albums_message": "ā¸Ēā¸Ŗāš‰ā¸˛ā¸‡ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāš€ā¸žā¸ˇāšˆā¸­āšā¸Šā¸ŖāšŒā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸ā¸ąā¸šā¸„ā¸™āšƒā¸™āš€ā¸„ā¸Ŗā¸ˇā¸­ā¸‚āšˆā¸˛ā¸ĸ⏂⏭⏇⏄⏏⏓", "not_in_any_album": "āš„ā¸Ąāšˆā¸­ā¸ĸā¸šāšˆāšƒā¸™ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąāšƒā¸” āš†", - "note_apply_storage_label_to_previously_uploaded assets": "ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏: ā¸Ģā¸˛ā¸ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸›āš‰ā¸˛ā¸ĸā¸ā¸ŗā¸ā¸ąā¸šā¸žā¸ˇāš‰ā¸™ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸ā¸ąā¸šāš€ā¸™ā¸ˇāš‰ā¸­ā¸Ģ⏞⏗ā¸ĩāšˆā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰ āšƒā¸Ģāš‰āš€ā¸Ŗā¸ĩā¸ĸā¸āšƒā¸Šāš‰", "notes": "ā¸Ģā¸Ąā¸˛ā¸ĸāš€ā¸Ģ⏕⏏", "notification_permission_dialog_content": "āš€ā¸žā¸ˇāšˆā¸­āš€ā¸›ā¸´ā¸”ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™ āš€ā¸‚āš‰ā¸˛ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛āšā¸Ĩāš‰ā¸§ā¸ā¸”ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•", "notification_permission_list_tile_content": "ā¸­ā¸™ā¸¸ā¸ā¸˛ā¸•ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", @@ -1302,6 +1383,7 @@ "offline": "ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", "ok": "⏕⏁ā¸Ĩ⏇", "oldest_first": "āš€ā¸Ŗā¸ĩā¸ĸā¸‡āš€ā¸āšˆā¸˛ā¸Ēā¸¸ā¸”ā¸āšˆā¸­ā¸™", + "on_this_device": "ā¸šā¸™ā¸­ā¸¸ā¸›ā¸ā¸Ŗā¸“āšŒā¸™ā¸ĩāš‰", "onboarding": "ā¸ā¸˛ā¸Ŗāš€ā¸Ŗā¸´āšˆā¸Ąā¸•āš‰ā¸™āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "onboarding_privacy_description": "⏟ā¸ĩāš€ā¸ˆā¸­ā¸ŖāšŒ (ā¸•ā¸ąā¸§āš€ā¸Ĩ⏎⏭⏁) ā¸•āšˆā¸­āš„ā¸›ā¸™ā¸ĩāš‰ā¸•āš‰ā¸­ā¸‡ā¸­ā¸˛ā¸¨ā¸ąā¸ĸ⏚⏪⏴⏁⏞⏪⏠⏞ā¸ĸ⏙⏭⏁ āšā¸Ĩ⏰ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™āš„ā¸”āš‰ā¸•ā¸Ĩā¸­ā¸”āš€ā¸§ā¸Ĩā¸˛āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗ", "onboarding_theme_description": "āš€ā¸Ĩ⏎⏭⏁⏘ā¸ĩā¸Ąā¸Ēā¸ĩ ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡āš„ā¸”āš‰āšƒā¸™ā¸ ā¸˛ā¸ĸā¸Ģā¸Ĩā¸ąā¸‡āšƒā¸™ā¸ā¸˛ā¸Ŗā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“", @@ -1369,7 +1451,8 @@ "permission_onboarding_permission_limited": "ā¸Ēā¸´ā¸—ā¸˜āšŒā¸ˆā¸ŗā¸ā¸ąā¸” āš€ā¸žā¸ˇāšˆā¸­āšƒā¸Ģāš‰ Immich ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸Ĩā¸°ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸„ā¸Ĩā¸ąā¸‡ā¸ ā¸˛ā¸žāš„ā¸”āš‰ ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸Ēā¸´ā¸—ā¸˜ā¸´āš€ā¸‚āš‰ā¸˛ā¸–ā¸ļā¸‡ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­", "permission_onboarding_request": "Immich ā¸ˆā¸ŗāš€ā¸›āš‡ā¸™ā¸ˆā¸°ā¸•āš‰ā¸­ā¸‡āš„ā¸”āš‰ā¸Ŗā¸ąā¸šā¸Ēā¸´ā¸—ā¸˜ā¸´āšŒā¸”ā¸šā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­", "person": "ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", - "person_birthdate": "āš€ā¸ā¸´ā¸”ā¸§ā¸ąā¸™{date}", + "person_age_years": "⏭⏞ā¸ĸ⏏ {years, plural, other {# ⏛ā¸ĩ}}", + "person_birthdate": "āš€ā¸ā¸´ā¸”āš€ā¸Ąā¸ˇāšˆā¸­ {date}", "photo_shared_all_users": "ā¸”ā¸šāš€ā¸Ģā¸Ąā¸ˇā¸­ā¸™ā¸§āšˆā¸˛ā¸„ā¸¸ā¸“āš„ā¸”āš‰āšā¸Šā¸ŖāšŒā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸žā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸ā¸ąā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸” ā¸Ģā¸Ŗā¸ˇā¸­ā¸„ā¸¸ā¸“āš„ā¸Ąāšˆā¸Ąā¸ĩā¸œā¸šāš‰āšƒā¸Šāš‰āšƒā¸”ā¸—ā¸ĩāšˆā¸ˆā¸°āšā¸Šā¸ŖāšŒā¸”āš‰ā¸§ā¸ĸ", "photos": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž", "photos_and_videos": "ā¸Ŗā¸šā¸›ā¸ ā¸˛ā¸ž āšā¸Ĩ⏰ ⏧⏴⏔ā¸ĩāš‚ā¸­", @@ -1443,7 +1526,7 @@ "reassigned_assets_to_new_person": "ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸ {count, plural, one {# ā¸Ēā¸ˇāšˆā¸­} other {# ā¸Ēā¸ˇāšˆā¸­}} āšƒā¸Ģāš‰ā¸ā¸ąā¸šā¸šā¸¸ā¸„ā¸„ā¸Ĩāšƒā¸Ģā¸Ąāšˆ", "reassing_hint": "ā¸Ąā¸­ā¸šā¸Ģā¸Ąā¸˛ā¸ĸā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸Ĩā¸ˇā¸­ā¸āšƒā¸Ģāš‰ā¸ā¸ąā¸šā¸šā¸¸ā¸„ā¸„ā¸Ĩ⏗ā¸ĩāšˆā¸Ąā¸ĩ⏭ā¸ĸā¸šāšˆāšā¸Ĩāš‰ā¸§", "recent": "ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", - "recent-albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", + "recent_albums": "ā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "recent_searches": "ā¸ā¸˛ā¸Ŗā¸„āš‰ā¸™ā¸Ģ⏞ā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "recently_added_page_title": "āš€ā¸žā¸´āšˆā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "refresh": "⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Š", @@ -1577,8 +1660,8 @@ "server_endpoint": "⏛ā¸Ĩ⏞ā¸ĸā¸—ā¸˛ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_info_box_app_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™āšā¸­ā¸ž", "server_info_box_server_url": "URL āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", - "server_offline": "Server ā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", - "server_online": "Server ā¸­ā¸­ā¸™āš„ā¸Ĩā¸™āšŒ", + "server_offline": "āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸­ā¸­ā¸Ÿāš„ā¸Ĩā¸™āšŒ", + "server_online": "āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸­ā¸­ā¸™āš„ā¸Ĩā¸™āšŒ", "server_privacy": "ā¸„ā¸§ā¸˛ā¸Ąāš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸•ā¸ąā¸§āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_stats": "ā¸Ēā¸–ā¸´ā¸•ā¸´āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", "server_version": "āš€ā¸§ā¸­ā¸ŖāšŒā¸Šā¸ąā¸™ā¸‚ā¸­ā¸‡āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒ", @@ -1605,7 +1688,7 @@ "setting_notifications_single_progress_subtitle": "ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸ˇā¸šā¸Ģā¸™āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸”ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”ā¸•āšˆā¸­ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "setting_notifications_single_progress_title": "āšā¸Ē⏔⏇⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸ⏔ā¸Ē⏖⏞⏙⏰⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡", "setting_notifications_subtitle": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛ā¸ā¸˛ā¸Ŗāšā¸ˆāš‰ā¸‡āš€ā¸•ā¸ˇā¸­ā¸™", - "setting_notifications_total_progress_subtitle": "ā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸ˇā¸šā¸Ģā¸™āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸”ā¸ĸā¸Ŗā¸§ā¸Ą (āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™/ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”)", + "setting_notifications_total_progress_subtitle": "ā¸„ā¸§ā¸˛ā¸Ąā¸„ā¸ˇā¸šā¸Ģā¸™āš‰ā¸˛ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āš‚ā¸”ā¸ĸā¸Ŗā¸§ā¸Ą (āš€ā¸Ēā¸Ŗāš‡ā¸ˆā¸Ēā¸´āš‰ā¸™/ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”)", "setting_notifications_total_progress_title": "āšā¸Ē⏔⏇ā¸Ē⏖⏞⏙⏰⏁⏞⏪ā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšƒā¸™āš€ā¸šā¸ˇāš‰ā¸­ā¸‡ā¸Ģā¸Ĩā¸ąā¸‡ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", "setting_video_viewer_looping_title": "⏧⏙ā¸Ĩā¸šā¸›", "settings": "ā¸•ā¸ąāš‰ā¸‡ā¸„āšˆā¸˛", @@ -1778,15 +1861,19 @@ "trash_page_select_assets_btn": "āš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "trash_page_title": "⏂ā¸ĸ⏰ ({count})", "trashed_items_will_be_permanently_deleted_after": "⏪⏞ā¸ĸ⏁⏞⏪⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸Ĩā¸šā¸ˆā¸°ā¸–ā¸šā¸ā¸Ĩā¸šā¸—ā¸´āš‰ā¸‡ā¸ ā¸˛ā¸ĸāšƒā¸™ {days, plural, one {# ā¸§ā¸ąā¸™} other {# ā¸§ā¸ąā¸™}}.", + "troubleshoot": "ā¸ā¸˛ā¸Ŗāšā¸āš‰ā¸›ā¸ąā¸ā¸Ģ⏞", "type": "ā¸›ā¸Ŗā¸°āš€ā¸ ā¸—", "unable_to_change_pin_code": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸ⏙⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "unable_to_setup_pin_code": "āš„ā¸Ąāšˆā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸•ā¸ąāš‰ā¸‡ā¸Ŗā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "unarchive": "ā¸™ā¸ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", + "unarchive_action_prompt": "{count} ā¸–ā¸šā¸ā¸™ā¸ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸—ā¸ĩāšˆāš€ā¸āš‡ā¸šā¸–ā¸˛ā¸§ā¸Ŗ", "undo": "āš€ā¸Ĩ⏴⏁⏗⏺", "unfavorite": "ā¸™ā¸ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", + "unfavorite_action_prompt": "{count} ā¸–ā¸šā¸ā¸™ā¸ŗā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸Ŗā¸˛ā¸ĸā¸ā¸˛ā¸Ŗāš‚ā¸›ā¸Ŗā¸”", "unhide_person": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸‹āšˆā¸­ā¸™ā¸šā¸¸ā¸„ā¸„ā¸Ĩ", "unknown": "āš„ā¸Ąāšˆā¸—ā¸Ŗā¸˛ā¸š", "unknown_country": "āš„ā¸Ąāšˆā¸—ā¸Ŗā¸˛ā¸šā¸›ā¸Ŗā¸°āš€ā¸—ā¸¨", + "unknown_date": "āš„ā¸Ąāšˆā¸—ā¸Ŗā¸˛ā¸šā¸§ā¸ąā¸™", "unknown_year": "āš„ā¸Ąāšˆā¸—ā¸Ŗā¸˛ā¸šā¸›ā¸ĩ", "unlimited": "āš„ā¸Ąāšˆā¸ˆā¸ŗā¸ā¸ąā¸”", "unlink_oauth": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸āš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ OAuth", @@ -1795,12 +1882,14 @@ "unnamed_album_delete_confirmation": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸ˆā¸°ā¸Ĩā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ąā¸™ā¸ĩāš‰ āšƒā¸Šāšˆā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ ?", "unnamed_share": "āšā¸Šā¸ŖāšŒāšā¸šā¸šāš„ā¸Ąāšˆā¸Ŗā¸°ā¸šā¸¸ā¸Šā¸ˇāšˆā¸­", "unselect_all": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", + "unselect_all_in": "ā¸ĸā¸āš€ā¸Ĩā¸´ā¸ā¸ā¸˛ā¸Ŗāš€ā¸Ĩā¸ˇā¸­ā¸ā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”āšƒā¸™ {group}", "unstack": "ā¸Ģā¸ĸā¸¸ā¸”ā¸‹āš‰ā¸­ā¸™", "up_next": "ā¸•āšˆā¸­āš„ā¸›", "updated_at": "ā¸­ā¸ąā¸žāš€ā¸”ā¸—", "updated_password": "⏪ā¸Ģā¸ąā¸Ēā¸œāšˆā¸˛ā¸™āš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸Ĩāš‰ā¸§", "upload": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "upload_concurrency": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸žā¸Ŗāš‰ā¸­ā¸Ąā¸ā¸ąā¸™", + "upload_details": "⏪⏞ā¸ĸā¸Ĩā¸°āš€ā¸­ā¸ĩā¸ĸā¸”ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔", "upload_dialog_info": "ā¸„ā¸¸ā¸“ā¸•āš‰ā¸­ā¸‡ā¸ā¸˛ā¸Ŗā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸā¸˛ā¸ā¸Ŗā¸”ā¸ąā¸‡ā¸ā¸Ĩāšˆā¸˛ā¸§ā¸šā¸™āš€ā¸‹ā¸´ā¸ŖāšŒā¸Ÿāš€ā¸§ā¸­ā¸ŖāšŒā¸Ģā¸Ŗā¸ˇā¸­āš„ā¸Ąāšˆ?", "upload_dialog_title": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪", "upload_status_duplicates": "ā¸Ŗā¸§ā¸Ąāš€ā¸‚āš‰ā¸˛ā¸”āš‰ā¸§ā¸ĸā¸ā¸ąā¸™", @@ -1808,7 +1897,7 @@ "upload_status_uploaded": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩā¸”āšā¸Ĩāš‰ā¸§", "upload_success": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ŗāš€ā¸Ŗāš‡ā¸ˆ, ⏪ā¸ĩāš€ā¸Ÿā¸Ŗā¸Šā¸Ģā¸™āš‰ā¸˛ā¸™ā¸ĩāš‰āšƒā¸Ģā¸Ąāšˆā¸„ā¸¸ā¸“ā¸ˆā¸°āš€ā¸Ģāš‡ā¸™ā¸Ēā¸ˇāšˆā¸­ā¸—ā¸ĩāšˆāš€ā¸žā¸´āšˆā¸Ąā¸Ĩāšˆā¸˛ā¸Ē⏏⏔", "uploading": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸žāš‚ā¸Ģā¸Ĩ⏔", - "uploading_media": "ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", + "uploading_media": "⏁⏺ā¸Ĩā¸ąā¸‡ā¸­ā¸ąā¸›āš‚ā¸Ģā¸Ĩ⏔ā¸Ēā¸ˇāšˆā¸­", "usage": "ā¸ā¸˛ā¸Ŗāšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "use_biometric": "āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗā¸žā¸´ā¸Ēā¸šā¸ˆā¸™āšŒā¸­ā¸ąā¸•ā¸Ĩā¸ąā¸ā¸Šā¸“āšŒ", "use_current_connection": "āšƒā¸Šāš‰ā¸ā¸˛ā¸Ŗāš€ā¸Šā¸ˇāšˆā¸­ā¸Ąā¸•āšˆā¸­ā¸›ā¸ąā¸ˆā¸ˆā¸¸ā¸šā¸ąā¸™", @@ -1818,6 +1907,7 @@ "user_id": "āš„ā¸­ā¸”ā¸ĩā¸œā¸šāš‰āšƒā¸Šāš‰", "user_pin_code_settings": "⏪ā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", "user_pin_code_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸Ŗā¸Ģā¸ąā¸Ēā¸›ā¸Ŗā¸°ā¸ˆā¸ŗā¸•ā¸ąā¸§ (PIN)", + "user_privacy": "ā¸„ā¸§ā¸˛ā¸Ąāš€ā¸›āš‡ā¸™ā¸Ēāšˆā¸§ā¸™ā¸•ā¸ąā¸§ā¸œā¸šāš‰āšƒā¸Šāš‰", "user_purchase_settings": "ā¸‹ā¸ˇāš‰ā¸­", "user_purchase_settings_description": "ā¸ˆā¸ąā¸”ā¸ā¸˛ā¸Ŗā¸ā¸˛ā¸Ŗā¸‹ā¸ˇāš‰ā¸­", "user_role_set": "ā¸•ā¸ąāš‰ā¸‡ {role} āšƒā¸Ģāš‰ā¸ā¸ąā¸š {user}", @@ -1829,6 +1919,7 @@ "utilities": "āš€ā¸„ā¸Ŗā¸ˇāšˆā¸­ā¸‡ā¸Ąā¸ˇā¸­", "validate": "ā¸•ā¸Ŗā¸§ā¸ˆā¸Ē⏭⏚", "validate_endpoint_error": "ā¸ā¸Ŗā¸¸ā¸“ā¸˛ā¸Ŗā¸°ā¸šā¸¸ URL ⏗ā¸ĩāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", + "validation_error": "ā¸ā¸˛ā¸Ŗā¸•ā¸Ŗā¸§ā¸ˆā¸Ēā¸­ā¸šā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩā¸Ĩāš‰ā¸Ąāš€ā¸Ģā¸Ĩ⏧", "variables": "ā¸•ā¸ąā¸§āšā¸›ā¸Ŗ", "version": "ā¸Ŗā¸¸āšˆā¸™", "version_announcement_closing": "āš€ā¸žā¸ˇāšˆā¸­ā¸™ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ ā¸­āš€ā¸Ĩāš‡ā¸ā¸‹āšŒ", @@ -1839,6 +1930,7 @@ "video_hover_setting": "āš€ā¸Ĩāšˆā¸™ā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­āšā¸šā¸šā¸ĸāšˆā¸­āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸Ĩā¸ˇāšˆā¸­ā¸™āš€ā¸Ąā¸˛ā¸ĒāšŒā¸­ā¸ĸā¸šāšˆā¸šā¸™", "video_hover_setting_description": "āš€ā¸Ĩāšˆā¸™ā¸§ā¸´ā¸”ā¸ĩāš‚ā¸­ā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡āš€ā¸Ąā¸ˇāšˆā¸­āš€ā¸Ąā¸˛ā¸ĒāšŒā¸ˆāšˆā¸­ā¸‚āš‰ā¸˛ā¸‡ā¸šā¸™ āš€ā¸Ąā¸ˇāšˆā¸­ā¸›ā¸´ā¸”āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™ ⏧⏴⏔ā¸ĩāš‚ā¸­ā¸•ā¸ąā¸§ā¸­ā¸ĸāšˆā¸˛ā¸‡ā¸ĸā¸ąā¸‡ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–āš€ā¸Ĩāšˆā¸™āš„ā¸”āš‰āš‚ā¸”ā¸ĸā¸ā¸”ā¸›ā¸¸āšˆā¸Ąāš€ā¸Ĩāšˆā¸™", "videos": "⏧⏴⏔ā¸ĩāš‚ā¸­", + "videos_only": "⏧⏴⏔ā¸ĩāš‚ā¸­āš€ā¸—āšˆā¸˛ā¸™ā¸ąāš‰ā¸™", "view": "ā¸”ā¸š", "view_album": "ā¸”ā¸šā¸­ā¸ąā¸Ĩā¸šā¸ąāš‰ā¸Ą", "view_all": "ā¸”ā¸šā¸—ā¸ąāš‰ā¸‡ā¸Ģā¸Ąā¸”", @@ -1850,6 +1942,7 @@ "view_next_asset": "ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­ā¸–ā¸ąā¸”āš„ā¸›", "view_previous_asset": "ā¸”ā¸šā¸Ēā¸ˇāšˆā¸­ā¸āšˆā¸­ā¸™ā¸Ģā¸™āš‰ā¸˛", "view_qr_code": "ā¸”ā¸šā¸„ā¸´ā¸§ā¸­ā¸˛ā¸ŖāšŒāš‚ā¸„āš‰ā¸”", + "view_similar_photos": "ā¸”ā¸šā¸Ŗā¸šā¸›ā¸—ā¸ĩāšˆā¸„ā¸Ĩāš‰ā¸˛ā¸ĸā¸ā¸ąā¸™", "view_user": "ā¸”ā¸šā¸œā¸šāš‰āšƒā¸Šāš‰ā¸‡ā¸˛ā¸™", "viewer_remove_from_stack": "āš€ā¸­ā¸˛ā¸­ā¸­ā¸ā¸ˆā¸˛ā¸ā¸—ā¸ĩāšˆā¸‹āš‰ā¸­ā¸™", "viewer_stack_use_as_main_asset": "āšƒā¸Šāš‰āš€ā¸›āš‡ā¸™ā¸—ā¸Ŗā¸ąā¸žā¸ĸ⏞⏁⏪ā¸Ģā¸Ĩā¸ąā¸", @@ -1860,6 +1953,7 @@ "week": "ā¸Ēā¸ąā¸›ā¸”ā¸˛ā¸ĢāšŒ", "welcome": "ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸š", "welcome_to_immich": "ā¸ĸ⏴⏙⏔ā¸ĩā¸•āš‰ā¸­ā¸™ā¸Ŗā¸ąā¸šā¸Ēā¸šāšˆ immich", + "width": "ā¸„ā¸§ā¸˛ā¸Ąā¸ā¸§āš‰ā¸˛ā¸‡", "wifi_name": "ā¸Šā¸ˇāšˆā¸­ Wi-Fi", "wrong_pin_code": "⏪ā¸Ģā¸ąā¸Ē PIN āš„ā¸Ąāšˆā¸–ā¸šā¸ā¸•āš‰ā¸­ā¸‡", "year": "⏛ā¸ĩ", diff --git a/i18n/tr.json b/i18n/tr.json index c2333d6ded..1331621ad9 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -5,6 +5,7 @@ "acknowledge": "Onayla", "action": "Eylem", "action_common_update": "GÃŧncelle", + "action_description": "Filtrelenmiş Ãļğeler Ãŧzerinde gerçekleştirilecek bir dizi eylem", "actions": "Eylemler", "active": "Aktif", "active_count": "Aktif: {count}", @@ -15,9 +16,14 @@ "add_a_location": "Bir konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Bir başlÄąk ekleyin", + "add_action": "Eylem ekle", + "add_action_description": "Gerçekleştirmek istediğiniz eylemi eklemek için tÄąklayÄąn", + "add_assets": "VarlÄąk ekle", "add_birthday": "Doğum gÃŧnÃŧ ekle", "add_endpoint": "Uç nokta ekle", "add_exclusion_pattern": "Hariç tutma deseni ekle", + "add_filter": "Filtre ekle", + "add_filter_description": "Filtre koşulu eklemek için tÄąklayÄąn", "add_location": "Konum ekle", "add_more_users": "Daha fazla kullanÄącÄą ekle", "add_partner": "Ortak ekle", @@ -36,6 +42,7 @@ "add_to_shared_album": "PaylaÅŸÄąlan albÃŧme ekle", "add_upload_to_stack": "YÃŧklemeyi yığına ekle", "add_url": "URL ekle", + "add_workflow_step": "İş akÄąÅŸÄą adÄąmÄą ekle", "added_to_archive": "Arşive eklendi", "added_to_favorites": "Favorilere eklendi", "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", @@ -97,6 +104,8 @@ "image_preview_description": "Orta boyutlu gÃļrÃŧntÃŧ, meta verisi Ã§ÄąkarÄąlmÄąÅŸ, tekil bir Ãļğe gÃļrÃŧntÃŧlenirken ve makine Ãļğrenimi için kullanÄąlÄąr", "image_preview_quality_description": "Ön izleme kalitesi 1-100 arasÄądÄąr. YÃŧksek değerler daha iyi kalite sağlar, ancak daha bÃŧyÃŧk dosyalar Ãŧretir ve uygulama yanÄąt verme hÄązÄąnÄą dÃŧşÃŧrebilir. DÃŧşÃŧk bir değer belirlemek, makine Ãļğrenimi kalitesini etkileyebilir.", "image_preview_title": "Ön İzleme AyarlarÄą", + "image_progressive": "AşamalÄą", + "image_progressive_description": "JPEG gÃļrsellerini, yÃŧklenirken kademeli (aşamalÄą) gÃļrÃŧntÃŧlenecek şekilde “progressive” olarak kodlayÄąn. WebP gÃļrselleri için etkisi yoktur.", "image_quality": "Kalite", "image_resolution": "ÇÃļzÃŧnÃŧrlÃŧk", "image_resolution_description": "Daha yÃŧksek çÃļzÃŧnÃŧrlÃŧkle, daha fazla detayÄą koruyabilir ancak kodlanmasÄą daha uzun sÃŧrer, daha bÃŧyÃŧk dosya boyutlarÄąna sahip olur ve uygulamanÄąn yanÄąt verme hÄązÄąnÄą azaltabilir.", @@ -106,13 +115,13 @@ "image_thumbnail_quality_description": "KÃŧçÃŧk resim kalitesi 1-100 arasÄąnda. Daha yÃŧksek değerler daha iyidir, ancak daha bÃŧyÃŧk dosyalar Ãŧretir ve uygulamanÄąn yanÄąt hÄązÄąnÄą azaltabilir.", "image_thumbnail_title": "KÃŧçÃŧk Fotoğraf AyarlarÄą", "import_config_from_json_description": "JSON yapÄąlandÄąrma dosyasÄą yÃŧkleyerek sistem yapÄąlandÄąrmasÄąnÄą içe aktar", - "job_concurrency": "{job} eş zamanlÄąlÄąk", + "job_concurrency": "{job} eşzamanlÄąlÄąk", "job_created": "GÃļrev oluşturuldu", - "job_not_concurrency_safe": "Bu işlem eşzamanlama için uygun değil.", + "job_not_concurrency_safe": "Bu işlem eşzamanlÄąlÄąk aÃ§ÄąsÄąndan gÃŧvenli değil.", "job_settings": "GÃļrev AyarlarÄą", "job_settings_description": "AynÄą anda çalÄąÅŸacak gÃļrevleri yÃļnet", "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}", - "jobs_failed": "{jobCount, plural, other {# BaşarÄąsÄąz}}", + "jobs_failed": "{jobCount, plural, other {# başarÄąsÄąz}}", "jobs_over_time": "Zaman içinde işler", "library_created": "Oluşturulan kÃŧtÃŧphane : {library}", "library_deleted": "KÃŧtÃŧphane silindi", @@ -131,7 +140,7 @@ "library_watching_settings": "KÃŧtÃŧphane izleme [DENEYSEL]", "library_watching_settings_description": "Değişen dosyalar için otomatik olarak izle", "logging_enable_description": "GÃŧnlÃŧğÃŧ etkinleştir", - "logging_level_description": "Etkinleştirildiğinde hangi gÃŧnlÃŧk seviyesi kullanÄąlÄąr.", + "logging_level_description": "Etkinleştirildiğinde, hangi gÃŧnlÃŧk dÃŧzeyinin kullanÄąlacağı.", "logging_settings": "GÃŧnlÃŧk Tutma", "machine_learning_availability_checks": "KullanÄąlabilirlik kontrolleri", "machine_learning_availability_checks_description": "KullanÄąlabilir makine Ãļğrenimi sunucularÄąnÄą otomatik olarak algÄąlayÄąn ve tercih edin", @@ -154,23 +163,23 @@ "machine_learning_facial_recognition_model_description": "Modeller, azalan boyut sÄąrasÄąna gÃļre listelenmiştir. Daha bÃŧyÃŧk modeller daha yavaştÄąr ve daha fazla bellek kullanÄąr, ancak daha iyi sonuçlar Ãŧretir. Bir modeli değiştirdikten sonra tÃŧm gÃļrÃŧntÃŧler için yÃŧz algÄąlama işini yeniden çalÄąÅŸtÄąrmanÄąz gerektiğini unutmayÄąn.", "machine_learning_facial_recognition_setting": "YÃŧz tanÄąmayÄą etkinleştir", "machine_learning_facial_recognition_setting_description": "Devre dÄąÅŸÄą bÄąrakÄąldığında fotoğraflar yÃŧz tanÄąma için işlenmeyecek ve Keşfet sayfasÄąndaki Kişiler sekmesini doldurmayacak.", - "machine_learning_max_detection_distance": "Maksimum tespit uzaklığı", + "machine_learning_max_detection_distance": "Maksimum algÄąlama mesafesi", "machine_learning_max_detection_distance_description": "Resimleri birbirinin çifti saymak için hesap edilecek azami benzerlik ÃļlçÃŧsÃŧ, 0.001-0.1 aralığında. Daha yÃŧksek değer daha hassas olup daha fazla çift tespit eder ancak çift olmayan resimleri birbirinin çifti sayabilir.", - "machine_learning_max_recognition_distance": "Maksimum tanÄąma uzaklığı", + "machine_learning_max_recognition_distance": "Maksimum tanÄąma mesafesi", "machine_learning_max_recognition_distance_description": "İki suretin aynÄą kişi olarak kabul edildiği azami benzerlik oranÄą; 0-2 aralığında bir değerdir. DÃŧşÃŧk değerler iki farklÄą kişinin sehven aynÄą kişi olarak algÄąlanmasÄąnÄą engeller ama aynÄą kişinin farklÄą pozlarÄąnÄąn farklÄą suretler olarak algÄąlanmasÄąna sebep olabilir. İki sureti birleştirmek daha kolay olduğu için mÃŧmkÃŧn olduğunca dÃŧşÃŧk değerler seçin.", "machine_learning_min_detection_score": "Minimum tespit skoru", "machine_learning_min_detection_score_description": "Bir yÃŧzÃŧn algÄąlanmasÄą için gerekli asgari kararlÄąlÄąk miktarÄą; 0-1 aralığında bir değerdir. DÃŧşÃŧk değerler daha fazla yÃŧz tanÄąr ama hatalÄą tanÄąma oranÄą artar.", - "machine_learning_min_recognized_faces": "Minimum tanÄąnan yÃŧzler", + "machine_learning_min_recognized_faces": "TanÄąnan minimum yÃŧz sayÄąsÄą", "machine_learning_min_recognized_faces_description": "Kişi oluşturulmasÄą için gereken minimum yÃŧzler. Bu değeri yÃŧkseltmek yÃŧz tanÄąma doğruluğunu arttÄąrÄąr fakat yÃŧzÃŧn bir kişiye atanmama olasÄąlığınÄą arttÄąrÄąr.", "machine_learning_ocr": "OCR", "machine_learning_ocr_description": "Resimlerdeki metni tanÄąmak için makine Ãļğrenimini kullan", "machine_learning_ocr_enabled": "OCR'yi etkinleştir", "machine_learning_ocr_enabled_description": "Devre dÄąÅŸÄą bÄąrakÄąlÄąrsa, resimler metin tanÄąma işleminden geçmeyecektir.", - "machine_learning_ocr_max_resolution": "En yÃŧksek çÃļzÃŧnÃŧrlÃŧk", + "machine_learning_ocr_max_resolution": "Maksimum çÃļzÃŧnÃŧrlÃŧk", "machine_learning_ocr_max_resolution_description": "Bu çÃļzÃŧnÃŧrlÃŧğÃŧn Ãŧzerindeki Ãļnizlemeler, en-boy oranÄą korunarak yeniden boyutlandÄąrÄąlacaktÄąr. Daha yÃŧksek değerler daha doğru sonuç verir, ancak işlemesi daha uzun sÃŧrer ve daha fazla bellek kullanÄąr.", - "machine_learning_ocr_min_detection_score": "En dÃŧşÃŧk tespit puanÄą", + "machine_learning_ocr_min_detection_score": "Minimum tespit puanÄą", "machine_learning_ocr_min_detection_score_description": "Metnin tespit edilmesi için minimum gÃŧven puanÄą 0-1 arasÄąndadÄąr. DÃŧşÃŧk değerler daha fazla metin tespit eder, ancak yanlÄąÅŸ pozitif sonuçlara yol açabilir.", - "machine_learning_ocr_min_recognition_score": "Minimum tespit puanÄą", + "machine_learning_ocr_min_recognition_score": "Minimum tanÄąma puanÄą", "machine_learning_ocr_min_score_recognition_description": "AlgÄąlanan metnin tanÄąnmasÄą için minimum gÃŧven puanÄą 0-1 arasÄąndadÄąr. Daha dÃŧşÃŧk değerler daha fazla metni tanÄąr, ancak yanlÄąÅŸ pozitif sonuçlara neden olabilir.", "machine_learning_ocr_model": "OCR modeli", "machine_learning_ocr_model_description": "Sunucu modelleri mobil modellerden daha doğrudur, ancak işlenmesi daha uzun sÃŧrer ve daha fazla bellek kullanÄąr.", @@ -181,10 +190,21 @@ "machine_learning_smart_search_enabled": "AkÄąllÄą aramayÄą etkinleştir", "machine_learning_smart_search_enabled_description": "Eğer devre dÄąÅŸÄą bÄąrakÄąlÄąrsa fotoğraflar akÄąllÄą arama için işlenmeyecek.", "machine_learning_url_description": "Makine Ãļğrenimi sunucusunun URL’si. Birden fazla URL sağlanÄąrsa, her sunucu sÄąrayla tek tek denenir ve biri başarÄąlÄą yanÄąt verene kadar devam edilir. YanÄąt vermeyen sunucular, çevrimiçi duruma gelene kadar geçici olarak yok sayÄąlÄąr.", + "maintenance_delete_backup": "Yedeği Sil", + "maintenance_delete_backup_description": "Bu dosya geri alÄąnamaz şekilde silinecektir.", + "maintenance_delete_error": "Yedek silinemedi.", + "maintenance_restore_backup": "Yedeği Geri YÃŧkle", + "maintenance_restore_backup_description": "Immich tamamen silinecek ve seçilen yedekten geri yÃŧklenecektir. İşleme devam etmeden Ãļnce bir yedek oluşturulacaktÄąr.", + "maintenance_restore_backup_different_version": "Bu yedek, Immich’in farklÄą bir sÃŧrÃŧmÃŧyle oluşturulmuş!", + "maintenance_restore_backup_unknown_version": "Yedek sÃŧrÃŧmÃŧ belirlenemedi.", + "maintenance_restore_database_backup": "VeritabanÄą yedeğini geri yÃŧkle", + "maintenance_restore_database_backup_description": "Bir yedek dosyasÄą kullanarak veritabanÄąnÄą daha Ãļnceki bir duruma geri dÃļndÃŧrÃŧn", "maintenance_settings": "BakÄąm", "maintenance_settings_description": "Immich'i bakÄąm moduna alÄąn.", - "maintenance_start": "BakÄąm modunu başlat", + "maintenance_start": "BakÄąm moduna geç", "maintenance_start_error": "BakÄąm modu başlatÄąlamadÄą.", + "maintenance_upload_backup": "VeritabanÄą yedek dosyasÄąnÄą yÃŧkle", + "maintenance_upload_backup_error": "Yedek yÃŧklenemedi, dosya .sql veya .sql.gz formatÄąnda mÄą?", "manage_concurrency": "AynÄą anda çalÄąÅŸmayÄą yÃļnet", "manage_concurrency_description": "İş eşzamanlÄąlığınÄą yÃļnetmek için işler sayfasÄąna gidin", "manage_log_settings": "GÃŧnlÃŧk ayarlarÄąnÄą yÃļnet", @@ -223,7 +243,7 @@ "nightly_tasks_settings_description": "Gece gÃļrevlerini yÃļnet", "nightly_tasks_start_time_setting": "BaşlangÄąÃ§ saati", "nightly_tasks_start_time_setting_description": "Sunucunun gece gÃļrevlerini çalÄąÅŸtÄąrmaya başladığı saat", - "nightly_tasks_sync_quota_usage_setting": "Kota kullanÄąmÄąnÄą eşzamanla", + "nightly_tasks_sync_quota_usage_setting": "Kota kullanÄąmÄąnÄą senkronize et", "nightly_tasks_sync_quota_usage_setting_description": "Mevcut kullanÄąma gÃļre kullanÄącÄą depolama kotasÄąnÄą gÃŧncelle", "no_paths_added": "Yol eklenmedi", "no_pattern_added": "Desen eklenmedi", @@ -252,7 +272,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_client_secret_description": "Gizli istemci için veya genel istemci için PKCE (Kod Değişimi için KanÄąt AnahtarÄą) desteklenmiyorsa 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", @@ -263,11 +283,11 @@ "oauth_settings_description": "OAuth giriş ayarlarÄąnÄą yÃļnet", "oauth_settings_more_details": "Bu Ãļzellik hakkÄąnda daha fazla bilgi için bu sayfayÄą ziyaret edin DÃļkÃŧmanlar.", "oauth_storage_label_claim": "Depolama etiketi talebi", - "oauth_storage_label_claim_description": "KullanÄącÄąnÄąn dosyalarÄąnÄą depolarken kullanÄąlan alt klasÃļrÃŧn adÄąnÄą belirlerken kulanÄąlacak değer (en: OAuth claim).", + "oauth_storage_label_claim_description": "KullanÄącÄąnÄąn depolama etiketini otomatik olarak bu talebin değerine ayarlayÄąn.", "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_claim_description": "KullanÄącÄąnÄąn depolama kotasÄąnÄą otomatik olarak bu talebin değerine ayarlayÄąn.", "oauth_storage_quota_default": "VarsayÄąlan depolama kotasÄą (GiB)", - "oauth_storage_quota_default_description": "Değer (en: OAuth claim) mevcut değilse GiB cinsinden konulacak kota.", + "oauth_storage_quota_default_description": "Talepte bulunulmadığı durumlarda GiB cinsinden kullanÄąlacak kota.", "oauth_timeout": "İstek Zaman AÅŸÄąmÄą", "oauth_timeout_description": "Milisaniye cinsinden istek zaman aÅŸÄąmÄą", "ocr_job_description": "Resimlerdeki metni tanÄąmak için makine Ãļğrenimini kullan", @@ -301,7 +321,7 @@ "server_welcome_message_description": "Giriş sayfasÄąnda gÃļsterilen mesaj.", "settings_page_description": "YÃļnetici ayarlar sayfasÄą", "sidecar_job": "Ek dosya ile taÅŸÄąnan metadata", - "sidecar_job_description": "Dosya sisteminden yan araç meta verilerini keşfedin veya eşzamanlayÄąn", + "sidecar_job_description": "Dosya sisteminden sidecar meta verilerini keşfedin veya senkronize edin", "slideshow_duration_description": "Her fotoğrafÄąn kaç saniye gÃļrÃŧntÃŧleneceği", "smart_search_job_description": "AkÄąllÄą aramayÄą desteklemek için tÃŧm Ãļğelerde makine Ãļğrenmesini çalÄąÅŸtÄąrÄąn", "storage_template_date_time_description": "Öğenin oluşturulma zaman damgasÄą, tarih ve saat bilgisi için kullanÄąlÄąr", @@ -331,7 +351,7 @@ "template_settings": "Bildirim ŞablonlarÄą", "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_custom_css_settings_description": "BasamaklÄą Stil SayfalarÄą (css), Immich tasarÄąmÄąnÄąn Ãļzelleştirilmesine olanak tanÄąr.", "theme_settings": "Tema AyarlarÄą", "theme_settings_description": "Immich web arayÃŧzÃŧnÃŧn Ãļzelleştirilmesi ayarlarÄąnÄą yÃļnet", "thumbnail_generation_job": "Önizlemeleri oluştur", @@ -339,7 +359,7 @@ "transcoding_acceleration_api": "HÄązlandÄąrma API", "transcoding_acceleration_api_description": "Video formatÄą çevriminde kullanÄąlacak API. Bu ayara 'mÃŧmkÃŧn olduğunca' uyulmaktadÄąr; seçilen API'da sorun Ã§Äąkarsa yazÄąlÄąm tabanlÄą çevirime dÃļnÃŧlÃŧr. VP9 donanÄąmÄąnÄąza bağlÄą olarak çalÄąÅŸmayabilir.", "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU gerektirir)", - "transcoding_acceleration_qsv": "HÄązlÄą Eşzamanlama (7. nesil veya daha yeni bir Intel CPU gerektirir)", + "transcoding_acceleration_qsv": "HÄązlÄą Senkronizasyon (7. nesil Intel işlemci veya Ãŧzeri gerektirir)", "transcoding_acceleration_rkmpp": "RKMPP (Sadece Rockchip SOC'ler)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Kabul edilen ses kodekleri", @@ -355,7 +375,7 @@ "transcoding_codecs_learn_more": "Buradaki terminolojiyi Ãļğrenmek için FFmpeg dokÃŧmantasyonlarÄąna bakabilirsiniz: H.264, HEVC ve VP9.", "transcoding_constant_quality_mode": "Sabit kalite modu", "transcoding_constant_quality_mode_description": "ICQ, CQP'den daha iyidir, ancak bazÄą donanÄąm hÄązlandÄąrma cihazlarÄą bu modu desteklemez. Bu seçeneğin ayarlanmasÄą, kalite tabanlÄą kodlama kullanÄąrken belirtilen modu tercih eder. ICQ'yu desteklemediği için NVENC tarafÄąndan gÃļz ardÄą edilir.", - "transcoding_constant_rate_factor": "Sabit oran faktÃļrÃŧ (-SOF)", + "transcoding_constant_rate_factor": "Sabit oran faktÃļrÃŧ (-crf)", "transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha dÃŧşÃŧk değerler daha iyi kalite sağlar, ancak daha bÃŧyÃŧk dosyalar Ãŧretir.", "transcoding_disabled_description": "VideolarÄą dÃļnÃŧştÃŧrmeyin, bazÄą istemcilerde oynatma bozulabilir", "transcoding_encoding_options": "Kodlama Seçenekleri", @@ -366,7 +386,7 @@ "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": "Maksimum bit hÄązÄą", "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. Birim belirtilmediğinde, k (kbit/s için) varsayÄąlÄąr; bu nedenle 5000, 5000k ve 5M (Mbit/s için) eşdeğerdir.", "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.", @@ -431,9 +451,12 @@ "admin_password": "YÃļnetici Şifresi", "administration": "YÃļnetim", "advanced": "Gelişmiş", - "advanced_settings_enable_alternate_media_filter_subtitle": "Eşzamanlama sÄąrasÄąnda medyayÄą alternatif ÃļlçÃŧtlere gÃļre sÃŧzgeçten geçirmek için bu seçeneği kullanÄąn. UygulamanÄąn tÃŧm albÃŧmleri algÄąlamasÄąnda sorun yaÅŸÄąyorsanÄąz yalnÄązca bu durumda deneyin.", - "advanced_settings_enable_alternate_media_filter_title": "[DENEYSEL] Alternatif cihaz albÃŧm eşzamanlama sÃŧzgeci kullanÄąn", - "advanced_settings_log_level_title": "GÃŧnlÃŧk dÃŧzeyi: {level}", + "advanced_settings_clear_image_cache": "GÃļrsel Önbelleğini Temizle", + "advanced_settings_clear_image_cache_error": "GÃļrsel Ãļnbelleği temizlenemedi", + "advanced_settings_clear_image_cache_success": "BaşarÄąyla temizlendi: {size}", + "advanced_settings_enable_alternate_media_filter_subtitle": "Bu seçeneği, senkronizasyon sÄąrasÄąnda medyayÄą alternatif ÃļlçÃŧtlere gÃļre filtrelemek için 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 senkronizasyon filtresini kullan", + "advanced_settings_log_level_title": "GÃŧnlÃŧk seviyesi: {level}", "advanced_settings_prefer_remote_subtitle": "BazÄą cihazlar yerel Ãļğelerden kÃŧçÃŧk resimleri yÃŧklerken çok yavaş çalÄąÅŸÄąr. Bunun yerine uzak gÃļrÃŧntÃŧleri yÃŧklemek için bu ayarÄą etkinleştirin.", "advanced_settings_prefer_remote_title": "Uzak gÃļrÃŧntÃŧleri tercih et", "advanced_settings_proxy_headers_subtitle": "Immich'in her ağ isteğiyle birlikte gÃļndermesi gereken proxy header'larÄą tanÄąmlayÄąn", @@ -443,7 +466,7 @@ "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": "Kendinden imzalÄą SSL sertifikalarÄąna izin ver [DENEYSEL]", "advanced_settings_sync_remote_deletions_subtitle": "Web Ãŧzerinde işlem yapÄąldığında, bu aygÄąttaki Ãļğeyi otomatik olarak sil veya geri yÃŧkle", - "advanced_settings_sync_remote_deletions_title": "Uzaktan silmeleri eşzamanla [DENEYSEL]", + "advanced_settings_sync_remote_deletions_title": "Uzaktan silme işlemlerini senkronize et [DENEYSEL]", "advanced_settings_tile_subtitle": "Gelişmiş kullanÄącÄą ayarlarÄą", "advanced_settings_troubleshooting_subtitle": "Sorun giderme için ek Ãļzellikleri etkinleştirin", "advanced_settings_troubleshooting_title": "Sorun Giderme", @@ -467,10 +490,12 @@ "album_remove_user": "KullanÄącÄąyÄą kaldÄąr?", "album_remove_user_confirmation": "{user} kullanÄącÄąsÄąnÄą kaldÄąrmak istediğinize emin misiniz?", "album_search_not_found": "AramanÄązla eşleşen albÃŧm bulunamadÄą", + "album_selected": "Seçilen albÃŧm", "album_share_no_users": "GÃļrÃŧnÃŧşe gÃļre bu albÃŧmÃŧ tÃŧm kullanÄącÄąlarla paylaştÄąnÄąz veya paylaşacak herhangi bir başka kullanÄącÄąnÄąz yok.", "album_summary": "AlbÃŧm Ãļzeti", "album_updated": "AlbÃŧm gÃŧncellendi", "album_updated_setting_description": "PaylaÅŸÄąlan bir albÃŧme yeni bir Ãļğe eklendiğinde e-posta bildirimi alÄąn", + "album_upload_assets": "BilgisayarÄąnÄązdan gÃļrseller yÃŧkleyin ve albÃŧme ekleyin", "album_user_left": "{album}den ayrÄąldÄąnÄąz", "album_user_removed": "{user} kaldÄąrÄąldÄą", "album_viewer_appbar_delete_confirm": "Bu albÃŧmÃŧ hesabÄąnÄązdan silmek istediğinizden emin misiniz?", @@ -488,9 +513,11 @@ "albums_default_sort_order_description": "Yeni albÃŧm oluştururken kullanÄąlacak başlangÄąÃ§ Ãļğe sÄąralama dÃŧzeni.", "albums_feature_description": "Diğer kullanÄącÄąlarla paylaÅŸÄąlabilen Ãļğe koleksiyonlarÄą.", "albums_on_device_count": "Cihazdaki albÃŧmler ({count})", + "albums_selected": "{count, plural, one {# albÃŧm seçildi} other {# albÃŧm seçildi}}", "all": "TÃŧmÃŧ", "all_albums": "TÃŧm AlbÃŧmler", "all_people": "TÃŧm Kişiler", + "all_photos": "TÃŧm fotoğraflar", "all_videos": "TÃŧm Videolar", "allow_dark_mode": "Koyu moda izin ver", "allow_edits": "DÃŧzenlemeye izin ver", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "Genel kullanÄącÄąnÄąn yÃŧklemesine aç", "allowed": "İzin verildi", "alt_text_qr_code": "QR kodu gÃļrseli", + "always_keep": "Her zaman sakla", + "always_keep_photos_hint": "Alan Aç, bu cihazdaki tÃŧm fotoğraflarÄą saklar.", + "always_keep_videos_hint": "Alan Aç, bu cihazdaki tÃŧm videolarÄą saklar.", "anti_clockwise": "Saat yÃļnÃŧnÃŧn tersine", "api_key": "API AnahtarÄą", "api_key_description": "Bu değer sadece bir kere gÃļsterilecek. LÃŧtfen bu pencereyi kapatmadan Ãļnce kopyaladığınÄąza emin olun.", @@ -524,10 +554,12 @@ "archived_count": "{count, plural, other {# arşivlendi}}", "are_these_the_same_person": "Bunlar aynÄą kişi mi?", "are_you_sure_to_do_this": "Bunu yapmak istediğinize emin misiniz?", + "array_field_not_fully_supported": "Dizi alanlarÄą manuel JSON dÃŧzenlemesi gerektirir", "asset_action_delete_err_read_only": "Salt okunur Ãļğeler silinemez, atlanÄąyor", "asset_action_share_err_offline": "ÇevrimdÄąÅŸÄą Ãļğeler alÄąnamÄąyor, atlanÄąyor", "asset_added_to_album": "AlbÃŧme eklendi", "asset_adding_to_album": "AlbÃŧme ekleniyorâ€Ļ", + "asset_created": "Öğe oluşturuldu", "asset_description_updated": "Öğe aÃ§ÄąklamasÄą gÃŧncellendi", "asset_filename_is_offline": "Öğe {filename} çevrimdÄąÅŸÄą", "asset_has_unassigned_faces": "Öğe, atanmamÄąÅŸ yÃŧzler içeriyor", @@ -540,6 +572,9 @@ "asset_list_layout_sub_title": "DÃŧzen", "asset_list_settings_subtitle": "Fotoğraf Äązgara dÃŧzeni ayarlarÄą", "asset_list_settings_title": "Fotoğraf IzgarasÄą", + "asset_not_found_on_device_android": "Cihazda varlÄąk bulunamadÄą", + "asset_not_found_on_device_ios": "Cihazda Ãļğe bulunamadÄą. iCloud kullanÄąyorsanÄąz, iCloud platformunda depolanan hatalÄą dosya nedeniyle Ãļğeye erişilemeyebilir", + "asset_not_found_on_icloud": "Öğe iCloud platformunda bulunamadÄą. Öğe, iCloud platformunda depolanan hatalÄą dosya nedeniyle erişilemeyebilir", "asset_offline": "Öğe Çevrim DÄąÅŸÄą", "asset_offline_description": "Bu harici Ãļğe artÄąk diskte bulunmuyor. YardÄąm için lÃŧtfen Immich yÃļneticinizle iletişime geçin.", "asset_restored_successfully": "Öğe başarÄąyla geri yÃŧklendi", @@ -588,10 +623,10 @@ "backup_album_selection_page_albums_device": "Cihazdaki albÃŧmler ({count})", "backup_album_selection_page_albums_tap": "Seçmek için dokunun, hariç tutmak için çift dokunun", "backup_album_selection_page_assets_scatter": "Öğeler birden fazla albÃŧme dağılabilir. Bu nedenle, yedekleme işlemi sÄąrasÄąnda albÃŧmler dahil edilebilir veya hariç tutulabilir.", - "backup_album_selection_page_select_albums": "AlbÃŧm seç", + "backup_album_selection_page_select_albums": "AlbÃŧmleri seç", "backup_album_selection_page_selection_info": "Seçim Bilgileri", "backup_album_selection_page_total_assets": "Toplam eşsiz Ãļğeler", - "backup_albums_sync": "Yedekleme albÃŧmlerinin senkronizasyonu", + "backup_albums_sync": "AlbÃŧm Yedekleme Senkronizasyonu", "backup_all": "TÃŧmÃŧ", "backup_background_service_backup_failed_message": "Yedekleme başarÄąsÄąz. Tekrar deneniyorâ€Ļ", "backup_background_service_complete_notification": "Öğe yedekleme tamamlandÄą", @@ -711,6 +746,8 @@ "change_password_form_password_mismatch": "Şifreler eşleşmiyor", "change_password_form_reenter_new_password": "Yeni Şifreyi Tekrar Giriniz", "change_pin_code": "PIN kodunu değiştirin", + "change_trigger": "Tetikleyiciyi değiştir", + "change_trigger_prompt": "Tetikleyiciyi değiştirmek istediğinizden emin misiniz? Bu, mevcut tÃŧm eylemleri ve filtreleri kaldÄąracaktÄąr.", "change_your_password": "Şifreni değiştir", "changed_visibility_successfully": "GÃļrÃŧnÃŧrlÃŧk başarÄąyla değiştirildi", "charging": "Şarj oluyor", @@ -722,6 +759,18 @@ "checksum": "Sağlama toplamÄą", "choose_matching_people_to_merge": "Birleştirmek için eşleşen kişileri seçiniz", "city": "Şehir", + "cleanup_confirm_description": "Immich, sunucuya gÃŧvenli bir şekilde yedeklenmiş {count} adet gÃļrsel ( {date} tarihinden Ãļnce oluşturulmuş) buldu. Yerel kopyalarÄą bu cihazdan kaldÄąrmak istiyor musunuz?", + "cleanup_confirm_prompt_title": "Bu cihazdan silinsin mi?", + "cleanup_deleted_assets": "{count} adet gÃļrsel çÃļp kutusuna taÅŸÄąndÄą", + "cleanup_deleting": "ÇÃļp kutusuna taÅŸÄąnÄąyor...", + "cleanup_found_assets": "{count} adet yedeklenmiş gÃļrsel bulundu", + "cleanup_found_assets_with_size": "{count} yedeklenmiş Ãļğe bulundu ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloud PaylaÅŸÄąlan AlbÃŧmleri tarama kapsamÄą dÄąÅŸÄąnda tutulmuştur", + "cleanup_no_assets_found": "YukarÄądaki kriterlere uyan hiçbir varlÄąk bulunamadÄą. Alan Boşaltma, yalnÄązca sunucuya yedeklenmiş varlÄąklarÄą kaldÄąrabilir", + "cleanup_preview_title": "Silinecek gÃļrseller ({count})", + "cleanup_step3_description": "Tarih ve saklama ayarlarÄąnÄąza uyan, yedeklenmiş Ãļğeleri tarayÄąn.", + "cleanup_step4_summary": "Yerel cihazÄąnÄązdan kaldÄąrÄąlacak {count} Ãļğe ({date} tarihinden Ãļnce oluşturulmuş). Fotoğraflara Immich uygulamasÄą Ãŧzerinden erişmeye devam edebilirsiniz.", + "cleanup_trash_hint": "Depolama alanÄąnÄą tamamen geri kazanmak için sistem galerisi uygulamasÄąnÄą aÃ§Äąn ve çÃļp kutusunu boşaltÄąn", "clear": "Temizle", "clear_all": "Hepsini temizle", "clear_all_recent_searches": "Son aramalarÄąn hepsini temizle", @@ -733,6 +782,8 @@ "client_cert_import": "İçe Aktar", "client_cert_import_success_msg": "İstemci sertifikasÄą içe aktarÄąldÄą", "client_cert_invalid_msg": "Geçersiz sertifika dosyasÄą veya yanlÄąÅŸ şifre", + "client_cert_password_message": "Bu sertifika için şifreyi girin", + "client_cert_password_title": "Sertifika Şifresi", "client_cert_remove_msg": "İstemci sertifikasÄą kaldÄąrÄąldÄą", "client_cert_subtitle": "YalnÄązca PKCS12 (.p12, .pfx) formatÄąnÄą destekler. Sertifika içe aktarma/kaldÄąrma işlemi yalnÄązca oturum açmadan Ãļnce yapÄąlabilir", "client_cert_title": "SSL istemci sertifikasÄą [DENEYSEL]", @@ -787,6 +838,7 @@ "create_album": "AlbÃŧm oluştur", "create_album_page_untitled": "BaşlÄąksÄąz", "create_api_key": "API anahtarÄą oluştur", + "create_first_workflow": "İlk iş akÄąÅŸÄąnÄą oluştur", "create_library": "KÃŧtÃŧphane Oluştur", "create_link": "Link oluştur", "create_link_to_share": "Paylaşmak için link oluştur", @@ -801,17 +853,25 @@ "create_tag": "Etiket oluştur", "create_tag_description": "Yeni bir etiket oluşturun. İç içe geçmiş etiketler için, etiketi tam yolu ve eğik çizgileri de dahil ederek giriniz.", "create_user": "KullanÄącÄą oluştur", + "create_workflow": "İş akÄąÅŸÄą oluştur", "created": "Oluşturuldu", "created_at": "Oluşturuldu", "creating_linked_albums": "BağlantÄąlÄą albÃŧmler oluşturuluyor...", "crop": "Kes", + "crop_aspect_ratio_fixed": "Sabitlenmiş", + "crop_aspect_ratio_free": "Boş", + "crop_aspect_ratio_original": "Orijinal", "curated_object_page_title": "Nesneler", "current_device": "Mevcut cihaz", "current_pin_code": "Mevcut PIN kodu", "current_server_address": "Mevcut sunucu adresi", + "custom_date": "Özel tarih", "custom_locale": "Özel Yerel Ayar", "custom_locale_description": "Tarihleri ve sayÄąlarÄą dile ve bÃļlgeye gÃļre biçimlendirin", "custom_url": "Özel URL", + "cutoff_date_description": "Son dÃļneme ait fotoğraflarÄą tut â€Ļ", + "cutoff_day": "{count, plural, one {gÃŧn} other {gÃŧn}}", + "cutoff_year": "{count, plural, one {yÄąl} other {yÄąl}}", "daily_title_text_date": "dd MMM E", "daily_title_text_date_year": "dd MMM yyyy E", "dark": "Koyu", @@ -867,6 +927,7 @@ "deselect_all": "TÃŧmÃŧnÃŧ Seçimi KaldÄąr", "details": "Detaylar", "direction": "YÃļn", + "disable": "Devre dÄąÅŸÄą bÄąrak", "disabled": "Devre dÄąÅŸÄą bÄąrakÄąldÄą", "disallow_edits": "Değişikliklere izin verme", "discord": "Discord", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "GÃļmÃŧlÃŧ videolar", "download_include_embedded_motion_videos_description": "GÃļrsel hareketli fotoğraflarda yer alan gÃļmÃŧlÃŧ videolarÄą ayrÄą bir dosya olarak dahil et", "download_notfound": "İndirme bulunamadÄą", + "download_original": "Orijinali indir", "download_paused": "İndirme duraklatÄąldÄą", "download_settings": "İndir", "download_settings_description": "Öğe indirme ile ilgili ayarlarÄą yÃļnetin", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "Yeniden denemek için bekleniyor", "downloading": "İndiriliyor", "downloading_asset_filename": "Öğe indiriliyor {filename}", + "downloading_from_icloud": "iCloud’dan indiriliyor", "downloading_media": "Medya indiriliyor", "drop_files_to_upload": "DosyalarÄą yÃŧklemek için herhangi bir yere bÄąrakÄąn", "duplicates": "Kopyalar", @@ -929,11 +992,22 @@ "edit_tag": "Etiketi dÃŧzenle", "edit_title": "Başlığı dÃŧzenle", "edit_user": "KullanÄącÄąyÄą dÃŧzenle", + "edit_workflow": "İş akÄąÅŸÄąnÄą dÃŧzenle", "editor": "EditÃļr", "editor_close_without_save_prompt": "Değişiklikler kaydedilmeyecek", "editor_close_without_save_title": "DÃŧzenleyici kapatÄąlsÄąn mÄą?", - "editor_crop_tool_h2_aspect_ratios": "En boy oranlarÄą", - "editor_crop_tool_h2_rotation": "Rotasyon", + "editor_confirm_reset_all_changes": "TÃŧm değişikleri iptal edilecek. Emin misiniz?", + "editor_discard_edits_confirm": "DÃŧzenlemeleri iptal et", + "editor_discard_edits_prompt": "Kaydedilmemiş dÃŧzenlemeleriniz var. BunlarÄą iptal etmek istediğinizden emin misiniz?", + "editor_discard_edits_title": "DÃŧzenlemeleri iptal edelim mi?", + "editor_edits_applied_error": "DÃŧzenlemeler uygulanamadÄą", + "editor_edits_applied_success": "DÃŧzenlemeler başarÄąyla uygulandÄą", + "editor_flip_horizontal": "Yatay çevir", + "editor_flip_vertical": "Dikey çevir", + "editor_orientation": "YÃļnlendirme", + "editor_reset_all_changes": "Değişiklikleri sÄąfÄąrla", + "editor_rotate_left": "90° Saat yÃļnÃŧnÃŧn tersine çevir", + "editor_rotate_right": "90° saat yÃļnÃŧnde çevir", "email": "E-posta", "email_notifications": "E-posta bildirimleri", "empty_folder": "Bu klasÃļr boş", @@ -952,11 +1026,14 @@ "error_change_sort_album": "AlbÃŧm sÄąralama dÃŧzeni değiştirilemedi", "error_delete_face": "Öğeden yÃŧz silme hatasÄą", "error_getting_places": "Konum bilgisi alÄąnÄąrken hata oluştu", + "error_loading_albums": "AlbÃŧmler yÃŧklenirken hata oluştu", "error_loading_image": "Resim yÃŧklenirken hata oluştu", "error_loading_partners": "OrtaklarÄą yÃŧkleme hatasÄą: {error}", + "error_retrieving_asset_information": "Öğe bilgileri alÄąnÄąrken 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", + "error_while_navigating": "Öğeye giderken hata oluştu", "errors": { "cannot_navigate_next_asset": "Sonraki Ãļğeye geçiş yapÄąlamÄąyor", "cannot_navigate_previous_asset": "Önceki Ãļğeye geçiş yapÄąlamÄąyor", @@ -1014,6 +1091,7 @@ "unable_to_complete_oauth_login": "OAuth giriş işlemi tamamlanamadÄą", "unable_to_connect": "BağlanÄąlamÄąyor", "unable_to_copy_to_clipboard": "Panoya kopyalanamÄąyor, sayfaya https Ãŧzerinden eriştiğinizden emin olun", + "unable_to_create": "İş akÄąÅŸÄą oluşturulamÄąyor", "unable_to_create_admin_account": "YÃļnetici hesabÄą oluşturulamÄąyor", "unable_to_create_api_key": "Yeni API anahtarÄą oluşturulamÄąyor", "unable_to_create_library": "KÃŧtÃŧphane oluşturulamÄąyor", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "Hariç tutma deseni silinemiyor", "unable_to_delete_shared_link": "PaylaÅŸÄąlan bağlantÄą silinemiyor", "unable_to_delete_user": "KullanÄącÄą silinemiyor", + "unable_to_delete_workflow": "İş akÄąÅŸÄą silinemiyor", "unable_to_download_files": "Dosyalar indirilemiyor", "unable_to_edit_exclusion_pattern": "Hariç tutma deseni dÃŧzenlenemiyor", "unable_to_empty_trash": "ÇÃļp boşaltÄąlamÄąyor", @@ -1063,6 +1142,7 @@ "unable_to_scan_library": "KÃŧtÃŧphane taranamÄąyor", "unable_to_set_feature_photo": "Özellikli fotoğraf ayarlanamÄąyor", "unable_to_set_profile_picture": "Profil resmi ayarlanamÄąyor", + "unable_to_set_rating": "Derecelendirme ayarlanamÄąyor", "unable_to_submit_job": "GÃļrev gÃļnderilemiyor", "unable_to_trash_asset": "Öğe çÃļp kutusuna taÅŸÄąnamÄąyor", "unable_to_unlink_account": "Hesap bağlantÄąsÄą kaldÄąrÄąlamÄąyor", @@ -1074,10 +1154,12 @@ "unable_to_update_settings": "Ayarlar gÃŧncellenemiyor", "unable_to_update_timeline_display_status": "Zaman çizelgesi gÃļrÃŧntÃŧleme durumu gÃŧncellenemiyor", "unable_to_update_user": "KullanÄącÄą gÃŧncellenemiyor", + "unable_to_update_workflow": "İş akÄąÅŸÄą gÃŧncelleyemiyor", "unable_to_upload_file": "Dosya yÃŧklenemiyor" }, + "errors_text": "Hatalar", "exclusion_pattern": "Hariç tutma modeli", - "exif": "EXIF", + "exif": "Exif", "exif_bottom_sheet_description": "AÃ§Äąklama Ekle...", "exif_bottom_sheet_description_error": "AÃ§Äąklama gÃŧncelleme hatasÄą", "exif_bottom_sheet_details": "DETAYLAR", @@ -1120,14 +1202,17 @@ "features": "Özellikler", "features_in_development": "Geliştirme AşamasÄąndaki Özellikler", "features_setting_description": "UygulamanÄąn Ãļzelliklerini yÃļnet", - "file_name": "Dosya adÄą", "file_name_or_extension": "Dosya adÄą veya uzantÄą", + "file_name_text": "Dosya adÄą", + "file_name_with_value": "Dosya adÄą: {file_name}", "file_size": "Dosya boyutu", - "filename": "Dosya adÄą", + "filename": "Dosya AdÄą", "filetype": "Dosya tipi", "filter": "Filtre", + "filter_description": "Hedef Ãļğeleri filtreleme koşullarÄą", "filter_people": "Kişileri filtrele", "filter_places": "Yerleri sÃŧz", + "filters": "Filtreler", "find_them_fast": "AdlarÄąna gÃļre hÄązlÄąca bul", "first": "İlk", "fix_incorrect_match": "YanlÄąÅŸ eşleştirmeyi dÃŧzelt", @@ -1137,12 +1222,16 @@ "folders_feature_description": "Dosya sistemindeki fotoğraf ve videolarÄą klasÃļr gÃļrÃŧnÃŧmÃŧyle keşfedin", "forgot_pin_code_question": "PIN kodunuzu mu unuttunuz?", "forward": "İleri", + "free_up_space": "AlanÄą boşalt", + "free_up_space_description": "Alan açmak için yedeklenmiş fotoğraf ve videolarÄą cihazÄąnÄązÄąn çÃļp kutusuna taÅŸÄąyÄąn. Sunucudaki kopyalarÄąnÄąz gÃŧvende kalÄąr.", + "free_up_space_settings_subtitle": "Cihaz depolama alanÄąnÄą boşalt", "full_path": "Tam yol: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Bu Ãļzellik, çalÄąÅŸabilmek için Google'dan harici kaynaklar yÃŧkler.", "general": "Genel", "geolocation_instruction_location": "GPS koordinatlarÄą olan bir Ãļğeyi tÄąklayarak konumunu kullanÄąn veya haritadan doğrudan bir konum seçin", "get_help": "YardÄąm Al", + "get_people_error": "Kişileri alÄąrken hata oluştu", "get_wifiname_error": "Wi-Fi adÄą alÄąnamadÄą. Gerekli izinleri verdiğinizden ve bir Wi-Fi ağına bağlÄą olduğunuzdan emin olun", "getting_started": "Başlarken", "go_back": "Geri git", @@ -1175,6 +1264,7 @@ "hide_named_person": "{name} adlÄą kişiyi gizle", "hide_password": "Şifreyi gizle", "hide_person": "Kişiyi gizle", + "hide_schema": "ŞemayÄą gizle", "hide_text_recognition": "Metin tanÄąmayÄą gizle", "hide_unnamed_people": "İsimsiz kişileri gizle", "home_page_add_to_album_conflicts": "{album} albÃŧmÃŧne {added} Ãļğe eklendi. {failed} Ãļğe zaten albÃŧmdeydi.", @@ -1240,16 +1330,25 @@ "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şzamanlama {dateTime}", + "ios_debug_info_last_sync_at": "Son senkronizasyon {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 arka plan eşzamanlama gÃļrevi çalÄąÅŸtÄąrÄąlmadÄą", + "ios_debug_info_no_sync_yet": "HenÃŧz hiçbir arka plan senkronizasyon 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", + "json_editor": "JSON dÃŧzenleyici", + "json_error": "JSON hatasÄą", "keep": "Koru", + "keep_albums": "AlbÃŧmleri sakla", + "keep_albums_count": "{count} {count, plural, one {albÃŧm} other {albÃŧm}} saklanÄąyor", "keep_all": "Hepsini koru", + "keep_description": "Alan açarken cihazÄąnÄązda kalacak Ãļğeleri seçin.", + "keep_favorites": "Favorileri tut", + "keep_on_device": "Cihazda sakla", + "keep_on_device_hint": "Bu cihazda saklanacak Ãļğeleri seçin", "keep_this_delete_others": "Bunu sakla, diğerlerini sil", + "keeping": "Saklananlar: {items}", "kept_this_deleted_others": "Bu Ãļğe tutuldu ve {count, plural, one {# varlÄąk} other {# varlÄąk}} silindi", "keyboard_shortcuts": "Klavye kÄąsayollarÄą", "language": "Dil", @@ -1343,10 +1442,28 @@ "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Ãŧ", + "maintenance_action_restore": "VeritabanÄą geri yÃŧkleniyor", "maintenance_description": "Immich, bakÄąm moduna alÄąnmÄąÅŸtÄąr.", "maintenance_end": "BakÄąm modunu sonlandÄąr", "maintenance_end_error": "BakÄąm modu sonlandÄąrÄąlamadÄą.", "maintenance_logged_in_as": "Şu anda {user} olarak oturum aÃ§ÄąlmÄąÅŸ durumda", + "maintenance_restore_from_backup": "Yedekten geri yÃŧkle", + "maintenance_restore_library": "KÃŧtÃŧphaneni Geri YÃŧkle", + "maintenance_restore_library_confirm": "Her şey doğru gÃļrÃŧnÃŧyorsa yedeği geri yÃŧklemeye devam edin!", + "maintenance_restore_library_description": "VeritabanÄą geri yÃŧkleniyor", + "maintenance_restore_library_folder_has_files": "{folder} içinde {count} klasÃļr var", + "maintenance_restore_library_folder_no_files": "{folder} içinde eksik dosyalar var!", + "maintenance_restore_library_folder_pass": "okunabilir ve yazÄąlabilir", + "maintenance_restore_library_folder_read_fail": "okunamÄąyor", + "maintenance_restore_library_folder_write_fail": "yazÄąlamÄąyor", + "maintenance_restore_library_hint_missing_files": "Önemli dosyalar eksik olabilir", + "maintenance_restore_library_hint_regenerate_later": "BunlarÄą daha sonra ayarlardan yeniden oluşturabilirsiniz", + "maintenance_restore_library_hint_storage_template_missing_files": "Depolama şablonu kullanÄąlÄąyor mu? Dosyalar eksik olabilir", + "maintenance_restore_library_loading": "BÃŧtÃŧnlÃŧk kontrolleri ve sezgisel analizler yÃŧkleniyorâ€Ļ", + "maintenance_task_backup": "Mevcut veritabanÄąnÄąn yedeği oluşturuluyorâ€Ļ", + "maintenance_task_migrations": "VeritabanÄą geçişleri çalÄąÅŸtÄąrÄąlÄąyorâ€Ļ", + "maintenance_task_restore": "Seçilen yedek geri yÃŧkleniyorâ€Ļ", + "maintenance_task_rollback": "Geri yÃŧkleme başarÄąsÄąz oldu, geri dÃļnÃŧş noktasÄąna alÄąnÄąyorâ€Ļ", "maintenance_title": "Geçici Olarak KullanÄąlamÄąyor", "make": "Marka", "manage_geolocation": "Konumu yÃļnet", @@ -1408,6 +1525,8 @@ "minimize": "KÃŧçÃŧlt", "minute": "Dakika", "minutes": "Dakika", + "mirror_horizontal": "Yatay", + "mirror_vertical": "Dikey", "missing": "Eksik", "mobile_app": "Mobil Uygulama", "mobile_app_download_onboarding_note": "Aşağıdaki seçenekleri kullanarak eşlik eden mobil uygulamayÄą indirin", @@ -1416,11 +1535,14 @@ "monthly_title_text_date_format": "AAAA y", "more": "Daha fazla", "move": "TaÅŸÄą", + "move_down": "Aşağı taÅŸÄą", "move_off_locked_folder": "Kilitli klasÃļrden taÅŸÄą", "move_to": "Şuraya taÅŸÄą", + "move_to_device_trash": "Cihaz çÃļp kutusuna taÅŸÄą", "move_to_lock_folder_action_prompt": "{count} kilitli klasÃļre eklendi", "move_to_locked_folder": "Kilitli klasÃļre taÅŸÄą", "move_to_locked_folder_confirmation": "Bu fotoğraflar ve videolar tÃŧm albÃŧmlerden kaldÄąrÄąlacak ve yalnÄązca kilitli klasÃļrden gÃļrÃŧntÃŧlenebilecektir", + "move_up": "YukarÄą taÅŸÄą", "moved_to_archive": "{count, plural, one {# Ãļğe} other {# Ãļğeler}} arşive taÅŸÄąndÄą", "moved_to_library": "{count, plural, one {# Ãļğe} other {# Ãļğeler}} kitaplığa taÅŸÄąndÄą", "moved_to_trash": "ÇÃļp kutusuna taÅŸÄąndÄą", @@ -1430,6 +1552,7 @@ "my_albums": "AlbÃŧmlerim", "name": "İsim", "name_or_nickname": "İsim veya takma isim", + "name_required": "Ad girilmesi zorunludur", "navigate": "Gezin", "navigate_to_time": "Zamana Git", "network_requirement_photos_upload": "FotoğraflarÄą yedeklemek için mobil veriyi kullan", @@ -1454,20 +1577,24 @@ "next": "Sonraki", "next_memory": "Sonraki anÄą", "no": "HayÄąr", + "no_actions_added": "HenÃŧz eklenen eylem yok", + "no_albums_found": "AlbÃŧm bulunamadÄą", "no_albums_message": "Fotoğraf ve videolarÄąnÄązÄą dÃŧzenlemek için yeni bir albÃŧm oluşturun", "no_albums_with_name_yet": "HenÃŧz bu isimde bir albÃŧmÃŧnÃŧz bulunmuyor.", "no_albums_yet": "HenÃŧz albÃŧm oluşturmadÄąnÄąz.", "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_message": "İlk fotoğrafÄąnÄązÄą yÃŧklemek için tÄąklayÄąn", "no_assets_to_show": "GÃļsterilecek Ãļğe yok", "no_cast_devices_found": "YansÄątÄąlacak cihaz bulunamadÄą", "no_checksum_local": "Sağlama toplamÄą mevcut değil - yerel varlÄąklarÄą alamÄąyor", "no_checksum_remote": "Sağlama toplamÄą mevcut değil - uzak varlÄąk alÄąnamÄąyor", + "no_configuration_needed": "YapÄąlandÄąrmaya gerek yok", "no_devices": "Yetkili cihaz yok", "no_duplicates_found": "Hiçbir kopya bulunamadÄą.", - "no_exif_info_available": "EXIF bilgisi mevcut değil", + "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 favorilere ekleyin", + "no_filters_added": "HenÃŧz filtre eklenmedi", "no_libraries_message": "Fotoğraf ve videolarÄąnÄązÄą gÃļrmek için bir harici kÃŧtÃŧphane oluşturun", "no_local_assets_found": "Bu sağlama toplamÄą ile yerel varlÄąk bulunamadÄą", "no_location_set": "Konum ayarlanmadÄą", @@ -1481,11 +1608,11 @@ "no_results_description": "Eş anlamlÄą ya da daha genel anlamlÄą bir kelime deneyin", "no_shared_albums_message": "FotoğraflarÄą ve videolarÄą ağınÄązdaki kişilerle paylaşmak için bir albÃŧm oluşturun", "no_uploads_in_progress": "YÃŧkleme işlemi yok", + "none": "Yok", "not_allowed": "İzin verilmiyor", "not_available": "YOK", "not_in_any_album": "Hiçbir albÃŧmde değil", "not_selected": "Seçilmedi", - "note_apply_storage_label_to_previously_uploaded assets": "Not: Daha Ãļnce yÃŧklenen Ãļğeler için bir depolama yolu etiketi uygulamak Ãŧzere şunu başlatÄąn", "notes": "Notlar", "nothing_here_yet": "Burada henÃŧz bir şey yok", "notification_permission_dialog_content": "Bildirimleri etkinleştirmek için cihaz ayarlarÄąna gidin ve izin verin.", @@ -1521,7 +1648,7 @@ "options": "Seçenekler", "or": "veya", "organize_into_albums": "AlbÃŧmler halinde dÃŧzenle", - "organize_into_albums_description": "Mevcut eşzamanlama ayarlarÄąnÄą kullanarak mevcut fotoğraflarÄą albÃŧmlere ekleyin", + "organize_into_albums_description": "Mevcut fotoğraflarÄą geçerli senkronizasyon ayarlarÄąnÄą kullanarak albÃŧmlere yerleştirin", "organize_your_library": "KÃŧtÃŧphanenizi dÃŧzenleyin", "original": "orijinal", "other": "Diğer", @@ -1563,6 +1690,7 @@ "people": "Kişiler", "people_edits_count": "{count, plural, one {# kişi} other {# kişi}} dÃŧzenlendi", "people_feature_description": "Kişilere gÃļre gruplanmÄąÅŸ fotoğraflarÄą ve videolarÄą inceleyin", + "people_selected": "{count, plural, one {# kişi seçildi} other {# kişi seçildi}}", "people_sidebar_description": "Yan panelde kişilere hÄązlÄą erişim bağlantÄąsÄą gÃļster", "permanent_deletion_warning": "KalÄącÄą silme uyarÄąsÄą", "permanent_deletion_warning_setting_description": "Öğeleri kalÄącÄą olarak silerken uyarÄą gÃļster", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# yaÅŸÄąnda}}", "person_birthdate": "{date} tarihinde doğdu", "person_hidden": "{name}{hidden, select, true { (gizli)} other {}}", + "person_recognized": "TanÄąnan kişi", + "person_selected": "Seçilen kişi", "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", "photos_and_videos": "Fotoğraflar & Videolar", "photos_count": "{count, plural, one {{count, number} fotoğraf} other {{count, number} fotoğraf}}", "photos_from_previous_years": "Önceki yÄąllardan fotoğraflar", + "photos_only": "Sadece Fotoğraflar", "pick_a_location": "Bir konum seçin", "pick_custom_range": "Özel aralÄąk", "pick_date_range": "Bir tarih aralığı seçin", @@ -1667,10 +1798,12 @@ "purchase_settings_server_activated": "Sunucu ÃŧrÃŧn anahtarÄą, yÃļnetici tarafÄąndan yÃļnetilir", "query_asset_id": "Öğe Kimliği Sorgulama", "queue_status": "SÄąrada {count}/{total}", + "rate_asset": "Öğeyi Derecelendir", "rating": "Derecelendirme", "rating_clear": "Derecelendirmeyi temizle", "rating_count": "{count, plural, one {# yÄąldÄąz} other {# yÄąldÄąz}}", "rating_description": "EXIF derecelendirmesini bilgi panelinde gÃļster", + "rating_set": "Derecelendirme {rating, plural, one {# yÄąldÄąz} other {# yÄąldÄąz}} olarak ayarlandÄą", "reaction_options": "Tepki seçenekleri", "read_changelog": "Değişiklik gÃŧnlÃŧğÃŧnÃŧ oku", "readonly_mode_disabled": "Salt okunur mod devre dÄąÅŸÄą", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "{count, plural, one {# Ãļğe} other {# Ãļğeler}} yeni bir kişiye atandÄą", "reassing_hint": "Seçili Ãļğeleri mevcut bir kişiye atayÄąn", "recent": "Son", - "recent-albums": "Son kaydedilen albÃŧmler", + "recent_albums": "Son kaydedilen albÃŧmler", "recent_searches": "Son aramalar", "recently_added": "Son eklenenler", "recently_added_page_title": "Son Eklenenler", @@ -1742,7 +1875,7 @@ "reset_pin_code_success": "PIN kodu başarÄąyla sÄąfÄąrlandÄą", "reset_pin_code_with_password": "PIN kodunuzu her zaman şifrenizle sÄąfÄąrlayabilirsiniz", "reset_sqlite": "SQLite VeritabanÄąnÄą SÄąfÄąrla", - "reset_sqlite_confirmation": "SQLite veritabanÄąnÄą sÄąfÄąrlamak istediğinizden emin misiniz? Verileri yeniden eşzamanlamak için oturumu kapatÄąp tekrar oturum açmanÄąz gerekecektir", + "reset_sqlite_confirmation": "SQLite veritabanÄąnÄą sÄąfÄąrlamak istediğinizden emin misiniz? Verileri yeniden senkronize etmek için oturumu kapatÄąp tekrar giriş yapmanÄąz gerekecek", "reset_sqlite_success": "SQLite veritabanÄąnÄą başarÄąyla sÄąfÄąrladÄąnÄąz", "reset_to_default": "VarsayÄąlana sÄąfÄąrla", "resolution": "ÇÃļzÃŧnÃŧrlÃŧk", @@ -1770,9 +1903,11 @@ "saved_settings": "Kaydedilen ayarlar", "say_something": "Bir şey sÃļyle", "scaffold_body_error_occurred": "Bir hata meydana geldi", + "scan": "Tara", "scan_all_libraries": "TÃŧm KÃŧtÃŧphaneleri Tara", "scan_library": "KÃŧtÃŧphaneyi tara", "scan_settings": "AyarlarÄą Tara", + "scanning": "TaranÄąyor", "scanning_for_album": "AlbÃŧm için taranÄąyor...", "search": "Ara", "search_albums": "AlbÃŧm ara", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "Medya tÃŧrÃŧ seç", "search_filter_ocr": "OCR'ye gÃļre ara", "search_filter_people_title": "Kişi seç", + "search_filter_star_rating": "YÄąldÄąz PuanÄą", "search_for": "AraştÄąr", "search_for_existing_person": "Mevcut bir kişiyi ara", "search_no_more_result": "Daha fazla sonuç yok", @@ -1836,17 +1972,23 @@ "second": "Saniye", "see_all_people": "TÃŧm kişileri gÃļr", "select": "Seç", + "select_album": "AlbÃŧm seç", "select_album_cover": "AlbÃŧm kapağı seç", + "select_albums": "AlbÃŧmleri 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_count": "{count, plural, one {Seç #} other {Seç #}}", + "select_cutoff_date": "Tarih sÄąnÄąrÄąnÄą seç", "select_face": "YÃŧzÃŧ seç", "select_featured_photo": "Öne Ã§Äąkan fotoğrafÄą seç", "select_from_computer": "Bilgisayardan seç", "select_keep_all": "Hepsini sakla", "select_library_owner": "KÃŧtÃŧphane sahibini seç", "select_new_face": "Yeni yÃŧz seç", + "select_people": "Kişi seç", + "select_person": "Kişileri seç", "select_person_to_tag": "Etiketlemek için bir kişi seçin", "select_photos": "FotoğraflarÄą seç", "select_trash_all": "Hepsini çÃļpe at", @@ -1938,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} yÄąl", "shared_link_edit_password_hint": "PaylaÅŸÄąm şifresini girin", "shared_link_edit_submit_button": "BağlantÄąyÄą gÃŧncelle", - "shared_link_error_server_url_fetch": "Sunucu URL'si alÄąnamadÄą", + "shared_link_error_server_url_fetch": "Sunucu url'si alÄąnamÄąyor", "shared_link_expires_day": "SÃŧresi {count} gÃŧn içinde doluyor", "shared_link_expires_days": "SÃŧresi {count} gÃŧn içinde doluyor", "shared_link_expires_hour": "SÃŧresi {count} saat içinde doluyor", @@ -1982,6 +2124,7 @@ "show_password": "Şifreyi gÃļster", "show_person_options": "Kişi seçeneklerini gÃļster", "show_progress_bar": "İlerleme Çubuğunu GÃļster", + "show_schema": "ŞemayÄą gÃļster", "show_search_options": "Arama seçeneklerini gÃļster", "show_shared_links": "PaylaÅŸÄąlan bağlantÄąlarÄą gÃļster", "show_slideshow_transition": "Slayt gÃļsterisi geçişini gÃļster", @@ -1999,6 +2142,8 @@ "skip_to_folders": "KlasÃļrlere atla", "skip_to_tags": "Etiketlere atla", "slideshow": "Slayt gÃļsterisi", + "slideshow_repeat": "Slayt gÃļsterisini tekrarla", + "slideshow_repeat_description": "Slayt gÃļsterisi bittiğinde başa dÃļn", "slideshow_settings": "Slayt gÃļsterisi ayarlarÄą", "sort_albums_by": "AlbÃŧmleri sÄąrala...", "sort_created": "Oluşturulma tarihi", @@ -2039,13 +2184,13 @@ "support_and_feedback": "Destek & Geri Bildirim", "support_third_party_description": "Immich kurulumu ÃŧçÃŧncÃŧ bir tarafça yapÄąldÄą. YaşadığınÄąz sorunlar bu paketle ilgili olabilir. LÃŧtfen Ãļncelikli olarak aşağıdaki bağlantÄąlarÄą kullanarak bu sağlayÄącÄąyla iletişime geçin.", "swap_merge_direction": "Birleştirme yÃļnÃŧnÃŧ değiştir", - "sync": "Eşzamanla", - "sync_albums": "AlbÃŧmleri eşzamanla", - "sync_albums_manual_subtitle": "YÃŧklenmiş fotoğraf ve videolarÄą yedekleme için seçili albÃŧmler ile eşzamanlayÄąn", - "sync_local": "Yerel Eşzamanlama", - "sync_remote": "Uzaktan Eşzamanlama", - "sync_status": "Eşzamanlama Durumu", - "sync_status_subtitle": "Eşzamanlama sistemini gÃļrÃŧntÃŧleyin ve yÃļnetin", + "sync": "Senkronizasyon", + "sync_albums": "AlbÃŧmleri senkronize et", + "sync_albums_manual_subtitle": "YÃŧklenen tÃŧm videolarÄą ve fotoğraflarÄą seçilen yedekleme albÃŧmlerine senkronize edin", + "sync_local": "Yerel Senkronizasyon", + "sync_remote": "Uzaktan Senkronizasyon", + "sync_status": "Senkronizasyon Durumu", + "sync_status_subtitle": "Senkronizasyon sistemini gÃļrÃŧntÃŧleyin ve yÃļnetin", "sync_upload_album_setting_subtitle": "FotoğraflarÄąnÄązÄą ve videolarÄąnÄązÄą oluşturun ve Immich'te seçtiğiniz albÃŧmlere yÃŧkleyin", "tag": "Etiket", "tag_assets": "Öğeleri etiketle", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "Uygulama temasÄą seç", "theme_setting_three_stage_loading_subtitle": "Üç aşamalÄą yÃŧkleme, yÃŧkleme performansÄąnÄą artÄąrabilir ancak ağ yÃŧkÃŧnÃŧ Ãļnemli ÃļlçÃŧde artÄąrÄąr", "theme_setting_three_stage_loading_title": "Üç aşamalÄą yÃŧklemeyi etkinleştir", + "then": "Sonra", "they_will_be_merged_together": "Birlikte birleştirilecekler", "third_party_resources": "ÜçÃŧncÃŧ taraf kaynaklar", "time": "Zaman", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "Öğeleri seç", "trash_page_title": "ÇÃļp Kutusu ({count})", "trashed_items_will_be_permanently_deleted_after": "Silinen Ãļğeler {days, plural, one {# gÃŧn} other {# gÃŧn}} sonra kalÄącÄą olarak silinecek.", + "trigger": "Tetikleyici", + "trigger_asset_uploaded": "Öğe KarÅŸÄąya YÃŧklendi", + "trigger_asset_uploaded_description": "Yeni bir Ãļğe karÅŸÄąya yÃŧklendiğinde tetiklenir", + "trigger_description": "İş akÄąÅŸÄąnÄą başlatan bir olay", + "trigger_person_recognized": "TanÄąnan Kişi", + "trigger_person_recognized_description": "Bir kişi algÄąlandığında tetiklenir", + "trigger_type": "Tetikleyici tÃŧrÃŧ", "troubleshoot": "Sorun giderme", "type": "TÃŧr", "unable_to_change_pin_code": "PIN kodu değiştirilemedi", @@ -2123,6 +2276,7 @@ "unhide_person": "Kişiyi gÃļster", "unknown": "Bilinmeyen", "unknown_country": "Bilinmeyen Ülke", + "unknown_date": "Bilinmeyen tarih", "unknown_year": "Bilinmeyen YÄąl", "unlimited": "SÄąnÄąrsÄąz", "unlink_motion_video": "Hareketli video bağlantÄąsÄąnÄą kaldÄąr", @@ -2139,17 +2293,19 @@ "unstack": "YığınÄą kaldÄąr", "unstack_action_prompt": "{count} istiflenmemiş", "unstacked_assets_count": "{count, plural, one {# Ãļğenin} other {# Ãļğelerin}} yığınÄą kaldÄąrÄąldÄą", + "unsupported_field_type": "Desteklenmeyen alan tÃŧrÃŧ", "untagged": "Etiketlenmemiş", + "untitled_workflow": "BaşlÄąksÄąz iş akÄąÅŸÄą", "up_next": "SÄąradaki", "update_location_action_prompt": "Seçilen {count} Ãļğenin konumunu şu şekilde gÃŧncelleyin:", "updated_at": "GÃŧncellenme", "updated_password": "GÃŧncellenen şifre", "upload": "YÃŧkle", - "upload_action_prompt": "{count} yÃŧkleme için sÄąraya alÄąndÄą", "upload_concurrency": "YÃŧkleme eşzamanlÄąlığı", "upload_details": "YÃŧkleme AyrÄąntÄąlarÄą", "upload_dialog_info": "Seçili Ãļğeleri sunucuya yedeklemek istiyor musunuz?", "upload_dialog_title": "Öğe YÃŧkle", + "upload_error_with_count": "{count, plural, one {# Ãļğe} other {# Ãļğeler}} için yÃŧkleme hatasÄą", "upload_errors": "{count, plural, one {# hata} other {# hatayla}} yÃŧkleme tamamlandÄą, yeni yÃŧklenen Ãļğeleri gÃļrmek için sayfayÄą gÃŧncelleyin.", "upload_finished": "YÃŧkleme tamamlandÄą", "upload_progress": "{remaining, number} kalan - {processed, number}/{total, number} işlendi", @@ -2164,7 +2320,7 @@ "url": "URL", "usage": "KullanÄąm", "use_biometric": "Biyometri kullan", - "use_current_connection": "mevcut bağlantÄąyÄą 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.", @@ -2185,6 +2341,7 @@ "utilities": "YardÄąmcÄą Programlar", "validate": "Doğrula", "validate_endpoint_error": "LÃŧtfen geçerli bir URL girin", + "validation_error": "Doğrulama hatasÄą", "variables": "Değişkenler", "version": "SÃŧrÃŧm", "version_announcement_closing": "ArkadaÅŸÄąnÄąz, Alex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "Öğe Ãŧzerinde fareyle durulduğunda video kÃŧçÃŧk resmini oynatÄąr. Bu Ãļzellik devre dÄąÅŸÄąyken, oynatma simgesine fareyle gidilerek oynatma başlatÄąlabilir.", "videos": "Videolar", "videos_count": "{count, plural, one {# video} other {# video}}", + "videos_only": "Sadece videolar", "view": "GÃļrÃŧnÃŧm", "view_album": "AlbÃŧmÃŧ gÃļrÃŧntÃŧle", "view_all": "TÃŧmÃŧnÃŧ gÃļr", @@ -2216,6 +2374,8 @@ "viewer_stack_use_as_main_asset": "Ana fotoğraf olarak kullan", "viewer_unstack": "YığınÄą KaldÄąr", "visibility_changed": "GÃļrÃŧnÃŧrlÃŧk {count, plural, one {# kişi} other {# kişi}} için değiştirildi", + "visual": "GÃļrsel", + "visual_builder": "GÃļrsel oluşturucu", "waiting": "Bekleniyor", "waiting_count": "Bekleyen: {count}", "warning": "UyarÄą", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "Immich'e hoş geldiniz", "width": "Genişlik", "wifi_name": "Wi-Fi AdÄą", - "workflow": "İş akÄąÅŸÄą", + "workflow_delete_prompt": "Bu iş akÄąÅŸÄąnÄą silmek istediğinizden emin misiniz?", + "workflow_deleted": "İş akÄąÅŸÄą silindi", + "workflow_description": "İş akÄąÅŸÄą aÃ§ÄąklamasÄą", + "workflow_info": "İş akÄąÅŸÄą bilgileri", + "workflow_json": "İş akÄąÅŸÄą JSON", + "workflow_json_help": "İş akÄąÅŸÄą yapÄąlandÄąrmasÄąnÄą JSON biçiminde dÃŧzenleyin. Değişiklikler gÃļrsel oluşturucuyla eşitlenir.", + "workflow_name": "İş akÄąÅŸÄą adÄą", + "workflow_navigation_prompt": "Değişikliklerinizi kaydetmeden ayrÄąlmak istediğinizden emin misiniz?", + "workflow_summary": "İş akÄąÅŸÄą Ãļzeti", + "workflow_update_success": "İş akÄąÅŸÄą başarÄąyla gÃŧncellendi", + "workflow_updated": "İş akÄąÅŸÄą gÃŧncellendi", + "workflows": "İş akÄąÅŸlarÄą", + "workflows_help_text": "İş akÄąÅŸlarÄą, tetikleyicilere ve filtrelere dayalÄą olarak Ãļğelerinizdeki eylemleri otomatikleştirir", "wrong_pin_code": "YanlÄąÅŸ PIN kodu", "year": "YÄąl", "years_ago": "{years, plural, one {bir yÄąl} other {# yÄąl}} Ãļnce", "yes": "Evet", "you_dont_have_any_shared_links": "Herhangi bir paylaÅŸÄąlan bağlantÄąnÄąz yok", "your_wifi_name": "Wi-Fi AdÄąnÄąz", + "zero_to_clear_rating": "Öğe derecelendirmesini temizlemek için 0'a basÄąn", "zoom_image": "GÃļrÃŧntÃŧyÃŧ yakÄąnlaştÄąr", "zoom_to_bounds": "SÄąnÄąrlara yakÄąnlaştÄąr" } diff --git a/i18n/uk.json b/i18n/uk.json index b58c8bcb78..0609edf28c 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -1,13 +1,14 @@ { - "about": "ĐŸŅ€Đž ĐŋŅ€ĐžĐŗŅ€Đ°Đŧ҃", + "about": "ĐŸŅ€Đž ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē", "account": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ", - "account_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", + "account_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ", "acknowledge": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Đ¸", "action": "Đ”Ņ–Ņ", "action_common_update": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸", + "action_description": "ĐĐ°ĐąŅ–Ņ€ Đ´Ņ–Đš, ŅĐēŅ– ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž виĐēĐžĐŊĐ°Ņ‚Đ¸ С Đ˛Ņ–Đ´Ņ„Ņ–ĐģŅŒŅ‚Ņ€ĐžĐ˛Đ°ĐŊиĐŧи Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "actions": "Đ”Ņ–Ņ—", - "active": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ", - "active_count": "АĐēŅ‚Đ¸Đ˛ĐŊиК: {count}", + "active": "АĐēŅ‚Đ¸Đ˛ĐŊиК", + "active_count": "АĐēŅ‚Đ¸Đ˛ĐŊŅ–: {count}", "activity": "АĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ", "activity_changed": "АĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ {enabled, select, true {ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž} other {виĐŧĐēĐŊĐĩĐŊĐž}}", "add": "Đ”ĐžĐ´Đ°Ņ‚Đ¸", @@ -15,9 +16,14 @@ "add_a_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", "add_a_name": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ–Đŧ'Ņ", "add_a_title": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ", + "add_action": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Ņ–ŅŽ", + "add_action_description": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Ņ–ŅŽ", + "add_assets": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", "add_birthday": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´ĐĩĐŊҌ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", "add_endpoint": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", "add_exclusion_pattern": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", + "add_filter": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҄ҖĐģŅŒŅ‚Ņ€", + "add_filter_description": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸ ҃ĐŧĐžĐ˛Ņƒ ҄ҖĐģŅŒŅ‚Ņ€Đ°", "add_location": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", "add_more_users": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", "add_partner": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", @@ -25,26 +31,27 @@ "add_photos": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "add_tag": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗ", "add_to": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҃â€Ļ", - "add_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҃ аĐģŅŒĐąĐžĐŧ", + "add_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "add_to_album_bottom_sheet_added": "ДодаĐŊĐž Đ´Đž {album}", "add_to_album_bottom_sheet_already_exists": "ВĐļĐĩ Ņ” в {album}", - "add_to_album_bottom_sheet_some_local_assets": "ДĐĩŅĐēŅ– ĐģĐžĐēаĐģҌĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐŊĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "add_to_album_bottom_sheet_some_local_assets": "ДĐĩŅĐēŅ– ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ вдаĐģĐžŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "add_to_album_toggle": "ПĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ Đ˛Đ¸ĐąĐžŅ€Ņƒ Đ´ĐģŅ {album}", "add_to_albums": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛", "add_to_albums_count": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛ ({count})", "add_to_bottom_bar": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž", - "add_to_shared_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ҃ ҁĐŋŅ–ĐģҌĐŊиК аĐģŅŒĐąĐžĐŧ", - "add_upload_to_stack": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´Đž ҁ҂ĐĩĐē҃", + "add_to_shared_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃", + "add_upload_to_stack": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ в ҁ҂ĐĩĐē", "add_url": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ URL", + "add_workflow_step": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēŅ€ĐžĐē Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", "added_to_archive": "ДодаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", "added_to_favorites": "ДодаĐŊĐž Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "added_to_favorites_count": "ДодаĐŊĐž {count, number} Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "admin": { - "add_exclusion_pattern_description": "Đ”ĐžĐ´Đ°ĐšŅ‚Đĩ ŅˆĐ°ĐąĐģĐžĐŊи виĐēĐģŅŽŅ‡ĐĩĐŊҌ. ĐŸŅ–Đ´ŅŅ‚Đ°ĐŊОвĐēа С виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅĐŧ *, ** Ņ‚Đ° ? ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ. ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ҃ ĐąŅƒĐ´ŅŒ-ŅĐēĐžĐŧ҃ ĐēĐ°Ņ‚Đ°ĐģĐžĐˇŅ– С Ņ–Đŧ'ŅĐŧ ÂĢRawÂģ, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/Raw/**\". ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž СаĐēŅ–ĐŊŅ‡ŅƒŅŽŅ‚ŅŒŅŅ ĐŊа \".tif\", виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"**/*.tif\". ДĐģŅ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐžĐŗĐž ҈ĐģŅŅ…Ņƒ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ \"/path/to/ignore/**\".", + "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_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€ĐžĐģŅĐŧи, OAuth Ņ‚Đ° Ņ–ĐŊŅˆĐ¸Đŧи ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", "authentication_settings_disable_all": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ– ĐŧĐĩŅ‚ĐžĐ´Đ¸ Đ˛Ņ…ĐžĐ´Ņƒ? Đ’Ņ…Ņ–Đ´ ĐąŅƒĐ´Đĩ ĐŋОвĐŊŅ–ŅŅ‚ŅŽ виĐŧĐēĐŊĐĩĐŊиК.", "authentication_settings_reenable": "ДĐģŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžĐŗĐž Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ КоĐŧаĐŊĐ´Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", "background_task_job": "ФОĐŊĐžĐ˛Ņ– ЗавдаĐŊĐŊŅ", @@ -52,9 +59,9 @@ "backup_database_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ даĐŧĐŋи йаСи даĐŊĐ¸Ņ…", "backup_keep_last_amount": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Ņ… даĐŧĐŋŅ–Đ˛, ŅĐēŅ– СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸", "backup_onboarding_1_description": "Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊа ĐēĐžĐŋŅ–Ņ ҃ Ņ…ĐŧĐ°Ņ€Ņ– айО в Ņ–ĐŊŅˆĐžĐŧ҃ Ņ„Ņ–ĐˇĐ¸Ņ‡ĐŊĐžĐŧ҃ ĐŧҖҁ҆Җ.", - "backup_onboarding_2_description": "ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” ĐžŅĐŊОвĐŊŅ– Ņ„Đ°ĐšĐģи Ņ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ†Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ĐģĐžĐēаĐģҌĐŊĐž.", - "backup_onboarding_3_description": "ĐˇĐ°ĐŗĐ°ĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” 1 Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ– 2 ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", - "backup_onboarding_description": "Đ ĐĩĐēĐžĐŧĐĩĐŊдОваĐŊĐž Đ´ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ 3-2-1 Đ´ĐģŅ ĐˇĐ°Ņ…Đ¸ŅŅ‚Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…. ЗбĐĩŅ€Ņ–ĐŗĐ°ĐšŅ‚Đĩ ĐēĐžĐŋŅ–Ņ— СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ йаСи даĐŊĐ¸Ņ… Immich, Ņ‰ĐžĐą СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ ĐŋОвĐŊĐžŅ†Ņ–ĐŊĐŊиК ĐˇĐ°Ņ…Đ¸ŅŅ‚ Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ.", + "backup_onboarding_2_description": "ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа Ņ€Ņ–ĐˇĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Ņ– Ņ—Ņ… ĐģĐžĐēаĐģҌĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", + "backup_onboarding_3_description": "ĐˇĐ°ĐŗĐ°ĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž. ĐĻĐĩ вĐēĐģŅŽŅ‡Đ°Ņ” 1 Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ– 2 ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ—.", + "backup_onboarding_description": "Đ ĐĩĐēĐžĐŧĐĩĐŊдОваĐŊĐž Đ´ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ 3-2-1 Đ´ĐģŅ ĐˇĐ°Ņ…Đ¸ŅŅ‚Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… даĐŊĐ¸Ņ…. ЗбĐĩŅ€Ņ–ĐŗĐ°ĐšŅ‚Đĩ ĐēĐžĐŋŅ–Ņ— виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ йаСи даĐŊĐ¸Ņ… Immich, Ņ‰ĐžĐą СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ ĐŋОвĐŊĐžŅ†Ņ–ĐŊĐŊиК ĐˇĐ°Ņ…Đ¸ŅŅ‚ Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ.", "backup_onboarding_footer": "ДоĐēĐģадĐŊŅ–ŅˆĐĩ ĐŋŅ€Đž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Immich ĐŧĐžĐļĐŊа Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ С Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", "backup_onboarding_parts_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Са ŅŅ‚Ņ€Đ°Ņ‚ĐĩĐŗŅ–Ņ”ŅŽ 3-2-1 вĐēĐģŅŽŅ‡Đ°Ņ”:", "backup_onboarding_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—", @@ -63,57 +70,59 @@ "cleared_jobs": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊŅ– СавдаĐŊĐŊŅ Đ´ĐģŅ: {job}", "config_set_by_file": "НаĐģĐ°ŅˆŅ‚ĐžĐ˛Đ°ĐŊĐž Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐēĐžĐŊŅ„Ņ–Đŗ-Ņ„Đ°ĐšĐģ҃", "confirm_delete_library": "Ви Đ´Ņ–ĐšŅĐŊĐž йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ \"{library}\"?", - "confirm_delete_library_assets": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃? ĐĻĐĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} other {all # ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}} С Immich . ФаКĐģи СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ ĐŊа Đ´Đ¸ŅĐē҃.", + "confirm_delete_library_assets": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃? ĐĻĐĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С Immich. ФаКĐģи СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ ĐŊа Đ´Đ¸ŅĐē҃.", "confirm_email_below": "ДĐģŅ ĐŋŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´ĐļĐĩĐŊĐŊŅ ввĐĩĐ´Ņ–Ņ‚ŅŒ \"{email}\" ĐŊиĐļ҇Đĩ", "confirm_reprocess_all_faces": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž виСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ? ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩĐ´Đĩ Đ´Đž видаĐģĐĩĐŊĐŊŅ Ņ–ĐŧĐĩĐŊ С ŅƒŅŅ–Ņ… ОйĐģĐ¸Ņ‡.", "confirm_user_password_reset": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user}?", "confirm_user_pin_code_reset": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд {user}?", - "copy_config_to_clipboard_description": "ĐĄĐēĐžĐŋŅ–ŅŽĐšŅ‚Đĩ ĐŋĐžŅ‚ĐžŅ‡ĐŊ҃ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи ŅĐē Ой'Ņ”ĐēŅ‚ JSON ҃ ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", + "copy_config_to_clipboard_description": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊ҃ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи ŅĐē Ой'Ņ”ĐēŅ‚ JSON ҃ ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", "create_job": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ", "cron_expression": "Cron Đ˛Đ¸Ņ€Đ°Đˇ", - "cron_expression_description": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Ņ–ĐŊŅ‚ĐĩŅ€Đ˛Đ°Đģ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ, виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‡Đ¸ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ cron. ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛ĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž ĐŊаĐŋŅ€. Crontab Guru", + "cron_expression_description": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ Ņ–ĐŊŅ‚ĐĩŅ€Đ˛Đ°Đģ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– cron. Đ”ĐžĐ´Đ°Ņ‚ĐēОва Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ: Crontab Guru", "cron_expression_presets": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ cron Đ˛Đ¸Ņ€Đ°ĐˇŅ–Đ˛", "disable_login": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛Ņ…Ņ–Đ´", - "duplicate_detection_job_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ ĐŊа Ņ€ĐĩŅŅƒŅ€ŅĐ°Ņ… Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ŅŅ…ĐžĐļĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” Ņ–ĐŊŅ‚ĐĩĐģĐĩĐēŅ‚ŅƒĐ°ĐģҌĐŊиК ĐŋĐžŅˆŅƒĐē", + "duplicate_detection_job_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ŅŅ…ĐžĐļĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” Ņ–ĐŊŅ‚ĐĩĐģĐĩĐēŅ‚ŅƒĐ°ĐģҌĐŊиК ĐŋĐžŅˆŅƒĐē", "exclusion_pattern_description": "ШайĐģĐžĐŊи виĐēĐģŅŽŅ‡ĐĩĐŊҌ дОСвОĐģŅŅŽŅ‚ŅŒ Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŋаĐŋĐēи ĐŋŅ–Đ´ Ņ‡Đ°Ņ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи. ĐĻĐĩ ĐēĐžŅ€Đ¸ŅĐŊĐž, ŅĐēŅ‰Đž ҃ Đ˛Đ°Ņ Ņ” ĐŋаĐŋĐēи, ŅĐēŅ– ĐŧŅ–ŅŅ‚ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģи, ŅĐēŅ– ви ĐŊĐĩ Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ņ–ĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸, ĐŊаĐŋŅ€Đ¸ĐēĐģад, RAW-Ņ„Đ°ĐšĐģи.", "export_config_as_json_description": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊ҃ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– JSON", "external_libraries_page_description": "ĐĄŅ‚ĐžŅ€Ņ–ĐŊĐēа СОвĐŊŅ–ŅˆĐŊŅŒĐžŅ— ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "face_detection": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ", - "face_detection_description": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģĐ°Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. ДĐģŅ Đ˛Ņ–Đ´ĐĩĐž ĐžĐąŅ€ĐžĐąĐģŅŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ ĐĩҁĐēŅ–Đˇ. \"ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸\" ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐžĐąŅ€ĐžĐąĐģŅŅ” Đ˛ŅŅ– Ņ„Đ°ĐšĐģи. \"ĐĄĐēиĐŊŅƒŅ‚Đ¸\" Đ´ĐžĐ´Đ°Ņ‚ĐēОвО ĐžŅ‡Đ¸Ņ‰Đ°Ņ” Đ˛ŅŅ– ĐŋĐžŅ‚ĐžŅ‡ĐŊŅ– даĐŊŅ– ĐŋŅ€Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ. \"Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–\" ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ Ņ„Đ°ĐšĐģи, ŅĐēŅ– ҉Đĩ ĐŊĐĩ ĐąŅƒĐģи ĐžĐąŅ€ĐžĐąĐģĐĩĐŊŅ–. Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ, ĐŗŅ€ŅƒĐŋŅƒŅŽŅ‡Đ¸ Ņ—Ņ… ҃ вĐļĐĩ ҖҁĐŊŅƒŅŽŅ‡Đ¸Ņ… айО ĐŊĐžĐ˛Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš.", + "face_detection_description": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŅ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. ДĐģŅ Đ˛Ņ–Đ´ĐĩĐž ĐžĐąŅ€ĐžĐąĐģŅŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ°. \\\"ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸\\\" ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐžĐąŅ€ĐžĐąĐģŅŅ” Đ˛ŅŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ. \\\"ĐĄĐēиĐŊŅƒŅ‚Đ¸\\\" Đ´ĐžĐ´Đ°Ņ‚ĐēОвО ĐžŅ‡Đ¸Ņ‰Đ°Ņ” Đ˛ŅŅ– ĐŋĐžŅ‚ĐžŅ‡ĐŊŅ– даĐŊŅ– ĐŋŅ€Đž ОйĐģĐ¸Ņ‡Ņ‡Ņ. \\\"Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–\\\" ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ, ŅĐēŅ– ҉Đĩ ĐŊĐĩ ĐąŅƒĐģи ĐžĐąŅ€ĐžĐąĐģĐĩĐŊŅ–. Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ, ĐŗŅ€ŅƒĐŋŅƒŅŽŅ‡Đ¸ Ņ—Ņ… ҃ вĐļĐĩ ҖҁĐŊŅƒŅŽŅ‡Đ¸Ņ… айО ĐŊĐžĐ˛Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš.", "facial_recognition_job_description": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡ ҃ ĐģŅŽĐ´ĐĩĐš. ĐĻĐĩĐš ĐēŅ€ĐžĐē виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡. \"ĐĄĐēиĐŊŅƒŅ‚Đ¸\" ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐž ĐēĐģĐ°ŅŅ‚ĐĩŅ€Đ¸ĐˇŅƒŅ” Đ˛ŅŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ. \"Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–\" ŅŅ‚Đ°Đ˛Đ¸Ņ‚ŅŒ ҃ ҇ĐĩŅ€ĐŗŅƒ ОйĐģĐ¸Ņ‡Ņ‡Ņ, ŅĐēиĐŧ ҉Đĩ ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž ĐģŅŽĐ´Đ¸ĐŊ҃.", "failed_job_command": "КоĐŧаĐŊда {command} ĐŊĐĩ виĐēĐžĐŊаĐģĐ°ŅŅ Đ´ĐģŅ СавдаĐŊĐŊŅ: {job}", - "force_delete_user_warning": "ĐŸĐžĐŸĐ•Đ Đ•Đ”Đ–Đ•ĐĐĐ¯: ĐĻĐĩ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋŅ€Đ¸ĐˇĐ˛ĐĩĐ´Đĩ Đ´Đž видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ– Đ˛ŅŅ–Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸, Ņ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸.", + "force_delete_user_warning": "ĐŸĐžĐŸĐ•Đ Đ•Đ”Đ–Đ•ĐĐĐ¯: ĐĻĐĩ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋŅ€Đ¸ĐˇĐ˛ĐĩĐ´Đĩ Đ´Đž видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ– Đ˛ŅŅ–Ņ… ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģŅ–Đ˛. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸, Ņ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸.", "image_format": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚", - "image_format_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ WebP Đ˛Đ¸Ņ€ĐžĐąĐģŅŅ” ĐŧĐĩĐŊŅŒŅˆŅ– Ņ„Đ°ĐšĐģŅ–Đ˛, ĐŊŅ–Đļ JPEG, аĐģĐĩ ĐšĐžĐŗĐž ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐ°ĐŗĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ.", + "image_format_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ WebP Đ˛Đ¸Ņ€ĐžĐąĐģŅŅ” ĐŧĐĩĐŊŅˆŅ– Ņ„Đ°ĐšĐģи, ĐŊŅ–Đļ JPEG, аĐģĐĩ ĐšĐžĐŗĐž ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐ°ĐŗĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ.", "image_fullsize_description": "ПовĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, ŅĐēŅ– виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐˇĐąŅ–ĐģҌ҈ĐĩĐŊĐŊŅ", "image_fullsize_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "image_fullsize_enabled_description": "ГĐĩĐŊĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋОвĐŊĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ Đ´ĐģŅ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–Đ˛, ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐ¸Ņ… Đ´ĐģŅ вĐĩĐąŅƒ. Đ¯ĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž \"ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋŅ€ĐĩĐ˛â€™ŅŽ\", Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋŅ€ĐĩĐ˛â€™ŅŽ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐąĐĩС ĐēĐžĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ–Ņ—. НĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа вĐĩĐą-Đ´Ņ€ŅƒĐļĐŊŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸, Ņ‚Đ°ĐēŅ– ŅĐē JPEG.", + "image_fullsize_enabled_description": "ГĐĩĐŊĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋОвĐŊĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ Đ´ĐģŅ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–Đ˛, ĐŊĐĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐ¸Ņ… Đ´ĐģŅ вĐĩĐąŅƒ. Đ¯ĐēŅ‰Đž ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž \"ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ\", Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒŅŅ ĐąĐĩС ĐēĐžĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ–Ņ—. НĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа вĐĩĐą-Đ´Ņ€ŅƒĐļĐŊŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸, Ņ‚Đ°ĐēŅ– ŅĐē JPEG.", "image_fullsize_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ 1 Đ´Đž 100. ЧиĐŧ Đ˛Đ¸Ņ‰Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ, Ņ‚Đ¸Đŧ ĐēŅ€Đ°Ņ‰Đĩ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ĐąŅ–ĐģҌ҈Đĩ Ņ€ĐžĐˇĐŧŅ–Ņ€ Ņ„Đ°ĐšĐģ҃.", "image_fullsize_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋОвĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "image_prefer_embedded_preview": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋŅ€ĐĩĐ˛â€™ŅŽ", - "image_prefer_embedded_preview_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋŅ€ĐĩĐ˛â€™ŅŽ в RAW-Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅŅ… ŅĐē Đ˛Ņ…Ņ–Đ´ĐŊŅ– даĐŊŅ– Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ŅĐēŅ‰Đž вОĐŊи Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–. ĐĻĐĩ ĐŧĐžĐļĐĩ СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– ĐēĐžĐģŅŒĐžŅ€Đ¸ Đ´ĐģŅ Đ´ĐĩŅĐēĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, аĐģĐĩ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€ĐĩĐ˛â€™ŅŽ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēаĐŧĐĩŅ€Đ¸ Ņ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ°Ņ€Ņ‚ĐĩŅ„Đ°ĐēŅ‚Ņ–Đ˛ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ.", - "image_prefer_wide_gamut": "Đ’Ņ–Đ´Đ´Đ°ŅŽŅ‚ŅŒ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ ŅˆĐ¸Ņ€ĐžĐēŅ–Đš ĐŗĐ°ĐŧŅ–", + "image_prefer_embedded_preview": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐžĐŧ҃ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ", + "image_prefer_embedded_preview_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ¸ в RAW-Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅŅ… ŅĐē Đ˛Ņ…Ņ–Đ´ĐŊŅ– даĐŊŅ– Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ŅĐēŅ‰Đž вОĐŊи Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–. ĐĻĐĩ ĐŧĐžĐļĐĩ СайĐĩСĐŋĐĩŅ‡Đ¸Ņ‚Đ¸ Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– ĐēĐžĐģŅŒĐžŅ€Đ¸ Đ´ĐģŅ Đ´ĐĩŅĐēĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, аĐģĐĩ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēаĐŧĐĩŅ€Đ¸ Ņ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ°Ņ€Ņ‚ĐĩŅ„Đ°ĐēŅ‚Ņ–Đ˛ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ.", + "image_prefer_wide_gamut": "Đ’Ņ–Đ´Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ ŅˆĐ¸Ņ€ĐžĐēŅ–Đš ĐŗĐ°ĐŧŅ–", "image_prefer_wide_gamut_setting_description": "ДĐģŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Đ´Đ¸ŅĐŋĐģĐĩĐš P3. ĐĻĐĩ ĐēŅ€Đ°Ņ‰Đĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ” ŅŅĐēŅ€Đ°Đ˛Ņ–ŅŅ‚ŅŒ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ С ŅˆĐ¸Ņ€ĐžĐēиĐŧ ĐēĐžĐģŅ–Ņ€ĐŊиĐŧ ĐŋŅ€ĐžŅŅ‚ĐžŅ€ĐžĐŧ, аĐģĐĩ ĐŊа ŅŅ‚Đ°Ņ€Đ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ… ĐˇŅ– ŅŅ‚Đ°Ņ€ĐžŅŽ вĐĩŅ€ŅŅ–Ņ”ŅŽ ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ° ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļŅƒŅ‚ŅŒ Đ˛Đ¸ĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ–ĐŊаĐē҈Đĩ. sRGB-ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ СйĐĩŅ€Ņ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– sRGB, Ņ‰ĐžĐą ҃ĐŊиĐēĐŊŅƒŅ‚Đ¸ ĐˇŅŅƒĐ˛Ņƒ ĐēĐžĐģŅŒĐžŅ€Ņ–Đ˛.", - "image_preview_description": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ С видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, ŅĐēĐĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– ОдĐŊĐžĐŗĐž Ой'Ņ”ĐēŅ‚Đ° Ņ‚Đ° Đ´ĐģŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", - "image_preview_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€ĐĩĐ˛â€™ŅŽ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи. Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ ĐŊĐ¸ĐˇŅŒĐēĐžĐŗĐž СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ вĐŋĐģиĐŊŅƒŅ‚Đ¸ ĐŊа ŅĐēŅ–ŅŅ‚ŅŒ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ.", - "image_preview_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€ĐĩĐ˛â€™ŅŽ", + "image_preview_description": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ€ĐžĐˇĐŧŅ–Ņ€Ņƒ ĐąĐĩС ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…, ŅĐēĐĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– ĐžĐēŅ€ĐĩĐŧĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ‚Đ° Đ´ĐģŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", + "image_preview_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃. ĐĐ¸ĐˇŅŒĐēĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ вĐŋĐģиĐŊŅƒŅ‚Đ¸ ĐŊа ŅĐēŅ–ŅŅ‚ŅŒ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ.", + "image_preview_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ", + "image_progressive": "ĐŸŅ€ĐžĐŗŅ€ĐĩŅĐ¸Đ˛ĐŊиК", + "image_progressive_description": "ĐšĐžĐ´ŅƒĐšŅ‚Đĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ JPEG ĐŋĐžŅŅ‚ŅƒĐŋОвО Đ´ĐģŅ ĐŋĐžŅŅ‚ŅƒĐŋĐžĐ˛ĐžĐŗĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ. ĐĻĐĩ ĐŊĐĩ вĐŋĐģĐ¸Đ˛Đ°Ņ” ĐŊа ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ WebP.", "image_quality": "Đ¯ĐēŅ–ŅŅ‚ŅŒ", - "image_resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–ŅŅ‚ŅŒ", - "image_resolution_description": "Đ’Đ¸Ņ‰Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–ŅŅ‚ŅŒ ĐŧĐžĐļĐĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš, аĐģĐĩ СаКĐŧĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, ĐŧĐ°Ņ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ€ĐžĐˇĐŧŅ–Ņ€Đ¸ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи.", + "image_resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", + "image_resolution_description": "Đ’Đ¸Ņ‰Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ ĐŧĐžĐļĐĩ СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ Đ´ĐĩŅ‚Đ°ĐģĐĩĐš, аĐģĐĩ СаКĐŧĐ°Ņ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ Đ´ĐģŅ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, ĐŧĐ°Ņ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ€ĐžĐˇĐŧŅ–Ņ€Đ¸ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "image_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "image_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŽ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ ĐˇĐŗĐĩĐŊĐĩŅ€ĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", "image_thumbnail_description": "МаĐģĐĩĐŊҌĐēа ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ° Ņ–Đˇ видаĐģĐĩĐŊиĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊиĐŧи, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐŗŅ€ŅƒĐŋ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš, ĐŊаĐŋŅ€Đ¸ĐēĐģад, ĐŊа ĐžŅĐŊОвĐŊŅ–Đš ĐģŅ–ĐŊŅ–Ņ— Ņ‡Đ°ŅŅƒ", - "image_thumbnail_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи.", + "image_thumbnail_quality_description": "Đ¯ĐēŅ–ŅŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ 1 Đ´Đž 100. Đ’Đ¸Ņ‰Đ° ĐžŅ†Ņ–ĐŊĐēа ОСĐŊĐ°Ņ‡Đ°Ņ” ĐēŅ€Đ°Ņ‰Ņƒ ŅĐēŅ–ŅŅ‚ŅŒ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи Ņ‚Đ° ĐŧĐžĐļĐĩ СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ ŅˆĐ˛Đ¸Đ´ĐēŅ–ŅŅ‚ŅŒ Ņ€ĐžĐąĐžŅ‚Đ¸ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "image_thumbnail_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", - "import_config_from_json_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи, СаваĐŊŅ‚Đ°ĐļĐ¸Đ˛ŅˆĐ¸ Ņ„Đ°ĐšĐģ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ— JSON", + "import_config_from_json_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ ŅĐ¸ŅŅ‚ĐĩĐŧи, виваĐŊŅ‚Đ°ĐļĐ¸Đ˛ŅˆĐ¸ Ņ„Đ°ĐšĐģ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ— JSON", "job_concurrency": "{job} ОдĐŊĐžŅ‡Đ°ŅĐŊĐž", "job_created": "ЗавдаĐŊĐŊŅ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", "job_not_concurrency_safe": "ĐĻĐĩ СавдаĐŊĐŊŅ ĐŊĐĩ Ņ” ĐąĐĩСĐŋĐĩ҇ĐŊиĐŧ Đ´ĐģŅ ОдĐŊĐžŅ‡Đ°ŅĐŊĐžĐŗĐž виĐēĐžĐŊаĐŊĐŊŅ.", "job_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СавдаĐŊҌ", - "job_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģŅ–ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", + "job_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", "jobs_delayed": "{jobCount, plural, other {# Đ˛Ņ–Đ´ĐēĐģадĐĩĐŊĐž}}", "jobs_failed": "{jobCount, plural, other {# ĐŊĐĩ вдаĐģĐžŅŅ}}", - "jobs_over_time": "Đ ĐžĐąĐžŅ‚Đ° С ĐŋĐģиĐŊĐžĐŧ Ņ‡Đ°ŅŅƒ", + "jobs_over_time": "ЗавдаĐŊĐŊŅ Са Ņ‡Đ°ŅĐžĐŧ", "library_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа: {library}", "library_deleted": "Đ‘Ņ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ видаĐģĐĩĐŊĐž", "library_details": "ДĐĩŅ‚Đ°ĐģŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", @@ -125,9 +134,9 @@ "library_scanning_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€Ņ–ĐžĐ´Đ¸Ņ‡ĐŊĐĩ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "library_settings": "ЗовĐŊŅ–ŅˆĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа", "library_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē", - "library_tasks_description": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ СОвĐŊŅ–ŅˆĐŊŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ–/айО СĐŧŅ–ĐŊĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "library_tasks_description": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ СОвĐŊŅ–ŅˆĐŊŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи ĐŊа ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ–/айО СĐŧŅ–ĐŊĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "library_updated": "ОĐŊОвĐģĐĩĐŊа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēа", - "library_watching_enable_description": "ĐĄĐģŅ–Đ´ĐēŅƒĐšŅ‚Đĩ Са СĐŧŅ–ĐŊаĐŧи Ņ„Đ°ĐšĐģŅ–Đ˛ ҃ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēĐ°Ņ…", + "library_watching_enable_description": "Đ’Ņ–Đ´ŅŅ‚ĐĩĐļŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи Ņ„Đ°ĐšĐģŅ–Đ˛ ҃ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēĐ°Ņ…", "library_watching_settings": "ĐĄĐŋĐžŅŅ‚ĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ Са ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēĐžŅŽ [ЕКСПЕРИМЕНĐĸАЛĐŦНЕ]", "library_watching_settings_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ ҁĐŋĐžŅŅ‚ĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ Са СĐŧŅ–ĐŊĐĩĐŊиĐŧи Ņ„Đ°ĐšĐģаĐŧи", "logging_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ вĐĩĐ´ĐĩĐŊĐŊŅ ĐļŅƒŅ€ĐŊаĐģ҃", @@ -144,7 +153,7 @@ "machine_learning_clip_model_description": "ІĐŧ'Ņ ОдĐŊҖҔҗ С ĐŧОдĐĩĐģĐĩĐš CLIP, ŅĐēа ĐŋĐĩŅ€ĐĩŅ€Đ°Ņ…ĐžĐ˛Đ°ĐŊа Ņ‚ŅƒŅ‚. Đ—Đ°ŅƒĐ˛Đ°ĐļŅ‚Đĩ, Ņ‰Đž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŊĐžĐ˛Ņƒ СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ ÂĢĐ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐēÂģ Đ´ĐģŅ Đ˛ŅŅ–Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ ĐŋҖҁĐģŅ СĐŧŅ–ĐŊи ĐŧОдĐĩĐģŅ–.", "machine_learning_duplicate_detection": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", "machine_learning_duplicate_detection_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", - "machine_learning_duplicate_detection_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐž Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ‡ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ Đ˛ŅĐĩ ОдĐŊĐž ĐąŅƒĐ´ŅƒŅ‚ŅŒ видаĐģĐĩĐŊŅ– ҇ĐĩŅ€ĐĩС Đ´ŅƒĐąĐģŅŽĐ˛Đ°ĐŊĐŊŅ.", + "machine_learning_duplicate_detection_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ°ĐąŅĐžĐģŅŽŅ‚ĐŊĐž Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ‡ĐŊŅ– Ņ„Đ°ĐšĐģи Đ˛ŅĐĩ ОдĐŊĐž ĐąŅƒĐ´ŅƒŅ‚ŅŒ видаĐģĐĩĐŊŅ– ҇ĐĩŅ€ĐĩС Đ´ŅƒĐąĐģŅŽĐ˛Đ°ĐŊĐŊŅ.", "machine_learning_duplicate_detection_setting_description": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Đ˛ĐąŅƒĐ´ĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ CLIP Đ´ĐģŅ ĐŋĐžŅˆŅƒĐē҃ ĐšĐŧĐžĐ˛Ņ–Ņ€ĐŊĐ¸Ņ… Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛", "machine_learning_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐĩ ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", "machine_learning_enabled_description": "Đ¯ĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛ŅŅ– Ņ„ŅƒĐŊĐē҆Җҗ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ ĐąŅƒĐ´ŅƒŅ‚ŅŒ виĐŧĐēĐŊĐĩĐŊŅ– ĐŊĐĩСаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ ĐŊиĐļ҇Đĩ.", @@ -175,42 +184,53 @@ "machine_learning_ocr_model": "МодĐĩĐģҌ OCR", "machine_learning_ocr_model_description": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ĐŊŅ– ĐŧОдĐĩĐģŅ– Ņ‚ĐžŅ‡ĐŊŅ–ŅˆŅ– Са ĐŧĐžĐąŅ–ĐģҌĐŊŅ–, аĐģĐĩ ĐžĐąŅ€ĐžĐąĐģŅŅŽŅ‚ŅŒ даĐŊŅ– Đ´ĐžĐ˛ŅˆĐĩ Ņ‚Đ° виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅŽŅ‚ŅŒ ĐąŅ–ĐģҌ҈Đĩ ĐŋаĐŧ'ŅŅ‚Ņ–.", "machine_learning_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", - "machine_learning_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģŅ–ĐŊĐŊŅ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅĐŧи Ņ‚Đ° ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", + "machine_learning_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅĐŧи Ņ‚Đ° ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ", "machine_learning_smart_search": "Đ ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē", "machine_learning_smart_search_description": "ĐŸĐžŅˆŅƒĐē ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ҁĐĩĐŧаĐŊŅ‚Đ¸Ņ‡ĐŊĐ¸Ņ… Đ˛ĐąŅƒĐ´ĐžĐ˛ŅƒĐ˛Đ°ĐŊҌ CLIP", "machine_learning_smart_search_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐžĐˇŅƒĐŧĐŊиК ĐŋĐžŅˆŅƒĐē", "machine_learning_smart_search_enabled_description": "Đ¯ĐēŅ‰Đž Ņ†Ņ Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ виĐŧĐēĐŊĐĩĐŊа, ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Đ´ĐģŅ Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃.", - "machine_learning_url_description": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. Đ¯ĐēŅ‰Đž ĐŊадаĐŊĐž ĐąŅ–ĐģҌ҈Đĩ ОдĐŊĐžĐŗĐž URL, ҁĐĩŅ€Đ˛ĐĩŅ€Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžĐŋĐ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ĐŋĐž ҇ĐĩŅ€ĐˇŅ–, ĐŋĐžĐēи ОдиĐŊ С ĐŊĐ¸Ņ… ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž, ҃ ĐŋĐžŅ€ŅĐ´Đē҃ Đ˛Ņ–Đ´ ĐŋĐĩŅ€ŅˆĐžĐŗĐž Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐž. ĐĄĐĩŅ€Đ˛ĐĩŅ€Đ¸, ŅĐēŅ– ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ, ĐąŅƒĐ´ŅƒŅ‚ŅŒ Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đž Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ, ĐŋĐžĐēи ĐŊĐĩ С'ŅĐ˛ĐģŅŅ‚ŅŒŅŅ ĐžĐŊĐģаКĐŊ.", + "machine_learning_url_description": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ. Đ¯ĐēŅ‰Đž ĐŊадаĐŊĐž ĐąŅ–ĐģҌ҈Đĩ ОдĐŊĐžĐŗĐž URL, ҁĐĩŅ€Đ˛ĐĩŅ€Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžĐŋĐ¸Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ ĐŋĐž ҇ĐĩŅ€ĐˇŅ–, ĐŋĐžĐēи ОдиĐŊ С ĐŊĐ¸Ņ… ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž, ҃ ĐŋĐžŅ€ŅĐ´Đē҃ Đ˛Ņ–Đ´ ĐŋĐĩŅ€ŅˆĐžĐŗĐž Đ´Đž ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐž. ĐĄĐĩŅ€Đ˛ĐĩŅ€Đ¸, ŅĐēŅ– ĐŊĐĩ Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ, ĐąŅƒĐ´ŅƒŅ‚ŅŒ Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đž Ņ–ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ, ĐŋĐžĐēи ĐŊĐĩ ŅŅ‚Đ°ĐŊŅƒŅ‚ŅŒ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи.", + "maintenance_delete_backup": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ", + "maintenance_delete_backup_description": "ĐĻĐĩĐš Ņ„Đ°ĐšĐģ ĐąŅƒĐ´Đĩ ĐąĐĩСĐŋĐžĐ˛ĐžŅ€ĐžŅ‚ĐŊĐž видаĐģĐĩĐŊĐž.", + "maintenance_delete_error": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", + "maintenance_restore_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", + "maintenance_restore_backup_description": "Immich ĐąŅƒĐ´Đĩ ҁ҂ĐĩŅ€Ņ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžŅ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—. ПĐĩŅ€ĐĩĐ´ ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐĩĐŊĐŊŅĐŧ ĐąŅƒĐ´Đĩ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", + "maintenance_restore_backup_different_version": "ĐĻŅŽ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ ĐąŅƒĐģĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ–ĐŊŅˆĐžŅ— вĐĩҀҁҖҗ Immich!", + "maintenance_restore_backup_unknown_version": "НĐĩ вдаĐģĐžŅŅ виСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ вĐĩŅ€ŅŅ–ŅŽ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—.", + "maintenance_restore_database_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— йаСи даĐŊĐ¸Ņ…", + "maintenance_restore_database_backup_description": "Đ’Ņ–Đ´ĐēĐ°Ņ‚ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ŅŅ‚Đ°ĐŊ҃ йаСи даĐŊĐ¸Ņ… Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ Ņ„Đ°ĐšĐģ҃ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", "maintenance_settings": "ĐĸĐĩŅ…ĐŊҖ҇ĐŊĐĩ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", - "maintenance_settings_description": "ПĐĩŅ€ĐĩвĐĩĐ´Ņ–Ņ‚ŅŒ Immich в Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", - "maintenance_start": "РОСĐŋĐžŅ‡Đ°Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_settings_description": "ПĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐŊŅ Immich ҃ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", + "maintenance_start": "ПĐĩŅ€ĐĩŅ…Ņ–Đ´ ҃ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", "maintenance_start_error": "НĐĩ вдаĐģĐžŅŅ СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "maintenance_upload_backup": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— йаСи даĐŊĐ¸Ņ…", + "maintenance_upload_backup_error": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ, ҆Đĩ Ņ„Đ°ĐšĐģ .sql/.sql.gz?", "manage_concurrency": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", - "manage_concurrency_description": "ПĐĩŅ€ĐĩĐšĐ´Ņ–Ņ‚ŅŒ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ СавдаĐŊҌ, Ņ‰ĐžĐą ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ СавдаĐŊҌ", + "manage_concurrency_description": "ПĐĩŅ€ĐĩŅ…Ņ–Đ´ Đ´Đž ŅŅ‚ĐžŅ€Ņ–ĐŊĐēи СавдаĐŊҌ Đ´ĐģŅ ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŽ", "manage_log_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐļŅƒŅ€ĐŊаĐģ҃", "map_dark_style": "ĐĸĐĩĐŧĐŊиК ŅŅ‚Đ¸ĐģҌ", "map_enable_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ŅƒĐŊĐē҆Җҗ ĐŧаĐŋи", - "map_gps_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐ°Ņ€Ņ‚Đ¸ Ņ‚Đ° GPS", - "map_gps_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐēĐ°Ņ€Ņ‚Đ¸ Ņ‚Đ° GPS (ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊиК ĐŗĐĩĐžĐēОдиĐŊĐŗ)", - "map_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐēĐ°Ņ€Ņ‚Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅ–Đš ҁĐĩŅ€Đ˛Ņ–Ņ ĐŋĐģĐ¸Ņ‚ĐžĐē (tiles.immich.cloud)", + "map_gps_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи Ņ‚Đ° ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ—", + "map_gps_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧаĐŋи Ņ‚Đ° ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ— (ĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊиК ĐŗĐĩĐžĐēОдиĐŊĐŗ)", + "map_implications": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ĐŧаĐŋи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅ–Đš ҁĐĩŅ€Đ˛Ņ–Ņ ĐŋĐģĐ¸Ņ‚ĐžĐē (tiles.immich.cloud)", "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_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧаĐŋи", "map_style_description": "URL Đ´Đž Ņ‚ĐĩĐŧи ĐŧаĐŋи ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– style.json", - "memory_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐŋаĐŧ'ŅŅ‚Ņ–", - "memory_generate_job": "ПоĐēĐžĐģŅ–ĐŊĐŊŅ ĐŋаĐŧ'ŅŅ‚Ņ–", + "memory_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", + "memory_generate_job": "ГĐĩĐŊĐĩŅ€Đ°Ņ†Ņ–Ņ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", "metadata_extraction_job": "Đ’Đ¸Ņ‚ŅĐŗĐŊŅƒŅ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ–", - "metadata_extraction_job_description": "Đ’Đ¸Ņ‚ŅĐŗĐŊи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– С ĐēĐžĐļĐŊĐžĐŗĐž Ой'Ņ”ĐēŅ‚Đ°, Ņ‚Đ°Đē҃ ŅĐē GPS, ОйĐģĐ¸Ņ‡Ņ‡Ņ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", - "metadata_faces_import_setting": "ĐŖĐ˛Ņ–ĐŧĐēĐŊи Ņ–ĐŧĐŋĐžŅ€Ņ‚ ОйĐģĐ¸Ņ‡", - "metadata_faces_import_setting_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐš ОйĐģĐ¸Ņ‡Ņ‡Ņ С EXIF-даĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "metadata_extraction_job_description": "Đ’Đ¸Đ´ĐžĐąŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…: ĐŗĐĩОдаĐŊŅ–, Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ Ņ‚Đ° Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊа ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", + "metadata_faces_import_setting": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ–ĐŧĐŋĐžŅ€Ņ‚ ОйĐģĐ¸Ņ‡", + "metadata_faces_import_setting_description": "ІĐŧĐŋĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ С EXIF-даĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ Ņ‚Đ° sidecar-Ņ„Đ°ĐšĐģŅ–Đ˛", "metadata_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", - "metadata_settings_description": "КĐĩŅ€ŅƒĐš ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", + "metadata_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", "migration_job": "ĐœŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ", - "migration_job_description": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ Ņ‚Đ° ОйĐģĐ¸Ņ‡Ņ‡Ņ Đ´Đž ĐžĐŊОвĐģĐĩĐŊĐžŅ— ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ¸ ĐŋаĐŋĐžĐē", + "migration_job_description": "ПĐĩŅ€ĐĩĐŊĐĩҁĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ‚Đ° ОйĐģĐ¸Ņ‡ŅŒ Đ´Đž ĐžĐŊОвĐģĐĩĐŊĐžŅ— ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€Đ¸ ĐŋаĐŋĐžĐē", "nightly_tasks_cluster_faces_setting_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ ОйĐģĐ¸Ņ‡ ĐŊа Ņ‰ĐžĐšĐŊĐž Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐ¸Ņ… ОйĐģĐ¸Ņ‡Ņ‡ŅŅ…", "nightly_tasks_cluster_new_faces_setting": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ОйĐģĐ¸Ņ‡Ņ‡Ņ", "nightly_tasks_database_cleanup_setting": "ЗавдаĐŊĐŊŅ С ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", @@ -227,11 +247,11 @@ "nightly_tasks_sync_quota_usage_setting_description": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐ˛ĐžŅ‚Ņƒ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "no_paths_added": "ШĐģŅŅ…Đ¸ ĐŊĐĩ дОдаĐŊĐž", "no_pattern_added": "ШайĐģĐžĐŊ ĐŊĐĩ дОдаĐŊĐž", - "note_apply_storage_label_previous_assets": "ĐŸŅ€Đ¸ĐŧŅ–Ņ‚Đēа: ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛, СаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ", + "note_apply_storage_label_previous_assets": "ĐŸŅ€Đ¸ĐŧŅ–Ņ‚Đēа: ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸", "note_cannot_be_changed_later": "ПРИМІĐĸКА: ĐĻĐĩ ĐŊĐĩ ĐŧĐžĐļĐŊа СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ!", - "notification_email_from_address": "З Đ°Đ´Ņ€ĐĩŅĐ¸", - "notification_email_from_address_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛ĐŊиĐēа, ĐŊаĐŋŅ€Đ¸ĐēĐģад: \"Immich Photo Server \". ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃, С ŅĐēĐžŅ— ваĐŧ дОСвОĐģĐĩĐŊĐž ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ ĐģĐ¸ŅŅ‚Đ¸.", - "notification_email_host_description": "ĐĨĐžŅŅ‚ ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŗĐž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° (ĐŊаĐŋŅ€Đ¸ĐēĐģад, smtp.immich.app)", + "notification_email_from_address": "ĐĐ´Ņ€ĐĩŅĐ° ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‡Đ°", + "notification_email_from_address_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‡Đ°, ĐŊаĐŋŅ€Đ¸ĐēĐģад: \"Immich Photo Server \". ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃, С ŅĐēĐžŅ— ваĐŧ дОСвОĐģĐĩĐŊĐž ĐŊĐ°Đ´ŅĐ¸ĐģĐ°Ņ‚Đ¸ ĐģĐ¸ŅŅ‚Đ¸.", + "notification_email_host_description": "ĐĐ´Ņ€ĐĩŅĐ° ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŗĐž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° (ĐŊаĐŋŅ€Đ¸ĐēĐģад, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Đ†ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐēи ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", "notification_email_ignore_certificate_errors_description": "Đ†ĐŗĐŊĐžŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŧиĐģĐēи ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Ņ–Đ˛ TLS (ĐŊĐĩ Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”Ņ‚ŅŒŅŅ)", "notification_email_password_description": "ĐŸĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Đ°ŅƒŅ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— ĐŊа ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", @@ -242,17 +262,17 @@ "notification_email_setting_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐģŅ ĐŊĐ°Đ´ŅĐ¸ĐģаĐŊĐŊŅ email-ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", "notification_email_test_email": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚", "notification_email_test_email_failed": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Ņ‚ĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ˛Đ°ŅˆŅ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", - "notification_email_test_email_sent": "ĐĸĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚ ĐąŅƒĐ˛ Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛ĐģĐĩĐŊиК ĐŊа {email}. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ ҁĐēŅ€Đ¸ĐŊҌĐē҃ Đ˛Ņ…Ņ–Đ´ĐŊĐ¸Ņ….", + "notification_email_test_email_sent": "ĐĸĐĩŅŅ‚ĐžĐ˛Đ¸Đš ĐģĐ¸ŅŅ‚ ĐąŅƒĐģĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž ĐŊа {email}. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ŅĐ˛ĐžŅŽ ҁĐēŅ€Đ¸ĐŊҌĐē҃ Đ˛Ņ…Ņ–Đ´ĐŊĐ¸Ņ….", "notification_email_username_description": "ІĐŧ'Ņ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Đ´ĐģŅ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ— ĐŊа ĐŋĐžŅˆŅ‚ĐžĐ˛ĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", "notification_enable_email_notifications": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "notification_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", - "notification_settings_description": "ĐŖĐŋŅ€Đ°Đ˛ĐģŅ–ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ, вĐēĐģŅŽŅ‡ĐŊĐž Ņ–Đˇ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", + "notification_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ, вĐēĐģŅŽŅ‡ĐŊĐž Ņ–Đˇ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ", "oauth_auto_launch": "ĐĐ˛Ņ‚ĐžĐˇĐ°Đŋ҃ҁĐē", "oauth_auto_launch_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž СаĐŋ҃ҁĐēĐ°Ņ‚Đ¸ ĐŋŅ€ĐžŅ†Đĩҁ Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Ņ– ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ Đ˛Ņ…ĐžĐ´Ņƒ", "oauth_auto_register": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊа Ņ€ĐĩŅ”ŅŅ‚Ņ€Đ°Ņ†Ņ–Ņ", "oauth_auto_register_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Ņ€ĐĩŅ”ŅŅ‚Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Đ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ĐŋҖҁĐģŅ Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth", "oauth_button_text": "ĐĸĐĩĐēҁ҂ ĐēĐŊĐžĐŋĐēи", - "oauth_client_secret_description": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐŊĐž, ŅĐēŅ‰Đž ĐŋĐžŅŅ‚Đ°Ņ‡Đ°ĐģҌĐŊиĐē OAuth ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” PKCE (Proof Key for Code Exchange)", + "oauth_client_secret_description": "Обов'ŅĐˇĐēОвО Đ´ĐģŅ ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊĐžĐŗĐž ĐēĐģŅ–Ņ”ĐŊŅ‚Đ° айО ŅĐēŅ‰Đž PKCE (ĐēĐģŅŽŅ‡ ĐŋŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ОйĐŧŅ–ĐŊ҃ ĐēОдОĐŧ) ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŗĐž ĐēĐģŅ–Ņ”ĐŊŅ‚Đ°.", "oauth_enable_description": "ĐŖĐ˛Ņ–ĐšŅ‚Đ¸ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ OAuth", "oauth_mobile_redirect_uri": "URI ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ", "oauth_mobile_redirect_uri_override": "ПĐĩŅ€ĐĩвиСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ URI ĐŧĐžĐąŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŊаĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ", @@ -262,7 +282,7 @@ "oauth_settings": "OAuth", "oauth_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth", "oauth_settings_more_details": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛ĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", - "oauth_storage_label_claim": "ĐĸĐĩĐŗ Đ´Đ¸Ņ€ĐĩĐēŅ‚ĐžŅ€Ņ–Ņ— ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", + "oauth_storage_label_claim": "ĐĸĐĩĐŗ ĐŋаĐŋĐēи ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", "oauth_storage_label_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ҆ҖҔҗ виĐŧĐžĐŗĐ¸.", "oauth_storage_quota_claim": "Đ—Đ°ŅĐ˛Đēа ĐŊа ĐēĐ˛ĐžŅ‚Ņƒ ĐŊа СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", "oauth_storage_quota_claim_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐ˛ĐžŅ‚Ņƒ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŊа СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ҆ҖҔҗ виĐŧĐžĐŗĐ¸.", @@ -285,11 +305,11 @@ "registration_description": "ĐžŅĐēŅ–ĐģҌĐēи ви ĐŋĐĩŅ€ŅˆĐ¸Đš ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ в ŅĐ¸ŅŅ‚ĐĩĐŧŅ–, ви ĐąŅƒĐ´ĐĩŅ‚Đĩ ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– АдĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ Ņ– Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°Ņ‚Đ¸ĐŧĐĩŅ‚Đĩ Са адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚Đ¸Đ˛ĐŊŅ– СавдаĐŊĐŊŅ, а Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ– ĐąŅƒĐ´ŅƒŅ‚ŅŒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊŅ– ваĐŧи.", "remove_failed_jobs": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ ĐŊĐĩвдаĐģŅ– СавдаĐŊĐŊŅ", "require_password_change_on_login": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ СĐŧŅ–ĐŊи ĐŋĐ°Ņ€ĐžĐģŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐŋŅ€Đ¸ ĐŋĐĩŅ€ŅˆĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–", - "reset_settings_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž ĐˇĐ°Đ˛ĐžĐ´ŅŅŒĐēĐ¸Ņ… СĐŊĐ°Ņ‡ĐĩĐŊҌ", + "reset_settings_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž ĐŋĐžŅ‡Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… СĐŊĐ°Ņ‡ĐĩĐŊҌ", "reset_settings_to_recent_saved": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´Đž ĐŊĐĩдавĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊĐ¸Ņ… ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", "scanning_library": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "search_jobs": "ĐŸĐžŅˆŅƒĐē СавдаĐŊҌâ€Ļ", - "send_welcome_email": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ ĐģĐ¸ŅŅ‚ С Đ˛Ņ–Ņ‚Đ°ĐŊĐŊŅĐŧ", + "send_welcome_email": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊиК ĐģĐ¸ŅŅ‚", "server_external_domain_settings": "ЗовĐŊŅ–ŅˆĐŊŅ–Đš Đ´ĐžĐŧĐĩĐŊ", "server_external_domain_settings_description": "ДоĐŧĐĩĐŊ Đ´ĐģŅ ĐŋŅƒĐąĐģҖ҇ĐŊĐ¸Ņ… ĐˇĐ°ĐŗĐ°ĐģҌĐŊĐžĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ http(s)://", "server_public_users": "ĐŸŅƒĐąĐģҖ҇ĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–", @@ -303,41 +323,41 @@ "sidecar_job": "МĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– С sidecar-Ņ„Đ°ĐšĐģŅ–Đ˛", "sidecar_job_description": "ĐŸĐžŅˆŅƒĐē айО ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ ŅĐ°ĐšĐ´ĐēĐ°Ņ€-ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ… С Ņ„Đ°ĐšĐģĐžĐ˛ĐžŅ— ŅĐ¸ŅŅ‚ĐĩĐŧи", "slideshow_duration_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҁĐĩĐē҃ĐŊĐ´ Đ´ĐģŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "smart_search_job_description": "ЗаĐŋ҃ҁĐē ĐŧĐ°ŅˆĐ¸ĐŊĐŊĐžĐŗĐž ĐŊĐ°Đ˛Ņ‡Đ°ĐŊĐŊŅ Đ´ĐģŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ Đ´ĐģŅ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃", - "storage_template_date_time_description": "ПозĐŊĐ°Ņ‡Đēа Ņ‡Đ°ŅŅƒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅƒ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Đ´Đ°Ņ‚Ņƒ Đš Ņ‡Đ°Ņ", + "smart_search_job_description": "РОСĐŋŅ–ĐˇĐŊĐ°Ņ” вĐŧҖҁ҂ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´ĐģŅ Ņ€ĐžĐˇŅƒĐŧĐŊĐžĐŗĐž ĐŋĐžŅˆŅƒĐē҃", + "storage_template_date_time_description": "Đ”Đ°Ņ‚ĐžŅŽ Ņ‚Đ° Ņ‡Đ°ŅĐžĐŧ Ņ” ĐŋОСĐŊĐ°Ņ‡Đēа Ņ‡Đ°ŅŅƒ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃", "storage_template_date_time_sample": "Đ§Đ°Ņ Đ˛Đ¸ĐąŅ–Ņ€Đēи {date}", "storage_template_enable_description": "Đ’Đ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŧĐĩŅ…Đ°ĐŊŅ–ĐˇĐŧ ŅˆĐ°ĐąĐģĐžĐŊŅ–Đ˛ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", "storage_template_hash_verification_enabled": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ Ņ…Đĩ҈҃", "storage_template_hash_verification_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ Ņ…ĐĩŅˆĐ°. НĐĩ виĐŧиĐēĐ°ĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ви ĐŊĐĩ вĐŋĐĩвĐŊĐĩĐŊŅ– в ĐŊĐ°ŅĐģŅ–Đ´ĐēĐ°Ņ…", "storage_template_migration": "ĐœŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ ŅˆĐ°ĐąĐģĐžĐŊŅ–Đ˛ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "storage_template_migration_description": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊиК {template} Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", - "storage_template_migration_info": "ШайĐģĐžĐŊ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžĐŊвĐĩŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧĐĩ Đ˛ŅŅ– Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ ҃ ĐŊиĐļĐŊŅ–Đš Ņ€ĐĩĐŗŅ–ŅŅ‚Ņ€. ЗĐŧŅ–ĐŊи ŅˆĐ°ĐąĐģĐžĐŊ҃ ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´Đž ĐŊĐžĐ˛Đ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛. ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛, СаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ {job}.", + "storage_template_migration_description": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊиК {template} Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "storage_template_migration_info": "ШайĐģĐžĐŊ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžĐŊвĐĩŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧĐĩ Đ˛ŅŅ– Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ ҃ ĐŊиĐļĐŊŅ–Đš Ņ€ĐĩĐŗŅ–ŅŅ‚Ņ€. ЗĐŧŅ–ĐŊи ŅˆĐ°ĐąĐģĐžĐŊ҃ ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´Đž ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛. ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛, СаĐŋŅƒŅŅ‚Ņ–Ņ‚ŅŒ {job}.", "storage_template_migration_job": "ЗавдаĐŊĐŊŅ ĐŧŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ— ŅˆĐ°ĐąĐģĐžĐŊ҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", "storage_template_more_details": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐĩŅ‚Đ°ĐģҌĐŊŅ–ŅˆĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ, СвĐĩŅ€Ņ‚Đ°ĐšŅ‚ĐĩҁҌ Đ´Đž ШайĐģĐžĐŊ҃ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ Ņ‚Đ° ĐšĐžĐŗĐž ĐŊĐ°ŅĐģŅ–Đ´ĐēŅ–Đ˛", "storage_template_onboarding_description_v2": "Đ¯ĐēŅ‰Đž Ņ†ŅŽ Ņ„ŅƒĐŊĐēŅ†Ņ–ŅŽ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž, Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž вĐŋĐžŅ€ŅĐ´ĐēĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Са ŅˆĐ°ĐąĐģĐžĐŊĐžĐŧ, виСĐŊĐ°Ņ‡ĐĩĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐŧ. ДоĐēĐģадĐŊŅ–ŅˆĐĩ Đ´Đ¸Đ˛Ņ–Ņ‚ŅŒŅŅ в Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ—.", "storage_template_path_length": "ĐŸŅ€Đ¸ĐąĐģиСĐŊа ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊа дОвĐļиĐŊа ҈ĐģŅŅ…Ņƒ: {length, number}/{limit, number}", "storage_template_settings": "ШайĐģĐžĐŊ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "storage_template_settings_description": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ Ņ‚ĐĩĐē Ņ‚Đ° Ņ–ĐŧĐĩĐŊĐĩĐŧ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", + "storage_template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅŅ‚Ņ€ŅƒĐēŅ‚ŅƒŅ€ĐžŅŽ ĐŋаĐŋĐžĐē Ņ‚Đ° Ņ–ĐŧĐĩĐŊаĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "storage_template_user_label": "{label} - ҆Đĩ ĐŧŅ–Ņ‚Đēа СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "system_settings": "ĐĄĐ¸ŅŅ‚ĐĩĐŧĐŊŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "tag_cleanup_job": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Ņ‚ĐĩĐŗ", - "template_email_available_tags": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊŅ– СĐŧŅ–ĐŊĐŊŅ– ҃ Đ˛Đ°ŅˆĐžĐŧ҃ ŅˆĐ°ĐąĐģĐžĐŊŅ–: {tags}", - "template_email_if_empty": "Đ¯ĐēŅ‰Đž ŅˆĐ°ĐąĐģĐžĐŊ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đš, ĐąŅƒĐ´Đĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž ŅŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐŊиК ĐĩĐģ. ĐģĐ¸ŅŅ‚.", + "tag_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ Ņ‚ĐĩĐŗŅ–Đ˛", + "template_email_available_tags": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊŅ– СĐŧŅ–ĐŊĐŊŅ– ҃ ŅĐ˛ĐžŅ”Đŧ҃ ŅˆĐ°ĐąĐģĐžĐŊŅ–: {tags}", + "template_email_if_empty": "Đ¯ĐēŅ‰Đž ŅˆĐ°ĐąĐģĐžĐŊ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đš, ĐąŅƒĐ´Đĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž ŅŅ‚Đ°ĐŊĐ´Đ°Ņ€Ņ‚ĐŊиК ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊиК ĐģĐ¸ŅŅ‚.", "template_email_invite_album": "ШайĐģĐžĐŊ СаĐŋŅ€ĐžŅˆĐĩĐŊĐŊŅ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "template_email_preview": "ĐŸŅ€ĐĩĐ˛â€™ŅŽ", - "template_email_settings": "ШайĐģĐžĐŊи ĐĩĐģ. ĐģĐ¸ŅŅ‚Ņ–Đ˛", + "template_email_preview": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´", + "template_email_settings": "ШайĐģĐžĐŊи ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐ¸Ņ… ĐģĐ¸ŅŅ‚Ņ–Đ˛", "template_email_update_album": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ аĐģŅŒĐąĐžĐŧ҃", - "template_email_welcome": "ШайĐģĐžĐŊ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊĐžĐŗĐž ĐĩĐģ. ĐģĐ¸ŅŅ‚Đ°", - "template_settings": "ШайĐģĐžĐŊи ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", - "template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊаĐŧи Đ´ĐģŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", + "template_email_welcome": "ШайĐģĐžĐŊ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊĐžĐŗĐž ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžĐŗĐž ĐģĐ¸ŅŅ‚Đ°", + "template_settings": "ШайĐģĐžĐŊи ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", + "template_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊаĐŧи Đ´ĐģŅ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊҌ", "theme_custom_css_settings": "ВĐģĐ°ŅĐŊиК CSS", "theme_custom_css_settings_description": "ĐšĐ°ŅĐēадĐŊŅ– Ņ‚Đ°ĐąĐģĐ¸Ņ†Ņ– ŅŅ‚Đ¸ĐģŅ–Đ˛ дОСвОĐģŅŅŽŅ‚ŅŒ ĐŊĐ°ŅŅ‚Ņ€ĐžŅŽĐ˛Đ°Ņ‚Đ¸ диСаКĐŊ Immich.", "theme_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚ĐĩĐŧи", "theme_settings_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐĩŅ€ŅĐžĐŊаĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅƒ Immich", "thumbnail_generation_job": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", - "thumbnail_generation_job_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ вĐĩĐģиĐēŅ–, ĐŧаĐģŅ– Ņ‚Đ° Ņ€ĐžĐˇĐŧĐ¸Ņ‚Ņ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅƒ, а Ņ‚Đ°ĐēĐžĐļ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžŅ— ĐžŅĐžĐąĐ¸", + "thumbnail_generation_job_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ вĐĩĐģиĐēŅ–, ĐŧаĐģŅ– Ņ‚Đ° Ņ€ĐžĐˇĐŧĐ¸Ņ‚Ņ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, а Ņ‚Đ°ĐēĐžĐļ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ´ĐģŅ ĐēĐžĐļĐŊĐžŅ— ĐžŅĐžĐąĐ¸", "transcoding_acceleration_api": "API ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ", - "transcoding_acceleration_api_description": "API, ŅĐēа ĐąŅƒĐ´Đĩ Đ˛ĐˇĐ°Ņ”ĐŧĐžĐ´Ņ–ŅŅ‚Đ¸ С Đ˛Đ°ŅˆĐ¸Đŧ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ”Đŧ Đ´ĐģŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐĻŅ ĐŊĐ°ŅŅ‚Ņ€ĐžĐšĐēа ĐŋŅ€Đ°Ņ†ŅŽŅ” ҃ \"ĐŊаКĐēŅ€Đ°Ņ‰Đ¸Ņ… ҃ĐŧĐžĐ˛Đ°Ņ…\" Ņ–, в Ņ€Đ°ĐˇŅ– ĐŊĐĩĐ˛Đ´Đ°Ņ‡Ņ–, ĐŋĐĩŅ€ĐĩКдĐĩ ĐŊа ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа VP9 ĐŧĐžĐļĐĩ айО ĐŊĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸, СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Đ˛Đ°ŅˆĐžĐŗĐž ОйĐģадĐŊаĐŊĐŊŅ.", + "transcoding_acceleration_api_description": "API, ŅĐēа ĐąŅƒĐ´Đĩ Đ˛ĐˇĐ°Ņ”ĐŧĐžĐ´Ņ–ŅŅ‚Đ¸ С Đ˛Đ°ŅˆĐ¸Đŧ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ”Đŧ Đ´ĐģŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐĻĐĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ°Ņ†ŅŽŅ” ҃ \"ĐŊаКĐēŅ€Đ°Ņ‰Đ¸Ņ… ҃ĐŧĐžĐ˛Đ°Ņ…\" Ņ–, в Ņ€Đ°ĐˇŅ– ĐŊĐĩĐ˛Đ´Đ°Ņ‡Ņ–, ĐŋĐĩŅ€ĐĩКдĐĩ ĐŊа ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēа VP9 ĐŧĐžĐļĐĩ айО ĐŊĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸, СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Đ˛Đ°ŅˆĐžĐŗĐž ОйĐģадĐŊаĐŊĐŊŅ.", "transcoding_acceleration_nvenc": "NVENC (виĐŧĐ°ĐŗĐ°Ņ” ĐŗŅ€Đ°Ņ„Ņ–Ņ‡ĐŊĐžĐŗĐž ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ° NVIDIA)", "transcoding_acceleration_qsv": "ШвидĐēа ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ (ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€ Intel 7-ĐŗĐž ĐŋĐžĐēĐžĐģŅ–ĐŊĐŊŅ айО ĐŊĐžĐ˛Ņ–ŅˆĐžŅ— вĐĩҀҁҖҗ)", "transcoding_acceleration_rkmpp": "RKMPP (҂ҖĐģҌĐēи ĐŊа SOC Rockchip)", @@ -348,18 +368,18 @@ "transcoding_accepted_containers_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ, ŅĐēŅ– Ņ„ĐžŅ€ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐŊŅ‚ĐĩĐšĐŊĐĩŅ€Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐŋĐĩŅ€ĐĩŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ в MP4. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", "transcoding_accepted_video_codecs": "ĐŸŅ€Đ¸ĐšĐŊŅŅ‚Ņ– Đ˛Ņ–Đ´ĐĩĐžĐēОдĐĩĐēи", "transcoding_accepted_video_codecs_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Ņ–Đ´ĐĩĐžĐēОдĐĩĐēи, ŅĐēŅ– ĐŊĐĩ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ ĐŋĐĩвĐŊĐ¸Ņ… ĐŋĐžĐģŅ–Ņ‚Đ¸Đē Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", - "transcoding_advanced_options_description": "ОĐŋ҆Җҗ, ŅĐēŅ– ĐąŅ–ĐģŅŒŅˆĐžŅŅ‚Ņ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŧŅ–ĐŊŅŽĐ˛Đ°Ņ‚Đ¸", + "transcoding_advanced_options_description": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸, ŅĐēŅ– ĐąŅ–ĐģŅŒŅˆĐžŅŅ‚Ņ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ ĐŊĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СĐŧŅ–ĐŊŅŽĐ˛Đ°Ņ‚Đ¸", "transcoding_audio_codec": "ĐŅƒĐ´Ņ–ĐžĐēОдĐĩĐē", "transcoding_audio_codec_description": "Opus - ҆Đĩ ĐžĐŋŅ†Ņ–Ņ ĐŊĐ°ĐšĐ˛Đ¸Ņ‰ĐžŅ— ŅĐēĐžŅŅ‚Ņ–, аĐģĐĩ ĐŧĐĩĐŊ҈Đĩ ҁ҃ĐŧҖҁĐŊа ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи айО ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊиĐŧ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅĐŧ.", "transcoding_bitrate_description": "Đ’Ņ–Đ´ĐĩĐž С ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚ĐžĐŧ Đ˛Đ¸Ņ‰Đĩ ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊĐžĐŗĐž айО ĐŊĐĩ в ĐŋŅ€Đ¸ĐšĐŊŅŅ‚ĐžĐŧ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–", "transcoding_codecs_learn_more": "ДĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛ĐžŅ— Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ‚ĐĩŅ€ĐŧŅ–ĐŊĐžĐģĐžĐŗŅ–ŅŽ, Ņ‰Đž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Ņ‚ŅƒŅ‚, СвĐĩŅ€Ņ‚Đ°ĐšŅ‚ĐĩŅŅ Đ´Đž Đ´ĐžĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ— FFmpeg Đ´ĐģŅ ĐēОдĐĩĐēŅ–Đ˛ H.264, HEVC Ņ‚Đ° VP9.", "transcoding_constant_quality_mode": "Đ ĐĩĐļиĐŧ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅĐēĐžŅŅ‚Ņ–", - "transcoding_constant_quality_mode_description": "ICQ ĐēŅ€Đ°Ņ‰Đĩ, ĐŊŅ–Đļ CQP, аĐģĐĩ Đ´ĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅŽŅ‚ŅŒ ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ. Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ ҆ҖҔҗ ĐžĐŋ҆Җҗ ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ СаСĐŊĐ°Ņ‡ĐĩĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧ҃ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ŅĐēĐžŅŅ‚Ņ–. Đ†ĐŗĐŊĐžŅ€ŅƒŅ”Ņ‚ŅŒŅŅ NVENC, ĐžŅĐēŅ–ĐģҌĐēи Đ˛Ņ–ĐŊ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ICQ.", - "transcoding_constant_rate_factor": "КоĐĩ҄Җ҆ҖҔĐŊŅ‚ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅŅ‚Đ°Đ˛Đēи (-crf)", + "transcoding_constant_quality_mode_description": "ICQ ĐēŅ€Đ°Ņ‰Đĩ, ĐŊŅ–Đļ CQP, аĐģĐĩ Đ´ĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅŽŅ‚ŅŒ ҆ĐĩĐš Ņ€ĐĩĐļиĐŧ. Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ СаСĐŊĐ°Ņ‡ĐĩĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧ҃ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐžŅĐŊĐžĐ˛Ņ– ŅĐēĐžŅŅ‚Ņ–. Đ†ĐŗĐŊĐžŅ€ŅƒŅ”Ņ‚ŅŒŅŅ NVENC, ĐžŅĐēŅ–ĐģҌĐēи Đ˛Ņ–ĐŊ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ICQ.", + "transcoding_constant_rate_factor": "КоĐĩ҄Җ҆ҖҔĐŊŅ‚ ĐŋĐžŅŅ‚Ņ–ĐšĐŊĐžŅ— ŅĐēĐžŅŅ‚Ņ– (-crf)", "transcoding_constant_rate_factor_description": "Đ Ņ–Đ˛ĐĩĐŊҌ ŅĐēĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐĩĐž. Đ—Đ°ĐˇĐ˛Đ¸Ņ‡Đ°Đš СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ Đ´ĐģŅ H.264 - 23, HEVC - 28, VP9 - 31, AV1 - 35. НиĐļ҇Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐēŅ€Đ°Ņ‰Đĩ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи.", - "transcoding_disabled_description": "НĐĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐšŅ‚Đĩ Đ˛Ņ–Đ´ĐĩĐž, ҆Đĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋŅ€ĐžĐąĐģĐĩĐŧ С Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅĐŧ ĐŊа Đ´ĐĩŅĐēĐ¸Ņ… ĐēĐģŅ–Ņ”ĐŊŅ‚Đ°Ņ…", + "transcoding_disabled_description": "БĐĩС Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž — ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋŅ€ĐžĐąĐģĐĩĐŧ С Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅĐŧ ĐŊа Đ´ĐĩŅĐēĐ¸Ņ… ĐēĐģŅ–Ņ”ĐŊŅ‚Đ°Ņ…", "transcoding_encoding_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", - "transcoding_encoding_options_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐšŅ‚Đĩ ĐēОдĐĩĐēи, Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊ҃ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ, ŅĐēŅ–ŅŅ‚ŅŒ Ņ‚Đ° Ņ–ĐŊŅˆŅ– ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Đ´ĐģŅ СаĐēОдОваĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž", + "transcoding_encoding_options_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēОдĐĩĐēŅ–Đ˛, Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–, ŅĐēĐžŅŅ‚Ņ– Ņ‚Đ° Ņ–ĐŊŅˆĐ¸Ņ… ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Ņ–Đ˛ Đ´ĐģŅ ĐēОдОваĐŊĐ¸Ņ… Đ˛Ņ–Đ´ĐĩĐž", "transcoding_hardware_acceleration": "АĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐĩ ĐŋŅ€Đ¸ŅĐēĐžŅ€ĐĩĐŊĐŊŅ", "transcoding_hardware_acceleration_description": "ЕĐēҁĐŋĐĩŅ€Đ¸ĐŧĐĩĐŊŅ‚Đ°ĐģҌĐŊĐž: ŅˆĐ˛Đ¸Đ´ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ ĐŧĐžĐļĐĩ СĐŊиĐļŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŒ ĐŋŅ€Đ¸ Ņ‚ĐžĐŧ҃ ŅĐ°ĐŧĐžĐŧ҃ ĐąŅ–Ņ‚Ņ€ĐĩĐšŅ‚Ņ–", "transcoding_hardware_decoding": "АĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐĩ Đ´ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", @@ -372,7 +392,7 @@ "transcoding_max_keyframe_interval_description": "Đ’ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” ĐŧаĐēŅĐ¸ĐŧаĐģҌĐŊ҃ Đ˛Ņ–Đ´ŅŅ‚Đ°ĐŊҌ ĐŧŅ–Đļ ĐēĐģŅŽŅ‡ĐžĐ˛Đ¸Đŧи ĐēĐ°Đ´Ņ€Đ°Đŧи. НиĐļ҇Җ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋĐžĐŗŅ–Ņ€ŅˆŅƒŅŽŅ‚ŅŒ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ŅŅ‚Đ¸ŅĐŊĐĩĐŊĐŊŅ, аĐģĐĩ ĐŋĐžĐēŅ€Đ°Ņ‰ŅƒŅŽŅ‚ŅŒ Ņ‡Đ°Ņ ĐŋĐžŅˆŅƒĐē҃ Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŋĐžĐēŅ€Đ°Ņ‰Đ¸Ņ‚Đ¸ ŅĐēŅ–ŅŅ‚ŅŒ в ҁ҆ĐĩĐŊĐ°Ņ… С ŅˆĐ˛Đ¸Đ´ĐēиĐŧи Ņ€ŅƒŅ…Đ°Đŧи. ЗĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ 0 Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” ҆Đĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ.", "transcoding_optimal_description": "Đ’Ņ–Đ´ĐĩĐž С Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ Đ˛Đ¸Ņ‰Đĩ ҆ҖĐģŅŒĐžĐ˛ĐžŅ— айО ĐŊĐĩ в ĐŋŅ€Đ¸ĐšĐŊŅŅ‚ĐžĐŧ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ–", "transcoding_policy": "ПоĐģŅ–Ņ‚Đ¸Đēа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", - "transcoding_policy_description": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ, ĐēĐžĐģи Đ˛Ņ–Đ´ĐĩĐž ĐąŅƒĐ´Đĩ Ņ‚Ņ€Đ°ĐŊҁĐēОдОваĐŊĐž", + "transcoding_policy_description": "ВизĐŊĐ°Ņ‡Đ°Ņ”, ĐēĐžĐģи Đ˛Ņ–Đ´ĐĩĐž ĐąŅƒĐ´Đĩ Ņ‚Ņ€Đ°ĐŊҁĐēОдОваĐŊĐž", "transcoding_preferred_hardware_device": "ПĐĩŅ€ĐĩваĐļĐŊиК аĐŋĐ°Ņ€Đ°Ņ‚ĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", "transcoding_preferred_hardware_device_description": "Đ—Đ°ŅŅ‚ĐžŅĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ ҂ҖĐģҌĐēи Đ´Đž VAAPI Ņ– QSV. Đ’ŅŅ‚Đ°ĐŊОвĐģŅŽŅ” Đ˛ŅƒĐˇĐžĐģ DRI, ŅĐēиК виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ Đ´ĐģŅ аĐŋĐ°Ņ€Đ°Ņ‚ĐŊĐžĐŗĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ.", "transcoding_preset_preset": "ĐŸĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ (-preset)", @@ -388,7 +408,7 @@ "transcoding_temporal_aq_description": "ĐĄŅ‚ĐžŅŅƒŅ”Ņ‚ŅŒŅŅ ĐģĐ¸ŅˆĐĩ NVENC. Đ§Đ°ŅĐžĐ˛Đ° адаĐŋŅ‚Đ¸Đ˛ĐŊа ĐēваĐŊŅ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ ĐŋŅ–Đ´Đ˛Đ¸Ņ‰ŅƒŅ” ŅĐēŅ–ŅŅ‚ŅŒ ҁ҆ĐĩĐŊ С Đ˛Đ¸ŅĐžĐēĐžŅŽ Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ”ŅŽ Ņ‚Đ° ĐŊĐ¸ĐˇŅŒĐēиĐŧ Ņ€Ņ–Đ˛ĐŊĐĩĐŧ Ņ€ŅƒŅ…Ņƒ. МоĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩҁ҃ĐŧҖҁĐŊиĐŧ ĐˇŅ– ŅŅ‚Đ°Ņ€Đ¸Đŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи.", "transcoding_threads": "ĐŸĐžŅ‚ĐžĐēи", "transcoding_threads_description": "Đ’Đ¸Ņ‰Ņ– СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ŅĐēĐžŅ€ŅŽŅŽŅ‚ŅŒ ĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒ ĐŧĐĩĐŊ҈Đĩ ĐŧŅ–ŅŅ†Ņ Đ´ĐģŅ ĐžĐąŅ€ĐžĐąĐēи Ņ–ĐŊŅˆĐ¸Ņ… СавдаĐŊҌ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ ĐŋŅ–Đ´ Ņ‡Đ°Ņ аĐēŅ‚Đ¸Đ˛ĐŊĐžŅŅ‚Ņ–. ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐŊĐĩ ĐŋОвиĐŊĐŊĐž ĐąŅƒŅ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ĐēŅ–ĐģҌĐēĐžŅŅ‚Ņ– ŅĐ´ĐĩŅ€ ĐŋŅ€ĐžŅ†ĐĩŅĐžŅ€Đ°. МаĐēŅĐ¸ĐŧŅ–ĐˇŅƒŅ” виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ, ŅĐēŅ‰Đž Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊа 0.", - "transcoding_tone_mapping": "ĐĸĐžĐŊОва ĐēĐ°Ņ€Ņ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ", + "transcoding_tone_mapping": "ĐĸĐžĐŊОвĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "transcoding_tone_mapping_description": "НаĐŧĐ°ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ˛Đ¸ĐŗĐģŅĐ´ HDR-Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ€Đ¸ ĐēĐžĐŊвĐĩŅ€Ņ‚Đ°Ņ†Ņ–Ņ— в SDR. КоĐļĐĩĐŊ аĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Ņ€ĐžĐąĐ¸Ņ‚ŅŒ Ņ€Ņ–ĐˇĐŊŅ– ĐēĐžĐŧĐŋŅ€ĐžĐŧŅ–ŅĐ¸ Ņ‰ĐžĐ´Đž ĐēĐžĐģŅŒĐžŅ€Ņƒ, Đ´ĐĩŅ‚Đ°ĐģŅ–ĐˇĐ°Ņ†Ņ–Ņ— Ņ‚Đ° ŅŅĐēŅ€Đ°Đ˛ĐžŅŅ‚Ņ–. АĐģĐŗĐžŅ€Đ¸Ņ‚Đŧ Hable СйĐĩŅ€Ņ–ĐŗĐ°Ņ” Đ´ĐĩŅ‚Đ°ĐģŅ–, Mobius - ĐēĐžĐģŅŒĐžŅ€Đ¸, Reinhard - ŅŅĐēŅ€Đ°Đ˛Ņ–ŅŅ‚ŅŒ.", "transcoding_transcode_policy": "ПоĐģŅ–Ņ‚Đ¸Đēа ĐŋĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ", "transcoding_transcode_policy_description": "ПоĐģŅ–Ņ‚Đ¸Đēа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐģŅ Đ˛Ņ–Đ´ĐĩĐž. HDR Đ˛Ņ–Đ´ĐĩĐž СавĐļди ĐąŅƒĐ´Đĩ Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ŅŅŒ (ĐēҀҖĐŧ виĐŋадĐēŅ–Đ˛, ĐēĐžĐģи Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž).", @@ -398,23 +418,23 @@ "transcoding_video_codec_description": "VP9 ĐŧĐ°Ņ” Đ˛Đ¸ŅĐžĐē҃ ĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ Ņ– ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ С вĐĩйОĐŧ, аĐģĐĩ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅ” ĐąŅ–ĐģҌ҈Đĩ Ņ‡Đ°ŅŅƒ ĐŊа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. HEVC ĐŋŅ€Đ°Ņ†ŅŽŅ” ŅŅ…ĐžĐļĐĩ, аĐģĐĩ ĐŧĐ°Ņ” ĐŧĐĩĐŊ҈҃ ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ С вĐĩйОĐŧ. H.264 ĐŧĐ°Ņ” ŅˆĐ¸Ņ€ĐžĐē҃ ҁ҃ĐŧҖҁĐŊŅ–ŅŅ‚ŅŒ Ņ– ŅˆĐ˛Đ¸Đ´ĐēĐž Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒŅ”Ņ‚ŅŒŅŅ, аĐģĐĩ ŅŅ‚Đ˛ĐžŅ€ŅŽŅ” СĐŊĐ°Ņ‡ĐŊĐž ĐąŅ–ĐģŅŒŅˆŅ– Ņ„Đ°ĐšĐģи. AV1 - ĐŊаКĐĩŅ„ĐĩĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅˆĐ¸Đš ĐēОдĐĩĐē, аĐģĐĩ ĐŊĐĩ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ”Ņ‚ŅŒŅŅ ĐŊа ŅŅ‚Đ°Ņ€Ņ–ŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ….", "trash_enabled_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐŊŅ ĐēĐžŅˆĐ¸Đēа", "trash_number_of_days": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛", - "trash_number_of_days_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛, ĐŋŅ€ĐžŅ‚ŅĐŗĐžĐŧ ŅĐēĐžŅ— СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ в ĐēĐžŅˆĐ¸Đē҃ ĐŋĐĩŅ€ĐĩĐ´ Ņ—Ņ… ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊиĐŧ видаĐģĐĩĐŊĐŊŅĐŧ", + "trash_number_of_days_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛, ĐŋŅ€ĐžŅ‚ŅĐŗĐžĐŧ ŅĐēĐ¸Ņ… СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ҃ ĐēĐžŅˆĐ¸Đē҃ ĐŋĐĩŅ€ĐĩĐ´ Ņ—Ņ… ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊиĐŧ видаĐģĐĩĐŊĐŊŅĐŧ", "trash_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅˆĐ¸Đēа", "trash_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐēĐžŅˆĐ¸Đēа", "unlink_all_oauth_accounts": "Đ’Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth", "unlink_all_oauth_accounts_description": "НĐĩ ĐˇĐ°ĐąŅƒĐ´ŅŒŅ‚Đĩ Đ˛Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth ĐŋĐĩŅ€ĐĩĐ´ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´ĐžĐŧ Đ´Đž ĐŊĐžĐ˛ĐžĐŗĐž ĐŋĐžŅŅ‚Đ°Ņ‡Đ°ĐģҌĐŊиĐēа.", "unlink_all_oauth_accounts_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´â€™Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ˛ŅŅ– ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸ OAuth? ĐĻĐĩ ҁĐēиĐŊĐĩ Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€ OAuth Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, Ņ– Ņ†ŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ĐąŅƒĐ´Đĩ ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸.", "user_cleanup_job": "ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "user_delete_delay": "АĐēĐ°ŅƒĐŊŅ‚ {user} Ņ– ĐšĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаĐŋĐģаĐŊОваĐŊŅ– Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ҇ĐĩŅ€ĐĩС {delay, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", + "user_delete_delay": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ {user} Ņ– ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаĐŋĐģаĐŊОваĐŊŅ– Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ҇ĐĩŅ€ĐĩС {delay, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", "user_delete_delay_settings": "Đ’Ņ–Đ´ĐēĐģадĐĩĐŊĐĩ видаĐģĐĩĐŊĐŊŅ", - "user_delete_delay_settings_description": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Đ´ĐŊŅ–Đ˛ ĐŋҖҁĐģŅ видаĐģĐĩĐŊĐŊŅ Đ´ĐģŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ аĐēĐ°ŅƒĐŊŅ‚Đ° ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐšĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛. Đ—Đ°Đ´Đ°Ņ‡Đ° видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ– Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛, ĐŗĐžŅ‚ĐžĐ˛Đ¸Ņ… Đ´Đž видаĐģĐĩĐŊĐŊŅ. ЗĐŧŅ–ĐŊи Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅ†Ņ–ĐŊĐĩĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž виĐēĐžĐŊаĐŊĐŊŅ.", - "user_delete_immediately": "АĐēĐ°ŅƒĐŊŅ‚ Ņ‚Đ° Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ.", - "user_delete_immediately_checkbox": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° Ņ€ĐĩŅŅƒŅ€ŅĐ¸ в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ ĐŊĐĩĐŗĐ°ĐšĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ", - "user_details": "ДаĐŊĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "user_delete_delay_settings_description": "ПĐĩŅ€Ņ–ĐžĐ´ Đ˛Ņ–Đ´Ņ‚ĐĩŅ€ĐŧŅ–ĐŊŅƒĐ˛Đ°ĐŊĐŊŅ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° ĐšĐžĐŗĐž Ņ„Đ°ĐšĐģŅ–Đ˛. ЗавдаĐŊĐŊŅ С видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ Ņ‰ĐžĐŊĐžŅ‡Ņ– Đž ĐŋŅ–Đ˛ĐŊĐžŅ‡Ņ– Ņ– ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅ” ОйĐģŅ–ĐēĐžĐ˛Ņ– СаĐŋĐ¸ŅĐ¸, ĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊŅ– Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ. ЗĐŧŅ–ĐŊи Ņ†ŅŒĐžĐŗĐž ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ° ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ˛Ņ€Đ°Ņ…ĐžĐ˛Đ°ĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž СаĐŋ҃ҁĐē҃ СавдаĐŊĐŊŅ.", + "user_delete_immediately": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ Ņ‚Đ° Ņ„Đ°ĐšĐģи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° {user} ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊĐĩĐŗĐ°ĐšĐŊĐž ĐŋĐžŅŅ‚Đ°Đ˛ĐģĐĩĐŊŅ– в ҇ĐĩŅ€ĐŗŅƒ ĐŊа ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐĩ видаĐģĐĩĐŊĐŊŅ.", + "user_delete_immediately_checkbox": "ĐŸĐžŅŅ‚Đ°Đ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° Ņ‚Đ° Ņ„Đ°ĐšĐģи в ҇ĐĩŅ€ĐŗŅƒ Đ´ĐģŅ ĐŊĐĩĐŗĐ°ĐšĐŊĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ", + "user_details": "ДаĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_management": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи", "user_password_has_been_reset": "ĐŸĐ°Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐąŅƒĐģĐž ҁĐēиĐŊŅƒŅ‚Đž:", "user_password_reset_description": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– Ņ‚Đ¸ĐŧŅ‡Đ°ŅĐžĐ˛Đ¸Đš ĐŋĐ°Ņ€ĐžĐģҌ Ņ– ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧŅ‚Đĩ КОĐŧ҃, Ņ‰Đž Đ˛Ņ–ĐŊ ĐŋОвиĐŊĐĩĐŊ ĐąŅƒĐ´Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ€Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–.", - "user_restore_description": "АĐēĐ°ŅƒĐŊŅ‚ {user} ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž.", + "user_restore_description": "ОбĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ {user} ĐąŅƒĐ´Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž.", "user_restore_scheduled_removal": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° - СаĐŋĐģаĐŊОваĐŊĐž ĐŊа видаĐģĐĩĐŊĐŊŅ {date, date, long}", "user_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", @@ -427,14 +447,17 @@ "video_conversion_job": "ПĐĩŅ€ĐĩĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", "video_conversion_job_description": "ĐĸŅ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž Đ´ĐģŅ ŅˆĐ¸Ņ€ŅˆĐžŅ— ҁ҃ĐŧҖҁĐŊĐžŅŅ‚Ņ– С ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°Đŧи Ņ‚Đ° ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи" }, - "admin_email": "Email АдĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", + "admin_email": "ЕĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊа ĐŋĐžŅˆŅ‚Đ° адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "admin_password": "ĐŸĐ°Ņ€ĐžĐģҌ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "administration": "АдĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€ŅƒĐ˛Đ°ĐŊĐŊŅ", "advanced": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊŅ–", - "advanced_settings_enable_alternate_media_filter_subtitle": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ҆ĐĩĐš Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚ Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— Са аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊиĐŧи ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧи. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ҃ Đ˛Đ°Ņ виĐŊиĐēĐ°ŅŽŅ‚ŅŒ ĐŋŅ€ĐžĐąĐģĐĩĐŧи С Ņ‚Đ¸Đŧ, Ņ‰Đž ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģŅŅ” Đ˛ŅŅ– аĐģŅŒĐąĐžĐŧи.", + "advanced_settings_clear_image_cache": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", + "advanced_settings_clear_image_cache_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", + "advanced_settings_clear_image_cache_success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž {size}", + "advanced_settings_enable_alternate_media_filter_subtitle": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ ҆ĐĩĐš Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚ Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— Са аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊиĐŧи ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧи. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ҆Đĩ, ŅĐēŅ‰Đž ҃ Đ˛Đ°Ņ виĐŊиĐēĐ°ŅŽŅ‚ŅŒ ĐŋŅ€ĐžĐąĐģĐĩĐŧи С Ņ‚Đ¸Đŧ, Ņ‰Đž ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģŅŅ” Đ˛ŅŅ– аĐģŅŒĐąĐžĐŧи.", "advanced_settings_enable_alternate_media_filter_title": "[ЕКСПЕРИМЕНĐĸАЛĐŦНИЙ] ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊиК ҄ҖĐģŅŒŅ‚Ņ€ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "advanced_settings_log_level_title": "Đ Ņ–Đ˛ĐĩĐŊҌ ĐģĐžĐŗŅƒĐ˛Đ°ĐŊĐŊŅ: {level}", - "advanced_settings_prefer_remote_subtitle": "ДĐĩŅĐēŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— вĐĩĐģҌĐŧи ĐŋĐžĐ˛Ņ–ĐģҌĐŊĐž СаваĐŊŅ‚Đ°ĐļŅƒŅŽŅ‚ŅŒ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Ņ–Đˇ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—. АĐēŅ‚Đ¸Đ˛ŅƒĐšŅ‚Đĩ ҆ĐĩĐš ĐŋĐ°Ņ€Đ°ĐŧĐĩ҂Ҁ, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ.", + "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": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēŅ– ĐŋŅ€ĐžĐēҁҖ-ĐˇĐ°ĐŗĐžĐģОвĐēи [ЕКСПЕРИМЕНĐĸАЛĐŦНА Đ’Đ•Đ ĐĄĐ†Đ¯]", @@ -442,7 +465,7 @@ "advanced_settings_readonly_mode_title": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ", "advanced_settings_self_signed_ssl_subtitle": "ĐŸŅ€ĐžĐŋ҃ҁĐēĐ°Ņ” ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°. ĐŸĐžŅ‚Ņ€Ņ–ĐąĐŊĐĩ Đ´ĐģŅ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐ¸Ņ… ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Ņ–Đ˛.", "advanced_settings_self_signed_ssl_title": "ДозвоĐģĐ¸Ņ‚Đ¸ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊŅ– SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ¸ [ЕКСПЕРИМЕНĐĸАЛĐŦНА Đ’Đ•Đ ĐĄĐ†Đ¯]", - "advanced_settings_sync_remote_deletions_subtitle": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž видаĐģŅŅ‚Đ¸ айО Đ˛Ņ–Đ´ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—, ĐēĐžĐģи Ņ†Ņ Đ´Ņ–Ņ виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ в вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅ–", + "advanced_settings_sync_remote_deletions_subtitle": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž видаĐģŅŅ‚Đ¸ айО Đ˛Ņ–Đ´ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—, ĐēĐžĐģи Ņ†Ņ Đ´Ņ–Ņ виĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ в вĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅŅ–", "advanced_settings_sync_remote_deletions_title": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… видаĐģĐĩĐŊҌ [ЕКСПЕРИМЕНĐĸАЛĐŦНО]", "advanced_settings_tile_subtitle": "Đ ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēŅ– ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "advanced_settings_troubleshooting_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– Ņ„ŅƒĐŊĐē҆Җҗ Đ´ĐģŅ ҃ҁ҃ĐŊĐĩĐŊĐŊŅ ĐŊĐĩҁĐŋŅ€Đ°Đ˛ĐŊĐžŅŅ‚ĐĩĐš", @@ -452,7 +475,7 @@ "age_years": "{years, plural, other {Đ’Ņ–Đē #}}", "album": "АĐģŅŒĐąĐžĐŧ", "album_added": "АĐģŅŒĐąĐžĐŧ дОдаĐŊĐž", - "album_added_notification_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ ĐŋĐž ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊŅ–Đš ĐŋĐžŅˆŅ‚Ņ–, ĐēĐžĐģи Đ˛Đ°Ņ Đ´ĐžĐ´Đ°ŅŽŅ‚ŅŒ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃", + "album_added_notification_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅŽ ĐŋĐžŅˆŅ‚ĐžŅŽ, ĐēĐžĐģи Đ˛Đ°Ņ Đ´ĐžĐ´Đ°ŅŽŅ‚ŅŒ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃", "album_cover_updated": "ОбĐēĐģадиĐŊĐēа аĐģŅŒĐąĐžĐŧ҃ ĐžĐŊОвĐģĐĩĐŊа", "album_delete_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ {album}?", "album_delete_confirmation_description": "Đ¯ĐēŅ‰Đž аĐģŅŒĐąĐžĐŧ ĐąŅƒĐ˛ ҁĐŋŅ–ĐģҌĐŊиĐŧ, Ņ–ĐŊŅˆŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ– ĐŊĐĩ СĐŧĐžĐļŅƒŅ‚ŅŒ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐŊŅŒĐžĐŗĐž.", @@ -463,60 +486,67 @@ "album_leave": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ?", "album_leave_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ {album}?", "album_name": "Назва АĐģŅŒĐąĐžĐŧ҃", - "album_options": "ОĐŋ҆Җҗ аĐģŅŒĐąĐžĐŧ҃", + "album_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ аĐģŅŒĐąĐžĐŧ҃", "album_remove_user": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°?", "album_remove_user_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {user}?", "album_search_not_found": "АĐģŅŒĐąĐžĐŧŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ Đ˛Đ°ŅˆĐžĐŧ҃ СаĐŋĐ¸Ņ‚Ņƒ, ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", + "album_selected": "АĐģŅŒĐąĐžĐŧ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", "album_share_no_users": "ĐĄŅ…ĐžĐļĐĩ, ви ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅ Ņ†Đ¸Đŧ аĐģŅŒĐąĐžĐŧĐžĐŧ С ŅƒŅŅ–Đŧа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи айО ҃ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, С ŅĐēиĐŧ ĐŧĐžĐļĐŊа ĐąŅƒĐģĐž Đą ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ.", "album_summary": "ĐšĐžŅ€ĐžŅ‚ĐēиК ĐžĐŋĐ¸Ņ аĐģŅŒĐąĐžĐŧ҃", "album_updated": "АĐģŅŒĐąĐžĐŧ ĐžĐŊОвĐģĐĩĐŊĐž", - "album_updated_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐšŅ‚Đĩ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐŊа ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ, ĐēĐžĐģи ҃ ҁĐŋŅ–ĐģҌĐŊĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ– С'ŅĐ˛ĐģŅŅŽŅ‚ŅŒŅŅ ĐŊĐžĐ˛Ņ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", + "album_updated_setting_description": "ĐžŅ‚Ņ€Đ¸ĐŧŅƒĐšŅ‚Đĩ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐŊа ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ, ĐēĐžĐģи ҃ ҁĐŋŅ–ĐģҌĐŊĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ– С'ŅĐ˛ĐģŅŅŽŅ‚ŅŒŅŅ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "album_upload_assets": "ВиваĐŊŅ‚Đ°ĐļŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐˇŅ– ŅĐ˛ĐžĐŗĐž ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ° Ņ‚Đ° Đ´ĐžĐ´Đ°ĐšŅ‚Đĩ Ņ—Ņ… Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "album_user_left": "Ви ĐŋĐžĐēиĐŊ҃Đģи {album}", "album_user_removed": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ {user} видаĐģĐĩĐŊиК", "album_viewer_appbar_delete_confirm": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš аĐģŅŒĐąĐžĐŧ ĐˇŅ– ŅĐ˛ĐžĐŗĐž ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ?", "album_viewer_appbar_share_err_delete": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "album_viewer_appbar_share_err_leave": "НĐĩ вдаĐģĐžŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", - "album_viewer_appbar_share_err_remove": "ВиĐŊиĐēĐģи ĐŋŅ€ĐžĐąĐģĐĩĐŧи С видаĐģĐĩĐŊĐŊŅĐŧ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", + "album_viewer_appbar_share_err_remove": "ВиĐŊиĐēĐģи ĐŋŅ€ĐžĐąĐģĐĩĐŧи С видаĐģĐĩĐŊĐŊŅĐŧ Ņ„Đ°ĐšĐģŅ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_err_title": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_leave": "Đ’Đ¸ĐšŅ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", "album_viewer_appbar_share_to": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", "album_viewer_page_share_add_users": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", - "album_with_link_access": "ĐŸĐžĐ´Ņ–ĐģŅ–Ņ‚ŅŒŅŅ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐŊа аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą Đ˛Đ°ŅˆŅ– Đ´Ņ€ŅƒĐˇŅ– ĐŧĐžĐŗĐģи ĐšĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸.", + "album_with_link_access": "Đ‘ŅƒĐ´ŅŒ-Ņ…Ņ‚Đž С ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐŧĐžĐļĐĩ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в Ņ†ŅŒĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ–.", "albums": "АĐģŅŒĐąĐžĐŧи", "albums_count": "{count, plural, one {1 аĐģŅŒĐąĐžĐŧ} few {{count, number} аĐģŅŒĐąĐžĐŧи} many {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {{count, number} аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", "albums_default_sort_order": "ĐŸĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊŅĐŧ", - "albums_default_sort_order_description": "ĐŸĐžŅ‡Đ°Ņ‚ĐēОвиК ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛.", - "albums_feature_description": "КоĐģĐĩĐē҆Җҗ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛, ŅĐēŅ– ĐŧĐžĐļĐŊа ҁĐŋŅ–ĐģҌĐŊĐž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ С Ņ–ĐŊŅˆĐ¸Đŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи.", + "albums_default_sort_order_description": "ĐŸĐžŅ‡Đ°Ņ‚ĐēОвиК ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛.", + "albums_feature_description": "КоĐģĐĩĐē҆Җҗ Ņ„Đ°ĐšĐģŅ–Đ˛, ŅĐēŅ– ĐŧĐžĐļĐŊа ҁĐŋŅ–ĐģҌĐŊĐž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ С Ņ–ĐŊŅˆĐ¸Đŧи ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи.", "albums_on_device_count": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ({count})", + "albums_selected": "{count, plural, one {# аĐģŅŒĐąĐžĐŧ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} few {# аĐģŅŒĐąĐžĐŧи Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} many {# аĐģŅŒĐąĐžĐŧŅ–Đ˛ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž} other {# аĐģŅŒĐąĐžĐŧŅ–Đ˛ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž}}", "all": "ĐŖŅŅ–", "all_albums": "ĐŖŅŅ– аĐģŅŒĐąĐžĐŧи", "all_people": "ĐŖŅŅ– ĐģŅŽĐ´Đ¸", + "all_photos": "ĐŖŅŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", "all_videos": "ĐŖŅŅ– Đ˛Ņ–Đ´ĐĩĐž", "allow_dark_mode": "ДозвоĐģĐ¸Ņ‚Đ¸ Ņ‚ĐĩĐŧĐŊиК Ņ€ĐĩĐļиĐŧ", "allow_edits": "ДозвоĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ", "allow_public_user_to_download": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋŅƒĐąĐģҖ҇ĐŊĐžĐŧ҃ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡ĐĩĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", - "allow_public_user_to_upload": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋŅƒĐąĐģҖ҇ĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧ СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸", + "allow_public_user_to_upload": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋŅƒĐąĐģҖ҇ĐŊиĐŧ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧ виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸", "allowed": "ДозвоĐģĐĩĐŊĐž", "alt_text_qr_code": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ QR-ĐēĐžĐ´Ņƒ", + "always_keep": "ЗавĐļди СйĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸", + "always_keep_photos_hint": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ СйĐĩŅ€ĐĩĐļĐĩ Đ˛ŅŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—.", + "always_keep_videos_hint": "Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ СйĐĩŅ€ĐĩĐļĐĩ Đ˛ŅŅ– Đ˛Ņ–Đ´ĐĩĐž ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—.", "anti_clockwise": "ĐŸŅ€ĐžŅ‚Đ¸ ĐŗĐžĐ´Đ¸ĐŊĐŊиĐēĐžĐ˛ĐžŅ— ҁ҂ҀҖĐģĐēи", "api_key": "КĐģŅŽŅ‡ API", "api_key_description": "ĐĻĐĩ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ĐąŅƒĐ´Đĩ ĐŋĐžĐēаСаĐŊĐĩ ĐģĐ¸ŅˆĐĩ ОдиĐŊ Ņ€Đ°Đˇ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ОйОв'ŅĐˇĐēОвО ҁĐēĐžĐŋŅ–ŅŽĐšŅ‚Đĩ ĐšĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐ´ СаĐēŅ€Đ¸Ņ‚Ņ‚ŅĐŧ Đ˛Ņ–ĐēĐŊа.", "api_key_empty": "Назва Đ˛Đ°ŅˆĐžĐŗĐž ĐēĐģŅŽŅ‡Đ° API ĐŊĐĩ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅŒĐžŅŽ", "api_keys": "КĐģŅŽŅ‡Ņ– API", "app_architecture_variant": "Đ’Đ°Ņ€Ņ–Đ°ĐŊŅ‚ (ĐŅ€Ņ…Ņ–Ņ‚ĐĩĐēŅ‚ŅƒŅ€Đ°)", - "app_bar_signout_dialog_content": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž йаĐļĐ°Ņ”Ņ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С аĐēĐēĐ°ŅƒĐŊŅ‚Đ°?", + "app_bar_signout_dialog_content": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ?", "app_bar_signout_dialog_ok": "ĐĸаĐē", - "app_bar_signout_dialog_title": "Đ’Đ¸ĐšŅ‚Đ¸ С аĐēĐēĐ°ŅƒĐŊŅ‚Đ°", - "app_download_links": "ĐŸĐžŅĐ¸ĐģаĐŊĐŊŅ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēŅ–Đ˛", - "app_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи", - "app_stores": "ĐœĐ°ĐŗĐ°ĐˇĐ¸ĐŊи Đ´ĐžĐ´Đ°Ņ‚ĐēŅ–Đ˛", - "app_update_available": "ОĐŊОвĐģĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ", + "app_bar_signout_dialog_title": "Đ’Đ¸ĐšŅ‚Đ¸", + "app_download_links": "ĐŸĐžŅĐ¸ĐģаĐŊĐŊŅ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēŅ–Đ˛", + "app_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "app_stores": "ĐœĐ°ĐŗĐ°ĐˇĐ¸ĐŊи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēŅ–Đ˛", + "app_update_available": "ОĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ", "appears_in": "З'ŅĐ˛ĐģŅŅ”Ņ‚ŅŒŅŅ в", "apply_count": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ({count, number})", "archive": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸", - "archive_action_prompt": "{count} дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "archive_action_prompt": "{count, plural, one {# Ņ„Đ°ĐšĐģ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# Ņ„Đ°ĐšĐģи дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# Ņ„Đ°ĐšĐģŅ–Đ˛ дОдаĐŊĐž Đ´Đž Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", "archive_or_unarchive_photo": "ĐŅ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ айО Ņ€ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", - "archive_page_no_archived_assets": "НĐĩĐŧĐ°Ņ” Đ°Ņ€Ņ…Ņ–Đ˛ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "archive_page_no_archived_assets": "НĐĩĐŧĐ°Ņ” Đ°Ņ€Ņ…Ņ–Đ˛ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "archive_page_title": "ĐŅ€Ņ…Ņ–Đ˛ ({count})", "archive_size": "РОСĐŧŅ–Ņ€ Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", "archive_size_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐˇĐŧŅ–Ņ€ Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ (҃ GiB)", @@ -524,56 +554,61 @@ "archived_count": "{count, plural, other {ĐŅ€Ņ…Ņ–Đ˛ĐžĐ˛Đ°ĐŊĐž #}}", "are_these_the_same_person": "ĐĻĐĩ Ņ‚Đ° ŅĐ°Đŧа ĐģŅŽĐ´Đ¸ĐŊа?", "are_you_sure_to_do_this": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҆Đĩ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸?", - "asset_action_delete_err_read_only": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "asset_action_share_err_offline": "НĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐžŅ„Ņ„ĐģаКĐŊ-ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и), ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "array_field_not_fully_supported": "ПоĐģŅ ĐŧĐ°ŅĐ¸Đ˛Ņƒ ĐŋĐžŅ‚Ņ€ĐĩĐąŅƒŅŽŅ‚ŅŒ Ņ€ŅƒŅ‡ĐŊĐžĐŗĐž Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ JSON", + "asset_action_delete_err_read_only": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ(и) ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "asset_action_share_err_offline": "НĐĩĐŧĐžĐļĐģивО ĐžĐŋŅ€Đ°Ņ†ŅŽĐ˛Đ°Ņ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– Ņ„Đ°ĐšĐģ(и), ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", "asset_added_to_album": "ДодаĐŊĐž Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "asset_adding_to_album": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃â€Ļ", - "asset_description_updated": "ОĐŊОвĐģĐĩĐŊĐž ĐžĐŋĐ¸Ņ Ņ€ĐĩŅŅƒŅ€ŅŅƒ", - "asset_filename_is_offline": "Đ ĐĩŅŅƒŅ€Ņ {filename} Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž", + "asset_created": "ФаКĐģ дОдаĐŊĐž", + "asset_description_updated": "ОĐŊОвĐģĐĩĐŊĐž ĐžĐŋĐ¸Ņ Ņ„Đ°ĐšĐģ҃", + "asset_filename_is_offline": "ФаКĐģ {filename} ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "asset_has_unassigned_faces": "Є ĐŊĐĩŅ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ", "asset_hashing": "ĐĨĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅâ€Ļ", "asset_list_group_by_sub_title": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Са", "asset_list_layout_settings_dynamic_layout_title": "ДиĐŊаĐŧҖ҇ĐŊĐĩ ĐēĐžĐŧĐŋĐžĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", "asset_list_layout_settings_group_automatically": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž", - "asset_list_layout_settings_group_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐž", + "asset_list_layout_settings_group_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐž", "asset_list_layout_settings_group_by_month_day": "ĐœŅ–ŅŅŅ†ŅŒ + Đ´ĐĩĐŊҌ", "asset_list_layout_sub_title": "РОСĐŧŅ–Ņ‚Đēа", "asset_list_settings_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ĐŗĐģŅĐ´Ņƒ ҁҖ҂Đēи Ņ„ĐžŅ‚Đž", "asset_list_settings_title": "Đ¤ĐžŅ‚Đž-ҁҖ҂Đēа", - "asset_offline": "Đ ĐĩŅŅƒŅ€Ņ ĐžŅ„ĐģаКĐŊ", - "asset_offline_description": "ĐĻĐĩĐš СОвĐŊŅ–ŅˆĐŊŅ–Đš Ņ€ĐĩŅŅƒŅ€Ņ ĐąŅ–ĐģҌ҈Đĩ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° Immich Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ.", - "asset_restored_successfully": "ЕĐģĐĩĐŧĐĩĐŊŅ‚ ҃ҁĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž", + "asset_not_found_on_device_android": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", + "asset_not_found_on_device_ios": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—. Đ¯ĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ iCloud, Ņ„Đ°ĐšĐģ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧ ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", + "asset_not_found_on_icloud": "ФаКĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž в iCloud. МоĐļĐģивО, Ņ„Đ°ĐšĐģ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК ҇ĐĩŅ€ĐĩС ĐŋĐžŅˆĐēОдĐļĐĩĐŊиК Ņ„Đ°ĐšĐģ, Ņ‰Đž СйĐĩŅ€Ņ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ в iCloud", + "asset_offline": "ФаКĐģ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", + "asset_offline_description": "ĐĻĐĩĐš Ņ„Đ°ĐšĐģ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐŊа Đ´Đ¸ŅĐē҃. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, СвĐĩŅ€ĐŊŅ–Ņ‚ŅŒŅŅ Đ´Đž адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ° Immich Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ.", + "asset_restored_successfully": "ФаКĐģ ҃ҁĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž", "asset_skipped": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", "asset_skipped_in_trash": "ĐŖ ĐēĐžŅˆĐ¸Đē҃", - "asset_trashed": "Об'Ņ”ĐēŅ‚ видаĐģĐĩĐŊĐž С ĐēĐžŅˆĐ¸Đēа", - "asset_troubleshoot": "Đ’Đ¸Ņ€Ņ–ŅˆĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐąĐģĐĩĐŧ С аĐēŅ‚Đ¸Đ˛Đ°Đŧи", - "asset_uploaded": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", - "asset_uploading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅâ€Ļ", - "asset_viewer_settings_subtitle": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡Đ° ĐŗĐ°ĐģĐĩŅ€ĐĩŅ—", + "asset_trashed": "ФаКĐģ видаĐģĐĩĐŊĐž", + "asset_troubleshoot": "Đ’Đ¸Ņ€Ņ–ŅˆĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐąĐģĐĩĐŧ С Ņ„Đ°ĐšĐģаĐŧи", + "asset_uploaded": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "asset_uploading": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅâ€Ļ", + "asset_viewer_settings_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡Đ° ĐŗĐ°ĐģĐĩŅ€ĐĩŅ—", "asset_viewer_settings_title": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", - "assets": "ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", - "assets_added_count": "ДодаĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_added_to_album_count": "ДодаĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "assets_added_to_albums_count": "ДодаĐŊĐž {assetTotal, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} other {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸}} Đ´Đž {albumTotal, plural, one {# аĐģŅŒĐąĐžĐŧ} other {# аĐģŅŒĐąĐžĐŧ}}", - "assets_cannot_be_added_to_album_count": "{count, plural, one {Đ ĐĩŅŅƒŅ€Ņ} other {Đ ĐĩŅŅƒŅ€ŅĐ¸}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "assets_cannot_be_added_to_albums": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐļОдĐŊĐžĐŗĐž С аĐģŅŒĐąĐžĐŧŅ–Đ˛", - "assets_count": "{count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_deleted_permanently": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž", - "assets_deleted_permanently_from_server": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) видаĐģĐĩĐŊĐž ĐŊаСавĐļди С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich", - "assets_downloaded_failed": "{count, plural, one {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ — {error} Ņ„Đ°ĐšĐģ ĐŊĐĩ вдаĐģĐžŅŅ} other {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — {error} Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŊĐĩ вдаĐģĐžŅŅ}}", - "assets_downloaded_successfully": "{count, plural, one {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ} other {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛}}", - "assets_moved_to_trash_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} ҃ ĐēĐžŅˆĐ¸Đē", - "assets_permanently_deleted_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_removed_count": "ВиĐģŅƒŅ‡ĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_removed_permanently_from_device": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) видаĐģĐĩĐŊŅ– ĐŊаСавĐļди С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "assets_restore_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐ˛ĐžŅ— ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С ĐēĐžŅˆĐ¸Đēа? ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸! ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐļОдĐŊŅ– ĐžŅ„ĐģаКĐŊ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐŊĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊŅ– Ņ‚Đ°ĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ.", - "assets_restored_count": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_restored_successfully": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ҃ҁĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž", - "assets_trashed": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐŋĐžĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", - "assets_trashed_count": "ПоĐŧҖ҉ĐĩĐŊĐž в ĐēĐžŅˆĐ¸Đē {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", - "assets_trashed_from_server": "{count} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐŋĐžĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich", - "assets_were_part_of_album_count": "{count, plural, one {Đ ĐĩŅŅƒŅ€Ņ ĐąŅƒĐ˛} few {Đ ĐĩŅŅƒŅ€ŅĐ¸ ĐąŅƒĐģи} other {Đ ĐĩŅŅƒŅ€ŅĐ¸ ĐąŅƒĐģи}} вĐļĐĩ Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧ҃", - "assets_were_part_of_albums_count": "{count, plural, one {ЕĐģĐĩĐŧĐĩĐŊŅ‚ вĐļĐĩ ĐąŅƒĐ˛} other {ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ вĐļĐĩ ĐąŅƒĐģи}} Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧŅ–Đ˛", + "assets": "Ņ„Đ°ĐšĐģи", + "assets_added_count": "ДодаĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_added_to_album_count": "ДодаĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "assets_added_to_albums_count": "ДодаĐŊĐž {assetTotal, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž {albumTotal, plural, one {# аĐģŅŒĐąĐžĐŧ҃} few {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} many {# аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {# аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", + "assets_cannot_be_added_to_album_count": "{count, plural, one {ФаКĐģ} few {ФаКĐģи} many {ФаКĐģи} other {ФаКĐģи}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "assets_cannot_be_added_to_albums": "{count, plural, one {ФаКĐģ} few {ФаКĐģи} many {ФаКĐģŅ–Đ˛} other {ФаКĐģŅ–Đ˛}} ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐļОдĐŊĐžĐŗĐž С аĐģŅŒĐąĐžĐŧŅ–Đ˛", + "assets_count": "{count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_deleted_permanently": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_deleted_permanently_from_server": "ВидаĐģĐĩĐŊĐž ĐŊаСавĐļди {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Immich", + "assets_downloaded_failed": "{count, plural, one {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ — {error} ĐŊĐĩ вдаĐģĐžŅŅ} few {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи — {error} ĐŊĐĩ вдаĐģĐžŅŅ} many {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — {error} ĐŊĐĩ вдаĐģĐžŅŅ} other {ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛ — {error} ĐŊĐĩ вдаĐģĐžŅŅ}}", + "assets_downloaded_successfully": "{count, plural, one {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģ} few {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģи} many {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛} other {ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž # Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_moved_to_trash_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "assets_permanently_deleted_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_removed_count": "ВиĐģŅƒŅ‡ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_removed_permanently_from_device": "НазавĐļди виĐģŅƒŅ‡ĐĩĐŊĐž С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_restore_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐ˛ĐžŅ— Ņ„Đ°ĐšĐģи С ĐēĐžŅˆĐ¸Đēа? ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸! ЗвĐĩŅ€ĐŊŅ–Ņ‚ŅŒ ŅƒĐ˛Đ°ĐŗŅƒ, Ņ‰Đž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊŅ– Ņ‚Đ°ĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ.", + "assets_restored_count": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_restored_successfully": "ĐŖŅĐŋŅ–ŅˆĐŊĐž Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_trashed": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_trashed_count": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_trashed_from_server": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "assets_were_part_of_album_count": "{count, plural, one {ФаКĐģ ĐąŅƒĐ˛} few {ФаКĐģи ĐąŅƒĐģи} other {ФаКĐģи ĐąŅƒĐģи}} вĐļĐĩ Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧ҃", + "assets_were_part_of_albums_count": "{count, plural, one {ФаКĐģ вĐļĐĩ ĐąŅƒĐ˛} few {ФаКĐģи вĐļĐĩ ĐąŅƒĐģи} many {ФаКĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģи} other {ФаКĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģи}} Ņ‡Đ°ŅŅ‚Đ¸ĐŊĐžŅŽ аĐģŅŒĐąĐžĐŧŅ–Đ˛", "authorized_devices": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "automatic_endpoint_switching_subtitle": "ĐŸŅ–Đ´ĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊĐž ҇ĐĩŅ€ĐĩС СаСĐŊĐ°Ņ‡ĐĩĐŊ҃ Wi-Fi ĐŧĐĩŅ€ĐĩĐļ҃, ĐēĐžĐģи ҆Đĩ ĐŧĐžĐļĐģивО, Ņ– виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛ĐŊŅ– С'Ņ”Đ´ĐŊаĐŊĐŊŅ в Ņ–ĐŊŅˆĐ¸Ņ… виĐŋадĐēĐ°Ņ…", "automatic_endpoint_switching_title": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐĩ ĐŋĐĩŅ€ĐĩĐŧиĐēаĐŊĐŊŅ URL", @@ -586,33 +621,33 @@ "background_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Ņ„ĐžĐŊ҃", "backup": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_album_selection_page_albums_device": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ({count})", - "backup_album_selection_page_albums_tap": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą вĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸, Đ´Đ˛Ņ–Ņ‡Ņ–, Ņ‰ĐžĐą виĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸", - "backup_album_selection_page_assets_scatter": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŊаĐģĐĩĐļĐ°Ņ‚Đ¸ Đ´Đž ĐēŅ–ĐģҌĐēĐžŅ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ. ĐĸаĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ, аĐģŅŒĐąĐžĐŧи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ вĐēĐģŅŽŅ‡ĐĩĐŊŅ– айО виĐģŅƒŅ‡ĐĩĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", + "backup_album_selection_page_albums_tap": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą Đ´ĐžĐ´Đ°Ņ‚Đ¸, Đ´Đ˛Ņ–Ņ‡Ņ–, Ņ‰ĐžĐą виĐģŅƒŅ‡Đ¸Ņ‚Đ¸", + "backup_album_selection_page_assets_scatter": "ФаКĐģи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐŊаĐģĐĩĐļĐ°Ņ‚Đ¸ Đ´Đž ĐēŅ–ĐģҌĐēĐžŅ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ. ĐĸаĐēиĐŧ Ņ‡Đ¸ĐŊĐžĐŧ, аĐģŅŒĐąĐžĐŧи ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ дОдаĐŊŅ– айО виĐģŅƒŅ‡ĐĩĐŊŅ– ĐŋŅ–Đ´ Ņ‡Đ°Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ.", "backup_album_selection_page_select_albums": "ОбĐĩŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧи", "backup_album_selection_page_selection_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž ĐžĐąŅ€Đ°ĐŊĐĩ", - "backup_album_selection_page_total_assets": "Đ—Đ°ĐŗĐ°ĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҃ĐŊŅ–ĐēаĐģҌĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "backup_album_selection_page_total_assets": "Đ—Đ°ĐŗĐ°ĐģҌĐŊа ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҃ĐŊŅ–ĐēаĐģҌĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "backup_albums_sync": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš аĐģŅŒĐąĐžĐŧŅ–Đ˛", "backup_all": "ĐŖŅŅ–", - "backup_background_service_backup_failed_message": "НĐĩ вдаĐģĐžŅŅ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", - "backup_background_service_complete_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ аĐēŅ‚Đ¸Đ˛Ņ–Đ˛ СавĐĩŅ€ŅˆĐĩĐŊĐž", + "backup_background_service_backup_failed_message": "НĐĩ вдаĐģĐžŅŅ ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Ņ„Đ°ĐšĐģŅ–Đ˛. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", + "backup_background_service_complete_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ СавĐĩŅ€ŅˆĐĩĐŊĐž", "backup_background_service_connection_failed_message": "НĐĩ вдаĐģĐžŅŅ Св'ŅĐˇĐ°Ņ‚Đ¸ŅŅ Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŸĐžĐ˛Ņ‚ĐžŅ€ŅŽŅŽâ€Ļ", - "backup_background_service_current_upload_notification": "ЗаваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ {filename}", - "backup_background_service_default_notification": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅŽ ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛â€Ļ", + "backup_background_service_current_upload_notification": "ВиваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ {filename}", + "backup_background_service_default_notification": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€ŅŅŽ ĐŊĐ°ŅĐ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŊĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛â€Ļ", "backup_background_service_error_title": "ПоĐŧиĐģĐēа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "backup_background_service_in_progress_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛â€Ļ", - "backup_background_service_upload_failure_notification": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {filename}", + "backup_background_service_in_progress_notification": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛â€Ļ", + "backup_background_service_upload_failure_notification": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ {filename}", "backup_controller_page_albums": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛", - "backup_controller_page_background_app_refresh_disabled_content": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ в ĐŧĐĩĐŊŅŽ \"НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ > Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ– > ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи\".", - "backup_controller_page_background_app_refresh_disabled_title": "ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи виĐŧĐēĐŊĐĩĐŊĐĩ", - "backup_controller_page_background_app_refresh_enable_button_text": "ПĐĩŅ€ĐĩĐšĐ´Ņ–Ņ‚ŅŒ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", - "backup_controller_page_background_battery_info_link": "ПоĐēаĐļŅ–Ņ‚ŅŒ ĐŧĐĩĐŊŅ– ŅĐē", + "backup_controller_page_background_app_refresh_disabled_content": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ŅƒĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ в ĐŧĐĩĐŊŅŽ \"НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ > Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ– > ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃\".", + "backup_controller_page_background_app_refresh_disabled_title": "ФОĐŊОвĐĩ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ виĐŧĐēĐŊĐĩĐŊĐĩ", + "backup_controller_page_background_app_refresh_enable_button_text": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ", + "backup_controller_page_background_battery_info_link": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ŅĐē", "backup_controller_page_background_battery_info_message": "ДĐģŅ ĐŊаКĐēŅ€Đ°Ņ‰ĐžĐŗĐž Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐąŅƒĐ´ŅŒ-ŅĐē҃ ĐžĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–ŅŽ аĐē҃Đŧ҃ĐģŅŅ‚ĐžŅ€Đ°, ŅĐēа ОйĐŧĐĩĐļŅƒŅ” Ņ„ĐžĐŊĐžĐ˛Ņƒ аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ Đ´ĐģŅ Immich.\n\nĐĄĐŋĐžŅŅ–Đą СаĐģĐĩĐļĐ¸Ņ‚ŅŒ Đ˛Ņ–Đ´ ĐēĐžĐŊĐēŅ€ĐĩŅ‚ĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, Ņ‚ĐžĐŧ҃ ҈҃ĐēĐ°ĐšŅ‚Đĩ ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊ҃ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ҃ Đ˛Đ¸Ņ€ĐžĐąĐŊиĐēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ.", "backup_controller_page_background_battery_info_ok": "ОК", "backup_controller_page_background_battery_info_title": "ОĐŋŅ‚Đ¸ĐŧŅ–ĐˇĐ°Ņ†Ņ–Ņ ĐąĐ°Ņ‚Đ°Ņ€ĐĩŅ—", "backup_controller_page_background_charging": "Đ›Đ¸ŅˆĐĩ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐˇĐ°Ņ€ŅĐ´ĐļаĐŊĐŊŅ", "backup_controller_page_background_configure_error": "НĐĩ вдаĐģĐžŅŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžĐŊОвиК ҁĐĩŅ€Đ˛Ņ–Ņ", - "backup_controller_page_background_delay": "Đ—Đ°Ņ‚Ņ€Đ¸ĐŧĐēа Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛: {duration}", - "backup_controller_page_background_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ„ĐžĐŊĐžĐ˛Ņƒ ҁĐģ҃ĐļĐąŅƒ, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ĐąŅƒĐ´ŅŒ-ŅĐēĐ¸Ņ… ĐŊĐžĐ˛Đ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐąĐĩС ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐēŅ€Đ¸Đ˛Đ°Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧ҃", + "backup_controller_page_background_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": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ„ĐžĐŊОвиК ҁĐĩŅ€Đ˛Ņ–Ņ", @@ -622,7 +657,7 @@ "backup_controller_page_backup_selected": "ĐžĐąŅ€Đ°ĐŊĐž: ", "backup_controller_page_backup_sub": "Đ ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "backup_controller_page_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž: {date}", - "backup_controller_page_desc_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐģаĐŊŅ–, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐŋŅ–Đ´ Ņ‡Đ°Ņ Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи.", + "backup_controller_page_desc_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŊа ĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŧ҃ ĐŋĐģаĐŊŅ–, Ņ‰ĐžĐą Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ ĐŋŅ–Đ´ Ņ‡Đ°Ņ Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Ņ‚Ņ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃.", "backup_controller_page_excluded": "ВиĐģŅƒŅ‡ĐĩĐŊĐž: ", "backup_controller_page_failed": "НĐĩвдаĐģŅ– ({count})", "backup_controller_page_filename": "Назва Ņ„Đ°ĐšĐģ҃: {filename} [{size}]", @@ -640,20 +675,20 @@ "backup_controller_page_total_sub": "ĐŖŅŅ– ҃ĐŊŅ–ĐēаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛", "backup_controller_page_turn_off": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", "backup_controller_page_turn_on": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ в аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", - "backup_controller_page_uploading_file_info": "ЗаваĐŊŅ‚Đ°ĐļŅƒŅŽ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž Ņ„Đ°ĐšĐģ", + "backup_controller_page_uploading_file_info": "ВиваĐŊŅ‚Đ°ĐļŅƒŅŽ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž Ņ„Đ°ĐšĐģ", "backup_err_only_album": "НĐĩ ĐŧĐžĐļ҃ видаĐģĐ¸Ņ‚Đ¸ Ņ”Đ´Đ¸ĐŊиК аĐģŅŒĐąĐžĐŧ", "backup_error_sync_failed": "ПоĐŧиĐģĐēа ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—. НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐąŅ€ĐžĐąĐ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ.", - "backup_info_card_assets": "ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "backup_info_card_assets": "Ņ„Đ°ĐšĐģи", "backup_manual_cancelled": "ĐĄĐēĐ°ŅĐžĐ˛Đ°ĐŊĐž", - "backup_manual_in_progress": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ вĐļĐĩ Đ˛Ņ–Đ´ĐąŅƒĐ˛Đ°Ņ”Ņ‚ŅŒŅŅ. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ĐˇĐŗĐžĐ´ĐžĐŧ", + "backup_manual_in_progress": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ вĐļĐĩ Đ˛Ņ–Đ´ĐąŅƒĐ˛Đ°Ņ”Ņ‚ŅŒŅŅ. ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ ĐˇĐŗĐžĐ´ĐžĐŧ", "backup_manual_success": "ĐŖŅĐŋŅ–Ņ…", - "backup_manual_title": "ĐĄŅ‚Đ°ĐŊ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "backup_manual_title": "ĐĄŅ‚Đ°ĐŊ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "backup_options": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "backup_options_page_title": "Đ ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", - "backup_setting_subtitle": "ĐŖĐŋŅ€Đ°Đ˛ĐģŅ–ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ‚Đ° аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", - "backup_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "backup_setting_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ Ņ„ĐžĐŊОвОĐŧ҃ Ņ‚Đ° аĐēŅ‚Đ¸Đ˛ĐŊĐžĐŧ҃ Ņ€ĐĩĐļиĐŧŅ–", + "backup_settings_subtitle": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "backup_upload_details_page_more_details": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą Đ´Ņ–ĐˇĐŊĐ°Ņ‚Đ¸ŅŅ ĐąŅ–ĐģҌ҈Đĩ", - "backward": "Đ—Đ˛ĐžŅ€ĐžŅ‚ĐŊŅ–Đš", + "backward": "Назад", "biometric_auth_enabled": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊа Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ ŅƒĐ˛Ņ–ĐŧĐēĐŊĐĩĐŊа", "biometric_locked_out": "ВаĐŧ СаĐēŅ€Đ¸Ņ‚Đž Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊĐžŅ— Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", "biometric_no_options": "Đ‘Ņ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊŅ– ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊŅ–", @@ -664,17 +699,17 @@ "bugs_and_feature_requests": "ПоĐŧиĐģĐēи Ņ‚Đ° ЗаĐŋĐ¸Ņ‚Đ¸", "build": "Đ—ĐąŅ–Ņ€Đēа", "build_image": "ВĐĩŅ€ŅŅ–Ņ ĐˇĐąŅ–Ņ€Đēи", - "bulk_delete_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŧĐ°ŅĐžĐ˛Đž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}? ĐĻĐĩ Đ´Ņ–Ņ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ€ĐĩŅŅƒŅ€Ņ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Ņ– ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩĐŧĐžĐļĐģивО ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", - "bulk_keep_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}? ĐĻĐĩ дОСвОĐģĐ¸Ņ‚ŅŒ Đ˛Đ¸Ņ€Ņ–ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŗŅ€ŅƒĐŋи Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ ĐąĐĩС видаĐģĐĩĐŊĐŊŅ Ņ‡ĐžĐŗĐž-ĐŊĐĩĐąŅƒĐ´ŅŒ.", - "bulk_trash_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ виĐēиĐŊŅƒŅ‚Đ¸ в ĐēĐžŅˆĐ¸Đē {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} ĐŧĐ°ŅĐžĐ˛Đž? ĐĻĐĩ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ€ĐĩŅŅƒŅ€Ņ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Ņ– виĐēиĐŊĐĩ в ĐēĐžŅˆĐ¸Đē Đ˛ŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸.", - "buy": "ĐŸŅ€Đ¸Đ´ĐąĐ°ĐšŅ‚Đĩ Immich", + "bulk_delete_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŧĐ°ŅĐžĐ˛Đž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ„Đ°ĐšĐģ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}? ĐĻŅ Đ´Ņ–Ņ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ„Đ°ĐšĐģ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Ņ– ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸. ĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩĐŧĐžĐļĐģивО ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", + "bulk_keep_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ„Đ°ĐšĐģ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}? ĐĻĐĩ дОСвОĐģĐ¸Ņ‚ŅŒ Đ˛Đ¸Ņ€Ņ–ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŗŅ€ŅƒĐŋи Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ ĐąĐĩС видаĐģĐĩĐŊĐŊŅ Ņ‡ĐžĐŗĐž-ĐŊĐĩĐąŅƒĐ´ŅŒ.", + "bulk_trash_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž ĐēĐžŅˆĐ¸Đēа {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ„Đ°ĐšĐģ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}? ĐĻĐĩ СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒ ĐŊĐ°ĐšĐąŅ–ĐģŅŒŅˆĐ¸Đš Ņ„Đ°ĐšĐģ ҃ ĐēĐžĐļĐŊŅ–Đš ĐŗŅ€ŅƒĐŋŅ– Đš ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚ŅŒ Đ´Đž ĐēĐžŅˆĐ¸Đēа Đ˛ŅŅ– Ņ–ĐŊŅˆŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸.", + "buy": "ĐŸŅ€Đ¸Đ´ĐąĐ°Ņ‚Đ¸ Immich", "cache_settings_clear_cache_button": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈", - "cache_settings_clear_cache_button_title": "ĐžŅ‡Đ¸Ņ‰Đ°Ņ” ĐēĐĩ҈ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи. ĐĻĐĩ ŅŅƒŅ‚Ņ‚Ņ”Đ˛Đž СĐŊĐ¸ĐˇĐ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи, Đ´ĐžĐēи ĐēĐĩ҈ ĐŊĐĩ ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐž.", + "cache_settings_clear_cache_button_title": "ĐžŅ‡Đ¸Ņ‰Đ°Ņ” ĐēĐĩ҈ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃. ĐĻĐĩ ŅŅƒŅ‚Ņ‚Ņ”Đ˛Đž СĐŊĐ¸ĐˇĐ¸Ņ‚ŅŒ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃, Đ´ĐžĐēи ĐēĐĩ҈ ĐŊĐĩ ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐąŅƒĐ´ĐžĐ˛Đ°ĐŊĐž.", "cache_settings_duplicated_assets_clear_button": "ОЧИСĐĸИĐĸИ", "cache_settings_duplicated_assets_subtitle": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, ŅĐēŅ– Ņ–ĐŗĐŊĐžŅ€ŅƒŅŽŅ‚ŅŒŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēĐžĐŧ", - "cache_settings_duplicated_assets_title": "Đ”ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ({count})", + "cache_settings_duplicated_assets_title": "Đ”ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ({count})", "cache_settings_statistics_album": "Đ‘Ņ–ĐąĐģŅ–ĐžŅ‚Đĩ҇ĐŊŅ– ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", - "cache_settings_statistics_full": "ПовĐŊĐžŅ€ĐˇĐžĐŧŅ–Ņ€ĐŊŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", + "cache_settings_statistics_full": "ПовĐŊĐžŅ€ĐžĐˇĐŧŅ–Ņ€ĐŊŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "cache_settings_statistics_shared": "ĐœŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛", "cache_settings_statistics_thumbnail": "ĐœŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸", "cache_settings_statistics_title": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ĐēĐĩ҈҃", @@ -705,34 +740,50 @@ "change_password_description": "ĐĻĐĩ айО ĐŋĐĩŅ€ŅˆĐ¸Đš Ņ€Đ°Đˇ, ĐēĐžĐģи ви ŅƒĐ˛Ņ–ĐšŅˆĐģи в ŅĐ¸ŅŅ‚ĐĩĐŧ҃, айО ĐąŅƒĐģĐž ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž СаĐŋĐ¸Ņ‚ ĐŊа СĐŧŅ–ĐŊ҃ Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐ°Ņ€ĐžĐģŅ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ ĐŊиĐļ҇Đĩ.", "change_password_form_confirm_password": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "change_password_form_description": "ĐŸŅ€Đ¸Đ˛Ņ–Ņ‚, {name},\n\nĐĻĐĩ айО Đ˛Đ°Ņˆ ĐŋĐĩŅ€ŅˆĐ¸Đš Đ˛Ņ…Ņ–Đ´ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃, айО ĐąŅƒĐģĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž СаĐŋĐ¸Ņ‚ ĐŊа СĐŧŅ–ĐŊ҃ ĐŋĐ°Ņ€ĐžĐģŅ. Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ ĐŊиĐļ҇Đĩ.", - "change_password_form_log_out": "Đ’Đ¸ĐšĐ´Ņ–Ņ‚ŅŒ Ņ–Đˇ ŅĐ¸ŅŅ‚ĐĩĐŧи ĐŊа Đ˛ŅŅ–Ņ… Ņ–ĐŊŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…", + "change_password_form_log_out": "Đ’Đ¸ĐšŅ‚Đ¸ Ņ–Đˇ ŅĐ¸ŅŅ‚ĐĩĐŧи ĐŊа Đ˛ŅŅ–Ņ… Ņ–ĐŊŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŅ…", "change_password_form_log_out_description": "Đ ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”Ņ‚ŅŒŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С ŅƒŅŅ–Ņ… Ņ–ĐŊŅˆĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", "change_password_form_new_password": "Новий ĐŋĐ°Ņ€ĐžĐģҌ", - "change_password_form_password_mismatch": "ĐŸĐ°Ņ€ĐžĐģŅ– ĐŊĐĩ ҁĐŋŅ–Đ˛ĐŋĐ°Đ´Đ°ŅŽŅ‚ŅŒ", + "change_password_form_password_mismatch": "ĐŸĐ°Ņ€ĐžĐģŅ– ĐŊĐĩ ĐˇĐąŅ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ", "change_password_form_reenter_new_password": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Ņ–Ņ‚ŅŒ ĐŊОвиК ĐŋĐ°Ņ€ĐžĐģҌ", "change_pin_code": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ PIN-ĐēОд", + "change_trigger": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€", + "change_trigger_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€? ĐĻĐĩ видаĐģĐ¸Ņ‚ŅŒ ŅƒŅŅ– ĐŊĐ°ŅĐ˛ĐŊŅ– Đ´Ņ–Ņ— Ņ‚Đ° ҄ҖĐģŅŒŅ‚Ņ€Đ¸.", "change_your_password": "ЗĐŧŅ–ĐŊŅ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš ĐŋĐ°Ņ€ĐžĐģҌ", "changed_visibility_successfully": "ВидиĐŧŅ–ŅŅ‚ŅŒ ҃ҁĐŋŅ–ŅˆĐŊĐž СĐŧŅ–ĐŊĐĩĐŊĐž", "charging": "Đ—Đ°Ņ€ŅĐ´Đēа", "charging_requirement_mobile_backup": "ДĐģŅ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš ĐŋОвиĐŊĐĩĐŊ ĐˇĐ°Ņ€ŅĐ´ĐļĐ°Ņ‚Đ¸ŅŅ", - "check_corrupt_asset_backup": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐŊа ĐŋĐžŅˆĐēОдĐļĐĩĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "check_corrupt_asset_backup": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐŊа ĐŋĐžŅˆĐēОдĐļĐĩĐŊŅ– Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛", "check_corrupt_asset_backup_button": "ВиĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃", - "check_corrupt_asset_backup_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ†ŅŽ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ ĐģĐ¸ŅˆĐĩ ҇ĐĩŅ€ĐĩС Wi-Fi Ņ‚Đ° ĐŋҖҁĐģŅ Ņ‚ĐžĐŗĐž, ŅĐē Đ˛ŅŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€. ĐŸŅ€ĐžŅ†Đĩҁ ĐŧĐžĐļĐĩ СаКĐŊŅŅ‚Đ¸ ĐēŅ–ĐģҌĐēа Ņ…Đ˛Đ¸ĐģиĐŊ.", + "check_corrupt_asset_backup_description": "ЗаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Ņ†ŅŽ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đē҃ ĐģĐ¸ŅˆĐĩ ҇ĐĩŅ€ĐĩС Wi-Fi Ņ‚Đ° ĐŋҖҁĐģŅ Ņ‚ĐžĐŗĐž, ŅĐē Đ˛ŅŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€. ĐŸŅ€ĐžŅ†Đĩҁ ĐŧĐžĐļĐĩ СаКĐŊŅŅ‚Đ¸ ĐēŅ–ĐģҌĐēа Ņ…Đ˛Đ¸ĐģиĐŊ.", "check_logs": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ ĐļŅƒŅ€ĐŊаĐģи", "checksum": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа", "choose_matching_people_to_merge": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš Đ´ĐģŅ Ой'Ņ”Đ´ĐŊаĐŊĐŊŅ", "city": "ĐœŅ–ŅŅ‚Đž", + "cleanup_confirm_description": "Immich СĐŊĐ°ĐšŅˆĐžĐ˛ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date}), ĐąĐĩСĐŋĐĩ҇ĐŊĐž СйĐĩŅ€ĐĩĐļĐĩĐŊĐ¸Ņ… ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ. ВидаĐģĐ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐēĐžĐŋŅ–Ņ— С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", + "cleanup_confirm_prompt_title": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸ С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", + "cleanup_deleted_assets": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "cleanup_deleting": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Đ´Đž ĐēĐžŅˆĐ¸Đēа...", + "cleanup_found_assets": "ЗĐŊаКдĐĩĐŊĐž {count} Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš Ņ„Đ°ĐšĐģŅ–Đ˛", + "cleanup_found_assets_with_size": "ЗĐŊаКдĐĩĐŊĐž {count} Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐ¸Ņ… ĐēĐžĐŋŅ–Đš Ņ„Đ°ĐšĐģŅ–Đ˛ ({size})", + "cleanup_icloud_shared_albums_excluded": "ĐĄĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи iCloud виĐēĐģŅŽŅ‡Đ°ŅŽŅ‚ŅŒŅŅ ĐˇŅ– ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "cleanup_no_assets_found": "НĐĩ СĐŊаКдĐĩĐŊĐž Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧ Đ˛Đ¸Ņ‰Đĩ ĐēŅ€Đ¸Ņ‚ĐĩŅ€Ņ–ŅĐŧ. Đ¤ŅƒĐŊĐēŅ†Ņ–Ņ ÂĢĐ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩÂģ ĐŧĐžĐļĐĩ видаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ Ņ„Đ°ĐšĐģи, Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— ŅĐēĐ¸Ņ… ĐąŅƒĐģĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ", + "cleanup_preview_title": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´ĐģŅ виĐģŅƒŅ‡ĐĩĐŊĐŊŅ ({count})", + "cleanup_step3_description": "ĐĄĐēаĐŊŅƒĐšŅ‚Đĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„Đ°ĐšĐģŅ–Đ˛, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ Đ˛Đ°ŅˆŅ–Đš Đ´Đ°Ņ‚Ņ–, Ņ‚Đ° СйĐĩŅ€ĐĩĐļŅ–Ņ‚ŅŒ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ.", + "cleanup_step4_summary": "{count} Ņ„Đ°ĐšĐģŅ–Đ˛ (ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐ¸Ņ… Đ´Đž {date}) Đ´ĐģŅ видаĐģĐĩĐŊĐŊŅ С Đ˛Đ°ŅˆĐžĐŗĐž ĐģĐžĐēаĐģҌĐŊĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ. Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— СаĐģĐ¸ŅˆĐ°Ņ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи Ņ–Đˇ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich.", + "cleanup_trash_hint": "ЊОй ĐŋОвĐŊŅ–ŅŅ‚ŅŽ ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ Đ´ĐģŅ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ, Đ˛Ņ–Đ´ĐēŅ€Đ¸ĐšŅ‚Đĩ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊ҃ ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ Ņ‚Đ° ĐžŅ‡Đ¸ŅŅ‚Ņ–Ņ‚ŅŒ ĐēĐžŅˆĐ¸Đē", "clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸", "clear_all": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "clear_all_recent_searches": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐžŅŅ‚Đ°ĐŊĐŊŅ– ĐŋĐžŅˆŅƒĐēĐžĐ˛Ņ– СаĐŋĐ¸Ņ‚Đ¸", "clear_file_cache": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐĩ҈ Ņ„Đ°ĐšĐģŅ–Đ˛", "clear_message": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", "clear_value": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ СĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", - "client_cert_dialog_msg_confirm": "ОĐē", + "client_cert_dialog_msg_confirm": "ОК", "client_cert_enter_password": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ", "client_cert_import": "ІĐŧĐŋĐžŅ€Ņ‚", "client_cert_import_success_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ Ņ–ĐŧĐŋĐžŅ€Ņ‚ĐžĐ˛Đ°ĐŊĐž", "client_cert_invalid_msg": "НĐĩĐ´Ņ–ĐšŅĐŊиК Ņ„Đ°ĐšĐģ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° айО ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК ĐŋĐ°Ņ€ĐžĐģҌ", + "client_cert_password_message": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Ņ†ŅŒĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", + "client_cert_password_title": "ĐŸĐ°Ņ€ĐžĐģҌ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ°", "client_cert_remove_msg": "КĐģŅ–Ņ”ĐŊŅ‚ŅŅŒĐēиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ видаĐģĐĩĐŊĐž", "client_cert_subtitle": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒŅ” ĐģĐ¸ŅˆĐĩ Ņ„ĐžŅ€ĐŧĐ°Ņ‚ PKCS12 (.p12, .pfx). ІĐŧĐŋĐžŅ€Ņ‚/видаĐģĐĩĐŊĐŊŅ ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ ĐģĐ¸ŅˆĐĩ ĐŋĐĩŅ€ĐĩĐ´ Đ˛Ņ…ĐžĐ´ĐžĐŧ ҃ ŅĐ¸ŅŅ‚ĐĩĐŧ҃", "client_cert_title": "SSL-ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ ĐēĐģŅ–Ņ”ĐŊŅ‚Đ° [ЕКСПЕРИМЕНĐĸАЛĐŦНИЙ]", @@ -745,15 +796,15 @@ "command": "КоĐŧаĐŊда", "comment_deleted": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€ видаĐģĐĩĐŊĐž", "comment_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", - "comments_and_likes": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ– Ņ‚Đ° ĐģаКĐēи", + "comments_and_likes": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ– Ņ‚Đ° вĐŋОдОйаĐŊĐŊŅ", "comments_are_disabled": "КоĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ– виĐŧĐēĐŊĐĩĐŊĐž", "common_create_new_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК аĐģŅŒĐąĐžĐŧ", "completed": "ЗавĐĩŅ€ŅˆĐĩĐŊĐž", - "confirm": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Ņ–Ņ‚ŅŒ", + "confirm": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸", "confirm_admin_password": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", - "confirm_delete_face": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ {name} С ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņƒ?", + "confirm_delete_face": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ {name} С Ņ†ŅŒĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ?", "confirm_delete_shared_link": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆Đĩ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ?", - "confirm_keep_this_delete_others": "ĐŖŅŅ– Ņ–ĐŊŅˆŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ в ҁ҂ĐĩĐē҃ ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž, ĐžĐēҀҖĐŧ Ņ†ŅŒĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅƒ. Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸?", + "confirm_keep_this_delete_others": "ĐŖŅŅ– Ņ–ĐŊŅˆŅ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ в ҁ҂ĐĩĐē҃ ĐąŅƒĐ´Đĩ видаĐģĐĩĐŊĐž, ĐžĐēҀҖĐŧ Ņ†ŅŒĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ. Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸?", "confirm_new_pin_code": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´ŅŒŅ‚Đĩ ĐŊОвиК PIN-ĐēОд", "confirm_password": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "confirm_tag_face": "БаĐļĐ°Ņ”Ņ‚Đĩ ĐŋОСĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ҆Đĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ ŅĐē {name}?", @@ -762,7 +813,7 @@ "connected_to": "ĐŸŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐž Đ´Đž", "contain": "ĐœŅ–ŅŅ‚Đ¸Ņ‚Đ¸", "context": "КоĐŊŅ‚ĐĩĐēҁ҂", - "continue": "ĐŸŅ€ĐžĐ´ĐžĐ˛ĐļŅƒĐšŅ‚Đĩ", + "continue": "ĐŸŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸", "control_bottom_app_bar_create_new_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК аĐģŅŒĐąĐžĐŧ", "control_bottom_app_bar_delete_from_immich": "ВидаĐģĐ¸Ņ‚Đ¸ С Immich", "control_bottom_app_bar_delete_from_local": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", @@ -771,7 +822,7 @@ "control_bottom_app_bar_share_link": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", "control_bottom_app_bar_share_to": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", "control_bottom_app_bar_trash_from_immich": "До ĐēĐžŅˆĐ¸Đēа", - "copied_image_to_clipboard": "КоĐŋŅ–ŅŽŅ”ĐŧĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃.", + "copied_image_to_clipboard": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃.", "copied_to_clipboard": "ĐĄĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃!", "copy_error": "ПоĐŧиĐģĐēа ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "copy_file_path": "ĐĄĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ ҈ĐģŅŅ… Đ´Đž Ņ„Đ°ĐšĐģ҃", @@ -787,31 +838,40 @@ "create_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "create_album_page_untitled": "БĐĩС ĐŊаСви", "create_api_key": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API", + "create_first_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐĩŅ€ŅˆĐ¸Đš Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "create_library": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "create_link": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "create_link_to_share": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", "create_link_to_share_description": "ДозвоĐģĐ¸Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ ĐąŅƒĐ´ŅŒ-ĐēĐžĐŧ҃", "create_new": "ĐĄĐĸВОРИĐĸИ НОВИЙ", "create_new_person": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", - "create_new_person_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊиĐŧ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Đŧ ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", + "create_new_person_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊиĐŧ Ņ„ĐžŅ‚Đž ĐŊĐžĐ˛Ņƒ ĐžŅĐžĐąŅƒ", "create_new_user": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛ĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "create_shared_album_page_share_add_assets": "ДОДАĐĸИ ЕЛЕМЕНĐĸИ", + "create_shared_album_page_share_add_assets": "ДОДАĐĸИ ФОĐĸО/ВІДЕО", "create_shared_album_page_share_select_photos": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "create_shared_link": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "create_tag": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ‚ĐĩĐŗ", "create_tag_description": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК Ņ‚ĐĩĐŗ. ДĐģŅ вĐēĐģадĐĩĐŊĐ¸Ņ… Ņ‚ĐĩĐŗŅ–Đ˛ вĐēаĐļŅ–Ņ‚ŅŒ ĐŋОвĐŊиК ҈ĐģŅŅ… Ņ‚ĐĩĐŗĐ°, вĐēĐģŅŽŅ‡Đ°ŅŽŅ‡Đ¸ ҁĐģĐĩŅˆŅ–.", "create_user": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "create_workflow": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", "created_at": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž", "creating_linked_albums": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŋĐžĐ˛â€™ŅĐˇĐ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛...", "crop": "ĐšĐ°Đ´Ņ€ŅƒĐ˛Đ°Ņ‚Đ¸", + "crop_aspect_ratio_fixed": "Đ¤Ņ–ĐēŅĐžĐ˛Đ°ĐŊĐĩ", + "crop_aspect_ratio_free": "Đ’Ņ–ĐģҌĐŊĐĩ", + "crop_aspect_ratio_original": "ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", "curated_object_page_title": "Đ Đĩ҇Җ", "current_device": "ĐŸĐžŅ‚ĐžŅ‡ĐŊиК ĐŋŅ€Đ¸ŅŅ‚Ņ€Ņ–Đš", "current_pin_code": "ĐŸĐžŅ‚ĐžŅ‡ĐŊиК PIN-ĐēОд", "current_server_address": "ĐŸĐžŅ‚ĐžŅ‡ĐŊа Đ°Đ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "custom_date": "ВĐģĐ°ŅĐŊа Đ´Đ°Ņ‚Đ°", "custom_locale": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Ņ€ĐĩĐŗŅ–ĐžĐŊ", "custom_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Đ¸ Ņ‚Đ° Ņ‡Đ¸ŅĐģа С ŅƒŅ€Đ°Ņ…ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ĐŧОви Ņ‚Đ° Ņ€ĐĩĐŗŅ–ĐžĐŊ҃", "custom_url": "ВĐģĐ°ŅĐŊа URL-Đ°Đ´Ņ€ĐĩŅĐ°", + "cutoff_date_description": "ЗбĐĩŅ€ĐĩĐļŅ–Ņ‚ŅŒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— С ĐžŅŅ‚Đ°ĐŊĐŊŅŒĐžĐŗĐžâ€Ļ", + "cutoff_day": "{count, plural, one {Đ´ĐĩĐŊҌ} few {Đ´ĐŊŅ–} many {Đ´ĐŊŅ–Đ˛} other {Đ´ĐŊŅ–Đ˛}}", + "cutoff_year": "{count, plural, one {ҀҖĐē} few {Ņ€ĐžĐēи} many {Ņ€ĐžĐēŅ–Đ˛} other {Ņ€ĐžĐēŅ–Đ˛}}", "daily_title_text_date": "Е, МММ Đ´Đ´", "daily_title_text_date_year": "Е, МММ Đ´Đ´, ҀҀҀҀ", "dark": "ĐĸĐĩĐŧĐŊа", @@ -830,17 +890,17 @@ "deduplication_criteria_2": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ даĐŊĐ¸Ņ… EXIF", "deduplication_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Đ´ĐĩĐ´ŅƒĐŋĐģŅ–ĐēĐ°Ņ†Ņ–ŅŽ", "deduplication_info_description": "ДĐģŅ Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐžĐŗĐž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Đ˛Đ¸ĐąĐžŅ€Ņƒ Ņ„Đ°ĐšĐģŅ–Đ˛ Ņ– ĐŧĐ°ŅĐžĐ˛ĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ Đŧи Đ˛Ņ€Đ°Ņ…ĐžĐ˛ŅƒŅ”ĐŧĐž:", - "default_locale": "Đ”Đ°Ņ‚Đ° Ņ– Ņ‡Đ°Ņ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ", + "default_locale": "Мова Ņ‚Đ° Ņ€ĐĩĐŗŅ–ĐžĐŊ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ", "default_locale_description": "Đ¤ĐžŅ€ĐŧĐ°Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Đ¸ Ņ‚Đ° Ņ‡Đ¸ŅĐģа С ŅƒŅ€Đ°Ņ…ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ ĐŧОви Đ˛Đ°ŅˆĐžĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", "delete": "ВидаĐģĐ¸Ņ‚Đ¸", "delete_action_confirmation_message": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ? Đ™ĐžĐŗĐž ĐąŅƒĐ´Đĩ ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ, а Ņ‚Đ°ĐēĐžĐļ СĘŧŅĐ˛Đ¸Ņ‚ŅŒŅŅ СаĐŋĐ¸Ņ‚ ĐŊа ĐšĐžĐŗĐž видаĐģĐĩĐŊĐŊŅ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_action_prompt": "{count} видаĐģĐĩĐŊĐž", + "delete_action_prompt": "ВидаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "delete_album": "ВидаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "delete_api_key_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš ĐēĐģŅŽŅ‡ API?", - "delete_dialog_alert": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich Ņ‚Đ° Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_dialog_alert_local": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich", - "delete_dialog_alert_local_non_backed_up": "ДĐĩŅĐēŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŊĐĩ ĐąŅƒĐģи СйĐĩŅ€ĐĩĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich Ņ– ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", - "delete_dialog_alert_remote": "ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊаСавĐļди видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich", + "delete_dialog_alert": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich Ņ‚Đ° Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_dialog_alert_local": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, аĐģĐĩ СаĐģĐ¸ŅˆĐ°Ņ‚ŅŒŅŅ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиĐŧи ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich", + "delete_dialog_alert_local_non_backed_up": "ДĐĩŅĐēŅ– Ņ„Đ°ĐšĐģи ĐŊĐĩ ĐąŅƒĐģи СйĐĩŅ€ĐĩĐļĐĩĐŊŅ– ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ Immich Ņ– ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_dialog_alert_remote": "ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐŊаСавĐļди видаĐģĐĩĐŊŅ– С ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ Immich", "delete_dialog_ok_force": "Đ’ŅĐĩ ОдĐŊĐž видаĐģĐ¸Ņ‚Đ¸", "delete_dialog_title": "ВидаĐģĐ¸Ņ‚Đ¸ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž", "delete_duplicates_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ ҆Җ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸?", @@ -848,28 +908,29 @@ "delete_key": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡", "delete_library": "ВидаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "delete_link": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "delete_local_action_prompt": "{count} видаĐģĐĩĐŊĐž С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "delete_local_action_prompt": "ВидаĐģĐĩĐŊĐž С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "delete_local_dialog_ok_backed_up_only": "ВидаĐģĐ¸Ņ‚Đ¸ ĐģĐ¸ŅˆĐĩ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ—", "delete_local_dialog_ok_force": "Đ’ŅĐĩ ОдĐŊĐž видаĐģĐ¸Ņ‚Đ¸", "delete_others": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", "delete_permanently": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŊаСавĐļди", - "delete_permanently_action_prompt": "{count} видаĐģĐĩĐŊĐž ĐŊаСавĐļди", + "delete_permanently_action_prompt": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "delete_shared_link": "ВидаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "delete_shared_link_dialog_title": "ВидаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "delete_tag": "ВидаĐģĐ¸Ņ‚Đ¸ ĐĸĐĩĐŗ", "delete_tag_confirmation_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ‚ĐĩĐŗ {tagName}?", "delete_user": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "deleted_shared_link": "ВидаĐģĐĩĐŊĐž ĐˇĐ°ĐŗĐ°ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "deletes_missing_assets": "ВидаĐģŅŅ” Ņ€ĐĩŅŅƒŅ€ŅĐ¸, ŅĐēŅ– Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– ĐŊа Đ´Đ¸ŅĐē҃", + "deleted_shared_link": "ВидаĐģĐĩĐŊĐž ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "deletes_missing_assets": "ВидаĐģŅŅ” Ņ„Đ°ĐšĐģи, ŅĐēŅ– Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– ĐŊа Đ´Đ¸ŅĐē҃", "description": "ОĐŋĐ¸Ņ", "description_input_hint_text": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ...", - "description_input_submit_error": "ПоĐŧиĐģĐēа ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐžĐŋĐ¸ŅŅƒ, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐģĐžĐŗĐ¸ Đ´ĐģŅ ĐŋĐžĐ´Ņ€ĐžĐąĐ¸Ņ†ŅŒ", + "description_input_submit_error": "ПоĐŧиĐģĐēа ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐžĐŋĐ¸ŅŅƒ, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐļŅƒŅ€ĐŊаĐģ Đ´ĐģŅ ĐŋĐžĐ´Ņ€ĐžĐąĐ¸Ņ†ŅŒ", "deselect_all": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ ŅƒŅŅ–Ņ…", - "details": "ПОДРОБИĐĻІ", + "details": "ДĐĩŅ‚Đ°ĐģŅ–", "direction": "НаĐŋŅ€ŅĐŧ", + "disable": "ВиĐŧĐēĐŊŅƒŅ‚Đ¸", "disabled": "ВиĐŧĐēĐŊĐĩĐŊĐž", "disallow_edits": "Đ—Đ°ĐąĐžŅ€ĐžĐŊĐ¸Ņ‚Đ¸ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°ĐŊĐŊŅ", - "discord": "Discord'", + "discord": "Discord", "discover": "Đ’Đ¸ŅĐ˛Đ¸Ņ‚Đ¸", "discovered_devices": "Đ’Đ¸ŅĐ˛ĐģĐĩĐŊŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "dismiss_all_errors": "ĐŸŅ€ĐžĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ĐŋĐžĐŧиĐģĐēи", @@ -877,11 +938,11 @@ "display_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "display_order": "ĐŸĐžŅ€ŅĐ´ĐžĐē Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "display_original_photos": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐ¸Ņ… Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš", - "display_original_photos_setting_description": "ПĐĩŅ€ĐĩĐ˛Đ°ĐŗĐ° Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– Ņ€ĐĩŅŅƒŅ€ŅŅƒ, ŅĐēŅ‰Đž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ ҁ҃ĐŧҖҁĐŊиК С вĐĩйОĐŧ. ĐĻĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐžĐŗĐž Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš.", + "display_original_photos_setting_description": "ĐĐ°Đ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅŽ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž ĐŋŅ€Đ¸ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—, ŅĐēŅ‰Đž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Ņ„ĐžŅ‚Đž ҁ҃ĐŧҖҁĐŊĐĩ С вĐĩйОĐŧ. ĐĻĐĩ ĐŧĐžĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐŋĐžĐ˛Ņ–ĐģҌĐŊŅ–ŅˆĐžĐŗĐž Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš.", "do_not_show_again": "НĐĩ ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ҆Đĩ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ СĐŊĐžĐ˛Ņƒ", "documentation": "ДоĐē҃ĐŧĐĩĐŊŅ‚Đ°Ņ†Ņ–Ņ", "done": "Đ“ĐžŅ‚ĐžĐ˛Đž", - "download": "ĐĄĐēĐ°Ņ‡Đ°Ņ‚Đ¸", + "download": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", "download_action_prompt": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ {count} Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "download_canceled": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҁĐēĐ°ŅĐžĐ˛Đ°ĐŊĐž", "download_complete": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СаĐēŅ–ĐŊ҇ĐĩĐŊĐž", @@ -890,29 +951,31 @@ "download_failed": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ вдаĐģĐžŅŅ", "download_finished": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СаĐēŅ–ĐŊ҇ĐĩĐŊĐž", "download_include_embedded_motion_videos": "Đ’ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž", - "download_include_embedded_motion_videos_description": "ВĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž, Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– в Ņ€ŅƒŅ…ĐžĐŧŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—, ŅĐē ĐžĐēŅ€ĐĩĐŧиК Ņ„Đ°ĐšĐģ", + "download_include_embedded_motion_videos_description": "ВĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž, Đ˛ĐąŅƒĐ´ĐžĐ˛Đ°ĐŊŅ– в Ņ€ŅƒŅ…ĐžĐŧŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—, ŅĐē ĐžĐēŅ€ĐĩĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", "download_notfound": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐž", + "download_original": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", "download_paused": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐž", - "download_settings": "ĐĄĐēĐ°Ņ‡Đ°Ņ‚Đ¸", - "download_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи, ĐŋОв'ŅĐˇĐ°ĐŊиĐŧи С СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅĐŧ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "download_settings": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", + "download_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи, ĐŋОв'ŅĐˇĐ°ĐŊиĐŧи С СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅĐŧ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "download_started": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Ņ€ĐžĐˇĐŋĐžŅ‡Đ°Ņ‚Đž", "download_sucess": "ĐŖŅĐŋŅ–ŅˆĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "download_sucess_android": "МĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģи СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž в DCIM/Immich", + "download_sucess_android": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž в DCIM/Immich", "download_waiting_to_retry": "ĐžŅ‡Ņ–ĐēŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžŅ— ҁĐŋŅ€ĐžĐąĐ¸", - "downloading": "ĐĄĐēĐ°Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅ", - "downloading_asset_filename": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅƒ {filename}", + "downloading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "downloading_asset_filename": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģ҃ {filename}", + "downloading_from_icloud": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ С iCloud", "downloading_media": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐĩĐ´Ņ–Đ°", - "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅŅ–Ņ‚ŅŒ Ņ„Đ°ĐšĐģи в ĐąŅƒĐ´ŅŒ-ŅĐēĐĩ ĐŧҖҁ҆Đĩ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "drop_files_to_upload": "ПĐĩŅ€ĐĩĐŊĐĩŅŅ–Ņ‚ŅŒ Ņ„Đ°ĐšĐģи в ĐąŅƒĐ´ŅŒ-ŅĐēĐĩ ĐŧҖҁ҆Đĩ Đ´ĐģŅ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "duplicates": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "duplicates_description": "ВизĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸, ŅĐēŅ– ĐŗŅ€ŅƒĐŋи Ņ” Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ°Đŧи", "duration": "ĐĸŅ€Đ¸Đ˛Đ°ĐģŅ–ŅŅ‚ŅŒ", - "edit": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸", + "edit": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸", "edit_album": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "edit_avatar": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ°Đ˛Đ°Ņ‚Đ°Ņ€", "edit_birthday": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", "edit_date": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", "edit_date_and_time": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ", - "edit_date_and_time_action_prompt": "{count} Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ СĐŧŅ–ĐŊĐĩĐŊĐž", + "edit_date_and_time_action_prompt": "ЗĐŧŅ–ĐŊĐĩĐŊĐž Đ´Đ°Ņ‚Ņƒ Ņ‚Đ° Ņ‡Đ°Ņ ҃ {count, plural, one {# Ņ„Đ°ĐšĐģŅ–} few {# Ņ„Đ°ĐšĐģĐ°Ņ…} other {# Ņ„Đ°ĐšĐģĐ°Ņ…}}", "edit_date_and_time_by_offset": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Са СĐŧҖ҉ĐĩĐŊĐŊŅĐŧ", "edit_date_and_time_by_offset_interval": "Новий Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚: {from} - {to}", "edit_description": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", @@ -929,16 +992,27 @@ "edit_tag": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗ", "edit_title": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐ°ĐŗĐžĐģОвОĐē", "edit_user": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "edit_workflow": "Đ ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "editor_close_without_save_prompt": "ЗĐŧŅ–ĐŊи ĐŊĐĩ ĐąŅƒĐ´ŅƒŅ‚ŅŒ СйĐĩŅ€ĐĩĐļĐĩĐŊŅ–", "editor_close_without_save_title": "ЗаĐēŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩдаĐēŅ‚ĐžŅ€?", - "editor_crop_tool_h2_aspect_ratios": "ĐŸŅ€ĐžĐŋĐžŅ€Ņ†Ņ–Ņ— ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "editor_crop_tool_h2_rotation": "ĐžŅ€Ņ–Ņ”ĐŊŅ‚Đ°Ņ†Ņ–Ņ", + "editor_confirm_reset_all_changes": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ– СĐŧŅ–ĐŊи?", + "editor_discard_edits_confirm": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", + "editor_discard_edits_prompt": "ĐŖ Đ˛Đ°Ņ Ņ” ĐŊĐĩСйĐĩŅ€ĐĩĐļĐĩĐŊŅ– СĐŧŅ–ĐŊи. Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ņ—Ņ… ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸?", + "editor_discard_edits_title": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи?", + "editor_edits_applied_error": "НĐĩ вдаĐģĐžŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", + "editor_edits_applied_success": "ЗĐŧŅ–ĐŊи ҃ҁĐŋŅ–ŅˆĐŊĐž ĐˇĐ°ŅŅ‚ĐžŅĐžĐ˛Đ°ĐŊĐž", + "editor_flip_horizontal": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐˇĐ¸Ņ‚Đ¸ ĐŗĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊĐž", + "editor_flip_vertical": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐˇĐ¸Ņ‚Đ¸ вĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊĐž", + "editor_orientation": "ĐžŅ€Ņ–Ņ”ĐŊŅ‚Đ°Ņ†Ņ–Ņ", + "editor_reset_all_changes": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ СĐŧŅ–ĐŊи", + "editor_rotate_left": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ ĐŊа 90° ĐŋŅ€ĐžŅ‚Đ¸ ĐŗĐžĐ´Đ¸ĐŊĐŊиĐēĐžĐ˛ĐžŅ— ҁ҂ҀҖĐģĐēи", + "editor_rotate_right": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ ĐŊа 90° Са ĐŗĐžĐ´Đ¸ĐŊĐŊиĐēĐžĐ˛ĐžŅŽ ҁ҂ҀҖĐģĐēĐžŅŽ", "email": "ЕĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊа ĐŋĐžŅˆŅ‚Đ°", "email_notifications": "ĐĄĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ ĐĩĐģ. ĐŋĐžŅˆŅ‚ĐžŅŽ", "empty_folder": "ĐĻŅ ĐŋаĐŋĐēа ĐŋĐžŅ€ĐžĐļĐŊŅ", "empty_trash": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē", - "empty_trash_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻĐĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ в ĐēĐžŅˆĐ¸Đē҃ С Immich.\nĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", + "empty_trash_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻĐĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚ŅŒ Đ˛ŅŅ– Ņ„Đ°ĐšĐģи ҃ ĐēĐžŅˆĐ¸Đē҃ С Immich.\nĐĻŅŽ Đ´Ņ–ŅŽ ĐŊĐĩ ĐŧĐžĐļĐŊа ҁĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸!", "enable": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸", "enable_backup": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐĩ ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "enable_biometric_auth_description": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš PIN-ĐēОд, Ņ‰ĐžĐą ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Đ¸Ņ‡ĐŊ҃ Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–ŅŽ", @@ -950,45 +1024,48 @@ "enter_your_pin_code_subtitle": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ŅĐ˛Ņ–Đš PIN-ĐēОд, Ņ‰ĐžĐą ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "error": "ПоĐŧиĐģĐēа", "error_change_sort_album": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžŅ€ŅĐ´ĐžĐē ŅĐžŅ€Ņ‚ŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃", - "error_delete_face": "ПоĐŧиĐģĐēа ĐŋŅ€Đ¸ видаĐģĐĩĐŊĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ С ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņƒ", + "error_delete_face": "ПоĐŧиĐģĐēа ĐŋŅ€Đ¸ видаĐģĐĩĐŊĐŊŅ– ОйĐģĐ¸Ņ‡Ņ‡Ņ С Ņ„Đ°ĐšĐģ҃", "error_getting_places": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐŧŅ–ŅŅ†ŅŒ", + "error_loading_albums": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ аĐģŅŒĐąĐžĐŧŅ–Đ˛", "error_loading_image": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "error_loading_partners": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛: {error}", + "error_retrieving_asset_information": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ„Đ°ĐšĐģ", "error_saving_image": "ПоĐŧиĐģĐēа: {error}", "error_tag_face_bounding_box": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋОСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡Ņ‡Ņ – ĐŊĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸ Ņ€Đ°ĐŧĐēи", "error_title": "ПоĐŧиĐģĐēа: Ņ‰ĐžŅŅŒ ĐŋŅ–ŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", + "error_while_navigating": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩŅ…ĐžĐ´Ņƒ Đ´Đž Ņ„Đ°ĐšĐģ҃", "errors": { - "cannot_navigate_next_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅƒ", - "cannot_navigate_previous_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ€ĐĩŅŅƒŅ€ŅŅƒ", + "cannot_navigate_next_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", + "cannot_navigate_previous_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", "cant_apply_changes": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи", "cant_change_activity": "НĐĩ ĐŧĐžĐļĐŊа {enabled, select, true {виĐŧĐēĐŊŅƒŅ‚Đ¸} other {ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸}} аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ", - "cant_change_asset_favorite": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ Đ´ĐģŅ Ņ€ĐĩŅŅƒŅ€ŅŅƒ", - "cant_change_metadata_assets_count": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– {count, plural, one {# Ņ€ĐĩŅŅƒŅ€ŅŅƒ} few {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", + "cant_change_asset_favorite": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ Đ´ĐģŅ Ņ„Đ°ĐšĐģ҃", + "cant_change_metadata_assets_count": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊŅ– {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "cant_get_faces": "НĐĩ ĐŧĐžĐļ҃ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊĐ°Ņ‚Đ¸ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "cant_get_number_of_comments": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐēŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€Ņ–Đ˛", "cant_search_people": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē ĐģŅŽĐ´ĐĩĐš", "cant_search_places": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ виĐēĐžĐŊĐ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē ĐŧŅ–ŅŅ†ŅŒ", - "error_adding_assets_to_album": "ПоĐŧиĐģĐēа дОдаваĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", + "error_adding_assets_to_album": "ПоĐŧиĐģĐēа дОдаваĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "error_adding_users_to_album": "ПоĐŧиĐģĐēа дОдаваĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "error_deleting_shared_user": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐˇŅ– ĐˇĐ°ĐŗĐ°ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ", + "error_deleting_shared_user": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ видаĐģĐĩĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ", "error_downloading": "ПоĐŧиĐģĐēа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ {filename}", "error_hiding_buy_button": "ПоĐŧиĐģĐēа ĐŋŅ€Đ¸ ҁĐŋŅ€ĐžĐąŅ– ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐēĐŊĐžĐŋĐē҃ ĐŋĐžĐē҃ĐŋĐēи", - "error_removing_assets_from_album": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐēĐžĐŊŅĐžĐģҌ Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Đ˛Ņ–Đ´ĐžĐŧĐžŅŅ‚ĐĩĐš", - "error_selecting_all_assets": "ПоĐŧиĐģĐēа Đ˛Đ¸ĐąĐžŅ€Ņƒ Đ˛ŅŅ–Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "error_removing_assets_from_album": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ ĐēĐžĐŊŅĐžĐģҌ Đ´ĐģŅ ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Đ˛Ņ–Đ´ĐžĐŧĐžŅŅ‚ĐĩĐš", + "error_selecting_all_assets": "ПоĐŧиĐģĐēа Đ˛Đ¸ĐąĐžŅ€Ņƒ Đ˛ŅŅ–Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "exclusion_pattern_already_exists": "ĐĻĐĩĐš ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ вĐļĐĩ ҖҁĐŊŅƒŅ”.", "failed_to_create_album": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "failed_to_create_shared_link": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "failed_to_edit_shared_link": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "failed_to_get_people": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ŅŽ ĐŋŅ€Đž ĐģŅŽĐ´ĐĩĐš", - "failed_to_keep_this_delete_others": "НĐĩ вдаĐģĐžŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ҆ĐĩĐš Ņ€ĐĩŅŅƒŅ€Ņ Ņ– видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", - "failed_to_load_asset": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ", - "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸", + "failed_to_keep_this_delete_others": "НĐĩ вдаĐģĐžŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ Ņ– видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ– Ņ„Đ°ĐšĐģи", + "failed_to_load_asset": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", + "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", "failed_to_load_notifications": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", "failed_to_load_people": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "failed_to_remove_product_key": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ", "failed_to_reset_pin_code": "НĐĩ вдаĐģĐžŅŅ ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд", - "failed_to_stack_assets": "НĐĩ вдаĐģĐžŅŅ ĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸", - "failed_to_unstack_assets": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸", + "failed_to_stack_assets": "НĐĩ вдаĐģĐžŅŅ ĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ„Đ°ĐšĐģи", + "failed_to_unstack_assets": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ Ņ„Đ°ĐšĐģи", "failed_to_update_notification_status": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ", "incorrect_email_or_password": "НĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊа Đ°Đ´Ņ€ĐĩŅĐ° ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊĐžŅ— ĐŋĐžŅˆŅ‚Đ¸ айО ĐŋĐ°Ņ€ĐžĐģҌ", "library_folder_already_exists": "ĐĻĐĩĐš ҈ĐģŅŅ… Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ вĐļĐĩ ҖҁĐŊŅƒŅ”.", @@ -997,33 +1074,35 @@ "quota_higher_than_disk_size": "Ви Đ˛ŅŅ‚Đ°ĐŊОвиĐģи ĐēĐ˛ĐžŅ‚Ņƒ, Ņ‰Đž ĐŋĐĩŅ€ĐĩĐ˛Đ¸Ņ‰ŅƒŅ” Ņ€ĐžĐˇĐŧŅ–Ņ€ Đ´Đ¸ŅĐēа", "something_went_wrong": "ĐŠĐžŅŅŒ ĐŋŅ–ŅˆĐģĐž ĐŊĐĩ Ņ‚Đ°Đē", "unable_to_add_album_users": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃", - "unable_to_add_assets_to_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_add_assets_to_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Đ´Đž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "unable_to_add_comment": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐēĐžĐŧĐĩĐŊŅ‚Đ°Ņ€", "unable_to_add_exclusion_pattern": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", "unable_to_add_partners": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Ņ–Đ˛", - "unable_to_add_remove_archive": "НĐĩĐŧĐžĐļĐģивО {archived, select, true {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ Ņ–Đˇ} other {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ Đ´Đž}} Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", - "unable_to_add_remove_favorites": "НĐĩĐŧĐžĐļĐģивО {favorite, select, true {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ Đ´Đž} other {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ Ņ–Đˇ}} ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…", + "unable_to_add_remove_archive": "НĐĩĐŧĐžĐļĐģивО {archived, select, true {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ–Đˇ} other {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Đ´Đž}} Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "unable_to_add_remove_favorites": "НĐĩĐŧĐžĐļĐģивО {favorite, select, true {Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Đ´Đž} other {виĐģŅƒŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ Ņ–Đˇ}} ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…", "unable_to_archive_unarchive": "НĐĩĐŧĐžĐļĐģивО {archived, select, true {Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸} other {Ņ€ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸}}", "unable_to_change_album_user_role": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Ņ€ĐžĐģҌ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° аĐģŅŒĐąĐžĐŧ҃", "unable_to_change_date": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ", "unable_to_change_description": "НĐĩ вдаĐģĐžŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐžĐŋĐ¸Ņ", - "unable_to_change_favorite": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž Đ´ĐģŅ Ņ€ĐĩŅŅƒŅ€ŅŅƒ", + "unable_to_change_favorite": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ŅŅ‚Đ°Ņ‚ŅƒŅ ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž Đ´ĐģŅ Ņ„Đ°ĐšĐģ҃", "unable_to_change_location": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", "unable_to_change_password": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "unable_to_change_visibility": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ видиĐŧŅ–ŅŅ‚ŅŒ Đ´ĐģŅ {count, plural, one {# ĐžŅĐžĐąĐ¸} few {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", "unable_to_complete_oauth_login": "НĐĩĐŧĐžĐļĐģивО СавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´ ҇ĐĩŅ€ĐĩС OAuth", "unable_to_connect": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ŅŅ", - "unable_to_copy_to_clipboard": "НĐĩĐŧĐžĐļĐģивО ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐˇĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đĩ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ ҇ĐĩŅ€ĐĩС HTTPS", + "unable_to_copy_to_clipboard": "НĐĩĐŧĐžĐļĐģивО ҁĐēĐžĐŋŅ–ŅŽĐ˛Đ°Ņ‚Đ¸ в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐˇĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đĩ ĐŊа ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃ ҇ĐĩŅ€ĐĩС https", + "unable_to_create": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "unable_to_create_admin_account": "НĐĩĐŧĐžĐļĐģивО ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€Đ°", "unable_to_create_api_key": "НĐĩĐŧĐžĐļĐģивО ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК ĐēĐģŅŽŅ‡ API", "unable_to_create_library": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "unable_to_create_user": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_delete_album": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", - "unable_to_delete_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ", - "unable_to_delete_assets": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "unable_to_delete_asset": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", + "unable_to_delete_assets": "ПоĐŧиĐģĐēа видаĐģĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛", "unable_to_delete_exclusion_pattern": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", "unable_to_delete_shared_link": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊĐĩ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "unable_to_delete_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", + "unable_to_delete_workflow": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "unable_to_download_files": "НĐĩĐŧĐžĐļĐģивО СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", "unable_to_edit_exclusion_pattern": "НĐĩ вдаĐģĐžŅŅ Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ŅˆĐ°ĐąĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", "unable_to_empty_trash": "НĐĩĐŧĐžĐļĐģивО ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē", @@ -1038,19 +1117,19 @@ "unable_to_log_out_device": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸ĐšŅ‚Đ¸ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "unable_to_login_with_oauth": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ OAuth", "unable_to_play_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Đ˛Ņ–Đ´ĐĩĐž", - "unable_to_reassign_assets_existing_person": "НĐĩ вдаĐģĐžŅŅ ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", - "unable_to_reassign_assets_new_person": "НĐĩĐŧĐžĐļĐģивО ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", + "unable_to_reassign_assets_existing_person": "НĐĩ вдаĐģĐžŅŅ ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", + "unable_to_reassign_assets_new_person": "НĐĩĐŧĐžĐļĐģивО ĐŋĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", "unable_to_refresh_user": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_remove_album_users": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛ С аĐģŅŒĐąĐžĐŧ҃", "unable_to_remove_api_key": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐēĐģŅŽŅ‡ API", - "unable_to_remove_assets_from_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", + "unable_to_remove_assets_from_shared_link": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "unable_to_remove_library": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "unable_to_remove_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ видаĐģĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", "unable_to_remove_reaction": "НĐĩ вдаĐģĐžŅŅ видаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩаĐēŅ†Ņ–ŅŽ", "unable_to_reset_password": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ҁĐēиĐŊŅƒŅ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "unable_to_reset_pin_code": "НĐĩĐŧĐžĐļĐģивО ҁĐēиĐŊŅƒŅ‚Đ¸ PIN-ĐēОд", "unable_to_resolve_duplicate": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Đ¸Ņ€Ņ–ŅˆĐ¸Ņ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚", - "unable_to_restore_assets": "НĐĩĐŧĐžĐļĐģивО Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "unable_to_restore_assets": "НĐĩĐŧĐžĐļĐģивО Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", "unable_to_restore_trash": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ вĐŧҖҁ҂", "unable_to_restore_user": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "unable_to_save_album": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ", @@ -1063,8 +1142,9 @@ "unable_to_scan_library": "НĐĩ вдаĐģĐžŅŅ ĐŋŅ€ĐžŅĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", "unable_to_set_feature_photo": "НĐĩ вдаĐģĐžŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅŽ ĐŊа ОйĐēĐģадиĐŊĐē҃", "unable_to_set_profile_picture": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", - "unable_to_submit_job": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´ĐŋŅ€Đ°Đ˛Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ", - "unable_to_trash_asset": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚", + "unable_to_set_rating": "НĐĩ вдаĐģĐžŅŅ Đ˛ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", + "unable_to_submit_job": "НĐĩ вдаĐģĐžŅŅ ĐŊĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ СавдаĐŊĐŊŅ", + "unable_to_trash_asset": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", "unable_to_unlink_account": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´Đ˛'ŅĐˇĐ°Ņ‚Đ¸ ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ", "unable_to_unlink_motion_video": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ˛Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", "unable_to_update_album_cover": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", @@ -1074,13 +1154,15 @@ "unable_to_update_settings": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "unable_to_update_timeline_display_status": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅŅ‚Đ°ĐŊ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҈ĐēаĐģи Ņ‡Đ°ŅŅƒ", "unable_to_update_user": "НĐĩĐŧĐžĐļĐģивО ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ даĐŊŅ– ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "unable_to_upload_file": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ" + "unable_to_update_workflow": "НĐĩ вдаĐģĐžŅŅ ĐžĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "unable_to_upload_file": "НĐĩ вдаĐģĐžŅŅ виваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ" }, + "errors_text": "ПоĐŧиĐģĐēи", "exclusion_pattern": "ШайĐģĐžĐŊ виĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", - "exif": "Exif'", + "exif": "Exif", "exif_bottom_sheet_description": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐžĐŋĐ¸Ņ...", "exif_bottom_sheet_description_error": "ПоĐŧиĐģĐēа ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐžĐŋĐ¸ŅŅƒ", - "exif_bottom_sheet_details": "ПОДРОБИĐĻІ", + "exif_bottom_sheet_details": "ДĐĩŅ‚Đ°ĐģŅ–", "exif_bottom_sheet_location": "МІСĐĻЕ", "exif_bottom_sheet_no_description": "БĐĩС ĐžĐŋĐ¸ŅŅƒ", "exif_bottom_sheet_people": "ЛЮДИ", @@ -1109,47 +1191,54 @@ "failed": "НĐĩ вдаĐģĐžŅŅ", "failed_count": "НĐĩ вдаĐģĐžŅŅ: {count}", "failed_to_authenticate": "ПоĐŧиĐģĐēа Đ°Đ˛Ņ‚ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ†Ņ–Ņ—", - "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€ŅĐ¸", + "failed_to_load_assets": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", "failed_to_load_folder": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŋаĐŋĐē҃", - "favorite": "До ҃ĐģŅŽĐąĐģĐĩĐŊĐ¸Ņ…", + "favorite": "До ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "favorite_action_prompt": "{count} дОдаĐŊĐž Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "favorite_or_unfavorite_photo": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… айО видаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž", - "favorites": "ĐŖĐģŅŽĐąĐģĐĩĐŊŅ–", - "favorites_page_no_favorites": "НĐĩĐŧĐ°Ņ” ҃ĐģŅŽĐąĐģĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "favorites": "ĐžĐąŅ€Đ°ĐŊĐĩ", + "favorites_page_no_favorites": "НĐĩĐŧĐ°Ņ” ĐžĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "feature_photo_updated": "Đ’Đ¸ĐąŅ€Đ°ĐŊĐĩ Ņ„ĐžŅ‚Đž ĐžĐŊОвĐģĐĩĐŊĐž", "features": "Đ”ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Ņ– ĐŧĐžĐļĐģĐ¸Đ˛ĐžŅŅ‚Ņ–", "features_in_development": "Đ¤ŅƒĐŊĐē҆Җҗ в Ņ€ĐžĐˇŅ€ĐžĐąŅ†Ņ–", "features_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ´ĐžĐ´Đ°Ņ‚ĐēОвиĐŧи ĐŧĐžĐļĐģĐ¸Đ˛ĐžŅŅ‚ŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", - "file_name": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃", "file_name_or_extension": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃ айО Ņ€ĐžĐˇŅˆĐ¸Ņ€ĐĩĐŊĐŊŅ", + "file_name_text": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃", + "file_name_with_value": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃: {file_name}", "file_size": "РОСĐŧŅ–Ņ€ Ņ„Đ°ĐšĐģ҃", "filename": "ІĐŧ'Ņ Ņ„Đ°ĐšĐģ҃", "filetype": "ĐĸиĐŋ Ņ„Đ°ĐšĐģ҃", "filter": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€", - "filter_people": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ ĐŋĐž ĐģŅŽĐ´ŅŅ…", - "filter_places": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ ĐŋĐž ĐŧŅ–ŅŅ†ŅŅ…", + "filter_description": "ĐŖĐŧОви Đ´ĐģŅ ҄ҖĐģŅŒŅ‚Ņ€Đ°Ņ†Ņ–Ņ— ҆ҖĐģŅŒĐžĐ˛Đ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", + "filter_people": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ Са ĐģŅŽĐ´ŅŒĐŧи", + "filter_places": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€ Са ĐŧŅ–ŅŅ†ŅĐŧи", + "filters": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€Đ¸", "find_them_fast": "ШвидĐēĐž СĐŊĐ°Ņ…ĐžĐ´ŅŒŅ‚Đĩ Ņ—Ņ… Са ĐŊĐ°ĐˇĐ˛ĐžŅŽ Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŋĐžŅˆŅƒĐē҃", "first": "ПĐĩŅ€ŅˆĐ¸Đš", "fix_incorrect_match": "ВиĐŋŅ€Đ°Đ˛Đ¸Ņ‚Đ¸ ĐŊĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК ĐˇĐąŅ–Đŗ", "folder": "ПаĐŋĐēа", "folder_not_found": "ПаĐŋĐē҃ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "folders": "ПаĐŋĐēи", - "folders_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐŋаĐŋĐžĐē Đ´ĐģŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž ҃ Ņ„Đ°ĐšĐģĐžĐ˛Ņ–Đš ŅĐ¸ŅŅ‚ĐĩĐŧŅ–", + "folders_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ĐŋаĐŋĐžĐē С Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ҃ Ņ„Đ°ĐšĐģĐžĐ˛Ņ–Đš ŅĐ¸ŅŅ‚ĐĩĐŧŅ–", "forgot_pin_code_question": "Đ—Đ°ĐąŅƒĐģи ŅĐ˛Ņ–Đš PIN-ĐēОд?", "forward": "ПĐĩŅ€ĐĩҁĐģĐ°Ņ‚Đ¸", + "free_up_space": "Đ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ", + "free_up_space_description": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Ņ–Ņ‚ŅŒ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа Đ˛Đ°ŅˆĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ, Ņ‰ĐžĐą ĐˇĐ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŧҖҁ҆Đĩ. Đ’Đ°ŅˆŅ– ĐēĐžĐŋŅ–Ņ— ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ СаĐģĐ¸ŅˆĐ°ŅŽŅ‚ŅŒŅŅ в ĐąĐĩСĐŋĐĩ҆Җ.", + "free_up_space_settings_subtitle": "Đ—Đ˛Ņ–ĐģҌĐŊĐ¸Ņ‚Đ¸ ĐŋаĐŧ'ŅŅ‚ŅŒ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "full_path": "ПовĐŊиК ҈ĐģŅŅ…: {path}", - "gcast_enabled": "Google Cast'", + "gcast_enabled": "Google Cast", "gcast_enabled_description": "ĐĻŅ Ņ„ŅƒĐŊĐēŅ†Ņ–Ņ СаваĐŊŅ‚Đ°ĐļŅƒŅ” СОвĐŊŅ–ŅˆĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ С Google Đ´ĐģŅ ŅĐ˛ĐžŅ”Ņ— Ņ€ĐžĐąĐžŅ‚Đ¸.", "general": "Đ—Đ°ĐŗĐ°ĐģҌĐŊŅ–", - "geolocation_instruction_location": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ĐŊа Ой'Ņ”ĐēŅ‚ Ņ–Đˇ GPS-ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ°Đŧи, Ņ‰ĐžĐą виĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ, айО вийĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž ĐŊа ĐēĐ°Ņ€Ņ‚Ņ–", + "geolocation_instruction_location": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ĐŊа Ņ„Đ°ĐšĐģ Ņ–Đˇ ĐŗĐĩОдаĐŊиĐŧи, Ņ‰ĐžĐą виĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ, айО вийĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž ĐŊа ĐŧаĐŋŅ–", "get_help": "ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ´ĐžĐŋĐžĐŧĐžĐŗŅƒ", + "get_people_error": "ПоĐŧиĐģĐēа ĐžŅ‚Ņ€Đ¸ĐŧаĐŊĐŊŅ ĐģŅŽĐ´ĐĩĐš", "get_wifiname_error": "НĐĩ вдаĐģĐžŅŅ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŊĐ°ĐˇĐ˛Ņƒ Wi-Fi. ПĐĩŅ€ĐĩĐēĐžĐŊĐ°ĐšŅ‚ĐĩŅŅ, Ņ‰Đž ви ĐŊадаĐģи ĐŊĐĩĐžĐąŅ…Ņ–Đ´ĐŊŅ– дОСвОĐģи Ņ‚Đ° ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊŅ– Đ´Đž Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", "getting_started": "ĐŸĐžŅ‡Đ°Ņ‚ĐžĐē", "go_back": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅ ĐŊаСад", "go_to_folder": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋаĐŋĐēи", "go_to_search": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋĐžŅˆŅƒĐē҃", - "gps": "GPS", - "gps_missing": "НĐĩĐŧĐ°Ņ” GPS", + "gps": "ГĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ", + "gps_missing": "НĐĩĐŧĐ°Ņ” ĐŗĐĩОдаĐŊĐ¸Ņ…", "grant_permission": "ĐĐ°Đ´Đ°Ņ‚Đ¸ Đ´ĐžĐˇĐ˛Ņ–Đģ", "group_albums_by": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Са...", "group_country": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Са ĐēŅ€Đ°Ņ—ĐŊĐžŅŽ", @@ -1160,7 +1249,7 @@ "haptic_feedback_switch": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚Đ°ĐēŅ‚Đ¸ĐģҌĐŊ҃ Đ˛Ņ–Đ´Đ´Đ°Ņ‡Ņƒ", "haptic_feedback_title": "ĐĸаĐēŅ‚Đ¸ĐģҌĐŊа Đ˛Ņ–Đ´Đ´Đ°Ņ‡Đ°", "has_quota": "ĐšĐ˛ĐžŅ‚Đ°", - "hash_asset": "ГĐĩŅˆŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", + "hash_asset": "ĐĨĐĩŅˆŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", "hashed_assets": "ĐĨĐĩŅˆĐ¸", "hashing": "ĐĨĐĩŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", "header_settings_add_header_tip": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ ĐˇĐ°ĐŗĐžĐģОвОĐē", @@ -1175,24 +1264,25 @@ "hide_named_person": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ {name}", "hide_password": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "hide_person": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´Đ¸ĐŊ҃", + "hide_schema": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ҁ҅ĐĩĐŧ҃", "hide_text_recognition": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаваĐŊĐŊŅ Ņ‚ĐĩĐēŅŅ‚Ņƒ", "hide_unnamed_people": "ĐŸŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš ĐąĐĩС Ņ–Đŧ'Ņ", - "home_page_add_to_album_conflicts": "ДодаĐŊĐž {added} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҃ аĐģŅŒĐąĐžĐŧ {album}. {failed} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вĐļĐĩ ĐąŅƒĐģĐž в аĐģŅŒĐąĐžĐŧŅ–.", - "home_page_add_to_album_err_local": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_add_to_album_success": "ДодаĐŊĐž {added} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҃ аĐģŅŒĐąĐžĐŧ {album}.", - "home_page_album_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž аĐģŅŒĐąĐžĐŧ҃, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_archive_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩĐŧĐžĐļĐģивО ĐˇĐ°Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_archive_err_partner": "НĐĩĐŧĐžĐļĐģивО Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_add_to_album_conflicts": "ДодаĐŊĐž {added} Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}. {failed} Ņ„Đ°ĐšĐģŅ–Đ˛ вĐļĐĩ ĐąŅƒĐģĐž в аĐģŅŒĐąĐžĐŧŅ–.", + "home_page_add_to_album_err_local": "НĐĩĐŧĐžĐļĐģивО Đ´ĐžĐ´Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи Đ´Đž аĐģŅŒĐąĐžĐŧŅ–Đ˛, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_add_to_album_success": "ДодаĐŊĐž {added} Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž аĐģŅŒĐąĐžĐŧ҃ {album}.", + "home_page_album_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ Đ´ĐžĐ´Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ° Đ´Đž аĐģŅŒĐąĐžĐŧ҃, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_archive_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩĐŧĐžĐļĐģивО ĐˇĐ°Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_archive_err_partner": "НĐĩĐŧĐžĐļĐģивО Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", "home_page_building_timeline": "ĐŸĐžĐąŅƒĐ´ĐžĐ˛Đ° Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", - "home_page_delete_err_partner": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_delete_remote_err_local": "ЛоĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚(и) вĐļĐĩ в ĐŋŅ€ĐžŅ†ĐĩҁҖ видаĐģĐĩĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_favorite_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ҃ĐģŅŽĐąĐģĐĩĐŊĐ¸Ņ… ĐģĐžĐēаĐģҌĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_favorite_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ҃ĐģŅŽĐąĐģĐĩĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_delete_err_partner": "НĐĩĐŧĐžĐļĐģивО видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_delete_remote_err_local": "ЛоĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģ(и) вĐļĐĩ в ĐŋŅ€ĐžŅ†ĐĩҁҖ видаĐģĐĩĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_favorite_err_local": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_favorite_err_partner": "ПоĐēи Ņ‰Đž ĐŊĐĩ ĐŧĐžĐļĐŊа Đ´ĐžĐ´Đ°Ņ‚Đ¸ Đ´Đž ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", "home_page_first_time_notice": "Đ¯ĐēŅ‰Đž ви ĐēĐžŅ€Đ¸ŅŅ‚ŅƒŅ”Ņ‚ĐĩŅŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐēĐžĐŧ вĐŋĐĩŅ€ŅˆĐĩ, ĐąŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ОйĐĩŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ, Ņ‰ĐžĐą ĐŊа ҈ĐēаĐģŅ– Ņ‡Đ°ŅŅƒ Đˇâ€™ŅĐ˛Đ¸ĐģĐ¸ŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", - "home_page_locked_error_local": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ", - "home_page_locked_error_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€ŅŅŒĐēŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ", - "home_page_share_err_local": "НĐĩĐŧĐžĐļĐģивО ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊиĐŧи ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°Đŧи ҇ĐĩŅ€ĐĩС ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", - "home_page_upload_err_limit": "МоĐļĐŊа ваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ĐŊĐĩ ĐąŅ–ĐģҌ҈Đĩ 30 ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "home_page_locked_error_local": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_locked_error_partner": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€ŅŅŒĐēŅ– Ņ„Đ°ĐšĐģи Đ´Đž ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_share_err_local": "НĐĩĐŧĐžĐļĐģивО ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ĐģĐžĐēаĐģҌĐŊиĐŧи Ņ„Đ°ĐšĐģаĐŧи ҇ĐĩŅ€ĐĩС ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "home_page_upload_err_limit": "МоĐļĐŊа виваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐŊĐĩ ĐąŅ–ĐģҌ҈Đĩ 30 Ņ„Đ°ĐšĐģŅ–Đ˛ вОдĐŊĐžŅ‡Đ°Ņ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", "host": "ĐĨĐžŅŅ‚", "hour": "ГодиĐŊа", "hours": "ГодиĐŊи", @@ -1213,19 +1303,19 @@ "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Đ’Ņ–Đ´ĐĩĐž} other {Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ}} ĐˇŅ€ĐžĐąĐģĐĩĐŊĐž в {city}, {country} С {person1}, {person2} Ņ‚Đ° ҉Đĩ {additionalCount, number} ĐžŅĐžĐąĐ°Đŧи {date}", "image_saved_successfully": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "image_viewer_page_state_provider_download_started": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžŅ‡Đ°ĐģĐžŅŅ", - "image_viewer_page_state_provider_download_success": "ĐŖŅŅ–Đŋ҈ĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "image_viewer_page_state_provider_download_success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", "image_viewer_page_state_provider_share_error": "ПоĐŧиĐģĐēа ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", "immich_logo": "Đ›ĐžĐŗĐžŅ‚Đ¸Đŋ Immich", - "immich_web_interface": "ВĐĩĐą Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅ Immich", + "immich_web_interface": "ВĐĩĐą-Ņ–ĐŊŅ‚ĐĩҀ҄ĐĩĐšŅ Immich", "import_from_json": "ІĐŧĐŋĐžŅ€Ņ‚ С JSON", "import_path": "ШĐģŅŅ… Ņ–ĐŧĐŋĐžŅ€Ņ‚Ņƒ", - "in_albums": "ĐŖ {count, plural, one {# аĐģŅŒĐąĐžĐŧŅ–} other {# аĐģŅŒĐąĐžĐŧĐ°Ņ…}}", + "in_albums": "ĐŖ {count, plural, one {# аĐģŅŒĐąĐžĐŧŅ–} few {# аĐģŅŒĐąĐžĐŧĐ°Ņ…} many {# аĐģŅŒĐąĐžĐŧĐ°Ņ…} other {# аĐģŅŒĐąĐžĐŧĐ°Ņ…}}", "in_archive": "В Đ°Ņ€Ņ…Ņ–Đ˛Ņ–", "in_year": "ĐŖ {year}", "in_year_selector": "ĐŖ", "include_archived": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛", "include_shared_albums": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи", - "include_shared_partner_assets": "ВĐēĐģŅŽŅ‡Đ°ĐšŅ‚Đĩ ҁĐŋŅ–ĐģҌĐŊŅ– ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€ŅŅŒĐēŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", + "include_shared_partner_assets": "ВĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", "individual_share": "ІĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", "individual_shares": "ОĐēŅ€ĐĩĐŧŅ– ҁĐŋŅ–ĐģҌĐŊŅ– Đ´ĐžŅŅ‚ŅƒĐŋи", "info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ", @@ -1243,14 +1333,23 @@ "ios_debug_info_last_sync_at": "ĐžŅŅ‚Đ°ĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ {dateTime}", "ios_debug_info_no_processes_queued": "ФОĐŊĐžĐ˛Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– в ҇ĐĩŅ€ĐˇŅ–", "ios_debug_info_no_sync_yet": "ФОĐŊОвĐĩ СавдаĐŊĐŊŅ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— ҉Đĩ ĐŊĐĩ СаĐŋ҃ҁĐēаĐģĐžŅŅ", - "ios_debug_info_processes_queued": "{count, plural, one {{count} Ņ„ĐžĐŊОвиК ĐŋŅ€ĐžŅ†Đĩҁ ҃ ҇ĐĩŅ€ĐˇŅ–} other {{count} Ņ„ĐžĐŊĐžĐ˛Đ¸Ņ… ĐŋŅ€ĐžŅ†ĐĩŅŅ–Đ˛ ҃ ҇ĐĩŅ€ĐˇŅ–}}", + "ios_debug_info_processes_queued": "{count, plural, one {{count} Ņ„ĐžĐŊОвиК ĐŋŅ€ĐžŅ†Đĩҁ ҃ ҇ĐĩŅ€ĐˇŅ–} few {{count} Ņ„ĐžĐŊĐžĐ˛Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸ ҃ ҇ĐĩŅ€ĐˇŅ–} many {{count} Ņ„ĐžĐŊĐžĐ˛Đ¸Ņ… ĐŋŅ€ĐžŅ†ĐĩŅŅ–Đ˛ ҃ ҇ĐĩŅ€ĐˇŅ–} other {{count} Ņ„ĐžĐŊĐžĐ˛Đ¸Ņ… ĐŋŅ€ĐžŅ†ĐĩŅŅ–Đ˛ ҃ ҇ĐĩŅ€ĐˇŅ–}}", "ios_debug_info_processing_ran_at": "ĐžĐąŅ€ĐžĐąĐē҃ виĐēĐžĐŊаĐŊĐž {dateTime}", - "items_count": "{count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚} few {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸} many {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ°}}", + "items_count": "{count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "jobs": "ЗавдаĐŊĐŊŅ", + "json_editor": "JSON-Ņ€ĐĩдаĐēŅ‚ĐžŅ€", + "json_error": "ПоĐŧиĐģĐēа JSON", "keep": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸", + "keep_albums": "ЗбĐĩŅ€Ņ–ĐŗĐ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", + "keep_albums_count": "ЗбĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ {count} {count, plural, one {аĐģŅŒĐąĐžĐŧ} few {аĐģŅŒĐąĐžĐŧи} many {аĐģŅŒĐąĐžĐŧŅ–Đ˛} other {аĐģŅŒĐąĐžĐŧŅ–Đ˛}}", "keep_all": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ Đ˛ŅĐĩ", - "keep_this_delete_others": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ€ĐĩŅŅƒŅ€Ņ, видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", - "kept_this_deleted_others": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž ҆ĐĩĐš Ņ€ĐĩŅŅƒŅ€Ņ Ņ– видаĐģĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅƒ}}", + "keep_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ, Ņ‰Đž СаĐģĐ¸ŅˆĐ¸Ņ‚ŅŒŅŅ ĐŊа Đ˛Đ°ŅˆĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— ĐŋҖҁĐģŅ ĐˇĐ˛Ņ–ĐģҌĐŊĐĩĐŊĐŊŅ ĐŧŅ–ŅŅ†Ņ.", + "keep_favorites": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ", + "keep_on_device": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", + "keep_on_device_hint": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸, ŅĐēŅ– ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž СйĐĩŅ€ĐĩĐŗŅ‚Đ¸ ĐŊа Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", + "keep_this_delete_others": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ„Đ°ĐšĐģ, видаĐģĐ¸Ņ‚Đ¸ Ņ–ĐŊŅˆŅ–", + "keeping": "ЗбĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ: {items}", + "kept_this_deleted_others": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž ҆ĐĩĐš Ņ„Đ°ĐšĐģ Ņ– видаĐģĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "keyboard_shortcuts": "ĐĄĐŋĐžĐģŅƒŅ‡ĐĩĐŊĐŊŅ ĐēĐģĐ°Đ˛Ņ–Ņˆ", "language": "Мова", "language_no_results_subtitle": "ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐēОвиК СаĐŋĐ¸Ņ‚", @@ -1259,7 +1358,7 @@ "language_setting_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧĐžĐ˛Ņƒ, ŅĐēŅ–Đš ви ĐŊĐ°Đ´Đ°Ņ”Ņ‚Đĩ ĐŋĐĩŅ€ĐĩĐ˛Đ°ĐŗŅƒ", "large_files": "ВĐĩĐģиĐēŅ– Ņ„Đ°ĐšĐģи", "last": "ĐžŅŅ‚Đ°ĐŊĐŊŅ–Đš", - "last_months": "{count, plural, one {МиĐŊ҃ĐģĐžĐŗĐž ĐŧŅ–ŅŅŅ†Ņ} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–}}", + "last_months": "{count, plural, one {МиĐŊ҃ĐģĐžĐŗĐž ĐŧŅ–ŅŅŅ†Ņ} few {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–} many {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {ĐžŅŅ‚Đ°ĐŊĐŊŅ– # ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", "last_seen": "Đ’ĐžŅŅ‚Đ°ĐŊĐŊŅ” ĐąĐ°Ņ‡Đ¸Đģи", "latest_version": "ĐžŅŅ‚Đ°ĐŊĐŊŅ вĐĩŅ€ŅŅ–Ņ", "latitude": "Đ¨Đ¸Ņ€ĐžŅ‚Đ°", @@ -1274,25 +1373,25 @@ "library_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "library_page_device_albums": "АĐģŅŒĐąĐžĐŧи ĐŊа ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "library_page_new_album": "Новий аĐģŅŒĐąĐžĐŧ", - "library_page_sort_asset_count": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "library_page_sort_asset_count": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģŅ–Đ˛", "library_page_sort_created": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊŅ–", "library_page_sort_last_modified": "ĐžŅŅ‚Đ°ĐŊĐŊŅ СĐŧŅ–ĐŊа", "library_page_sort_title": "Назва аĐģŅŒĐąĐžĐŧ҃", "licenses": "Đ›Ņ–Ņ†ĐĩĐŊĐˇŅ–Ņ—", "light": "ĐĄĐ˛Ņ–Ņ‚Đģа", "like": "ĐŸĐžĐ´ĐžĐąĐ°Ņ”Ņ‚ŅŒŅŅ", - "like_deleted": "ЛайĐē видаĐģĐĩĐŊĐž", + "like_deleted": "ВĐŋОдОйаĐŊĐŊŅ видаĐģĐĩĐŊĐž", "link_motion_video": "ĐŸĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", "link_to_oauth": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊаĐŊĐŊŅ Đ´Đž OAuth", - "linked_oauth_account": "ĐŸŅ€Đ¸Ņ”Đ´ĐŊаĐŊиК аĐēĐ°ŅƒĐŊŅ‚ OAuth", + "linked_oauth_account": "ĐŸŅ€Đ¸Đ˛'ŅĐˇĐ°ĐŊиК ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", "list": "ПĐĩŅ€ĐĩĐģŅ–Đē", "loading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "loading_search_results_failed": "НĐĩ вдаĐģĐžŅŅ СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Đ¸ ĐŋĐžŅˆŅƒĐē҃", "local": "На ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "local_asset_cast_failed": "НĐĩĐŧĐžĐļĐģивО Ņ‚Ņ€Đ°ĐŊҁĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ, ŅĐēиК ĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€", + "local_asset_cast_failed": "НĐĩĐŧĐžĐļĐģивО Ņ‚Ņ€Đ°ĐŊҁĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģ, ŅĐēиК ĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€", "local_assets": "ЛоĐēаĐģҌĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "local_id": "ĐœŅ–ŅŅ†ĐĩвиК Ņ–Đ´ĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€", - "local_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ ĐŧҖҁ҆ĐĩĐ˛Đ¸Ņ… ЗМІ", + "local_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģŅ–Đ˛", "local_network": "ЛоĐēаĐģҌĐŊа ĐŧĐĩŅ€ĐĩĐļа", "local_network_sheet_info": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē ĐŋŅ–Đ´ĐēĐģŅŽŅ‡Đ°Ņ‚Đ¸ĐŧĐĩŅ‚ŅŒŅŅ Đ´Đž ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ҇ĐĩŅ€ĐĩС ҆ĐĩĐš URL, ĐēĐžĐģи виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚ŅŒŅŅ вĐēаСаĐŊа Wi-Fi ĐŧĐĩŅ€ĐĩĐļа", "location": "Đ ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", @@ -1312,21 +1411,21 @@ "logged_out_all_devices": "Đ’Đ¸ĐšŅˆĐģи С ŅƒŅŅ–Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", "logged_out_device": "Đ’Đ¸Ņ…Ņ–Đ´ С ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", "login": "Đ’Ņ…Ņ–Đ´", - "login_disabled": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Ņ–Ņ ĐąŅƒĐģа Đ˛Ņ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊа", + "login_disabled": "ĐĐ˛Ņ‚ĐžŅ€Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ виĐŧĐēĐŊĐĩĐŊĐž", "login_form_api_exception": "ПоĐŧиĐģĐēа API. ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Ņ– ҁĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ СĐŊĐžĐ˛Ņƒ.", "login_form_back_button_text": "Назад", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http://your-server-ip:port'", + "login_form_endpoint_hint": "http://your-server-ip:port", "login_form_endpoint_url": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", "login_form_err_http": "ВĐēаĐļŅ–Ņ‚ŅŒ http:// айО https://", - "login_form_err_invalid_email": "ĐĨийĐŊиК Ņ–ĐŧĐĩĐšĐģ", - "login_form_err_invalid_url": "ĐĨийĐŊиК URL", + "login_form_err_invalid_email": "НĐĩĐ´Ņ–ĐšŅĐŊа ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊа Đ°Đ´Ņ€ĐĩŅĐ°", + "login_form_err_invalid_url": "НĐĩĐ´Ņ–ĐšŅĐŊиК URL", "login_form_err_leading_whitespace": "ĐŸŅ€ĐžĐąŅ–Đģ ĐŊа ĐŋĐžŅ‡Đ°Ņ‚Đē҃", "login_form_err_trailing_whitespace": "ĐŸŅ€ĐžĐąŅ–Đģ в ĐēŅ–ĐŊ҆Җ", "login_form_failed_get_oauth_server_config": "ПоĐŧиĐģĐēа Đ˛Ņ…ĐžĐ´Ņƒ ҇ĐĩŅ€ĐĩС OAuth, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "login_form_failed_get_oauth_server_disable": "OAuth ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК ĐŊа Ņ†ŅŒĐžĐŧ҃ ҁĐĩŅ€Đ˛ĐĩҀҖ", "login_form_failed_login": "ПоĐŧиĐģĐēа Đ˛Ņ…ĐžĐ´Ņƒ, ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Ņ‚Đĩ URL-Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°, ĐĩĐģĐĩĐēŅ‚Ņ€ĐžĐŊĐŊ҃ ĐŋĐžŅˆŅ‚Ņƒ Ņ‚Đ° ĐŋĐ°Ņ€ĐžĐģҌ", - "login_form_handshake_exception": "ВиĐŊŅŅ‚ĐžĐē Ņ€ŅƒĐēĐžŅŅ‚Đ¸ŅĐēаĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐē҃ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚.", + "login_form_handshake_exception": "ПоĐŧиĐģĐēа Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ С'Ņ”Đ´ĐŊаĐŊĐŊŅ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅ–Ņ‚ŅŒ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐē҃ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊĐžĐŗĐž ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚Đ° в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…, ŅĐēŅ‰Đž ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅĐ°ĐŧĐžĐŋŅ–Đ´ĐŋĐ¸ŅĐ°ĐŊиК ҁĐĩŅ€Ņ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚.", "login_form_password_hint": "ĐŋĐ°Ņ€ĐžĐģҌ", "login_form_save_login": "ЗаĐŋаĐŧ'ŅŅ‚Đ°Ņ‚Đ¸ Đ˛Ņ…Ņ–Đ´", "login_form_server_empty": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ URL-Đ°Đ´Ņ€Đĩҁ҃ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°.", @@ -1338,39 +1437,57 @@ "logout_this_device_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С Ņ†ŅŒĐžĐŗĐž ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ?", "logs": "Đ–ŅƒŅ€ĐŊаĐģи", "longitude": "Đ”ĐžĐ˛ĐŗĐžŅ‚Đ°", - "look": "Đ”Đ¸Đ˛Đ¸Ņ‚Đ¸ŅŅ", + "look": "Đ’Đ¸ĐŗĐģŅĐ´", "loop_videos": "ĐĻиĐēĐģҖ҇ĐŊŅ– Đ˛Ņ–Đ´ĐĩĐž", "loop_videos_description": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ†Đ¸ĐēĐģҖ҇ĐŊĐĩ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž.", "main_branch_warning": "Ви виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ вĐĩŅ€ŅŅ–ŅŽ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛; ĐŊĐ°ŅŅ‚Ņ–ĐšĐŊĐž Ņ€ĐĩĐēĐžĐŧĐĩĐŊĐ´ŅƒŅ”ĐŧĐž виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩĐģŅ–ĐˇĐŊ҃ вĐĩŅ€ŅŅ–ŅŽ!", "main_menu": "ГоĐģОвĐŊĐĩ ĐŧĐĩĐŊŅŽ", + "maintenance_action_restore": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", "maintenance_description": "Immich ĐŋĐĩŅ€ĐĩвĐĩĐ´ĐĩĐŊĐž в Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", "maintenance_end": "ЗавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ Ņ‚ĐĩŅ…ĐŊҖ҇ĐŊĐžĐŗĐž ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ", "maintenance_end_error": "НĐĩ вдаĐģĐžŅŅ СавĐĩŅ€ŅˆĐ¸Ņ‚Đ¸ Ņ€ĐĩĐļиĐŧ ĐžĐąŅĐģŅƒĐŗĐžĐ˛ŅƒĐ˛Đ°ĐŊĐŊŅ.", "maintenance_logged_in_as": "ĐĐ°Ņ€Đ°ĐˇŅ– ви Đ˛Đ˛Ņ–ĐšŅˆĐģи ŅĐē {user}", + "maintenance_restore_from_backup": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ С Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—", + "maintenance_restore_library": "Đ’Ņ–Đ´ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅĐ˛ĐžŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "maintenance_restore_library_confirm": "Đ¯ĐēŅ‰Đž ҆Đĩ Đ˛Đ¸ĐŗĐģŅĐ´Đ°Ņ” ĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊĐž, ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļŅƒĐšŅ‚Đĩ Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—!", + "maintenance_restore_library_description": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ йаСи даĐŊĐ¸Ņ…", + "maintenance_restore_library_folder_has_files": "{folder} ĐŧĐ°Ņ” {count} ĐŋаĐŋĐžĐē(ĐžĐē)", + "maintenance_restore_library_folder_no_files": "ĐŖ ĐŋаĐŋ҆Җ {folder} Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– Ņ„Đ°ĐšĐģи!", + "maintenance_restore_library_folder_pass": "Ņ‡Đ¸Ņ‚Đ°ĐąĐĩĐģҌĐŊиК Ņ‚Đ° СаĐŋĐ¸ŅŅƒĐ˛Đ°ĐŊиК", + "maintenance_restore_library_folder_read_fail": "ĐŊĐĩŅ‡Đ¸Ņ‚Đ°ĐąĐĩĐģҌĐŊĐž", + "maintenance_restore_library_folder_write_fail": "ĐŊĐĩ ĐŧĐžĐļĐŊа СаĐŋĐ¸ŅŅƒĐ˛Đ°Ņ‚Đ¸", + "maintenance_restore_library_hint_missing_files": "МоĐļĐģивО, ви ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°Ņ”Ņ‚Đĩ ваĐļĐģĐ¸Đ˛Ņ– Ņ„Đ°ĐšĐģи", + "maintenance_restore_library_hint_regenerate_later": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ—Ņ… ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ…", + "maintenance_restore_library_hint_storage_template_missing_files": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒŅ”Ņ‚Đĩ ŅˆĐ°ĐąĐģĐžĐŊ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°? МоĐļĐģивО, ваĐŧ ĐąŅ€Đ°ĐēŅƒŅ” Ņ„Đ°ĐšĐģŅ–Đ˛", + "maintenance_restore_library_loading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€ĐžĐē ҆ҖĐģҖҁĐŊĐžŅŅ‚Ņ– Ņ‚Đ° ĐĩĐ˛Ņ€Đ¸ŅŅ‚Đ¸Đēâ€Ļ", + "maintenance_task_backup": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ— ҖҁĐŊŅƒŅŽŅ‡ĐžŅ— йаСи даĐŊĐ¸Ņ…â€Ļ", + "maintenance_task_migrations": "ВиĐēĐžĐŊаĐŊĐŊŅ ĐŧŅ–ĐŗŅ€Đ°Ņ†Ņ–Ņ— йаСи даĐŊĐ¸Ņ…â€Ļ", + "maintenance_task_restore": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐžŅ— Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžŅ— ĐēĐžĐŋŅ–Ņ—â€Ļ", + "maintenance_task_rollback": "НĐĩ вдаĐģĐžŅŅ Đ˛Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸, ĐŋОвĐĩŅ€ĐŊĐĩĐŊĐŊŅ Đ´Đž Ņ‚ĐžŅ‡Đēи Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅâ€Ļ", "maintenance_title": "ĐĸиĐŧŅ‡Đ°ŅĐžĐ˛Đž ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊĐž", "make": "Đ’Đ¸Ņ€ĐžĐąĐŊиĐē", "manage_geolocation": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅĐŧ", - "manage_media_access_rationale": "ĐĻĐĩĐš Đ´ĐžĐˇĐ˛Ņ–Đģ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐģŅ ĐŊаĐģĐĩĐļĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛ Đ´Đž ĐēĐžŅˆĐ¸Đēа Ņ‚Đ° Ņ—Ņ… Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ С ĐŊŅŒĐžĐŗĐž.", + "manage_media_access_rationale": "ĐĻĐĩĐš Đ´ĐžĐˇĐ˛Ņ–Đģ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐģŅ ĐŊаĐģĐĩĐļĐŊĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐŊŅ Ņ„Đ°ĐšĐģŅ–Đ˛ Đ´Đž ĐēĐžŅˆĐ¸Đēа Ņ‚Đ° Ņ—Ņ… Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ С ĐŊŅŒĐžĐŗĐž.", "manage_media_access_settings": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "manage_media_access_subtitle": "ДозвоĐģŅŒŅ‚Đĩ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧŅ– Immich ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģаĐŧи Ņ‚Đ° ĐŋĐĩŅ€ĐĩĐŧŅ–Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ….", + "manage_media_access_subtitle": "ДозвоĐģŅŒŅ‚Đĩ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģаĐŧи Ņ‚Đ° ĐŋĐĩŅ€ĐĩĐŧŅ–Ņ‰ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ….", "manage_media_access_title": "Đ”ĐžŅŅ‚ŅƒĐŋ Đ´Đž ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧĐĩĐ´Ņ–Đ°", "manage_shared_links": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧи ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи", - "manage_sharing_with_partners": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ҁĐŋŅ–ĐģҌĐŊиĐŧ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅĐŧ С ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°Đŧи", - "manage_the_app_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи", - "manage_your_account": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅ—Đŧ ОйĐģŅ–ĐēОвиĐŧ СаĐŋĐ¸ŅĐžĐŧ", + "manage_sharing_with_partners": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧ Đ´ĐžŅŅ‚ŅƒĐŋĐžĐŧ С ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°Đŧи", + "manage_the_app_settings": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", + "manage_your_account": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ОйĐģŅ–ĐēОвиĐŧ СаĐŋĐ¸ŅĐžĐŧ", "manage_your_api_keys": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ĐēĐģŅŽŅ‡Đ°Đŧи API", - "manage_your_devices": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи, ŅĐēŅ– ŅƒĐ˛Ņ–ĐšŅˆĐģи в ŅĐ¸ŅŅ‚ĐĩĐŧ҃", + "manage_your_devices": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊиĐŧи ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅĐŧи", "manage_your_oauth_connection": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐžĐŗĐž OAuth", "map": "МаĐŋа", - "map_assets_in_bounds": "{count, plural, =0 {НĐĩĐŧĐ°Ņ” Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš ҃ Ņ†Ņ–Đš ĐŧҖҁ҆ĐĩĐ˛ĐžŅŅ‚Ņ–} one {# Ņ„ĐžŅ‚Đž} other {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—}}", + "map_assets_in_bounds": "{count, plural, =0 {НĐĩĐŧĐ°Ņ” Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš ҃ Ņ†Ņ–Đš ĐŧҖҁ҆ĐĩĐ˛ĐžŅŅ‚Ņ–} one {# Ņ„ĐžŅ‚Đž} few {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—} many {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš} other {# Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš}}", "map_cannot_get_user_location": "НĐĩ ĐŧĐžĐļ҃ ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", "map_location_dialog_yes": "ĐĸаĐē", - "map_location_picker_page_use_location": "ĐĻĐĩ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", - "map_location_service_disabled_content": "ĐĄĐģ҃Đļйа ĐģĐžĐēĐ°Ņ†Ņ–Ņ— ĐŧĐ°Ņ” ĐąŅƒŅ‚Đ¸ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐžŅŽ, Ņ‰ĐžĐą Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ—Ņ— ĐˇĐ°Ņ€Đ°Đˇ?", + "map_location_picker_page_use_location": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ҆Đĩ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", + "map_location_service_disabled_content": "ĐĄĐģ҃Đļйа ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–Ņ— ĐŧĐ°Ņ” ĐąŅƒŅ‚Đ¸ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐžŅŽ, Ņ‰ĐžĐą Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи С Đ˛Đ°ŅˆĐžĐŗĐž ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ—Ņ— ĐˇĐ°Ņ€Đ°Đˇ?", "map_location_service_disabled_title": "ĐĄĐģ҃Đļйа ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊа", "map_marker_for_images": "ĐœĐ°Ņ€ĐēĐĩŅ€ ĐŊа ĐŧаĐŋŅ– Đ´ĐģŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ, ĐˇŅ€ĐžĐąĐģĐĩĐŊĐ¸Ņ… ҃ ĐŧҖҁ҂Җ {city}, {country}", "map_marker_with_image": "ĐœĐ°Ņ€ĐēĐĩŅ€ ĐŊа ĐŧаĐŋŅ– Ņ–Đˇ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅĐŧ", - "map_no_location_permission_content": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ, айи ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Ņ–Đˇ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐĐ°Đ´Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐˇĐ°Ņ€Đ°Đˇ?", + "map_no_location_permission_content": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ, айи ĐŋĐžĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„Đ°ĐšĐģи Ņ–Đˇ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐžĐŗĐž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ. ĐĐ°Đ´Đ°Ņ‚Đ¸ ĐšĐžĐŗĐž ĐˇĐ°Ņ€Đ°Đˇ?", "map_no_location_permission_title": "ПоĐŧиĐģĐēа Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ", "map_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи", "map_settings_dark_mode": "ĐĸĐĩĐŧĐŊиК Ņ€ĐĩĐļиĐŧ", @@ -1381,55 +1498,61 @@ "map_settings_dialog_title": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŧаĐŋи", "map_settings_include_show_archived": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Đ°Ņ€Ņ…Ņ–Đ˛", "map_settings_include_show_partners": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ°", - "map_settings_only_show_favorites": "Đ›Đ¸ŅˆĐĩ ҃ĐģŅŽĐąĐĩĐŊŅ–", - "map_settings_theme_settings": "ĐĸĐĩĐŧа ĐēĐ°Ņ€Ņ‚Đ¸", + "map_settings_only_show_favorites": "Đ›Đ¸ŅˆĐĩ ĐžĐąŅ€Đ°ĐŊŅ–", + "map_settings_theme_settings": "ĐĸĐĩĐŧа ĐŧаĐŋи", "map_zoom_to_see_photos": "ЗĐŧĐĩĐŊŅˆŅ‚Đĩ ĐŧĐ°ŅŅˆŅ‚Đ°Đą, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "mark_all_as_read": "ПозĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ Đ˛ŅŅ– ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊŅ–", "mark_as_read": "ПозĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊĐĩ", "marked_all_as_read": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Đ˛ŅŅ– ŅĐē ĐŋŅ€ĐžŅ‡Đ¸Ņ‚Đ°ĐŊŅ–", "matches": "Đ—ĐąŅ–ĐŗĐ¸", - "matching_assets": "Đ’Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊŅ– аĐēŅ‚Đ¸Đ˛Đ¸", + "matching_assets": "Đ’Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´ĐŊŅ– Ņ„Đ°ĐšĐģи", "media_type": "ĐĸиĐŋ ĐŧĐĩĐ´Ņ–Đ°", "memories": "ĐĄĐŋĐžĐŗĐ°Đ´Đ¸", "memories_all_caught_up": "ĐĻĐĩ Đ˛ŅĐĩ ĐŊа ŅŅŒĐžĐŗĐžĐ´ĐŊŅ–", "memories_check_back_tomorrow": "Đ—Đ°Đ˛Ņ–Ņ‚Đ°ĐšŅ‚Đĩ ĐˇĐ°Đ˛Ņ‚Ņ€Đ°, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐąŅ–ĐģҌ҈Đĩ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", - "memories_setting_description": "КĐĩŅ€ŅƒĐšŅ‚Đĩ Ņ‚Đ¸Đŧ, Ņ‰Đž ĐąĐ°Ņ‡Đ¸Ņ‚Đĩ ҃ ŅĐ˛ĐžŅ—Ņ… ҁĐŋĐžĐŗĐ°Đ´Đ°Ņ…", + "memories_setting_description": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ вĐŧŅ–ŅŅ‚Ņƒ ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", "memories_start_over": "ĐŸĐžŅ‡Đ°Ņ‚Đ¸ СаĐŊОвО", "memories_swipe_to_close": "ЗĐŧĐ°Ņ…ĐŊŅ–Ņ‚ŅŒ Đ˛ĐŗĐžŅ€Ņƒ, Ņ‰ĐžĐą СаĐēŅ€Đ¸Ņ‚Đ¸", - "memory": "ПаĐŧ'ŅŅ‚ŅŒ", + "memory": "ĐĄĐŋĐžĐŗĐ°Đ´", "memory_lane_title": "АĐģĐĩŅ ĐĄĐŋĐžĐŗĐ°Đ´Ņ–Đ˛ {title}", "menu": "МĐĩĐŊŅŽ", "merge": "Об'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸", - "merge_people": "Об'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ĐŋĐĩŅ€ŅĐžĐŊи", + "merge_people": "Об'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", "merge_people_limit": "Ви ĐŧĐžĐļĐĩŅ‚Đĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Đ´Đž 5 ОйĐģĐ¸Ņ‡ ОдĐŊĐžŅ‡Đ°ŅĐŊĐž", "merge_people_prompt": "Ви Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Ой'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ†Đ¸Ņ… ĐģŅŽĐ´ĐĩĐš? ĐĻŅ Đ´Ņ–Ņ ĐŊĐĩĐˇĐ˛ĐžŅ€ĐžŅ‚ĐŊа.", "merge_people_successfully": "ĐŖŅĐŋŅ–ŅˆĐŊĐĩ Ой'Ņ”Đ´ĐŊаĐŊĐŊŅ ĐģŅŽĐ´ĐĩĐš", "merged_people_count": "Об'Ņ”Đ´ĐŊаĐŊĐž {count, plural, one {# ĐžŅĐžĐąĐ°} few {# ĐžŅĐžĐąĐ¸} many {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", "minimize": "ĐœŅ–ĐŊŅ–ĐŧŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸", - "minute": "ĐĨвиĐģиĐŊĐē҃", + "minute": "ĐĨвиĐģиĐŊа", "minutes": "ĐĨвиĐģиĐŊи", + "mirror_horizontal": "Đ“ĐžŅ€Đ¸ĐˇĐžĐŊŅ‚Đ°ĐģҌĐŊиК", + "mirror_vertical": "ВĐĩŅ€Ņ‚Đ¸ĐēаĐģҌĐŊиК", "missing": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", - "mobile_app": "ĐœĐžĐąŅ–ĐģҌĐŊиК Đ´ĐžĐ´Đ°Ņ‚ĐžĐē", - "mobile_app_download_onboarding_note": "ЗаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ҁ҃ĐŋŅƒŅ‚ĐŊŅ–Đš ĐŧĐžĐąŅ–ĐģҌĐŊиК Đ´ĐžĐ´Đ°Ņ‚ĐžĐē, ҁĐēĐžŅ€Đ¸ŅŅ‚Đ°Đ˛ŅˆĐ¸ŅŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧи ĐŊиĐļ҇Đĩ ĐžĐŋŅ†Ņ–ŅĐŧи", + "mobile_app": "ĐœĐžĐąŅ–ĐģҌĐŊиК ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē", + "mobile_app_download_onboarding_note": "ЗаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ҁ҃ĐŋŅƒŅ‚ĐŊŅ–Đš ĐŧĐžĐąŅ–ĐģҌĐŊиК ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē, ҁĐēĐžŅ€Đ¸ŅŅ‚Đ°Đ˛ŅˆĐ¸ŅŅŒ ĐŊавĐĩĐ´ĐĩĐŊиĐŧи ĐŊиĐļ҇Đĩ ĐžĐŋŅ†Ņ–ŅĐŧи", "model": "МодĐĩĐģҌ", "month": "ĐœŅ–ŅŅŅ†ŅŒ", "monthly_title_text_date_format": "ММММ Ņ€", "more": "Đ‘Ņ–ĐģҌ҈Đĩ", "move": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸", + "move_down": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ вĐŊиС", "move_off_locked_folder": "Đ’Đ¸ĐšŅ‚Đ¸ С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "move_to": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ´Đž", - "move_to_lock_folder_action_prompt": "{count} дОдаĐŊĐž Đ´Đž ĐˇĐ°Ņ…Đ¸Ņ‰ĐĩĐŊĐžŅ— Ņ‚ĐĩĐēи", + "move_to_device_trash": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ в ĐēĐžŅˆĐ¸Đē ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ", + "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": "НĐĩĐŧĐžĐļĐģивО Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧҖҁ҆ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž", + "move_up": "ПĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ Đ˛ĐŗĐžŅ€Ņƒ", + "moved_to_archive": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} в Đ°Ņ€Ņ…Ņ–Đ˛", + "moved_to_library": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} в ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃", + "moved_to_trash": "ПĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", + "multiselect_grid_edit_date_time_err_read_only": "НĐĩĐŧĐžĐļĐģивО Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", + "multiselect_grid_edit_gps_err_read_only": "НĐĩĐŧĐžĐļĐģивО Ņ€ĐĩĐ´Đ°ĐŗŅƒĐ˛Đ°Ņ‚Đ¸ ĐŗĐĩĐžĐģĐžĐēĐ°Ņ†Ņ–ŅŽ Ņ„Đ°ĐšĐģŅ–Đ˛ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ, ĐŋŅ€ĐžĐŋ҃ҁĐēĐ°ŅŽ", "mute_memories": "ĐŸŅ€Đ¸ĐŗĐģŅƒŅˆĐ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "my_albums": "ĐœĐžŅ— аĐģŅŒĐąĐžĐŧи", "name": "ІĐŧ'Ņ", "name_or_nickname": "ІĐŧ'Ņ айО ĐŋҁĐĩвдОĐŊŅ–Đŧ", + "name_required": "ІĐŧ'Ņ ОйОв'ŅĐˇĐēОвĐĩ", "navigate": "ĐĐ°Đ˛Ņ–ĐŗĐ°Ņ†Ņ–Ņ", "navigate_to_time": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž Đ§Đ°ŅŅƒ", "network_requirement_photos_upload": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҁ҂ҖĐģҌĐŊиĐēĐžĐ˛Ņ– даĐŊŅ– Đ´ĐģŅ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ Ņ„ĐžŅ‚Đž", @@ -1454,38 +1577,42 @@ "next": "ДаĐģŅ–", "next_memory": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊиК ҁĐŋĐžĐŗĐ°Đ´", "no": "ĐŅ–", + "no_actions_added": "ПоĐēи Ņ‰Đž ĐļОдĐŊĐ¸Ņ… Đ´Ņ–Đš ĐŊĐĩ дОдаĐŊĐž", + "no_albums_found": "АĐģŅŒĐąĐžĐŧи ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "no_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą ҃ĐŋĐžŅ€ŅĐ´ĐēŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "no_albums_with_name_yet": "ĐĄŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” аĐģŅŒĐąĐžĐŧŅ–Đ˛ С Ņ‚Đ°ĐēĐžŅŽ ĐŊĐ°ĐˇĐ˛ĐžŅŽ.", "no_albums_yet": "ĐĄŅ…ĐžĐļĐĩ, ҃ Đ˛Đ°Ņ ҉Đĩ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž аĐģŅŒĐąĐžĐŧ҃.", "no_archived_assets_message": "Đ—Đ°Đ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž, Ņ‰ĐžĐą ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°Ņ‚Đ¸ Ņ—Ņ… ҃ Đ˛Đ°ŅˆĐžĐŧ҃ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņ– Ņ„ĐžŅ‚Đž", - "no_assets_message": "НАĐĸИСНІĐĸĐŦ, ЩОБ ЗАВАНĐĸАЖИĐĸИ ВАШЕ ПЕРШЕ ФОĐĸО", - "no_assets_to_show": "ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", + "no_assets_message": "ĐĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ŅĐ˛ĐžŅ” ĐŋĐĩŅ€ŅˆĐĩ Ņ„ĐžŅ‚Đž", + "no_assets_to_show": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", "no_cast_devices_found": "ĐŸŅ€Đ¸ŅŅ‚Ņ€ĐžŅ— Đ´ĐģŅ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ— ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", - "no_checksum_local": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", - "no_checksum_remote": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ", + "no_checksum_local": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐģĐžĐēаĐģҌĐŊŅ– Ņ„Đ°ĐšĐģи", + "no_checksum_remote": "КоĐŊŅ‚Ņ€ĐžĐģҌĐŊа ҁ҃Đŧа ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа – ĐŊĐĩĐŧĐžĐļĐģивО ĐžŅ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊиК Ņ„Đ°ĐšĐģ", + "no_configuration_needed": "НĐĩ ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–Ņ", "no_devices": "НĐĩĐŧĐ°Ņ” Đ°Đ˛Ņ‚ĐžŅ€Đ¸ĐˇĐžĐ˛Đ°ĐŊĐ¸Ņ… ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—Đ˛", "no_duplicates_found": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Ņ–Đ˛ ĐŊĐĩ Đ˛Đ¸ŅĐ˛ĐģĐĩĐŊĐž.", "no_exif_info_available": "Đ’Ņ–Đ´ŅŅƒŅ‚ĐŊŅ Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž exif", "no_explore_results_message": "ЗаваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ĐąŅ–ĐģҌ҈Đĩ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš, Ņ‰ĐžĐą ĐŊĐ°ŅĐžĐģОдĐļŅƒĐ˛Đ°Ņ‚Đ¸ŅŅ Đ˛Đ°ŅˆĐžŅŽ ĐēĐžĐģĐĩĐēŅ†Ņ–Ņ”ŅŽ.", - "no_favorites_message": "Đ”ĐžĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ ҃ĐģŅŽĐąĐģĐĩĐŊŅ– Ņ„Đ°ĐšĐģи, Ņ‰ĐžĐą ŅˆĐ˛Đ¸Đ´ĐēĐž СĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đ¸ Đ˛Đ°ŅˆŅ– ĐŊаКĐēŅ€Đ°Ņ‰Ņ– ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", + "no_favorites_message": "Đ”ĐžĐ´Đ°Đ˛Đ°ĐšŅ‚Đĩ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в ĐžĐąŅ€Đ°ĐŊĐĩ, Ņ‰ĐžĐą ŅˆĐ˛Đ¸Đ´ĐēĐž СĐŊĐ°Ņ…ĐžĐ´Đ¸Ņ‚Đ¸ ĐŊаКĐēŅ€Đ°Ņ‰Ņ–", + "no_filters_added": "Đ¤Ņ–ĐģŅŒŅ‚Ņ€Đ¸ ҉Đĩ ĐŊĐĩ дОдаĐŊĐž", "no_libraries_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ СОвĐŊŅ–ŅˆĐŊŅŽ ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐē҃ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž", - "no_local_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "no_local_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž ĐģĐžĐēаĐģҌĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "no_location_set": "ĐœŅ–ŅŅ†ĐĩСĐŊĐ°Ņ…ĐžĐ´ĐļĐĩĐŊĐŊŅ ĐŊĐĩ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž", "no_locked_photos_message": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž в ĐžŅĐžĐąĐ¸ŅŅ‚Ņ–Đš ĐŋаĐŋ҆Җ ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°ĐŊŅ– Ņ– ĐŊĐĩ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°ŅŽŅ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Ņ‡Đ¸ ĐŋĐžŅˆŅƒĐē҃ ҃ Đ˛Đ°ŅˆŅ–Đš ĐąŅ–ĐąĐģŅ–ĐžŅ‚Đĩ҆Җ.", "no_name": "БĐĩС Ņ–ĐŧĐĩĐŊŅ–", "no_notifications": "НĐĩĐŧĐ°Ņ” ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", "no_people_found": "Đ›ŅŽĐ´ĐĩĐš, Ņ‰Đž Đ˛Ņ–Đ´ĐŋĐžĐ˛Ņ–Đ´Đ°ŅŽŅ‚ŅŒ СаĐŋĐ¸Ņ‚Ņƒ, ĐŊĐĩ СĐŊаКдĐĩĐŊĐž", "no_places": "ĐœŅ–ŅŅ†ŅŒ ĐŊĐĩĐŧĐ°Ņ”", - "no_remote_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "no_remote_assets_found": "З Ņ†Ņ–Ņ”ŅŽ ĐēĐžĐŊŅ‚Ņ€ĐžĐģҌĐŊĐžŅŽ ҁ҃ĐŧĐžŅŽ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "no_results": "НĐĩĐŧĐ°Ņ” Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛", "no_results_description": "ĐĄĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐ¸ĐŊĐžĐŊŅ–Đŧ айО ĐąŅ–ĐģҌ҈ ĐˇĐ°ĐŗĐ°ĐģҌĐŊĐĩ ĐēĐģŅŽŅ‡ĐžĐ˛Đĩ ҁĐģОвО", "no_shared_albums_message": "ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ аĐģŅŒĐąĐžĐŧ, Ņ‰ĐžĐą Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐģŅŽĐ´ŅŒĐŧи ҃ Đ˛Đ°ŅˆŅ–Đš ĐŧĐĩŅ€ĐĩĐļŅ–", - "no_uploads_in_progress": "НĐĩĐŧĐ°Ņ” аĐēŅ‚Đ¸Đ˛ĐŊĐ¸Ņ… СаваĐŊŅ‚Đ°ĐļĐĩĐŊҌ", + "no_uploads_in_progress": "НĐĩĐŧĐ°Ņ” аĐēŅ‚Đ¸Đ˛ĐŊĐ¸Ņ… виваĐŊŅ‚Đ°ĐļĐĩĐŊҌ", + "none": "ЖодĐĩĐŊ", "not_allowed": "НĐĩ дОСвОĐģĐĩĐŊĐž", "not_available": "НĐĩĐŧĐ°Ņ” даĐŊĐ¸Ņ…", "not_in_any_album": "ĐŖ ĐļОдĐŊĐžĐŧ҃ аĐģŅŒĐąĐžĐŧŅ–", "not_selected": "НĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐž", - "note_apply_storage_label_to_previously_uploaded assets": "ĐŸŅ€Đ¸ĐŧŅ–Ņ‚Đēа: ЊОй ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸ ĐŧŅ–Ņ‚Đē҃ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° Đ´Đž Ņ€Đ°ĐŊŅ–ŅˆĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛, виĐēĐžĐŊĐ°ĐšŅ‚Đĩ ĐēĐžĐŧаĐŊĐ´Ņƒ", "notes": "ĐĐžŅ‚Đ°Ņ‚Đēи", "nothing_here_yet": "ĐĸŅƒŅ‚ ҉Đĩ ĐŊŅ–Ņ‡ĐžĐŗĐž ĐŊĐĩĐŧĐ°Ņ”", "notification_permission_dialog_content": "ЊОй ŅƒĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅ, ĐŋĐĩŅ€ĐĩĐšĐ´Ņ–Ņ‚ŅŒ Đ´Đž НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ Ņ– ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ Đ´ĐžĐˇĐ˛Ņ–Đģ.", @@ -1497,25 +1624,25 @@ "notifications_setting_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊĐŊŅĐŧи", "oauth": "OAuth", "obtainium_configurator": "КоĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ‚ĐžŅ€ Obtainium", - "obtainium_configurator_instructions": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Obtainium Đ´ĐģŅ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ‚Đ° ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи Android ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž С Ņ€ĐĩĐģŅ–ĐˇŅƒ Immich ĐŊа GitHub. ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ ĐēĐģŅŽŅ‡ API Ņ‚Đ° вийĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚, Ņ‰ĐžĐą ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Obtainium", + "obtainium_configurator_instructions": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐšŅ‚Đĩ Obtainium Đ´ĐģŅ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ Ņ‚Đ° ĐžĐŊОвĐģĐĩĐŊĐŊŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Android ĐąĐĩСĐŋĐžŅĐĩŅ€ĐĩĐ´ĐŊŅŒĐž С Ņ€ĐĩĐģŅ–ĐˇŅƒ Immich ĐŊа GitHub. ĐĄŅ‚Đ˛ĐžŅ€Ņ–Ņ‚ŅŒ ĐēĐģŅŽŅ‡ API Ņ‚Đ° вийĐĩŅ€Ņ–Ņ‚ŅŒ Đ˛Đ°Ņ€Ņ–Đ°ĐŊŅ‚, Ņ‰ĐžĐą ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Obtainium", "ocr": "OCR", "official_immich_resources": "ĐžŅ„Ņ–Ņ†Ņ–ĐšĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ Immich", - "offline": "ĐžŅ„ĐģаКĐŊ", + "offline": "НĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "offset": "Đ—ŅŅƒĐ˛", - "ok": "ОК", + "ok": "ОĐē", "oldest_first": "ĐĄĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŊĐ°ĐšŅŅ‚Đ°Ņ€ŅˆŅ–", "on_this_device": "На Ņ†ŅŒĐžĐŧ҃ ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", "onboarding": "ВвĐĩĐ´ĐĩĐŊĐŊŅ", "onboarding_locale_description": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ йаĐļаĐŊ҃ ĐŧĐžĐ˛Ņƒ. Ви СĐŧĐžĐļĐĩŅ‚Đĩ СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ҆Đĩ ĐŋŅ–ĐˇĐŊŅ–ŅˆĐĩ в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "onboarding_privacy_description": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊŅ– (ĐŊĐĩĐžĐąĐžĐ˛â€™ŅĐˇĐēĐžĐ˛Ņ–) Ņ„ŅƒĐŊĐē҆Җҗ СаĐģĐĩĐļĐ°Ņ‚ŅŒ Đ˛Ņ–Đ´ СОвĐŊŅ–ŅˆĐŊŅ–Ņ… ҁĐĩŅ€Đ˛Ņ–ŅŅ–Đ˛ Ņ– ĐŧĐžĐļŅƒŅ‚ŅŒ ĐąŅƒŅ‚Đ¸ виĐŧĐēĐŊĐĩĐŊŅ– ĐąŅƒĐ´ŅŒ-ĐēĐžĐģи в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", - "onboarding_server_welcome_description": "Đ”Đ°Đ˛Đ°ĐšŅ‚Đĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒŅ”ĐŧĐž Đ˛Đ°ŅˆŅƒ Ņ–ĐŊŅŅ‚Đ°ĐŊŅ†Ņ–ŅŽ С Đ´ĐĩŅĐēиĐŧи ĐŋĐžŅˆĐ¸Ņ€ĐĩĐŊиĐŧи ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ°Đŧи.", - "onboarding_theme_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_map_view": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ ĐŊа ĐŧаĐŋŅ–", "open_in_openstreetmap": "Đ’Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸ в OpenStreetMap", "open_the_search_filters": "Đ’Ņ–Đ´ĐēŅ€Đ¸ĐšŅ‚Đĩ ҄ҖĐģŅŒŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", "options": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", @@ -1526,7 +1653,7 @@ "original": "ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ", "other": "ІĐŊ҈Đĩ", "other_devices": "ІĐŊŅˆŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅ—", - "other_entities": "ІĐŊŅˆŅ– Ой'Ņ”ĐēŅ‚Đ¸", + "other_entities": "ІĐŊŅˆŅ– Ņ„Đ°ĐšĐģи", "other_variables": "ІĐŊŅˆŅ– СĐŧŅ–ĐŊĐŊŅ–", "owned": "ВĐģĐ°ŅĐŊŅ–", "owner": "ВĐģĐ°ŅĐŊиĐē", @@ -1546,16 +1673,16 @@ "partner_sharing": "ĐĄĐŋŅ–ĐģҌĐŊĐĩ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "partners": "ĐŸĐ°Ņ€Ņ‚ĐŊĐĩŅ€Đ¸", "password": "ĐŸĐ°Ņ€ĐžĐģҌ", - "password_does_not_match": "ĐŸĐ°Ņ€ĐžĐģŅ– ĐŊĐĩ ĐˇĐąŅ–ĐŗĐ°Ņ”Ņ‚ŅŒŅŅ", + "password_does_not_match": "ĐŸĐ°Ņ€ĐžĐģŅ– ĐŊĐĩ ĐˇĐąŅ–ĐŗĐ°ŅŽŅ‚ŅŒŅŅ", "password_required": "ĐŸĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ ĐŋĐ°Ņ€ĐžĐģҌ", - "password_reset_success": "ĐŖŅĐŋŅ–ŅˆĐŊĐĩ ҁĐēидаĐŊĐŊŅ ĐŋĐ°Ņ€ĐžĐģŅ", + "password_reset_success": "ĐŸĐ°Ņ€ĐžĐģҌ ĐąŅƒĐģĐž ҃ҁĐŋŅ–ŅˆĐŊĐž ҁĐēиĐŊŅƒŅ‚Đž", "past_durations": { "days": "ĐŸŅ€ĐžĐšŅˆĐģĐž {days, plural, one {Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}", "hours": "За ĐžŅŅ‚Đ°ĐŊĐŊŅ– {hours, plural, one {ĐŗĐžĐ´Đ¸ĐŊ҃} few {# ĐŗĐžĐ´Đ¸ĐŊи} many {# ĐŗĐžĐ´Đ¸ĐŊ} other {# ĐŗĐžĐ´Đ¸ĐŊи}}", "years": "ĐŸŅ€ĐžĐšŅˆĐģĐž {years, plural, one {ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐē҃}}" }, "path": "ШĐģŅŅ…", - "pattern": "ĐŸĐ°Ņ‚ĐĩŅ€ĐŊ", + "pattern": "ШайĐģĐžĐŊ", "pause": "ĐŸĐ°ŅƒĐˇĐ°", "pause_memories": "ĐŸŅ€Đ¸ĐˇŅƒĐŋиĐŊĐ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "paused": "ĐŸŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐž", @@ -1563,16 +1690,17 @@ "people": "Đ›ŅŽĐ´Đ¸", "people_edits_count": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗĐžĐ˛Đ°ĐŊĐž {count, plural, one {# ĐžŅĐžĐąŅƒ} few {# ĐžŅĐžĐąĐ¸} many {# ĐžŅŅ–Đą} other {# ĐģŅŽĐ´ĐĩĐš}}", "people_feature_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš Ņ– Đ˛Ņ–Đ´ĐĩĐž, ĐˇĐŗŅ€ŅƒĐŋОваĐŊĐ¸Ņ… Са ĐģŅŽĐ´ŅŒĐŧи", + "people_selected": "{count, plural, one {# ĐžĐąŅ€Đ°ĐŊа ĐžŅĐžĐąĐ°} few {# Đ˛Đ¸ĐąŅ€Đ°ĐŊŅ– ĐžŅĐžĐąĐ¸} many {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžŅŅ–Đą} other {# Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžŅŅ–Đą}}", "people_sidebar_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐģŅŽĐ´ĐĩĐš ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", "permanent_deletion_warning": "ПоĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đž видаĐģĐĩĐŊĐŊŅ", - "permanent_deletion_warning_setting_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŧ҃ видаĐģĐĩĐŊĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛", + "permanent_deletion_warning_setting_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ ĐŋŅ€Đ¸ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐžĐŧ҃ видаĐģĐĩĐŊĐŊŅ– Ņ„Đ°ĐšĐģŅ–Đ˛", "permanently_delete": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŊаСавĐļди", - "permanently_delete_assets_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {Ņ€ĐĩŅŅƒŅ€Ņ} other {Ņ€ĐĩŅŅƒŅ€ŅĐ¸}}", - "permanently_delete_assets_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {҆ĐĩĐš Ņ€ĐĩŅŅƒŅ€Ņ?} other {҆Җ # Ņ€ĐĩŅŅƒŅ€ŅĐ¸?}} ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {ĐšĐžĐŗĐž С ĐšĐžĐŗĐž} other {Ņ—Ņ… С Ņ—Ņ…ĐŊŅ–Ņ…}} аĐģŅŒĐąĐžĐŧ҃(Ņ–Đ˛).", + "permanently_delete_assets_count": "ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "permanently_delete_assets_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŊаСавĐļди видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {҆ĐĩĐš Ņ„Đ°ĐšĐģ?} few {҆Җ # Ņ„Đ°ĐšĐģи?} many {҆Җ # Ņ„Đ°ĐšĐģŅ–Đ˛?} other {҆Җ # Ņ„Đ°ĐšĐģŅ–Đ˛?}} ĐĻĐĩ Ņ‚Đ°ĐēĐžĐļ видаĐģĐ¸Ņ‚ŅŒ {count, plural, one {ĐšĐžĐŗĐž С} few {Ņ—Ņ… С} many {Ņ—Ņ… С} other {Ņ—Ņ… С}} аĐģŅŒĐąĐžĐŧ҃(Ņ–Đ˛).", "permanently_deleted_asset": "ФаКĐģ видаĐģĐĩĐŊĐž ĐŊаСавĐļди", - "permanently_deleted_assets_count": "ВидаĐģĐĩĐŊĐž ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", + "permanently_deleted_assets_count": "ВидаĐģĐĩĐŊĐž ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "permission": "ДозвоĐģи", - "permission_empty": "ДозвоĐģи ĐŊĐĩ ĐŋОвиĐŊŅ– ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đŧи", + "permission_empty": "ДозвоĐģи ĐŊĐĩ ĐŋОвиĐŊĐŊŅ– ĐąŅƒŅ‚Đ¸ ĐŋĐžŅ€ĐžĐļĐŊŅ–Đŧи", "permission_onboarding_back": "Назад", "permission_onboarding_continue_anyway": "Đ’ŅĐĩ ОдĐŊĐž ĐŋŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸", "permission_onboarding_get_started": "РОСĐŋĐžŅ‡Đ°Ņ‚Đ¸", @@ -1582,16 +1710,19 @@ "permission_onboarding_permission_limited": "Đ”ĐžŅŅ‚ŅƒĐŋ ОйĐŧĐĩĐļĐĩĐŊĐž. ЊОйи дОСвОĐģĐ¸Ņ‚Đ¸ Immich ŅŅ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊŅ– ĐēĐžĐŋŅ–Ņ— Ņ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ”ŅŽ ĐŗĐ°ĐģĐĩŅ€ĐĩŅ”ŅŽ, ĐŊĐ°Đ´Đ°ĐšŅ‚Đĩ дОСвОĐģи ĐŊа Ņ„ĐžŅ‚Đž Đš Đ˛Ņ–Đ´ĐĩĐž в ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅŅ….", "permission_onboarding_request": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ Immich ĐŋĐžŅ‚Ņ€Ņ–ĐąĐĩĐŊ Đ´ĐžĐˇĐ˛Ņ–Đģ Đ´ĐģŅ ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ Đ˛Đ°ŅˆĐ¸Ņ… Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", "person": "Đ›ŅŽĐ´Đ¸ĐŊа", - "person_age_months": "{months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} other {# ĐŧŅ–ŅŅŅ†Ņ–}}", - "person_age_year_months": "1 year , {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} other {# ĐŧŅ–ŅŅŅ†Ņ–}}", - "person_age_years": "{years, plural, other {# Ņ€ĐžĐēŅ–Đ˛}}", - "person_birthdate": "ĐĐ°Ņ€ĐžĐ´Đ¸Đ˛ŅŅ {date}", + "person_age_months": "{months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "person_age_year_months": "1 ҀҖĐē, {months, plural, one {# ĐŧŅ–ŅŅŅ†ŅŒ} few {# ĐŧŅ–ŅŅŅ†Ņ–} many {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛} other {# ĐŧŅ–ŅŅŅ†Ņ–Đ˛}}", + "person_age_years": "{years, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}}", + "person_birthdate": "Đ”Đ°Ņ‚Đ° ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ: {date}", "person_hidden": "{name}{hidden, select, true { (ĐŋŅ€Đ¸Ņ…ĐžĐ˛Đ°ĐŊĐž)} other {}}", + "person_recognized": "ĐžŅĐžĐąŅƒ Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐģи", + "person_selected": "ĐžĐąŅ€Đ°ĐŊа ĐžŅĐžĐąĐ°", "photo_shared_all_users": "Đ’Đ¸ĐŗĐģŅĐ´Đ°Ņ” Ņ‚Đ°Đē, Ņ‰Đž ви ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅ ŅĐ˛ĐžŅ—Đŧи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–ŅĐŧи С ŅƒŅŅ–Đŧа ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°Đŧи айО ҃ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ĐļОдĐŊĐžĐŗĐž ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°, С ŅĐēиĐŧ ĐŧĐžĐļĐŊа ĐŋĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ.", "photos": "Đ¤ĐžŅ‚Đž", "photos_and_videos": "Đ¤ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "photos_count": "{count, plural, one {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ} few {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—} many {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš} other {{count, number} Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Đš}}", "photos_from_previous_years": "Đ¤ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— ĐŧиĐŊ҃ĐģĐ¸Ņ… Ņ€ĐžĐēŅ–Đ˛ ҃ ҆ĐĩĐš Đ´ĐĩĐŊҌ", + "photos_only": "ĐĸŅ–ĐģҌĐēи Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", "pick_a_location": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐŧҖҁ҆Đĩ Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ", "pick_custom_range": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ", "pick_date_range": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", @@ -1601,7 +1732,7 @@ "pin_verification": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēа PIN-ĐēĐžĐ´Ņƒ", "place": "ĐœŅ–ŅŅ†Đĩ", "places": "ĐœŅ–ŅŅ†Ņ", - "places_count": "{count, plural, one {{count, number} ĐœŅ–ŅŅ†Đĩ} other {{count, number} ĐœŅ–ŅŅ†Ņ}}", + "places_count": "{count, plural, one {{count, number} ĐœŅ–ŅŅ†Đĩ} few {{count, number} ĐœŅ–ŅŅ†Ņ} many {{count, number} ĐœŅ–ŅŅ†ŅŒ} other {{count, number} ĐœŅ–ŅŅ†ŅŒ}}", "play": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸", "play_memories": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋĐžĐŗĐ°Đ´Đ¸", "play_motion_photo": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧŅ– Ņ„ĐžŅ‚Đž", @@ -1615,7 +1746,7 @@ "preferences_settings_title": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸", "preparing": "ĐŸŅ–Đ´ĐŗĐžŅ‚ĐžĐ˛Đēа", "preset": "ПĐĩŅ€ĐĩĐ´Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐŊŅ", - "preview": "ĐŸŅ€Đĩв'ŅŽ", + "preview": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´", "previous": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ”", "previous_memory": "ПоĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš ҁĐŋĐžĐŗĐ°Đ´", "previous_or_next_day": "ДĐĩĐŊҌ вĐŋĐĩŅ€ĐĩĐ´/ĐŊаСад", @@ -1652,7 +1783,7 @@ "purchase_license_subtitle": "ĐšŅƒĐŋŅ–Ņ‚ŅŒ Immich, Ņ‰ĐžĐą ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŋОдаĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐ˛Đ¸Ņ‚ĐžĐē ҁĐĩŅ€Đ˛Ņ–ŅŅƒ", "purchase_lifetime_description": "НазавĐļди", "purchase_option_title": "ВАРІАНĐĸИ ĐšĐŖĐŸĐ†Đ’Đ›Đ†", - "purchase_panel_info_1": "Đ ĐžĐˇŅ€ĐžĐąĐēа Immich виĐŧĐ°ĐŗĐ°Ņ” ĐąĐ°ĐŗĐ°Ņ‚Đž Ņ‡Đ°ŅŅƒ Ņ‚Đ° ĐˇŅƒŅĐ¸ĐģҌ. Ми ĐŧĐ°Ņ”ĐŧĐž ŅˆŅ‚Đ°Ņ‚ĐŊĐ¸Ņ… Ņ–ĐŊĐļĐĩĐŊĐĩŅ€Ņ–Đ˛, ŅĐēŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŊад Ņ‚Đ¸Đŧ, Ņ‰ĐžĐą ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐšĐžĐŗĐž ŅĐēĐžĐŧĐžĐŗĐ° ĐēŅ€Đ°Ņ‰Đ¸Đŧ. ĐĐ°ŅˆĐ° ĐŧŅ–ŅŅ–Ņ — ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅ С Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸Đŧ ĐēОдОĐŧ Ņ‚Đ° ĐĩŅ‚Đ¸Ņ‡ĐŊŅ– ĐąŅ–ĐˇĐŊĐĩҁ-ĐŋŅ€Đ°ĐēŅ‚Đ¸Đēи ŅŅ‚Ņ–ĐšĐēиĐŧ Đ´ĐļĐĩŅ€ĐĩĐģĐžĐŧ Đ´ĐžŅ…ĐžĐ´Ņƒ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛ Ņ– ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐĩĐēĐžŅĐ¸ŅŅ‚ĐĩĐŧ҃, Ņ‰Đž ĐŋОваĐļĐ°Ņ” ĐŋŅ€Đ¸Đ˛Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ, С Ņ€ĐĩаĐģҌĐŊиĐŧи аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛Đ°Đŧи ĐĩĐēҁĐŋĐģŅƒĐ°Ņ‚Đ°Ņ‚ĐžŅ€ŅŅŒĐēиĐŧ Ņ…ĐŧĐ°Ņ€ĐŊиĐŧ ҁĐĩŅ€Đ˛Ņ–ŅĐ°Đŧ.", + "purchase_panel_info_1": "Đ ĐžĐˇŅ€ĐžĐąĐēа Immich виĐŧĐ°ĐŗĐ°Ņ” ĐąĐ°ĐŗĐ°Ņ‚Đž Ņ‡Đ°ŅŅƒ Ņ‚Đ° ĐˇŅƒŅĐ¸ĐģҌ. Ми ĐŧĐ°Ņ”ĐŧĐž ŅˆŅ‚Đ°Ņ‚ĐŊĐ¸Ņ… Ņ–ĐŊĐļĐĩĐŊĐĩŅ€Ņ–Đ˛, ŅĐēŅ– ĐŋŅ€Đ°Ņ†ŅŽŅŽŅ‚ŅŒ ĐŊад Ņ‚Đ¸Đŧ, Ņ‰ĐžĐą ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐšĐžĐŗĐž ŅĐēĐžĐŧĐžĐŗĐ° ĐēŅ€Đ°Ņ‰Đ¸Đŧ. ĐĐ°ŅˆĐ° ĐŧŅ–ŅŅ–Ņ — ĐˇŅ€ĐžĐąĐ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐŗŅ€Đ°ĐŧĐŊĐĩ СайĐĩСĐŋĐĩ҇ĐĩĐŊĐŊŅ С Đ˛Ņ–Đ´ĐēŅ€Đ¸Ņ‚Đ¸Đŧ ĐēОдОĐŧ Ņ‚Đ° ĐĩŅ‚Đ¸Ņ‡ĐŊŅ– ĐąŅ–ĐˇĐŊĐĩҁ-ĐŋŅ€Đ°ĐēŅ‚Đ¸Đēи ŅŅ‚Ņ–ĐšĐēиĐŧ Đ´ĐļĐĩŅ€ĐĩĐģĐžĐŧ Đ´ĐžŅ…ĐžĐ´Ņƒ Đ´ĐģŅ Ņ€ĐžĐˇŅ€ĐžĐąĐŊиĐēŅ–Đ˛ Ņ– ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐĩĐēĐžŅĐ¸ŅŅ‚ĐĩĐŧ҃, Ņ‰Đž ĐŋОваĐļĐ°Ņ” ĐēĐžĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ, С Ņ€ĐĩаĐģҌĐŊиĐŧи аĐģŅŒŅ‚ĐĩŅ€ĐŊĐ°Ņ‚Đ¸Đ˛Đ°Đŧи ĐĩĐēҁĐŋĐģŅƒĐ°Ņ‚Đ°Ņ‚ĐžŅ€ŅŅŒĐēиĐŧ Ņ…ĐŧĐ°Ņ€ĐŊиĐŧ ҁĐĩŅ€Đ˛Ņ–ŅĐ°Đŧ.", "purchase_panel_info_2": "ĐžŅĐēŅ–ĐģҌĐēи Đŧи ĐˇĐžĐąĐžĐ˛â€™ŅĐˇŅƒŅ”ĐŧĐžŅŅ ĐŊĐĩ Đ´ĐžĐ´Đ°Đ˛Đ°Ņ‚Đ¸ ĐŋĐģĐ°Ņ‚ĐŊŅ– ОйĐŧĐĩĐļĐĩĐŊĐŊŅ, Ņ†Ņ ĐŋĐžĐē҃ĐŋĐēа ĐŊĐĩ ĐŊĐ°Đ´Đ°ŅŅ‚ŅŒ ваĐŧ Đ´ĐžĐ´Đ°Ņ‚ĐēĐžĐ˛Đ¸Ņ… Ņ„ŅƒĐŊĐēŅ†Ņ–Đš в Immich. Ми ĐŋĐžĐēĐģĐ°Đ´Đ°Ņ”ĐŧĐžŅŅ ĐŊа Ņ‚Đ°ĐēĐ¸Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛, ŅĐē ви, Ņ‰ĐžĐą ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋОдаĐģŅŒŅˆĐ¸Đš Ņ€ĐžĐˇĐ˛Đ¸Ņ‚ĐžĐē Immich.", "purchase_panel_title": "ĐŸŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐ°Ņ‚Đ¸ ĐŋŅ€ĐžŅ”ĐēŅ‚", "purchase_per_server": "На ҁĐĩŅ€Đ˛ĐĩŅ€", @@ -1665,23 +1796,25 @@ "purchase_server_description_2": "ĐĄŅ‚Đ°Ņ‚ŅƒŅ ĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧĐēи", "purchase_server_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€", "purchase_settings_server_activated": "КĐģŅŽŅ‡ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Ņƒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° ĐēĐĩŅ€ŅƒŅ”Ņ‚ŅŒŅŅ адĐŧŅ–ĐŊŅ–ŅŅ‚Ņ€Đ°Ņ‚ĐžŅ€ĐžĐŧ", - "query_asset_id": "ІдĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€ Ņ€ĐĩŅŅƒŅ€ŅŅƒ СаĐŋĐ¸Ņ‚Ņƒ", + "query_asset_id": "ІдĐĩĐŊŅ‚Đ¸Ņ„Ņ–ĐēĐ°Ņ‚ĐžŅ€ Ņ„Đ°ĐšĐģ҃ СаĐŋĐ¸Ņ‚Ņƒ", "queue_status": "ĐŖ ҇ĐĩŅ€ĐˇŅ– {count} С {total}", + "rate_asset": "ĐžŅ†Ņ–ĐŊĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", "rating": "Đ—ĐžŅ€ŅĐŊиК Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating_clear": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "rating_count": "{count, plural, one {# ĐˇŅ–Ņ€Đēа} few {# ĐˇŅ–Ņ€Đēи} many {# ĐˇŅ–Ņ€ĐžĐē} other {# ĐˇŅ–Ņ€ĐžĐē}}", "rating_description": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ EXIF ĐŊа Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–ĐšĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", - "reaction_options": "ОĐŋ҆Җҗ Ņ€ĐĩаĐē҆Җҗ", + "rating_set": "Đ ĐĩĐšŅ‚Đ¸ĐŊĐŗ Đ˛ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž ĐŊа {rating, plural, one {# ĐˇŅ–Ņ€Đē҃} few {# ĐˇŅ–Ņ€Đēи} many {# ĐˇŅ–Ņ€ĐžĐē} other {# ĐˇŅ–Ņ€ĐžĐē}}", + "reaction_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ Ņ€ĐĩаĐē҆Җҗ", "read_changelog": "ĐŸŅ€ĐžŅ‡Đ¸Ņ‚Đ°Ņ‚Đ¸ СĐŧŅ–ĐŊи в ĐžĐŊОвĐģĐĩĐŊĐŊŅ–", "readonly_mode_disabled": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž", "readonly_mode_enabled": "Đ ĐĩĐļиĐŧ ĐģĐ¸ŅˆĐĩ Đ´ĐģŅ Ņ‡Đ¸Ņ‚Đ°ĐŊĐŊŅ Đ˛Đ˛Ņ–ĐŧĐēĐŊĐĩĐŊĐž", - "ready_for_upload": "Đ“ĐžŅ‚ĐžĐ˛Đž Đ´Đž СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "ready_for_upload": "Đ“ĐžŅ‚ĐžĐ˛Đž Đ´Đž виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "reassign": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸", - "reassigned_assets_to_existing_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", - "reassigned_assets_to_new_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} other {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸}} ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", - "reassing_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸ ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–", + "reassigned_assets_to_existing_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} {name, select, null {ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–} other {{name}}}", + "reassigned_assets_to_new_person": "ПĐĩŅ€ĐĩĐŋŅ€Đ¸ĐˇĐŊĐ°Ņ‡ĐĩĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} ĐŊĐžĐ˛Ņ–Đš ĐžŅĐžĐąŅ–", + "reassing_hint": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи ҖҁĐŊŅƒŅŽŅ‡Ņ–Đš ĐžŅĐžĐąŅ–", "recent": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž", - "recent-albums": "ĐžŅŅ‚Đ°ĐŊĐŊŅ– аĐģŅŒĐąĐžĐŧи", + "recent_albums": "ĐžŅŅ‚Đ°ĐŊĐŊŅ– аĐģŅŒĐąĐžĐŧи", "recent_searches": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ– ĐŋĐžŅˆŅƒĐēĐžĐ˛Ņ– СаĐŋĐ¸Ņ‚Đ¸", "recently_added": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊĐž дОдаĐŊŅ–", "recently_added_page_title": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ–", @@ -1697,20 +1830,20 @@ "refreshing_encoded_video": "ОĐŊОвĐģĐĩĐŊĐŊŅ СаĐēОдОваĐŊĐžĐŗĐž Đ˛Ņ–Đ´ĐĩĐž", "refreshing_faces": "ОĐŊОвĐģĐĩĐŊĐŊŅ ОйĐģĐ¸Ņ‡", "refreshing_metadata": "ОĐŊОвĐģĐĩĐŊĐŊŅ ĐŧĐĩŅ‚Đ°Đ´Đ°ĐŊĐ¸Ņ…", - "regenerating_thumbnails": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", + "regenerating_thumbnails": "ĐŸĐžĐ˛Ņ‚ĐžŅ€ĐŊĐĩ ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€", "remote": "На ҁĐĩŅ€Đ˛ĐĩҀҖ", "remote_assets": "Đ’Ņ–Đ´Đ´Đ°ĐģĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "remote_media_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Đ˛Ņ–Đ´Đ´Đ°ĐģĐĩĐŊĐ¸Ņ… ĐŧĐĩĐ´Ņ–Đ°Ņ„Đ°ĐšĐģŅ–Đ˛", "remove": "ВиĐģŅƒŅ‡Đ¸Ņ‚Đ¸", - "remove_assets_album_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} С аĐģŅŒĐąĐžĐŧ҃?", - "remove_assets_shared_link_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}} С Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ?", - "remove_assets_title": "ВидаĐģĐ¸Ņ‚Đ¸ Ой'Ņ”ĐēŅ‚Đ¸?", + "remove_assets_album_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С аĐģŅŒĐąĐžĐŧ҃?", + "remove_assets_shared_link_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}} С Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ?", + "remove_assets_title": "ВидаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи?", "remove_custom_date_range": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", "remove_deleted_assets": "ВидаĐģĐĩĐŊĐŊŅ Đ°Đ˛Ņ‚ĐžĐŊĐžĐŧĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "remove_from_album": "ВидаĐģĐ¸Ņ‚Đ¸ С аĐģŅŒĐąĐžĐŧ҃", "remove_from_album_action_prompt": "{count} видаĐģĐĩĐŊĐž С аĐģŅŒĐąĐžĐŧ҃", "remove_from_favorites": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", - "remove_from_lock_folder_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С ĐˇĐ°Ņ…Đ¸Ņ‰ĐĩĐŊĐžŅ— Ņ‚ĐĩĐēи", + "remove_from_lock_folder_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "remove_from_locked_folder": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи", "remove_from_locked_folder_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐŋĐĩŅ€ĐĩĐŧŅ–ŅŅ‚Đ¸Ņ‚Đ¸ ҆Җ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐžŅĐžĐąĐ¸ŅŅ‚ĐžŅ— ĐŋаĐŋĐēи? ВоĐŊи ĐąŅƒĐ´ŅƒŅ‚ŅŒ видиĐŧŅ– ҃ Đ˛Đ°ŅˆŅ–Đš ĐąŅ–ĐąĐģŅ–ĐžŅ‚Đĩ҆Җ.", "remove_from_shared_link": "ВидаĐģĐ¸Ņ‚Đ¸ ĐˇŅ– ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", @@ -1723,13 +1856,13 @@ "removed_from_archive": "ВидаĐģĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", "removed_from_favorites": "ВидаĐģĐĩĐŊĐž С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "removed_from_favorites_count": "{count, plural, other {ВидаĐģĐĩĐŊĐž #}} С ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…", - "removed_memory": "ВидаĐģĐĩĐŊа ĐŋаĐŧ'ŅŅ‚ŅŒ", - "removed_photo_from_memory": "Đ¤ĐžŅ‚Đž видаĐģĐĩĐŊĐĩ С ĐŋаĐŧ'ŅŅ‚Ņ–", - "removed_tagged_assets": "ВидаĐģĐĩĐŊĐž Ņ‚ĐĩĐŗ Ņ–Đˇ {count, plural, one {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņƒ} other {# ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛}}", + "removed_memory": "ВидаĐģĐĩĐŊиК ҁĐŋĐžĐŗĐ°Đ´", + "removed_photo_from_memory": "Đ¤ĐžŅ‚Đž видаĐģĐĩĐŊĐĩ ĐˇŅ– ҁĐŋĐžĐŗĐ°Đ´Ņƒ", + "removed_tagged_assets": "ВидаĐģĐĩĐŊĐž Ņ‚ĐĩĐŗ Ņ–Đˇ {count, plural, one {# Ņ„Đ°ĐšĐģ҃} few {# Ņ„Đ°ĐšĐģŅ–Đ˛} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "rename": "ПĐĩŅ€ĐĩĐšĐŧĐĩĐŊŅƒĐ˛Đ°Ņ‚Đ¸", - "repair": "Đ ĐĩĐŧĐžĐŊŅ‚", + "repair": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊĐŊŅ", "repair_no_results_message": "НĐĩĐ˛Ņ–Đ´ŅŅ‚ĐĩĐļŅƒĐ˛Đ°ĐŊŅ– Ņ‚Đ° Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊŅ– Ņ‚ŅƒŅ‚", - "replace_with_upload": "ЗаĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐĩ", + "replace_with_upload": "ЗаĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŊа виваĐŊŅ‚Đ°ĐļĐĩĐŊĐĩ", "repository": "Đ ĐĩĐŋĐžĐˇĐ¸Ņ‚ĐžŅ€Ņ–Đš", "require_password": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "require_user_to_change_password_on_first_login": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ Đ˛Ņ–Đ´ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° СĐŧŅ–ĐŊŅŽĐ˛Đ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ ĐŋŅ€Đ¸ ĐŋĐĩŅ€ŅˆĐžĐŧ҃ Đ˛Ņ…ĐžĐ´Ņ–", @@ -1742,9 +1875,9 @@ "reset_pin_code_success": "PIN-ĐēОд ҃ҁĐŋŅ–ŅˆĐŊĐž ҁĐēиĐŊŅƒŅ‚Đž", "reset_pin_code_with_password": "Ви СавĐļди ĐŧĐžĐļĐĩŅ‚Đĩ ҁĐēиĐŊŅƒŅ‚Đ¸ ŅĐ˛Ņ–Đš PIN-ĐēОд Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ ĐŋĐ°Ņ€ĐžĐģŅ", "reset_sqlite": "ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite", - "reset_sqlite_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite? ĐŸŅ–ŅĐģŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐąŅƒĐ´Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С аĐēĐ°ŅƒĐŊŅ‚Đ° Ņ‚Đ° ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ СĐŊĐžĐ˛Ņƒ Đ´ĐģŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžŅ— ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— даĐŊĐ¸Ņ…", + "reset_sqlite_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐąĐ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite? ĐŸŅ–ŅĐģŅ Ņ†ŅŒĐžĐŗĐž ĐŋĐžŅ‚Ņ€Ņ–ĐąĐŊĐž ĐąŅƒĐ´Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ С ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ Ņ‚Đ° ŅƒĐ˛Ņ–ĐšŅ‚Đ¸ СĐŊĐžĐ˛Ņƒ Đ´ĐģŅ ĐŋĐžĐ˛Ņ‚ĐžŅ€ĐŊĐžŅ— ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ— даĐŊĐ¸Ņ…", "reset_sqlite_success": "Đ‘Đ°ĐˇŅƒ даĐŊĐ¸Ņ… SQLite ҃ҁĐŋŅ–ŅˆĐŊĐž ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž", - "reset_to_default": "ĐĄĐēидаĐŊĐŊŅ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ", + "reset_to_default": "ĐĄĐēиĐŊŅƒŅ‚Đ¸ Đ´Đž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Са СаĐŧĐžĐ˛Ņ‡ŅƒĐ˛Đ°ĐŊĐŊŅĐŧ", "resolution": "Đ ĐžĐˇĐ´Ņ–ĐģҌĐŊа Đ—Đ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŒ", "resolve_duplicates": "ĐŖŅŅƒĐŊŅƒŅ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "resolved_all_duplicates": "ĐŖŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸ ҃ҁ҃ĐŊŅƒŅ‚Đž", @@ -1752,16 +1885,16 @@ "restore_all": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "restore_trash_action_prompt": "{count} Đ˛Ņ–Đ´ĐŊОвĐģĐĩĐŊĐž С ĐēĐžŅˆĐ¸Đēа", "restore_user": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "restored_asset": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ", + "restored_asset": "Đ’Ņ–Đ´ĐŊОвĐģĐĩĐŊиК Ņ„Đ°ĐšĐģ", "resume": "ĐŸŅ€ĐžĐ´ĐžĐ˛ĐļĐ¸Ņ‚Đ¸", - "resume_paused_jobs": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ {count, plural, one {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐĩ СавдаĐŊĐŊŅ} other {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊŅ– СавдаĐŊĐŊŅ}}", - "retry_upload": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đ¸ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "resume_paused_jobs": "Đ’Ņ–Đ´ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ {count, plural, one {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐĩ СавдаĐŊĐŊŅ} few {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊŅ– СавдаĐŊĐŊŅ} many {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐ¸Ņ… СавдаĐŊҌ} other {# ĐŋŅ€Đ¸ĐˇŅƒĐŋиĐŊĐĩĐŊĐ¸Ņ… СавдаĐŊҌ}}", + "retry_upload": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đ¸ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "review_duplicates": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "review_large_files": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ вĐĩĐģиĐēĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛", "role": "Đ ĐžĐģҌ", "role_editor": "Đ ĐĩдаĐēŅ‚ĐžŅ€", "role_viewer": "ГĐģŅĐ´Đ°Ņ‡", - "running": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ", + "running": "АĐēŅ‚Đ¸Đ˛ĐŊиК", "save": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸", "save_to_gallery": "ЗбĐĩŅ€ĐĩĐŗŅ‚Đ¸ в ĐŗĐ°ĐģĐĩŅ€ĐĩŅŽ", "saved": "ЗбĐĩŅ€ĐĩĐļĐĩĐŊĐž", @@ -1770,9 +1903,11 @@ "saved_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊĐž", "say_something": "ĐĄĐēаĐļŅ–Ņ‚ŅŒ Ņ‰Đž-ĐŊĐĩĐąŅƒĐ´ŅŒ", "scaffold_body_error_occurred": "ВиĐŊиĐēĐģа ĐŋĐžĐŧиĐģĐēа", + "scan": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", "scan_all_libraries": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛ŅŅ– ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "scan_library": "ĐĄĐēаĐŊŅƒĐ˛Đ°Ņ‚Đ¸", "scan_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", + "scanning": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ", "scanning_for_album": "ĐĄĐēаĐŊŅƒĐ˛Đ°ĐŊĐŊŅ аĐģŅŒĐąĐžĐŧ҃...", "search": "ĐŸĐžŅˆŅƒĐē", "search_albums": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", @@ -1802,16 +1937,17 @@ "search_filter_media_type_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ Ņ‚Đ¸Đŋ ĐŧĐĩĐ´Ņ–Đ°", "search_filter_ocr": "ĐŸĐžŅˆŅƒĐē Са OCR", "search_filter_people_title": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´ĐĩĐš", + "search_filter_star_rating": "Đ—ĐžŅ€ŅĐŊиК Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ", "search_for": "Đ¨ŅƒĐēĐ°Ņ‚Đ¸ Đ´ĐģŅ", "search_for_existing_person": "ĐŸĐžŅˆŅƒĐē ҖҁĐŊŅƒŅŽŅ‡ĐžŅ— ĐžŅĐžĐąĐ¸", "search_no_more_result": "Đ‘Ņ–ĐģҌ҈Đĩ Ņ€ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛ ĐŊĐĩĐŧĐ°Ņ”", "search_no_people": "НĐĩĐŧĐ°Ņ” ĐģŅŽĐ´ĐĩĐš", "search_no_people_named": "НĐĩĐŧĐ°Ņ” ĐžŅŅ–Đą С Ņ–ĐŧĐĩĐŊĐĩĐŧ \"{name}\"", "search_no_result": "Đ ĐĩĐˇŅƒĐģŅŒŅ‚Đ°Ņ‚Ņ–Đ˛ ĐŊĐĩ СĐŊаКдĐĩĐŊĐž, ҁĐŋŅ€ĐžĐąŅƒĐšŅ‚Đĩ Ņ–ĐŊŅˆĐ¸Đš СаĐŋĐ¸Ņ‚ айО ĐēĐžĐŧĐąŅ–ĐŊĐ°Ņ†Ņ–ŅŽ", - "search_options": "ОĐŋ҆Җҗ ĐŋĐžŅˆŅƒĐē҃", + "search_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", "search_page_categories": "ĐšĐ°Ņ‚ĐĩĐŗĐžŅ€Ņ–Ņ—", "search_page_motion_photos": "Đ–Đ¸Đ˛Ņ– Ņ„ĐžŅ‚Đž", - "search_page_no_objects": "НĐĩĐŧĐ°Ņ” Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ой'Ņ”ĐēŅ‚Đ¸", + "search_page_no_objects": "НĐĩĐŧĐ°Ņ” Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ— ĐŋŅ€Đž Ņ„Đ°ĐšĐģи", "search_page_no_places": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž ĐŧŅ–ŅŅ†Ņ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊа", "search_page_screenshots": "ЗĐŊŅ–ĐŧĐēи ĐĩĐēŅ€Đ°ĐŊ҃", "search_page_search_photos_videos": "Đ¨ŅƒĐēĐ°ĐšŅ‚Đĩ Đ˛Đ°ŅˆŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", @@ -1835,50 +1971,56 @@ "searching_locales": "ĐĸŅ€Đ¸Đ˛Đ°Ņ” ĐŋĐžŅˆŅƒĐē ĐŋĐĩŅ€ĐĩĐēĐģĐ°Đ´Ņ–Đ˛...", "second": "ĐĄĐĩĐē҃ĐŊда", "see_all_people": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ–Ņ… ĐģŅŽĐ´ĐĩĐš", - "select": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ", + "select": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸", + "select_album": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "select_album_cover": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", + "select_albums": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", "select_all": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅĐĩ", "select_all_duplicates": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅŅ– Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "select_all_in": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Đ˛ŅĐĩ в {group}", "select_avatar_color": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐēĐžĐģŅ–Ņ€ Đ°Đ˛Đ°Ņ‚Đ°Ņ€Đ°", + "select_count": "{count, plural, one {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} few {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} many {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #} other {Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ #}}", + "select_cutoff_date": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐēŅ–ĐŊ҆ĐĩĐ˛Ņƒ Đ´Đ°Ņ‚Ņƒ", "select_face": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ОйĐģĐ¸Ņ‡Ņ‡Ņ", "select_featured_photo": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊĐĩ Ņ„ĐžŅ‚Đž", "select_from_computer": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ С ĐēĐžĐŧĐŋ'ŅŽŅ‚ĐĩŅ€Đ°", "select_keep_all": "ЗаĐģĐ¸ŅˆĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ ĐžĐąŅ€Đ°ĐŊĐĩ", "select_library_owner": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ вĐģĐ°ŅĐŊиĐēа ĐąŅ–ĐąĐģŅ–ĐžŅ‚ĐĩĐēи", "select_new_face": "ĐžĐąŅ€Đ°Ņ‚Đ¸ ĐŊОвĐĩ ОйĐģĐ¸Ņ‡Ņ‡Ņ", + "select_people": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš", + "select_person": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐžŅĐžĐąŅƒ", "select_person_to_tag": "ВибĐĩŅ€Ņ–Ņ‚ŅŒ ĐģŅŽĐ´Đ¸ĐŊ҃ Đ´ĐģŅ ĐŋОСĐŊĐ°Ņ‡ĐĩĐŊĐŊŅ", "select_photos": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "select_trash_all": "ВидаĐģĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐĩ", "select_user_for_sharing_page_err_album": "НĐĩ вдаĐģĐžŅŅ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "selected": "ĐžĐąŅ€Đ°ĐŊĐž", - "selected_count": "{count, plural, one {# ĐžĐąŅ€Đ°ĐŊиК} other {# ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…}}", - "selected_gps_coordinates": "Đ’Đ¸ĐąŅ€Đ°ĐŊŅ– GPS-ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸", + "selected_count": "{count, plural, one {# ĐžĐąŅ€Đ°ĐŊиК} few {# ĐžĐąŅ€Đ°ĐŊŅ–} many {# ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…} other {# ĐžĐąŅ€Đ°ĐŊĐ¸Ņ…}}", + "selected_gps_coordinates": "Đ’Đ¸ĐąŅ€Đ°ĐŊŅ– ĐēĐžĐžŅ€Đ´Đ¸ĐŊĐ°Ņ‚Đ¸", "send_message": "ĐĐ°Đ´Ņ–ŅĐģĐ°Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", "send_welcome_email": "ĐĐ°Đ´Ņ–ŅˆĐģŅ–Ņ‚ŅŒ Đ˛Ņ–Ņ‚Đ°ĐģҌĐŊиК ĐģĐ¸ŅŅ‚", "server_endpoint": "ĐĐ´Ņ€ĐĩŅĐ° ҁĐĩŅ€Đ˛ĐĩŅ€Ņƒ", "server_info_box_app_version": "ВĐĩŅ€ŅŅ–Ņ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "server_info_box_server_url": "URL ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "server_offline": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐžŅ„ĐģаКĐŊ", - "server_online": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐžĐŊĐģаКĐŊ", + "server_offline": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐŊĐĩĐ´ĐžŅŅ‚ŅƒĐŋĐŊиК", + "server_online": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ Đ´ĐžŅŅ‚ŅƒĐŋĐŊиК", "server_privacy": "КоĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_restarting_description": "ĐĻŅ ŅŅ‚ĐžŅ€Ņ–ĐŊĐēа ĐžĐŊĐžĐ˛Đ¸Ņ‚ŅŒŅŅ ĐŧĐ¸Ņ‚Ņ‚Ņ”Đ˛Đž.", "server_restarting_title": "ĐĄĐĩŅ€Đ˛ĐĩŅ€ ĐŋĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅƒŅ”Ņ‚ŅŒŅŅ", "server_stats": "ĐĄŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "server_update_available": "ОĐŊОвĐģĐĩĐŊĐŊŅ ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐĩ", "server_version": "ВĐĩŅ€ŅŅ–Ņ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", - "set": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Ņ–Ņ‚ŅŒ", + "set": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸", "set_as_album_cover": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅĐē ОйĐēĐģадиĐŊĐē҃ аĐģŅŒĐąĐžĐŧ҃", "set_as_featured_photo": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊĐĩ Ņ„ĐžŅ‚Đž", "set_as_profile_picture": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅĐē ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", "set_date_of_birth": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Đ´Đ°Ņ‚Ņƒ ĐŊĐ°Ņ€ĐžĐ´ĐļĐĩĐŊĐŊŅ", "set_profile_picture": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋŅ€ĐžŅ„Ņ–ĐģŅŽ", "set_slideshow_to_fullscreen": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ҁĐģаКд-ŅˆĐžŅƒ ĐŊа вĐĩҁҌ ĐĩĐēŅ€Đ°ĐŊ", - "set_stack_primary_asset": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ", + "set_stack_primary_asset": "Đ’ŅŅ‚Đ°ĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК Ņ„Đ°ĐšĐģ", "setting_image_viewer_help": "ПовĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊиК ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Đ°Ņ‡ ҁĐŋĐžŅ‡Đ°Ņ‚Đē҃ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ в ĐŊĐ¸ĐˇŅŒĐēŅ–Đš Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–Đš ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–, ĐŋĐžŅ‚Ņ–Đŧ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ в СĐŧĐĩĐŊ҈ĐĩĐŊŅ–Đš Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊŅ–Đš ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ– Đ˛Ņ–Đ´ĐŊĐžŅĐŊĐž ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ҃ (ŅĐēŅ‰Đž вĐēĐģŅŽŅ‡ĐĩĐŊĐž) Ņ– ĐˇŅ€ĐĩŅˆŅ‚ĐžŅŽ СаваĐŊŅ‚Đ°ĐļŅƒŅ” ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ (ŅĐēŅ‰Đž вĐēĐģŅŽŅ‡ĐĩĐŊĐž).", "setting_image_viewer_original_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ С ĐŋОвĐŊĐžŅŽ Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅŽ ĐˇĐ´Đ°Ņ‚ĐŊŅ–ŅŅ‚ŅŽ (вĐĩĐģиĐēĐĩ!). ВиĐŧĐēĐŊŅƒŅ‚Đ¸, Ņ‰ĐžĐą СĐŧĐĩĐŊŅˆĐ¸Ņ‚Đ¸ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ даĐŊĐ¸Ņ… (ŅĐē ҇ĐĩŅ€ĐĩС ĐŧĐĩŅ€ĐĩĐļ҃, Ņ‚Đ°Đē Ņ– ĐŊа ĐēĐĩŅˆŅ– ĐŋŅ€Đ¸ŅŅ‚Ņ€ĐžŅŽ).", "setting_image_viewer_original_title": "ЗаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", - "setting_image_viewer_preview_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžŅ— Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–. ВиĐŧĐēĐŊŅƒŅ‚Đ¸, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ айО виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҂ҖĐģҌĐēи ĐĩҁĐēŅ–Đˇ.", + "setting_image_viewer_preview_subtitle": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Đ´ĐģŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ҁĐĩŅ€ĐĩĐ´ĐŊŅŒĐžŅ— Ņ€ĐžĐˇĐ´Ņ–ĐģҌĐŊĐžŅ— ĐˇĐ´Đ°Ņ‚ĐŊĐžŅŅ‚Ņ–. ВиĐŧĐēĐŊŅƒŅ‚Đ¸, Ņ‰ĐžĐą СаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ айО виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ҂ҖĐģҌĐēи ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Ņƒ.", "setting_image_viewer_preview_title": "ЗаваĐŊŅ‚Đ°ĐļŅƒĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅŒĐžĐŗĐž ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ", "setting_image_viewer_title": "Đ—ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "setting_languages_apply": "Đ—Đ°ŅŅ‚ĐžŅŅƒĐ˛Đ°Ņ‚Đ¸", @@ -1889,7 +2031,7 @@ "setting_notifications_notify_minutes": "{count} Ņ…Đ˛Đ¸ĐģиĐŊ", "setting_notifications_notify_never": "ĐŊŅ–ĐēĐžĐģи", "setting_notifications_notify_seconds": "{count} ҁĐĩĐē҃ĐŊĐ´", - "setting_notifications_single_progress_subtitle": "ДĐĩŅ‚Đ°ĐģҌĐŊа Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ…Ņ–Đ´ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņƒ", + "setting_notifications_single_progress_subtitle": "ДĐĩŅ‚Đ°ĐģҌĐŊа Ņ–ĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ…Ņ–Đ´ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ ĐēĐžĐļĐŊĐžĐŗĐž Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž", "setting_notifications_single_progress_title": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Ņ…Ņ–Đ´ Ņ„ĐžĐŊĐžĐ˛ĐžĐŗĐž Ņ€ĐĩСĐĩŅ€Đ˛ĐŊĐžĐŗĐž ĐēĐžĐŋŅ–ŅŽĐ˛Đ°ĐŊĐŊŅ", "setting_notifications_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Ņ–Đ˛ ҁĐŋĐžĐ˛Ņ–Ņ‰ĐĩĐŊҌ", "setting_notifications_total_progress_subtitle": "Đ—Đ°ĐŗĐ°ĐģҌĐŊиК ĐŋŅ€ĐžĐŗŅ€Đĩҁ (виĐēĐžĐŊаĐŊĐž/ĐˇĐ°ĐŗĐ°ĐģĐžĐŧ)", @@ -1900,10 +2042,10 @@ "setting_video_viewer_original_video_subtitle": "ĐŸŅ€Đ¸ Ņ‚Ņ€Đ°ĐŊҁĐģŅŅ†Ņ–Ņ— Đ˛Ņ–Đ´ĐĩĐž С ҁĐĩŅ€Đ˛ĐĩŅ€Đ° Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģ, ĐŊĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž Đ´ĐžŅŅ‚ŅƒĐŋĐŊа Ņ‚Ņ€Đ°ĐŊҁĐēĐžĐ´ŅƒĐ˛Đ°ĐŊĐŊŅ. МоĐļĐĩ ĐŋŅ€Đ¸ĐˇĐ˛ĐĩŅŅ‚Đ¸ Đ´Đž ĐąŅƒŅ„ĐĩŅ€Đ¸ĐˇĐ°Ņ†Ņ–Ņ—. Đ’Ņ–Đ´ĐĩĐž, Đ´ĐžŅŅ‚ŅƒĐŋĐŊŅ– ĐģĐžĐēаĐģҌĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽŅŽŅ‚ŅŒŅŅ в ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊŅ–Đš ŅĐēĐžŅŅ‚Ņ–, ĐŊĐĩСваĐļĐ°ŅŽŅ‡Đ¸ ĐŊа ҆Đĩ ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ.", "setting_video_viewer_original_video_title": "ĐŸŅ€Đ¸ĐŧŅƒŅĐžĐ˛Đž Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐžŅ€Đ¸ĐŗŅ–ĐŊаĐģҌĐŊĐĩ Đ˛Ņ–Đ´ĐĩĐž", "settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", - "settings_require_restart": "ПĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧ҃ Đ´ĐģŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°ĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", + "settings_require_restart": "ПĐĩŅ€ĐĩСаваĐŊŅ‚Đ°ĐļŅ‚Đĩ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐžĐē Đ´ĐģŅ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐ˛Đ°ĐŊĐŊŅ Ņ†ŅŒĐžĐŗĐž ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ", "settings_saved": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ СйĐĩŅ€ĐĩĐļĐĩĐŊŅ–", "setup_pin_code": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ PIN-ĐēОд", - "share": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ", + "share": "ĐŸĐžŅˆĐ¸Ņ€Đ¸Ņ‚Đ¸", "share_action_prompt": "{count} Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž ĐŊĐ°Đ´Ņ–ŅĐģаĐŊĐž", "share_add_photos": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ„ĐžŅ‚Đž", "share_assets_selected": "{count} ĐžĐąŅ€Đ°ĐŊĐž", @@ -1911,8 +2053,8 @@ "share_link": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ", "shared": "ĐĄĐŋŅ–ĐģҌĐŊŅ–", "shared_album_activities_input_disable": "КоĐŧĐĩĐŊŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ виĐŧĐēĐŊĐĩĐŊĐž", - "shared_album_activity_remove_content": "Ви йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆Đĩ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ?", - "shared_album_activity_remove_title": "ВидаĐģĐ¸Ņ‚Đ¸ ĐŋĐžĐ˛Ņ–Đ´ĐžĐŧĐģĐĩĐŊĐŊŅ", + "shared_album_activity_remove_content": "Ви йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ Ņ†ŅŽ аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ?", + "shared_album_activity_remove_title": "ВидаĐģĐ¸Ņ‚Đ¸ аĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ", "shared_album_section_people_action_error": "ПоĐŧиĐģĐēа Đ˛Đ¸Ņ…ĐžĐ´Ņƒ/видаĐģĐĩĐŊĐŊŅ С аĐģŅŒĐąĐžĐŧ҃", "shared_album_section_people_action_leave": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° С аĐģŅŒĐąĐžĐŧ҃", "shared_album_section_people_action_remove_user": "ВидаĐģĐ¸Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° С аĐģŅŒĐąĐžĐŧ҃", @@ -1921,7 +2063,7 @@ "shared_by_user": "ĐĄĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ С {user}", "shared_by_you": "Ви ĐŋĐžĐ´Ņ–ĐģиĐģĐ¸ŅŅŒ", "shared_from_partner": "Đ¤ĐžŅ‚Đž Đ˛Ņ–Đ´ {partner}", - "shared_intent_upload_button_progress_text": "{current} / {total} ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "shared_intent_upload_button_progress_text": "{current} / {total} ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", "shared_link_app_bar_title": "ĐĄĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "shared_link_clipboard_copied_massage": "ĐĄĐēĐžĐŋŅ–ĐšĐžĐ˛Đ°ĐŊĐž в ĐąŅƒŅ„ĐĩŅ€ ОйĐŧŅ–ĐŊ҃", "shared_link_clipboard_text": "ĐŸĐžŅĐ¸ĐģаĐŊĐŊŅ: {link}\nĐŸĐ°Ņ€ĐžĐģҌ: {password}", @@ -1938,7 +2080,7 @@ "shared_link_edit_expire_after_option_year": "{count} Ņ€ĐžĐēŅ–Đ˛", "shared_link_edit_password_hint": "ВвĐĩĐ´Ņ–Ņ‚ŅŒ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž Đ´ĐžŅŅ‚ŅƒĐŋ҃", "shared_link_edit_submit_button": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", - "shared_link_error_server_url_fetch": "НĐĩĐŧĐžĐļĐģивО СаĐŋĐ¸Ņ‚Đ°Ņ‚Đ¸ URL Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "shared_link_error_server_url_fetch": "НĐĩĐŧĐžĐļĐģивО СаĐŋĐ¸Ņ‚Đ°Ņ‚Đ¸ url Ņ–Đˇ ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "shared_link_expires_day": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} Đ´ĐĩĐŊҌ", "shared_link_expires_days": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} Đ´ĐŊŅ–Đ˛", "shared_link_expires_hour": "ЗаĐēŅ–ĐŊŅ‡ŅƒŅ”Ņ‚ŅŒŅŅ ҇ĐĩŅ€ĐĩС {count} ĐŗĐžĐ´Đ¸ĐŊ҃", @@ -1951,7 +2093,7 @@ "shared_link_individual_shared": "ІĐŊĐ´Đ¸Đ˛Ņ–Đ´ŅƒĐ°ĐģҌĐŊиК ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ", "shared_link_info_chip_metadata": "EXIF", "shared_link_manage_links": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐŋŅ–ĐģҌĐŊиĐŧи ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧи", - "shared_link_options": "ОĐŋ҆Җҗ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", + "shared_link_options": "ĐŸĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", "shared_link_password_description": "ВиĐŧĐ°ĐŗĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ Đ´ĐģŅ Đ´ĐžŅŅ‚ŅƒĐŋ҃ Đ´Đž Ņ†ŅŒĐžĐŗĐž ҁĐŋŅ–ĐģҌĐŊĐžĐŗĐž ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "shared_links": "ĐĄĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "shared_links_description": "Đ”Ņ–ĐģŅ–Ņ‚ŅŒŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Са ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅĐŧ", @@ -1963,10 +2105,10 @@ "sharing_page_album": "ĐĄĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи", "sharing_page_description": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐšŅ‚Đĩ ҁĐŋŅ–ĐģҌĐŊŅ– аĐģŅŒĐąĐžĐŧи, Ņ‰ĐžĐą Đ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž С ĐģŅŽĐ´ŅŒĐŧи ĐˇŅ– ŅĐ˛ĐžŅ”Ņ— ĐŧĐĩŅ€ĐĩĐļŅ–.", "sharing_page_empty_list": "ПОРОЖНІЙ СПИСОК", - "sharing_sidebar_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ĐˇĐ°ĐŗĐ°ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", + "sharing_sidebar_description": "Đ’Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐ°Ņ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ ĐŊа ҁĐŋŅ–ĐģҌĐŊиК Đ´ĐžŅŅ‚ŅƒĐŋ ҃ ĐąŅ–Ņ‡ĐŊŅ–Đš ĐŋаĐŊĐĩĐģŅ–", "sharing_silver_appbar_create_shared_album": "ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊиК аĐģŅŒĐąĐžĐŧ", "sharing_silver_appbar_share_partner": "ĐŸĐžĐ´Ņ–ĐģĐ¸Ņ‚Đ¸ŅŅ С ĐŋĐ°Ņ€Ņ‚ĐŊĐĩŅ€ĐžĐŧ", - "shift_to_permanent_delete": "ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ⇧ Ņ‰ĐžĐą видаĐģĐ¸Ņ‚Đ¸ Ой'Ņ”ĐēŅ‚ ĐŊаСавĐļди", + "shift_to_permanent_delete": "ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ ⇧ Ņ‰ĐžĐą видаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ ĐŊаСавĐļди", "show_album_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ аĐģŅŒĐąĐžĐŧ҃", "show_albums": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи", "show_all_people": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Đ˛ŅŅ–Ņ… ĐģŅŽĐ´ĐĩĐš", @@ -1982,6 +2124,7 @@ "show_password": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "show_person_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐģŅŽĐ´Đ¸ĐŊи", "show_progress_bar": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ Ņ–ĐŊдиĐēĐ°Ņ‚ĐžŅ€ ĐŋŅ€ĐžĐŗŅ€Đĩҁ҃", + "show_schema": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ҁ҅ĐĩĐŧ҃", "show_search_options": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐ°Ņ€Đ°ĐŧĐĩŅ‚Ņ€Đ¸ ĐŋĐžŅˆŅƒĐē҃", "show_shared_links": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ҁĐŋŅ–ĐģҌĐŊŅ– ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "show_slideshow_transition": "ПоĐēĐ°ĐˇĐ°Ņ‚Đ¸ ĐŋĐĩŅ€ĐĩŅ…Ņ–Đ´ ҁĐģаКд-ŅˆĐžŅƒ", @@ -1999,23 +2142,25 @@ "skip_to_folders": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž ĐŋаĐŋĐžĐē", "skip_to_tags": "ПĐĩŅ€ĐĩĐšŅ‚Đ¸ Đ´Đž Ņ‚ĐĩĐŗŅ–Đ˛", "slideshow": "ĐĄĐģĐ°ĐšĐ´ŅˆĐžŅƒ", + "slideshow_repeat": "ĐŸĐžĐ˛Ņ‚ĐžŅ€Đ¸Ņ‚Đ¸ ҁĐģаКд-ŅˆĐžŅƒ", + "slideshow_repeat_description": "ПовĐĩŅ€ĐŊĐĩĐŊĐŊŅ Đ´Đž ĐŋĐžŅ‡Đ°Ņ‚Đē҃ ĐŋҖҁĐģŅ СавĐĩŅ€ŅˆĐĩĐŊĐŊŅ ҁĐģаКд-ŅˆĐžŅƒ", "slideshow_settings": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ҁĐģаКд-ŅˆĐžŅƒ", "sort_albums_by": "ĐĄĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ аĐģŅŒĐąĐžĐŧи Са...", "sort_created": "Đ”Đ°Ņ‚Đ° ŅŅ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ", - "sort_items": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", + "sort_items": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ Ņ„Đ°ĐšĐģŅ–Đ˛", "sort_modified": "Đ”Đ°Ņ‚Đ° СĐŧŅ–ĐŊи", "sort_newest": "НайĐŊĐžĐ˛Ņ–ŅˆĐĩ Ņ„ĐžŅ‚Đž", "sort_oldest": "ĐĄŅ‚Đ°Ņ€Ņ– Ņ„ĐžŅ‚Đž", "sort_people_by_similarity": "ĐĄĐžŅ€Ņ‚ŅƒĐ˛Đ°Ņ‚Đ¸ ĐģŅŽĐ´ĐĩĐš Са ŅŅ…ĐžĐļŅ–ŅŅ‚ŅŽ", "sort_recent": "НĐĩŅ‰ĐžĐ´Đ°Đ˛ĐŊŅ–", "sort_title": "Đ—Đ°ĐŗĐžĐģОвОĐē", - "source": "Đ’Đ¸Ņ…Ņ–Đ´ĐŊиК ĐēОд", + "source": "ДĐļĐĩŅ€ĐĩĐģĐž", "stack": "ĐŖ ŅŅ‚ĐžĐŋĐē҃", "stack_action_prompt": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž: {count}", "stack_duplicates": "Đ“Ņ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ Đ´ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "stack_select_one_photo": "Đ’Đ¸ĐąŅ€Đ°Ņ‚Đ¸ ОдĐŊĐĩ ĐžŅĐŊОвĐŊĐĩ Ņ„ĐžŅ‚Đž Đ´ĐģŅ ĐŗŅ€ŅƒĐŋи", - "stack_selected_photos": "ĐĄĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", - "stacked_assets_count": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", + "stack_selected_photos": "Đ—ĐŗŅ€ŅƒĐŋŅƒĐ˛Đ°Ņ‚Đ¸ ĐžĐąŅ€Đ°ĐŊŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", + "stacked_assets_count": "Đ—ĐŗŅ€ŅƒĐŋОваĐŊĐž {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "stacktrace": "ĐĄŅ‚ĐĩĐē виĐēĐģиĐēŅ–Đ˛", "start": "ĐĄŅ‚Đ°Ņ€Ņ‚", "start_date": "Đ”Đ°Ņ‚Đ° ĐŋĐžŅ‡Đ°Ņ‚Đē҃", @@ -2030,7 +2175,7 @@ "storage": "ĐĄŅ…ĐžĐ˛Đ¸Ņ‰Đĩ", "storage_label": "ĐœŅ–Ņ‚Đēа Đ´ĐģŅ СйĐĩŅ€Ņ–ĐŗĐ°ĐŊĐŊŅ", "storage_quota": "ĐžĐąŅŅĐŗ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ°", - "storage_usage": "{used} С {available} Đ´ĐžŅŅ‚ŅƒĐŋĐŊĐ¸Ņ…", + "storage_usage": "{used} С {available} виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐž", "submit": "ĐŸŅ–Đ´Ņ‚Đ˛ĐĩŅ€Đ´Đ¸Ņ‚Đ¸", "success": "ĐŖŅĐŋŅ–ŅˆĐŊĐž", "suggestions": "ĐŸŅ€ĐžĐŋĐžĐˇĐ¸Ņ†Ņ–Ņ—", @@ -2046,7 +2191,7 @@ "sync_remote": "ХиĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ С ҁĐĩŅ€Đ˛ĐĩŅ€ĐžĐŧ", "sync_status": "ĐĄŅ‚Đ°ĐŊ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", "sync_status_subtitle": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ Ņ‚Đ° ĐēĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐ¸ŅŅ‚ĐĩĐŧĐžŅŽ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐ°Ņ†Ņ–Ņ—", - "sync_upload_album_setting_subtitle": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐšŅ‚Đĩ Ņ‚Đ° СаваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", + "sync_upload_album_setting_subtitle": "ĐĄŅ‚Đ˛ĐžŅ€ŅŽĐšŅ‚Đĩ Ņ‚Đ° виваĐŊŅ‚Đ°ĐļŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅ— Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ— Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž Đ´Đž Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… аĐģŅŒĐąĐžĐŧŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩŅ€ Immich", "tag": "ĐĸĐĩĐŗ", "tag_assets": "Đ”ĐžĐ´Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŗĐ¸", "tag_created": "ĐĄŅ‚Đ˛ĐžŅ€ĐĩĐŊĐž Ņ‚ĐĩĐŗ: {tag}", @@ -2054,7 +2199,7 @@ "tag_not_found_question": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ СĐŊĐ°ĐšŅ‚Đ¸ Ņ‚ĐĩĐŗ? ĐĄŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ ĐŊОвиК Ņ‚ĐĩĐŗ.", "tag_people": "ĐĸĐĩĐŗ ĐģŅŽĐ´ĐĩĐš", "tag_updated": "ОĐŊОвĐģĐĩĐŊĐž Ņ‚ĐĩĐŗ: {tag}", - "tagged_assets": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Ņ‚ĐĩĐŗĐžĐŧ {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} other {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸}}", + "tagged_assets": "ПозĐŊĐ°Ņ‡ĐĩĐŊĐž Ņ‚ĐĩĐŗĐžĐŧ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", "tags": "ĐĸĐĩĐŗĐ¸", "tap_to_run_job": "ĐĸĐžŅ€ĐēĐŊŅ–Ņ‚ŅŒŅŅ, Ņ‰ĐžĐą СаĐŋŅƒŅŅ‚Đ¸Ņ‚Đ¸ СавдаĐŊĐŊŅ", "template": "ШайĐģĐžĐŊ", @@ -2062,8 +2207,8 @@ "theme": "ĐĸĐĩĐŧа", "theme_selection": "Đ’Đ¸ĐąŅ–Ņ€ Ņ‚ĐĩĐŧи", "theme_selection_description": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸Ņ‡ĐŊĐž Đ˛ŅŅ‚Đ°ĐŊОвĐģŅŽĐ˛Đ°Ņ‚Đ¸ Ņ‚ĐĩĐŧ҃ ĐŊа ŅĐ˛Ņ–Ņ‚Đģ҃ айО Ņ‚ĐĩĐŧĐŊ҃ СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ ŅĐ¸ŅŅ‚ĐĩĐŧĐŊĐ¸Ņ… ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊҌ Đ˛Đ°ŅˆĐžĐŗĐž ĐąŅ€Đ°ŅƒĐˇĐĩŅ€Đ°", - "theme_setting_asset_list_storage_indicator_title": "ПоĐēĐ°ĐˇŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋŅ–ĐēŅ‚ĐžĐŗŅ€Đ°Đŧ҃ ŅŅ…ĐžĐ˛Đ¸Ņ‰Đ° ĐŊа ĐŋĐģĐ¸Ņ‚ĐēĐ°Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛", - "theme_setting_asset_list_tiles_per_row_title": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ҃ Ņ€ŅĐ´Đē҃ ({count})", + "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": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ ŅĐēĐžŅŅ‚Ņ– ĐŋĐĩŅ€ĐĩĐŗĐģŅĐ´Ņƒ ĐŋОвĐŊĐžĐĩĐēŅ€Đ°ĐŊĐŊĐ¸Ņ… ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊҌ", @@ -2075,8 +2220,9 @@ "theme_setting_theme_subtitle": "НаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°ĐŊĐŊŅ Ņ‚ĐĩĐŧи ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃", "theme_setting_three_stage_loading_subtitle": "ĐĸŅ€Đ¸ĐĩŅ‚Đ°ĐŋĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐŋŅ–Đ´Đ˛Đ¸Ņ‰Đ¸Ņ‚Đ¸ ĐŋŅ€ĐžĐ´ŅƒĐēŅ‚Đ¸Đ˛ĐŊŅ–ŅŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ, аĐģĐĩ ҁĐŋŅ€Đ¸Ņ‡Đ¸ĐŊĐ¸Ņ‚ŅŒ СĐŊĐ°Ņ‡ĐŊĐž ĐąŅ–ĐģҌ҈Đĩ ĐŊаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊа ĐŧĐĩŅ€ĐĩĐļ҃", "theme_setting_three_stage_loading_title": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ Ņ‚Ņ€Đ¸ĐĩŅ‚Đ°ĐŋĐŊĐĩ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "then": "ĐĸĐžĐ´Ņ–", "they_will_be_merged_together": "ВоĐŊи ĐąŅƒĐ´ŅƒŅ‚ŅŒ Ой'Ņ”Đ´ĐŊаĐŊŅ– Ņ€Đ°ĐˇĐžĐŧ", - "third_party_resources": "Đ ĐĩŅŅƒŅ€ŅĐ¸ ҂ҀĐĩ҂Җ҅ ŅŅ‚ĐžŅ€Ņ–ĐŊ", + "third_party_resources": "ĐĄŅ‚ĐžŅ€ĐžĐŊĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸", "time": "Đ§Đ°Ņ", "time_based_memories": "ĐĄĐŋĐžĐŗĐ°Đ´Đ¸, Ņ‰Đž ĐąĐ°ĐˇŅƒŅŽŅ‚ŅŒŅŅ ĐŊа Ņ‡Đ°ŅŅ–", "time_based_memories_duration": "ĐšŅ–ĐģҌĐēŅ–ŅŅ‚ŅŒ ҁĐĩĐē҃ĐŊĐ´ Đ´ĐģŅ Đ˛Ņ–Đ´ĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ ĐēĐžĐļĐŊĐžĐŗĐž ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ.", @@ -2086,7 +2232,7 @@ "to_change_password": "ЗĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ ĐŋĐ°Ņ€ĐžĐģҌ", "to_favorite": "ĐžĐąŅ€Đ°ĐŊĐĩ", "to_login": "Đ’Ņ…Ņ–Đ´", - "to_multi_select": "Đ´ĐģŅ ĐąĐ°ĐŗĐ°Ņ‚ĐžŅ€Đ°ĐˇĐžĐ˛ĐžĐŗĐž Đ˛Đ¸ĐąĐžŅ€Ņƒ", + "to_multi_select": "Đ´ĐģŅ ĐŧĐŊĐžĐļиĐŊĐŊĐžĐŗĐž Đ˛Đ¸ĐąĐžŅ€Ņƒ", "to_parent": "ПовĐĩŅ€ĐŊŅƒŅ‚Đ¸ŅŅŒ ĐŊаСад", "to_select": "Đ˛Đ¸ĐąŅ€Đ°Ņ‚Đ¸", "to_trash": "ĐšĐžŅˆĐ¸Đē", @@ -2098,36 +2244,44 @@ "trash_action_prompt": "{count} ĐŋĐĩŅ€ĐĩĐŧҖ҉ĐĩĐŊĐž Đ´Đž ĐēĐžŅˆĐ¸Đēа", "trash_all": "ВидаĐģĐ¸Ņ‚Đ¸ Đ˛ŅĐĩ", "trash_count": "ВидаĐģĐ¸Ņ‚Đ¸ {count, number}", - "trash_delete_asset": "ĐŖ ĐēĐžŅˆĐ¸Đē/ВидаĐģĐ¸Ņ‚Đ¸ Ņ€ĐĩŅŅƒŅ€Ņ", + "trash_delete_asset": "ĐŖ ĐšĐžŅˆĐ¸Đē/ВидаĐģĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģ", "trash_emptied": "ĐšĐžŅˆĐ¸Đē ĐžŅ‡Đ¸Ņ‰ĐĩĐŊĐž", "trash_no_results_message": "ĐĸŅƒŅ‚ С'ŅĐ˛ĐģŅŅ‚Đ¸ĐŧŅƒŅ‚ŅŒŅŅ видаĐģĐĩĐŊŅ– Ņ„ĐžŅ‚Đž Ņ‚Đ° Đ˛Ņ–Đ´ĐĩĐž.", "trash_page_delete_all": "ВидаĐģĐ¸Ņ‚Đ¸ ҃ҁĐĩ", - "trash_page_empty_trash_dialog_content": "Ви Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Immich", - "trash_page_info": "ПоĐŧҖ҉ĐĩĐŊŅ– ҃ ĐēĐžŅˆĐ¸Đē ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ ĐąŅƒĐ´Đĩ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊĐž ҇ĐĩŅ€ĐĩС {days} Đ´ĐŊŅ–Đ˛", - "trash_page_no_assets": "ВидаĐģĐĩĐŊŅ– ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸ Đ˛Ņ–Đ´ŅŅƒŅ‚ĐŊŅ–", + "trash_page_empty_trash_dialog_content": "Ви Ņ…ĐžŅ‡ĐĩŅ‚Đĩ ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ ĐēĐžŅˆĐ¸Đē? ĐĻŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– С Immich", + "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 {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", + "trashed_items_will_be_permanently_deleted_after": "ВидаĐģĐĩĐŊŅ– Ņ„Đ°ĐšĐģи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ĐžŅŅ‚Đ°Ņ‚ĐžŅ‡ĐŊĐž видаĐģĐĩĐŊŅ– ҇ĐĩŅ€ĐĩС {days, plural, one {# Đ´ĐĩĐŊҌ} few {# Đ´ĐŊŅ–} many {# Đ´ĐŊŅ–Đ˛} other {# Đ´ĐŊŅ–Đ˛}}.", + "trigger": "ĐĸŅ€Đ¸ĐŗĐĩŅ€", + "trigger_asset_uploaded": "ФаКĐģ дОдаĐŊĐž", + "trigger_asset_uploaded_description": "ЗаĐŋ҃ҁĐēĐ°Ņ”Ņ‚ŅŒŅŅ ĐŋŅ–Đ´ Ņ‡Đ°Ņ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ĐŊĐžĐ˛ĐžĐŗĐž Ņ„Đ°ĐšĐģ҃", + "trigger_description": "ĐŸĐžĐ´Ņ–Ņ, ŅĐēа СаĐŋ҃ҁĐēĐ°Ņ” Đ°Đ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–ŅŽ", + "trigger_person_recognized": "ĐžŅĐžĐąĐ° Ņ€ĐžĐˇĐŋŅ–ĐˇĐŊаĐŊа", + "trigger_person_recognized_description": "ĐĄĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛ŅƒŅ”, ĐēĐžĐģи Đ˛Đ¸ŅĐ˛ĐģŅŅ”Ņ‚ŅŒŅŅ ĐģŅŽĐ´Đ¸ĐŊа", + "trigger_type": "ĐĸиĐŋ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Đ°", "troubleshoot": "ВиĐŋŅ€Đ°Đ˛ĐģĐĩĐŊĐŊŅ ĐŊĐĩĐŋĐžĐģадОĐē", "type": "ĐĸиĐŋ", "unable_to_change_pin_code": "НĐĩĐŧĐžĐļĐģивО СĐŧŅ–ĐŊĐ¸Ņ‚Đ¸ PIN-ĐēОд", - "unable_to_check_version": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ вĐĩŅ€ŅŅ–ŅŽ ĐŋŅ€ĐžĐŗŅ€Đ°Đŧи айО ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", + "unable_to_check_version": "НĐĩ Đ˛Đ´Đ°Ņ”Ņ‚ŅŒŅŅ ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸ вĐĩŅ€ŅŅ–ŅŽ ĐˇĐ°ŅŅ‚ĐžŅŅƒĐŊĐē҃ айО ҁĐĩŅ€Đ˛ĐĩŅ€Đ°", "unable_to_setup_pin_code": "НĐĩĐŧĐžĐļĐģивО ĐŊаĐģĐ°ŅˆŅ‚ŅƒĐ˛Đ°Ņ‚Đ¸ PIN-ĐēОд", "unarchive": "Đ ĐžĐˇĐ°Ņ€Ņ…Ņ–Đ˛ŅƒĐ˛Đ°Ņ‚Đ¸", - "unarchive_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ", + "unarchive_action_prompt": "{count, plural, one {# Ņ„Đ°ĐšĐģ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} few {# Ņ„Đ°ĐšĐģи виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ} other {# Ņ„Đ°ĐšĐģŅ–Đ˛ виĐģŅƒŅ‡ĐĩĐŊĐž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ}}", "unarchived_count": "{count, plural, other {ПовĐĩŅ€ĐŊŅƒŅ‚Đž С Đ°Ņ€Ņ…Ņ–Đ˛Ņƒ #}}", "undo": "ĐĄĐēĐ°ŅŅƒĐ˛Đ°Ņ‚Đ¸", - "unfavorite": "ВидаĐģĐ¸Ņ‚Đ¸ С ҃ĐģŅŽĐąĐģĐĩĐŊĐ¸Ņ…", + "unfavorite": "ВидаĐģĐ¸Ņ‚Đ¸ С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "unfavorite_action_prompt": "{count} виĐģŅƒŅ‡ĐĩĐŊĐž С ĐžĐąŅ€Đ°ĐŊĐžĐŗĐž", "unhide_person": "РОСĐēŅ€Đ¸Ņ‚Đ¸ ĐžŅĐžĐąŅƒ", "unknown": "НĐĩĐ˛Ņ–Đ´ĐžĐŧĐž", "unknown_country": "НĐĩĐ˛Ņ–Đ´ĐžĐŧа ĐēŅ€Đ°Ņ—ĐŊа", + "unknown_date": "НĐĩĐ˛Ņ–Đ´ĐžĐŧа Đ´Đ°Ņ‚Đ°", "unknown_year": "НĐĩĐ˛Ņ–Đ´ĐžĐŧиК ҀҖĐē", "unlimited": "БĐĩС ОйĐŧĐĩĐļĐĩĐŊҌ", "unlink_motion_video": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ Ņ€ŅƒŅ…ĐžĐŧĐĩ Đ˛Ņ–Đ´ĐĩĐž", - "unlink_oauth": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊĐ°ĐšŅ‚Đĩ OAuth", - "unlinked_oauth_account": "Đ’Ņ–Đ´ĐēĐģŅŽŅ‡Đ¸Ņ‚Đ¸ аĐēĐ°ŅƒĐŊŅ‚ OAuth", + "unlink_oauth": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊĐ°Ņ‚Đ¸ OAuth", + "unlinked_oauth_account": "Đ’Ņ–Đ´'Ņ”Đ´ĐŊаĐŊиК ОйĐģŅ–ĐēОвиК СаĐŋĐ¸Ņ OAuth", "unmute_memories": "ĐŖĐ˛Ņ–ĐŧĐēĐŊŅƒŅ‚Đ¸ ĐˇĐ˛ŅƒĐē ҁĐŋĐžĐŗĐ°Đ´Ņ–Đ˛", "unnamed_album": "АĐģŅŒĐąĐžĐŧ ĐąĐĩС ĐŊаСви", "unnamed_album_delete_confirmation": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž йаĐļĐ°Ņ”Ņ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš аĐģŅŒĐąĐžĐŧ?", @@ -2138,53 +2292,56 @@ "unselect_all_in": "ЗĐŊŅŅ‚Đ¸ Đ˛Đ¸ĐąŅ–Ņ€ ҃ Đ˛ŅŅŒĐžĐŧ҃ {group}", "unstack": "Đ ĐžĐˇŅ–ĐąŅ€Đ°Ņ‚Đ¸ ҁ҂ĐĩĐē", "unstack_action_prompt": "{count} Ņ€ĐžĐˇâ€™Ņ”Đ´ĐŊаĐŊĐž", - "unstacked_assets_count": "Đ ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ {count, plural, one {# Ņ€ĐĩŅŅƒŅ€Ņ} few {# Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", + "unstacked_assets_count": "Đ ĐžĐˇĐŗĐžŅ€ĐŊŅƒŅ‚Đ¸ {count, plural, one {# Ņ„Đ°ĐšĐģ} few {# Ņ„Đ°ĐšĐģи} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "unsupported_field_type": "НĐĩĐŋŅ–Đ´Ņ‚Ņ€Đ¸ĐŧŅƒĐ˛Đ°ĐŊиК Ņ‚Đ¸Đŋ ĐŋĐžĐģŅ", "untagged": "БĐĩС Ņ‚ĐĩĐŗŅ–Đ˛", + "untitled_workflow": "БĐĩĐˇŅ–ĐŧĐĩĐŊĐŊиК Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", "up_next": "ĐĐ°ŅŅ‚ŅƒĐŋĐŊĐĩ", "update_location_action_prompt": "ОĐŊĐžĐ˛Đ¸Ņ‚Đ¸ Ņ€ĐžĐˇŅ‚Đ°ŅˆŅƒĐ˛Đ°ĐŊĐŊŅ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐžĐąâ€™Ņ”ĐēŅ‚Ņ–Đ˛ ({count}) Са Đ´ĐžĐŋĐžĐŧĐžĐŗĐžŅŽ:", "updated_at": "ОĐŊОвĐģĐĩĐŊĐž", "updated_password": "ĐŸĐ°Ņ€ĐžĐģҌ ĐžĐŊОвĐģĐĩĐŊĐž", - "upload": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", - "upload_action_prompt": "{count} ҃ ҇ĐĩŅ€ĐˇŅ– ĐŊа СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "upload_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŒ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "upload_details": "ДĐĩŅ‚Đ°ĐģŅ– СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "upload_dialog_info": "БаĐļĐ°Ņ”Ņ‚Đĩ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Ņ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ?", - "upload_dialog_title": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ ЕĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", - "upload_errors": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž С {count, plural, one {# ĐŋĐžĐŧиĐģĐēĐžŅŽ} few {# ĐŋĐžĐŧиĐģĐēаĐŧи} many {# ĐŋĐžĐŧиĐģĐēаĐŧи} other {# ĐŋĐžĐŧиĐģĐēаĐŧи}}, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸.", - "upload_finished": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž", + "upload": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸", + "upload_concurrency": "ĐŸĐ°Ņ€Đ°ĐģĐĩĐģҌĐŊŅ–ŅŅ‚ŅŒ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "upload_details": "ДĐĩŅ‚Đ°ĐģŅ– виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "upload_dialog_info": "БаĐļĐ°Ņ”Ņ‚Đĩ ŅŅ‚Đ˛ĐžŅ€Đ¸Ņ‚Đ¸ Ņ€ĐĩСĐĩŅ€Đ˛ĐŊ҃ ĐēĐžĐŋŅ–ŅŽ Đ˛Đ¸ĐąŅ€Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛ ĐŊа ҁĐĩŅ€Đ˛ĐĩҀҖ?", + "upload_dialog_title": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ Ņ„Đ°ĐšĐģи", + "upload_error_with_count": "ПоĐŧиĐģĐēа виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ Đ´ĐģŅ {count, plural, one {# Ņ„Đ°ĐšĐģ҃} few {# Ņ„Đ°ĐšĐģŅ–Đ˛} many {# Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Ņ„Đ°ĐšĐģŅ–Đ˛}}", + "upload_errors": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž С {count, plural, one {# ĐŋĐžĐŧиĐģĐēĐžŅŽ} few {# ĐŋĐžĐŧиĐģĐēаĐŧи} many {# ĐŋĐžĐŧиĐģĐēаĐŧи} other {# ĐŋĐžĐŧиĐģĐēаĐŧи}}, ĐžĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„Đ°ĐšĐģи.", + "upload_finished": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ СавĐĩŅ€ŅˆĐĩĐŊĐž", "upload_progress": "ЗаĐģĐ¸ŅˆĐ¸ĐģĐžŅŅŒ {remaining, number} - ОĐŋŅ€Đ°Ņ†ŅŒĐžĐ˛Đ°ĐŊĐž {processed, number}/{total, number}", - "upload_skipped_duplicates": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ€ĐĩŅŅƒŅ€ŅŅ–Đ˛}}", + "upload_skipped_duplicates": "ĐŸŅ€ĐžĐŋŅƒŅ‰ĐĩĐŊĐž {count, plural, one {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊиК Ņ„Đ°ĐšĐģ} few {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊŅ– Ņ„Đ°ĐšĐģи} many {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛} other {# Đ´ŅƒĐąĐģŅŒĐžĐ˛Đ°ĐŊĐ¸Ņ… Ņ„Đ°ĐšĐģŅ–Đ˛}}", "upload_status_duplicates": "Đ”ŅƒĐąĐģŅ–ĐēĐ°Ņ‚Đ¸", "upload_status_errors": "ПоĐŧиĐģĐēи", - "upload_status_uploaded": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", - "upload_success": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐĩ. ОĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– СаваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ€ĐĩŅŅƒŅ€ŅĐ¸.", - "upload_to_immich": "ЗаваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ в Immich ({count})", - "uploading": "ЗаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", - "uploading_media": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ СаваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "upload_status_uploaded": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐž", + "upload_success": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ ҃ҁĐŋŅ–ŅˆĐŊĐĩ. ОĐŊĐžĐ˛Ņ–Ņ‚ŅŒ ŅŅ‚ĐžŅ€Ņ–ĐŊĐē҃, Ņ‰ĐžĐą ĐŋĐžĐąĐ°Ņ‡Đ¸Ņ‚Đ¸ ĐŊĐžĐ˛Ņ– виваĐŊŅ‚Đ°ĐļĐĩĐŊŅ– Ņ„Đ°ĐšĐģи.", + "upload_to_immich": "ВиваĐŊŅ‚Đ°ĐļĐ¸Ņ‚Đ¸ в Immich ({count})", + "uploading": "ВиваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", + "uploading_media": "ВиĐēĐžĐŊŅƒŅ”Ņ‚ŅŒŅŅ виваĐŊŅ‚Đ°ĐļĐĩĐŊĐŊŅ", "url": "URL", "usage": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ", "use_biometric": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐąŅ–ĐžĐŧĐĩŅ‚Ņ€Ņ–ŅŽ", - "use_current_connection": "виĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐĩ ĐŋŅ–Đ´ĐēĐģŅŽŅ‡ĐĩĐŊĐŊŅ", + "use_current_connection": "ВиĐēĐžŅ€Đ¸ŅŅ‚Đ°Ņ‚Đ¸ ĐŋĐžŅ‚ĐžŅ‡ĐŊĐĩ С'Ņ”Đ´ĐŊаĐŊĐŊŅ", "use_custom_date_range": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ†ŅŒĐēиК Đ´Ņ–Đ°ĐŋаСОĐŊ Đ´Đ°Ņ‚", "user": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡", "user_has_been_deleted": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ° видаĐģĐĩĐŊĐž.", "user_id": "ID ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "user_liked": "{user} вĐŋОдОйав {type, select, photo {҆Đĩ Ņ„ĐžŅ‚Đž} video {҆Đĩ Đ˛Ņ–Đ´ĐĩĐž} asset {҆ĐĩĐš Ņ€ĐĩŅŅƒŅ€Ņ} other {҆Đĩ}}", + "user_liked": "{user} вĐŋОдОйав {type, select, photo {҆Đĩ Ņ„ĐžŅ‚Đž} video {҆Đĩ Đ˛Ņ–Đ´ĐĩĐž} asset {҆ĐĩĐš Ņ„Đ°ĐšĐģ} other {҆Đĩ}}", "user_pin_code_settings": "PIN-ĐēОд", - "user_pin_code_settings_description": "КĐĩŅ€ŅƒĐšŅ‚Đĩ ŅĐ˛ĐžŅ—Đŧ PIN-ĐēОдОĐŧ", + "user_pin_code_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°ĐŊĐŊŅ PIN-ĐēОдОĐŧ", "user_privacy": "КоĐŊŅ„Ņ–Đ´ĐĩĐŊŅ†Ņ–ĐšĐŊŅ–ŅŅ‚ŅŒ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "user_purchase_settings": "ĐŸŅ€Đ¸Đ´ĐąĐ°Ņ‚Đ¸", "user_purchase_settings_description": "КĐĩŅ€ŅƒĐ˛Đ°Ņ‚Đ¸ Đ˛Đ°ŅˆĐžŅŽ ĐŋĐžĐē҃ĐŋĐēĐžŅŽ", "user_role_set": "ĐŸŅ€Đ¸ĐˇĐŊĐ°Ņ‡Đ¸Ņ‚Đ¸ {user} ĐŊа Ņ€ĐžĐģҌ {role}", "user_usage_detail": "ДĐĩŅ‚Đ°ĐģŅ– виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", - "user_usage_stats": "ĐĄŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ аĐēĐ°ŅƒĐŊŅ‚Đ°", - "user_usage_stats_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đē҃ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ аĐēĐ°ŅƒĐŊŅ‚Đ°", + "user_usage_stats": "ĐĄŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đēа виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ", + "user_usage_stats_description": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅŅ‚Đ°Ņ‚Đ¸ŅŅ‚Đ¸Đē҃ виĐēĐžŅ€Đ¸ŅŅ‚Đ°ĐŊĐŊŅ ОйĐģŅ–ĐēĐžĐ˛ĐžĐŗĐž СаĐŋĐ¸ŅŅƒ", "username": "ІĐŧ'Ņ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "users": "ĐšĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–", "users_added_to_album_count": "{count, plural, one {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°} few {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–} many {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛} other {# ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛}} дОдаĐŊĐž Đ´Đž аĐģŅŒĐąĐžĐŧ҃", "utilities": "ĐŖŅ‚Đ¸ĐģŅ–Ņ‚Đ¸", "validate": "ПĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đ¸Ņ‚Đ¸", "validate_endpoint_error": "Đ‘ŅƒĐ´ŅŒ ĐģĐ°ŅĐēа, ввĐĩĐ´Ņ–Ņ‚ŅŒ Đ´Ņ–ĐšŅĐŊ҃ URL-Đ°Đ´Ņ€Đĩҁ҃", + "validation_error": "ПоĐŧиĐģĐēа ĐŋĐĩŅ€ĐĩĐ˛Ņ–Ņ€Đēи", "variables": "ЗĐŧŅ–ĐŊĐŊŅ–", "version": "ВĐĩŅ€ŅŅ–Ņ", "version_announcement_closing": "ĐĸĐ˛Ņ–Đš Đ´Ņ€ŅƒĐŗ, АĐģĐĩĐēҁ", @@ -2193,44 +2350,60 @@ "version_history_item": "Đ’ŅŅ‚Đ°ĐŊОвĐģĐĩĐŊĐž {version} {date}", "video": "Đ’Ņ–Đ´ĐĩĐž", "video_hover_setting": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧŅ–ĐŊŅ–Đ°Ņ‚ŅŽŅ€Đ¸ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ–Đ´ Ņ‡Đ°Ņ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ ĐēŅƒŅ€ŅĐžŅ€Ņƒ ĐŧĐ¸ŅˆŅ–", - "video_hover_setting_description": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ€Đ¸ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ– ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚. ĐĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ СаĐŋŅƒŅ‰ĐĩĐŊĐž, ĐŊĐ°Đ˛Ņ–Đ˛ŅˆĐ¸ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа ĐŋŅ–ĐēŅ‚ĐžĐŗŅ€Đ°Đŧ҃ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ.", + "video_hover_setting_description": "Đ’Ņ–Đ´Ņ‚Đ˛ĐžŅ€ŅŽĐ˛Đ°Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ Đ˛Ņ–Đ´ĐĩĐž ĐŋŅ€Đ¸ ĐŊавĐĩĐ´ĐĩĐŊĐŊŅ– ĐēŅƒŅ€ŅĐžŅ€Đ° ĐŊа Ņ„Đ°ĐšĐģ. ĐĐ°Đ˛Ņ–Ņ‚ŅŒ ŅĐēŅ‰Đž виĐŧĐēĐŊĐĩĐŊĐž, Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ ĐŧĐžĐļĐĩ ĐąŅƒŅ‚Đ¸ СаĐŋŅƒŅ‰ĐĩĐŊĐž, ĐŊĐ°Đ˛Ņ–Đ˛ŅˆĐ¸ ĐēŅƒŅ€ŅĐžŅ€ ĐŊа ĐŋŅ–ĐēŅ‚ĐžĐŗŅ€Đ°Đŧ҃ Đ˛Ņ–Đ´Ņ‚Đ˛ĐžŅ€ĐĩĐŊĐŊŅ.", "videos": "Đ’Ņ–Đ´ĐĩĐž", "videos_count": "{count, plural, one {# Đ’Ņ–Đ´ĐĩĐž} few {# Đ’Ņ–Đ´ĐĩĐž} many {# Đ’Ņ–Đ´ĐĩĐž} other {# Đ’Ņ–Đ´ĐĩĐž}}", + "videos_only": "ĐĸŅ–ĐģҌĐēи Đ˛Ņ–Đ´ĐĩĐž", "view": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´", "view_album": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ аĐģŅŒĐąĐžĐŧ", "view_all": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅƒŅŅ–", "view_all_users": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ Đ˛ŅŅ–Ņ… ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Ņ–Đ˛", - "view_asset_owners": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ вĐģĐ°ŅĐŊиĐēŅ–Đ˛ аĐēŅ‚Đ¸Đ˛Ņ–Đ˛", + "view_asset_owners": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ вĐģĐ°ŅĐŊиĐēŅ–Đ˛ Ņ„Đ°ĐšĐģŅ–Đ˛", "view_details": "ДĐĩŅ‚Đ°ĐģҌĐŊŅ–ŅˆĐĩ", "view_in_timeline": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ в Ņ…Ņ€ĐžĐŊĐžĐģĐžĐŗŅ–Ņ—", "view_link": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "view_links": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžŅĐ¸ĐģаĐŊĐŊŅ", "view_name": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸", - "view_next_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊиК Ņ€ĐĩŅŅƒŅ€Ņ", - "view_previous_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš Ņ€ĐĩŅŅƒŅ€Ņ", + "view_next_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŊĐ°ŅŅ‚ŅƒĐŋĐŊиК Ņ„Đ°ĐšĐģ", + "view_previous_asset": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐŋĐžĐŋĐĩŅ€ĐĩĐ´ĐŊŅ–Đš Ņ„Đ°ĐšĐģ", "view_qr_code": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ QR-ĐēОд", "view_similar_photos": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ŅŅ…ĐžĐļŅ– Ņ„ĐžŅ‚ĐžĐŗŅ€Đ°Ņ„Ņ–Ņ—", "view_stack": "ПĐĩŅ€ĐĩĐŗĐģŅĐ´ ҁ҂ĐĩĐē҃", "view_user": "ПĐĩŅ€ĐĩĐŗĐģŅĐŊŅƒŅ‚Đ¸ ĐēĐžŅ€Đ¸ŅŅ‚ŅƒĐ˛Đ°Ņ‡Đ°", "viewer_remove_from_stack": "ВидаĐģĐ¸Ņ‚Đ¸ ĐˇŅ– ҁ҂ĐĩĐē҃", - "viewer_stack_use_as_main_asset": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ¸", + "viewer_stack_use_as_main_asset": "ВиĐēĐžŅ€Đ¸ŅŅ‚ĐžĐ˛ŅƒĐ˛Đ°Ņ‚Đ¸ ŅĐē ĐžŅĐŊОвĐŊиК Ņ„Đ°ĐšĐģ", "viewer_unstack": "Đ ĐžĐˇŅ–ĐąŅ€Đ°Ņ‚Đ¸ ҁ҂ĐĩĐē", "visibility_changed": "ВидиĐŧŅ–ŅŅ‚ŅŒ СĐŧŅ–ĐŊĐĩĐŊĐž Đ´ĐģŅ {count, plural, one {# ĐžŅĐžĐąĐ¸} few {# ĐžŅŅ–Đą} many {# ĐžŅŅ–Đą} other {# ĐžŅŅ–Đą}}", - "waiting": "ĐžŅ‡Ņ–ĐēŅƒŅŽŅ‚ŅŒ", - "waiting_count": "ĐžŅ‡Ņ–ĐēŅƒĐ˛Đ°ĐŊĐŊŅ: {count}", + "visual": "Đ’Ņ–ĐˇŅƒĐ°ĐģҌĐŊиК", + "visual_builder": "Đ’Ņ–ĐˇŅƒĐ°ĐģҌĐŊиК ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€", + "waiting": "ĐŖ ҇ĐĩŅ€ĐˇŅ–", + "waiting_count": "ĐžŅ‡Ņ–ĐēŅƒŅŽŅ‚ŅŒ: {count}", "warning": "ПоĐŋĐĩŅ€ĐĩĐ´ĐļĐĩĐŊĐŊŅ", "week": "ĐĸиĐļĐ´ĐĩĐŊҌ", "welcome": "Đ›Đ°ŅĐēавО ĐŋŅ€ĐžŅĐ¸ĐŧĐž", "welcome_to_immich": "Đ›Đ°ŅĐēавО ĐŋŅ€ĐžŅĐ¸ĐŧĐž Đ´Đž Immich", "width": "Đ¨Đ¸Ņ€Đ¸ĐŊа", "wifi_name": "Назва Wi-Fi", - "workflow": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_delete_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ видаĐģĐ¸Ņ‚Đ¸ ҆ĐĩĐš Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ?", + "workflow_deleted": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ видаĐģĐĩĐŊĐž", + "workflow_description": "ОĐŋĐ¸Ņ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", + "workflow_info": "ІĐŊŅ„ĐžŅ€ĐŧĐ°Ņ†Ņ–Ņ ĐŋŅ€Đž Ņ€ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ", + "workflow_json": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ JSON", + "workflow_json_help": "Đ’Ņ–Đ´Ņ€ĐĩĐ´Đ°ĐŗŅƒĐšŅ‚Đĩ ĐēĐžĐŊŅ„Ņ–ĐŗŅƒŅ€Đ°Ņ†Ņ–ŅŽ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃ ҃ Ņ„ĐžŅ€ĐŧĐ°Ņ‚Ņ– JSON. ЗĐŧŅ–ĐŊи ĐąŅƒĐ´ŅƒŅ‚ŅŒ ŅĐ¸ĐŊŅ…Ņ€ĐžĐŊŅ–ĐˇĐžĐ˛Đ°ĐŊŅ– С Đ˛Ņ–ĐˇŅƒĐ°ĐģҌĐŊиĐŧ ĐēĐžĐŊŅŅ‚Ņ€ŅƒĐēŅ‚ĐžŅ€ĐžĐŧ.", + "workflow_name": "Назва Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", + "workflow_navigation_prompt": "Ви вĐŋĐĩвĐŊĐĩĐŊŅ–, Ņ‰Đž Ņ…ĐžŅ‡ĐĩŅ‚Đĩ Đ˛Đ¸ĐšŅ‚Đ¸ ĐąĐĩС СйĐĩŅ€ĐĩĐļĐĩĐŊĐŊŅ СĐŧŅ–ĐŊ?", + "workflow_summary": "ЗвĐĩĐ´ĐĩĐŊĐŊŅ Ņ€ĐžĐąĐžŅ‡ĐžĐŗĐž ĐŋŅ€ĐžŅ†Đĩҁ҃", + "workflow_update_success": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ ҃ҁĐŋŅ–ŅˆĐŊĐž ĐžĐŊОвĐģĐĩĐŊĐž", + "workflow_updated": "Đ ĐžĐąĐžŅ‡Đ¸Đš ĐŋŅ€ĐžŅ†Đĩҁ ĐžĐŊОвĐģĐĩĐŊĐž", + "workflows": "Đ ĐžĐąĐžŅ‡Ņ– ĐŋŅ€ĐžŅ†ĐĩŅĐ¸", + "workflows_help_text": "ĐĐ˛Ņ‚ĐžĐŧĐ°Ņ‚Đ¸ĐˇĐ°Ņ†Ņ–Ņ— виĐēĐžĐŊŅƒŅŽŅ‚ŅŒ Đ´Ņ–Ņ— С Ņ„Đ°ĐšĐģаĐŧи СаĐģĐĩĐļĐŊĐž Đ˛Ņ–Đ´ Ņ‚Ņ€Đ¸ĐŗĐĩŅ€Ņ–Đ˛ Ņ– ҃ĐŧОв", "wrong_pin_code": "НĐĩĐŋŅ€Đ°Đ˛Đ¸ĐģҌĐŊиК PIN-ĐēОд", "year": "Đ Ņ–Đē", "years_ago": "{years, plural, one {# ҀҖĐē} few {# Ņ€ĐžĐēи} many {# Ņ€ĐžĐēŅ–Đ˛} other {# Ņ€ĐžĐēŅ–Đ˛}} Ņ‚ĐžĐŧ҃", "yes": "ĐĸаĐē", "you_dont_have_any_shared_links": "ĐŖ Đ˛Đ°Ņ ĐŊĐĩĐŧĐ°Ņ” ҁĐŋŅ–ĐģҌĐŊĐ¸Ņ… ĐŋĐžŅĐ¸ĐģаĐŊҌ", "your_wifi_name": "Назва Đ˛Đ°ŅˆĐžŅ— Wi-Fi ĐŧĐĩŅ€ĐĩĐļŅ–", + "zero_to_clear_rating": "ĐŊĐ°Ņ‚Đ¸ŅĐŊŅ–Ņ‚ŅŒ 0, Ņ‰ĐžĐą ĐžŅ‡Đ¸ŅŅ‚Đ¸Ņ‚Đ¸ Ņ€ĐĩĐšŅ‚Đ¸ĐŊĐŗ Ņ„Đ°ĐšĐģ҃", "zoom_image": "Đ—ĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ ĐˇĐžĐąŅ€Đ°ĐļĐĩĐŊĐŊŅ", "zoom_to_bounds": "Đ—ĐąŅ–ĐģŅŒŅˆĐ¸Ņ‚Đ¸ ĐŧĐ°ŅŅˆŅ‚Đ°Đą Đ´Đž ĐŧĐĩĐļ" } diff --git a/i18n/ur.json b/i18n/ur.json index 06ae5d60c3..5329f74c5c 100644 --- a/i18n/ur.json +++ b/i18n/ur.json @@ -5,9 +5,10 @@ "acknowledge": "ØĒØŗŲ„ÛŒŲ… ÚŠØąŲ†Ø§", "action": "ØšŲ…Ų„", "action_common_update": "Ø§ŲžÚˆÛŒŲš ÚŠØąÛŒÚē", + "action_description": "ŲŲ„ŲšØą شدہ اØĢاØĢ؈Úē ŲžØą Ø§Ų†ØŦØ§Ų… Ø¯ÛŒŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ ÚŠØ§ØąØąŲˆØ§ØĻی ڊا ایڊ Ų…ØŦŲ…ŲˆØšÛ", "actions": "Ø§ØšŲ…Ø§Ų„", "active": "ŲØšØ§Ų„", - "active_count": "ŲØšØ§Ų„: {ØĒؚداد}", + "active_count": "ŲØšØ§Ų„: {count}", "activity": "ØŗØąÚ¯ØąŲ…ÛŒ", "activity_changed": "ØŗØąÚ¯ØąŲ…ÛŒ {enabled, select, true {ŲØšØ§Ų„ ہے} other {ØēÛŒØą ŲØšØ§Ų„ ہے}}", "add": "Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", @@ -15,9 +16,14 @@ "add_a_location": "Ų…Ų‚Ø§Ų… Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_a_name": "Ų†Ø§Ų… ڊا Ø§Ų†Ø¯ØąØ§ØŦ ÚŠØąÛŒÚē", "add_a_title": "ØšŲ†ŲˆØ§Ų† ڊا Ø§Ų†Ø¯ØąØ§ØŦ ÚŠØąÛŒÚē", + "add_action": "ØšŲ…Ų„ Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", + "add_action_description": "ØšŲ…Ų„ Ø´Ø§Ų…Ų„ ÚŠØąŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ یہاÚē ÚŠŲ„ÚŠ ÚŠØąÛŒÚē", + "add_assets": "اØĢاØĢے Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_birthday": "ØŗØ§Ų„Ú¯ØąÛ Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_endpoint": "Ø§ÛŒŲ†Úˆ ŲžŲˆØ§ØĻŲ†Ųš Ø¯ØąØŦ ÚŠØąÛŒÚē", "add_exclusion_pattern": "ØŽØ§ØąØŦ ÚŠØąŲ†Û’ ڊا Ų†Ų…ŲˆŲ†Û Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", + "add_filter": "ŲŲ„ŲšØą Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", + "add_filter_description": "ŲŲ„ŲšØą ÚŠÛŒ Ø´ØąØˇ Ø´Ø§Ų…Ų„ ÚŠØąŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ ÚŠŲ„ÚŠ ÚŠØąÛŒÚē", "add_location": "ØŦگہ Ø¯ØąØŦ ÚŠØąÛŒÚē", "add_more_users": "Ų…Ø˛ÛŒØ¯ ØĩØ§ØąŲÛŒŲ† Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_partner": "ØŗØ§ØĒÚžÛŒ Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", @@ -29,12 +35,14 @@ "add_to_album_bottom_sheet_added": "{album} Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØąØ¯ÛŒØ§Ú¯ÛŒØ§", "add_to_album_bottom_sheet_already_exists": "ŲžÛŲ„Û’ ØŗÛ’ ہی {album} Ų…ÛŒÚē Ų…ŲˆØŦŲˆØ¯ ہے", "add_to_album_bottom_sheet_some_local_assets": "کچھ Ų…Ų‚Ø§Ų…ÛŒ اØĢاØĢے Ø§Ų„Ø¨Ų… Ų…ÛŒÚē Ø´Ø§Ų…Ų„ Ų†ÛÛŒÚē ڊیے ØŦا ØŗÚŠÛ’", - "add_to_album_toggle": "Ų…Ų†ØĒ؎ب ÚŠØąŲ†Û’ ڊا ØˇØąÛŒŲ‚Û {album}", + "add_to_album_toggle": "Ų…Ų†ØĒ؎ب ÚŠØąŲ†Û’ ڊا ØˇØąÛŒŲ‚Û {album} ÚŠÛ’ Ų„ÛŒÛ’", "add_to_albums": "Ø§Ų„Ø¨Ų…ŲˆÚē Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠÛŒØŦیے", "add_to_albums_count": "Ø§Ų„Ø¨Ų…ŲˆÚē Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠÛŒØŦیے ({count})", "add_to_bottom_bar": "Ø§Øŗ Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_to_shared_album": "Ų…Ø´ØĒØąÚŠÛ Ø§Ų„Ø¨Ų… Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", + "add_upload_to_stack": "Ø§Ųž Ų„ŲˆÚˆ ÚŠŲˆ Ø§ØŗŲšÛŒÚŠ Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "add_url": "URL Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", + "add_workflow_step": "ŲˆØąÚŠ ŲŲ„Ųˆ ڊا Ų…ØąØ­Ų„Û Ø´Ø§Ų…Ų„ ÚŠØąÛŒÚē", "added_to_archive": "ØĸØąÚŠØ§ØĻÛŒŲˆ Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØą دیا گیا", "added_to_favorites": "ŲžØŗŲ†Ø¯ÛŒØ¯Û Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ÚŠØąØ¯ÛŒØ§ گیا", "added_to_favorites_count": "ŲžØŗŲ†Ø¯ÛŒØ¯Û Ų…ÛŒÚē {count, number} Ø´Ø§Ų…Ų„ ڊیے Ú¯ØĻے", @@ -45,7 +53,7 @@ "authentication_settings": "ØĒØĩØ¯ÛŒŲ‚ ÚŠÛŒ ØĒØąØĒیباØĒ", "authentication_settings_description": "ŲžØ§Øŗ ŲˆØąÚˆØŒ OAuth، Ø§ŲˆØą Ø¯ÛŒÚ¯Øą ØĒØĩØ¯ÛŒŲ‚ÛŒ ØĒØąØĒیباØĒ ڊا Ų†Ø¸Ų… ÚŠØąÛŒÚē", "authentication_settings_disable_all": "ڊیا ØĸŲž ŲˆØ§Ų‚ØšÛŒ Ų„Ø§Ú¯ Ø§Ų† ÚŠÛ’ ØĒŲ…Ø§Ų… ØˇØąÛŒŲ‚ŲˆÚē ÚŠŲˆ ØēÛŒØą ŲØšØ§Ų„ ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟 Ų„Ø§Ú¯ Ø§Ų† Ų…ÚŠŲ…Ų„ ØˇŲˆØą ŲžØą ØēÛŒØą ŲØšØ§Ų„ ÛŲˆ ØŦاØĻے گا۔", - "authentication_settings_reenable": "Ø¯ŲˆØ¨Ø§ØąÛ ŲØšØ§Ų„ ÚŠØąŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ØŒ ایڊ ØŗØąŲˆØą ÚŠŲ…Ø§Ų†Úˆ Ø§ØŗØĒØšŲ…Ø§Ų„ ÚŠØąÛŒÚē", + "authentication_settings_reenable": "Ø¯ŲˆØ¨Ø§ØąÛ ŲØšØ§Ų„ ÚŠØąŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ØŒ ایڊ ØŗØąŲˆØą ÚŠŲ…Ø§Ų†Úˆ Ø§ØŗØĒØšŲ…Ø§Ų„ ÚŠØąÛŒÚē.", "background_task_job": "ŲžØŗ Ų…Ų†Ø¸Øą ÚŠÛ’ ÚŠØ§Ų…", "backup_database": "ÚˆÛŒŲšØ§ Ø¨ÛŒØŗ ڊا بیڊ Ø§Ųž Ø¨Ų†Ø§ØĻیÚē", "backup_database_enable_description": "ÚˆÛŒŲšØ§ Ø¨ÛŒØŗ ÚŠÛ’ بیڊ Ø§Ųž ÚŠŲˆ ŲØšØ§Ų„ ÚŠØąÛŒÚē", @@ -53,22 +61,38 @@ "backup_onboarding_1_description": "Øĸ؁ ØŗØ§ØĻŲš ÚŠØ§ŲžÛŒ ÚŠŲ„Ø§Ø¤Úˆ Ų…ÛŒÚē یا ÚŠØŗÛŒ Ø§ŲˆØą Ų…Ų‚Ø§Ų… ŲžØąÛ”", "backup_onboarding_2_description": "Ų…ØŽØĒ؄؁ ØĸŲ„Ø§ØĒ ŲžØą Ų…Ų‚Ø§Ų…ÛŒ ÚŠØ§ŲžÛŒØ§Úē۔ Ø§Øŗ Ų…ÛŒÚē Ø¨Ų†ÛŒØ§Ø¯ÛŒ ŲØ§ØĻŲ„ÛŒÚē Ø§ŲˆØą Ų…Ų‚Ø§Ų…ÛŒ ØˇŲˆØą ŲžØą Ø§Ų† ŲØ§ØĻŲ„ŲˆÚē ڊا بیڊ Ø§Ųž Ø´Ø§Ų…Ų„ ہے۔", "backup_onboarding_3_description": "اØĩŲ„ ŲØ§ØĻŲ„ŲˆÚē ØŗŲ…ÛŒØĒ ØĸŲž ÚŠÛ’ ÚˆÛŒŲšØ§ ÚŠÛŒ ÚŠŲ„ ÚŠØ§ŲžÛŒØ§Úē۔ Ø§Øŗ Ų…ÛŒÚē 1 Øĸ؁ ØŗØ§ØĻŲš ÚŠØ§ŲžÛŒ Ø§ŲˆØą 2 Ų…Ų‚Ø§Ų…ÛŒ ÚŠØ§ŲžÛŒØ§Úē Ø´Ø§Ų…Ų„ ہیÚē۔", + "backup_onboarding_parts_title": "ایڊ 3-2-1 بیڊ Ø§Ųž Ų…ÛŒÚē Ø´Ø§Ų…Ų„ ہے:", + "backup_onboarding_title": "بیڊ Ø§ŲžØŗ", "backup_settings": "ÚˆÛŒŲšØ§ Ø¨ÛŒØŗ ÚˆŲ…Ųž ÚŠÛŒ ØĒØąØĒیباØĒ", - "backup_settings_description": "ÚˆÛŒŲšØ§ Ø¨ÛŒØŗ ÚˆŲ…Ųž ÚŠÛŒ ØĒØąØĒیباØĒ ڊا Ų†Ø¸Ų… ÚŠØąÛŒÚē۔ Ų†ŲˆŲš: Ø§Ų† Ų…Ų„Ø§Ø˛Ų…ØĒ؈Úē ÚŠÛŒ Ų†Ú¯ØąØ§Ų†ÛŒ Ų†ÛÛŒÚē ÚŠÛŒ ØŦاØĒی ہے Ø§ŲˆØą ØĸŲž ÚŠŲˆ Ų†Ø§ÚŠØ§Ų…ÛŒ ÚŠÛŒ Ø§ØˇŲ„Ø§Øš Ų†ÛÛŒÚē Ø¯ÛŒ ØŦاØĻے گی", + "backup_settings_description": "ÚˆÛŒŲšØ§ Ø¨ÛŒØŗ ÚˆŲ…Ųž ÚŠÛŒ ØĒØąØĒیباØĒ ڊا Ų†Ø¸Ų… ÚŠØąÛŒÚē.", "cleared_jobs": "Ų…Ų„Ø§Ø˛Ų…ØĒیÚē Ø§Øŗ ÚŠÛ’ Ų„ÛŒÛ’ ØĩØ§Ų ÚŠÛŒ Ú¯ØĻیÚē: {job}", "config_set_by_file": "Config ŲÛŒ Ø§Ų„Ø­Ø§Ų„ ایڊ config ŲØ§ØĻŲ„ ÚŠÛ’ Ø°ØąÛŒØšÛ ØĒØąØĒیب دی Ú¯ØĻی ہے", "confirm_delete_library": "ڊیا ØĸŲž ŲˆØ§Ų‚ØšÛŒ {library} Ų„Ø§ØĻØ¨ØąÛŒØąÛŒ ÚŠŲˆ Ø­Ø°Ų ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟", "confirm_delete_library_assets": "ڊیا ØĸŲž ŲˆØ§Ų‚ØšÛŒ Ø§Øŗ Ų„Ø§ØĻØ¨ØąÛŒØąÛŒ ÚŠŲˆ Ø­Ø°Ų ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟 یہ Immich ØŗÛ’ {count, plural, one {# contained asset} Ø¯ÛŒÚ¯Øą {all # contained assets}} ÚŠŲˆ Ø­Ø°Ų ÚŠØą دے گا Ø§ŲˆØą Ø§ØŗÛ’ ÚŠØ§Ų„ØšØ¯Ų… Ų†ÛÛŒÚē ڊیا ØŦا ØŗÚŠØĒا۔ ŲØ§ØĻŲ„ÛŒÚē ÚˆØŗÚŠ ŲžØą Ų…ŲˆØŦŲˆØ¯ ØąÛÛŒÚē گی۔", "confirm_email_below": "ØĒØĩØ¯ÛŒŲ‚ ÚŠØąŲ†Û’ ÚŠÛ’ Ų„ÛŒÛ’ØŒ Ų†ÛŒÚ†Û’ ای Ų…ÛŒŲ„ ŲšØ§ØĻŲž ÚŠØąÛŒÚē {email}", "confirm_reprocess_all_faces": "ڊیا ØĸŲž ŲˆØ§Ų‚ØšÛŒ ØĒŲ…Ø§Ų… Ú†ÛØąŲˆÚē ÚŠŲˆ Ø¯ŲˆØ¨Ø§ØąÛ ŲžØąŲˆØŗÛŒØŗ ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟 Ø§Øŗ ØŗÛ’ Ų†Ø§Ų… ŲˆØ§Ų„Û’ Ø§ŲØąØ§Ø¯ بڞی ØĩØ§Ų ÛŲˆ ØŦاØĻیÚē گے۔", + "confirm_user_password_reset": "ڊیا ØĸŲž {user} ڊا ŲžØ§Øŗ ŲˆØąÚˆ ØąÛŒ ØŗÛŒŲš ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟", + "confirm_user_pin_code_reset": "ڊیا ØĸŲž {user} ڊا ŲžŲ† ÚŠŲˆÚˆ ØąÛŒ ØŗÛŒŲš ÚŠØąŲ†Ø§ چاہØĒے ہیÚ稟", + "create_job": "ÚŠØ§Ų… Ø¨Ų†Ø§ØĻیÚē", + "face_detection": "Ú†ÛØąÛ’ ÚŠÛŒ ŲžÛÚ†Ø§Ų†", + "failed_job_command": "ÚŠØ§Ų…: {job} ÚŠÛ’ Ų„ÛŒÛ’ ÚŠŲ…Ø§Ų†Úˆ: {command} Ų†Ø§ÚŠØ§Ų… ÛŲˆ Ú¯ØĻی", "image_preview_title": "ŲžÛŒØ´ Ų†Ø¸Ø§ØąÛ", "image_quality": "Ų…ØšÛŒØ§Øą", "image_settings": "ØĒØĩŲˆÛŒØą ÚŠÛŒ ØĒØąØĒیباØĒ" }, "change_pin_code": "ŲžŲ† ÚŠŲˆÚˆ ØĒØ¨Ø¯ÛŒŲ„ ÚŠØąÛŒÚē", "confirm_new_pin_code": "Ų†ØĻے ŲžŲ† ÚŠŲˆÚˆ ÚŠÛŒ ØĒØĩØ¯ÛŒŲ‚ ÚŠØąÛŒÚē", + "crop_aspect_ratio_fixed": "Ų…Ų‚ØąØąÛ", + "crop_aspect_ratio_free": "ØĸØ˛Ø§Ø¯", + "crop_aspect_ratio_original": "اØĩŲ„", "current_pin_code": "Ų…ŲˆØŦŲˆØ¯Û ŲžŲ† ÚŠŲˆÚˆ", + "custom_date": "Ø§ŲžŲ†ÛŒ ØĒØ§ØąÛŒØŽ", + "download_original": "ØĩŲ„ ÚˆØ§Ø¤Ų† Ų„ŲˆÚˆ ÚŠØąÛŒÚē", + "errors_text": "ØēŲ„ØˇÛŒØ§Úē", + "free_up_space": "ØŦگہ ØŽØ§Ų„ÛŒ ÚŠØąÛŒÚē", + "keep_favorites": "ŲžØŗŲ†Ø¯ÛŒØ¯Û ØąÚŠÚžÛŒÚē", "new_pin_code": "Ų†ÛŒØ§ ŲžŲ† ÚŠŲˆÚˆ", + "photos_only": "ØĩØąŲ ØĒØĩØ§ŲˆÛŒØą", "pin_code_changed_successfully": "ŲžŲ† ÚŠŲˆÚˆ ÚŠŲˆ ÚŠØ§Ų…ÛŒØ§Ø¨ÛŒ ØŗÛ’ ØĒØ¨Ø¯ÛŒŲ„ ÚŠØą دیا گیا", "pin_code_reset_successfully": "ŲžŲ† ÚŠŲˆÚˆ ÚŠØ§Ų…ÛŒØ§Ø¨ÛŒ ÚŠÛ’ ØŗØ§ØĒÚž ØąÛŒ ØŗÛŒŲš ÛŲˆ گیا", "pin_code_setup_successfully": "ŲžŲ† ÚŠŲˆÚˆ ÚŠØ§Ų…ÛŒØ§Ø¨ÛŒ ÚŠÛ’ ØŗØ§ØĒÚž ØŗÛŒŲš Ø§Ųž ÛŲˆ گیا", @@ -84,6 +108,7 @@ "version_announcement_closing": "ØĸŲž ڊا Ø¯ŲˆØŗØĒ، Ø§ÛŒŲ„ÚŠØŗ", "video": "ŲˆÛŒÚˆÛŒŲˆ", "videos": "ŲˆÛŒÚˆÛŒŲˆØ˛", + "videos_only": "ØĩØąŲ ŲˆÛŒÚˆÛŒŲˆØ˛", "view": "دیڊڞیÚē", "view_all": "ØŗØ¨ دیڊڞیÚē", "waiting": "Ø§Ų†ØĒØ¸Ø§Øą", diff --git a/i18n/vi.json b/i18n/vi.json index 0f0fce413f..0ba340ad2c 100644 --- a/i18n/vi.json +++ b/i18n/vi.json @@ -5,6 +5,7 @@ "acknowledge": "Ghi nháē­n", "action": "Hành đáģ™ng", "action_common_update": "Cáē­p nháē­t", + "action_description": "Máģ™t táē­p háģŖp cÃĄc hành đáģ™ng cáē§n tháģąc hiáģ‡n trÃĒn cÃĄc táģ‡p Ä‘ÃŖ đưáģŖc láģc", "actions": "Hành đáģ™ng", "active": "Đang hoáēĄt đáģ™ng", "active_count": "HoáēĄt đáģ™ng: {count}", @@ -15,9 +16,13 @@ "add_a_location": "ThÃĒm đáģ‹a điáģƒm", "add_a_name": "ThÃĒm tÃĒn", "add_a_title": "ThÃĒm tÃĒn", + "add_action": "ThÃĒm hành đáģ™ng", + "add_action_description": "NháēĨn đáģƒ thÃĒm hành đáģ™ng cáē§n tháģąc hiáģ‡n", "add_birthday": "ThÃĒm sinh nháē­t", "add_endpoint": "ThÃĒm endpoint", "add_exclusion_pattern": "ThÃĒm quy táē¯c loáēĄi tráģĢ", + "add_filter": "ThÃĒm báģ™ láģc", + "add_filter_description": "NháēĨn đáģƒ thÃĒm điáģu kiáģ‡n láģc", "add_location": "ThÃĒm đáģ‹a điáģƒm", "add_more_users": "ThÃĒm ngưáģi dÚng", "add_partner": "ThÃĒm ngưáģi thÃĸn", @@ -36,6 +41,7 @@ "add_to_shared_album": "ThÃĒm vào album chia sáēģ", "add_upload_to_stack": "TáēŖi lÃĒn thÃĒm vào nhÃŗm", "add_url": "ThÃĒm URL", + "add_workflow_step": "ThÃĒm bưáģ›c workflow", "added_to_archive": "ÄÃŖ lưu tráģ¯", "added_to_favorites": "ÄÃŖ thích", "added_to_favorites_count": "ÄÃŖ thích {count, number} máģĨc", @@ -181,6 +187,8 @@ "machine_learning_smart_search_enabled": "Báē­t TÃŦm kiáēŋm Thông minh", "machine_learning_smart_search_enabled_description": "Náēŋu táē¯t, áēŖnh sáēŊ không đưáģŖc mÃŖ hÃŗa đáģƒ tÃŦm kiáēŋm thông minh.", "machine_learning_url_description": "Đáģ‹a cháģ‰ mÃĄy cháģ§ háģc mÃĄy. Náēŋu cÃŗ nhiáģu hÆĄn máģ™t đáģ‹a cháģ‰ Ä‘Æ°áģŖc cung cáēĨp, máģ—i mÃĄy cháģ§ sáēŊ đưáģŖc kiáģƒm tra máģ™t láē§n cho đáēŋn khi cÃŗ máģ™t mÃĄy cháģ§ tráēŖ láģi thành công, theo tháģŠ táģą táģĢ mÃĄy cháģ§ Ä‘áē§u tiÃĒn đáēŋn mÃĄy cháģ§ cuáģ‘i cÚng. MÃĄy cháģ§ không pháēŖn háģ“i sáēŊ táēĄm tháģi đưáģŖc báģ qua cho đáēŋn khi mÃĄy cháģ§ online tráģŸ láēĄi.", + "maintenance_delete_backup_description": "Táģ‡p này sáēŊ báģ‹ xoÃĄ vÄŠnh viáģ…n.", + "maintenance_restore_backup": "Khôi pháģĨc sao lưu", "maintenance_settings": "BáēŖo trÃŦ", "maintenance_settings_description": "Đáēˇt [immich] vào cháēŋ đáģ™ báēŖo trÃŦ.", "maintenance_start": "Báē¯t đáē§u cháēŋ đáģ™ báēŖo trÃŦ", @@ -467,6 +475,7 @@ "album_remove_user": "XÃŗa ngưáģi dÚng?", "album_remove_user_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa {user}?", "album_search_not_found": "Không tÃŦm tháēĨy album trÚng kháģ›p", + "album_selected": "Album Ä‘ÃŖ cháģn", "album_share_no_users": "CÃŗ váēģ như báēĄn Ä‘ÃŖ chia sáēģ album này váģ›i táēĨt cáēŖ ngưáģi dÚng hoáēˇc báēĄn không cÃŗ ngưáģi dÚng nào đáģƒ chia sáēģ.", "album_summary": "Mô táēŖ album", "album_updated": "ÄÃŖ cáē­p nháē­t album", @@ -481,21 +490,22 @@ "album_viewer_appbar_share_leave": "Ráģi kháģi album", "album_viewer_appbar_share_to": "Chia sáēģ váģ›i", "album_viewer_page_share_add_users": "ThÃĒm ngưáģi dÚng", - "album_with_link_access": "Cho phÊp báēĨt káģŗ ai cÃŗ liÃĒn káēŋt xem áēŖnh và ngưáģi trong album này.", + "album_with_link_access": "Ai cÃŗ liÃĒn káēŋt sáēŊ xem đưáģŖc cÃĄc áēŖnh và ngưáģi trong album này.", "albums": "Album", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Album}}", "albums_default_sort_order": "TháģŠ táģą sáē¯p xáēŋp album máēˇc đáģ‹nh", "albums_default_sort_order_description": "TháģŠ táģą sáē¯p xáēŋp ban đáē§u cho cÃĄc áēŖnh khi táēĄo album máģ›i.", "albums_feature_description": "CÃĄc báģ™ sưu táē­p táģ‡p cÃŗ tháģƒ Ä‘Æ°áģŖc chia sáēģ váģ›i nháģ¯ng ngưáģi dÚng khÃĄc.", "albums_on_device_count": "Album trÃĒn thiáēŋt báģ‹ ({count})", + "albums_selected": "{count, plural, one {# album Ä‘ÃŖ cháģn} other {# album Ä‘ÃŖ cháģn}}", "all": "TáēĨt cáēŖ", "all_albums": "TáēĨt cáēŖ album", "all_people": "TáēĨt cáēŖ máģi ngưáģi", "all_videos": "TáēĨt cáēŖ video", "allow_dark_mode": "Cho phÊp cháēŋ đáģ™ táģ‘i", "allow_edits": "Cho phÊp cháģ‰nh sáģ­a", - "allow_public_user_to_download": "Cho phÊp ngưáģi dÚng công khai táēŖi xuáģ‘ng", - "allow_public_user_to_upload": "Cho phÊp ngưáģi dÚng công khai táēŖi lÃĒn", + "allow_public_user_to_download": "Cho phÊp táēŖi áēŖnh xuáģ‘ng", + "allow_public_user_to_upload": "Cho phÊp táēŖi áēŖnh lÃĒn", "allowed": "Cho phÊp", "alt_text_qr_code": "áēĸnh mÃŖ QR", "anti_clockwise": "Xoay trÃĄi", @@ -524,10 +534,12 @@ "archived_count": "{count, plural, other {ÄÃŖ lưu tráģ¯ # máģĨc}}", "are_these_the_same_person": "ĐÃĸy cÃŗ pháēŖi cÚng máģ™t ngưáģi không?", "are_you_sure_to_do_this": "BáēĄn cÃŗ cháē¯c muáģ‘n tháģąc hiáģ‡n điáģu này?", + "array_field_not_fully_supported": "CÃĄc trưáģng máēŖng yÃĒu cáē§u cháģ‰nh sáģ­a JSON tháģ§ công", "asset_action_delete_err_read_only": "Không tháģƒ xÃŗa táģ‡p cháģ‰ cÃŗ quyáģn đáģc, báģ qua", "asset_action_share_err_offline": "Không tháģƒ táēŖi táģ‡p ngoáēĄi tuyáēŋn, báģ qua", "asset_added_to_album": "ÄÃŖ thÃĒm vào album", "asset_adding_to_album": "Đang thÃĒm vào albumâ€Ļ", + "asset_created": "ÄÃŖ táēĄo táģ‡p", "asset_description_updated": "Mô táēŖ áēŖnh Ä‘ÃŖ đưáģŖc cáē­p nháē­t", "asset_filename_is_offline": "Táģ‡p {filename} đang ngoáēĄi tuyáēŋn", "asset_has_unassigned_faces": "Táģ‡p chưa đưáģŖc gÃĄn khuôn máēˇt", @@ -550,7 +562,7 @@ "asset_uploaded": "ÄÃŖ táēŖi lÃĒn", "asset_uploading": "Đang táēŖi lÃĒnâ€Ļ", "asset_viewer_settings_subtitle": "CÃĄch thư viáģ‡n hiáģƒn tháģ‹", - "asset_viewer_settings_title": "TrÃŦnh xem táģ‡p", + "asset_viewer_settings_title": "TrÃŦnh xem áēŖnh", "assets": "Táģ‡p", "assets_added_count": "ÄÃŖ thÃĒm {count, plural, one {# táģ‡p} other {# táģ‡p}}", "assets_added_to_album_count": "ÄÃŖ thÃĒm {count, plural, one {# táģ‡p} other {# táģ‡p}} vào album", @@ -711,6 +723,8 @@ "change_password_form_password_mismatch": "Máē­t kháēŠu không giáģ‘ng nhau", "change_password_form_reenter_new_password": "Nháē­p láēĄi máē­t kháēŠu máģ›i", "change_pin_code": "Thay đáģ•i mÃŖ PIN", + "change_trigger": "Thay đáģ•i trÃŦnh kích hoáēĄt", + "change_trigger_prompt": "BáēĄn cÃŗ cháē¯c muáģ‘n thay đáģ•i trÃŦnh kích hoáēĄt? Thao tÃĄc này sáēŊ xÃŗa táēĨt cáēŖ cÃĄc hành đáģ™ng và báģ™ láģc hiáģ‡n cÃŗ.", "change_your_password": "Đáģ•i máē­t kháēŠu cáģ§a báēĄn", "changed_visibility_successfully": "ÄÃŖ đáģ•i tráēĄng thÃĄi hiáģƒn tháģ‹ thành công", "charging": "SáēĄc", @@ -760,7 +774,7 @@ "confirm_tag_face_unnamed": "BáēĄn cÃŗ muáģ‘n gáē¯n tháēģ gÆ°ÆĄng máēˇt này?", "connected_device": "Thiáēŋt báģ‹ Ä‘Æ°áģŖc káēŋt náģ‘i", "connected_to": "ÄÃŖ káēŋt náģ‘i táģ›i", - "contain": "CháģŠa", + "contain": "VáģĢa màn hÃŦnh", "context": "Ngáģ¯ cáēŖnh", "continue": "Tiáēŋp táģĨc", "control_bottom_app_bar_create_new_album": "TáēĄo album máģ›i", @@ -781,16 +795,17 @@ "copy_password": "Sao chÊp máē­t kháēŠu", "copy_to_clipboard": "Sao chÊp vào báģ™ nháģ› táēĄm", "country": "Quáģ‘c gia", - "cover": "áēĸnh bÃŦa", - "covers": "áēĸnh bÃŦa", + "cover": "Táģ‘i đa", + "covers": "Lưáģ›i", "create": "TáēĄo", "create_album": "TáēĄo album", "create_album_page_untitled": "Không tÃĒn", "create_api_key": "TáēĄo khÃŗa API", + "create_first_workflow": "TáēĄo workflow đáē§u tiÃĒn", "create_library": "TáēĄo thư viáģ‡n", "create_link": "TáēĄo liÃĒn káēŋt", "create_link_to_share": "TáēĄo liÃĒn káēŋt đáģƒ chia sáēģ", - "create_link_to_share_description": "Cho phÊp báēĨt káģŗ ai cÃŗ liÃĒn káēŋt xem cÃĄc áēŖnh Ä‘ÃŖ cháģn", + "create_link_to_share_description": "Ai cÃŗ liÃĒn káēŋt sáēŊ xem đưáģŖc cÃĄc áēŖnh Ä‘ÃŖ cháģn", "create_new": "Táē O MáģšI", "create_new_person": "TáēĄo ngưáģi máģ›i", "create_new_person_hint": "GÃĄn cÃĄc áēŖnh Ä‘ÃŖ cháģn cho máģ™t ngưáģi máģ›i", @@ -801,6 +816,7 @@ "create_tag": "TáēĄo tháēģ", "create_tag_description": "TáēĄo tháēģ máģ›i. Váģ›i cÃĄc tháēģ láģ“ng nhau, vui lÃ˛ng nháē­p đưáģng dáēĢn đáē§y đáģ§ cáģ§a tháēģ bao gáģ“m dáēĨu gáēĄch chÊo.", "create_user": "TáēĄo ngưáģi dÚng", + "create_workflow": "TáēĄo workflow", "created": "ÄÃŖ táēĄo", "created_at": "ÄÃŖ táēĄo", "creating_linked_albums": "Đang táēĄo album đưáģŖc liÃĒn káēŋt...", @@ -867,6 +883,7 @@ "deselect_all": "Báģ cháģn táēĨt cáēŖ", "details": "Chi tiáēŋt", "direction": "Hưáģ›ng", + "disable": "Vô hiáģ‡u hÃŗa", "disabled": "ÄÃŖ táē¯t", "disallow_edits": "Không cho phÊp cháģ‰nh sáģ­a", "discord": "Discord", @@ -929,11 +946,10 @@ "edit_tag": "Cháģ‰nh sáģ­a tháēģ", "edit_title": "Cháģ‰nh sáģ­a tiÃĒu đáģ", "edit_user": "Cháģ‰nh sáģ­a ngưáģi dÚng", + "edit_workflow": "Sáģ­a workflow", "editor": "TrÃŦnh cháģ‰nh sáģ­a", "editor_close_without_save_prompt": "Nháģ¯ng thay đáģ•i sáēŊ không đưáģŖc lưu", "editor_close_without_save_title": "ÄÃŗng trÃŦnh cháģ‰nh sáģ­a?", - "editor_crop_tool_h2_aspect_ratios": "Táģˇ láģ‡ khung hÃŦnh", - "editor_crop_tool_h2_rotation": "Xoay", "email": "Email", "email_notifications": "Thông bÃĄo qua email", "empty_folder": "Thư máģĨc tráģ‘ng", @@ -966,7 +982,7 @@ "cant_change_metadata_assets_count": "Không tháģƒ thay đáģ•i siÃĒu dáģ¯ liáģ‡u cáģ§a {count, plural, one {# táģ‡p} other {# táģ‡p}}", "cant_get_faces": "Không tháģƒ táēŖi khuôn máēˇt", "cant_get_number_of_comments": "Không tháģƒ táēŖi sáģ‘ lưáģŖng bÃŦnh luáē­n", - "cant_search_people": "Không tháģƒ tÃŦm kiáēŋm ngưáģi", + "cant_search_people": "Không tháģƒ tÃŦm ngưáģi", "cant_search_places": "Không tháģƒ tÃŦm kiáēŋm đáģ‹a điáģƒm", "error_adding_assets_to_album": "Láģ—i khi thÃĒm táģ‡p vào album", "error_adding_users_to_album": "Láģ—i khi thÃĒm ngưáģi dÚng vào album", @@ -1014,6 +1030,7 @@ "unable_to_complete_oauth_login": "Không tháģƒ hoàn táēĨt đăng nháē­p OAuth", "unable_to_connect": "Không tháģƒ káēŋt náģ‘i", "unable_to_copy_to_clipboard": "Không tháģƒ sao chÊp vào báģ™ nháģ› táēĄm, hÃŖy đáēŖm báēŖo báēĄn đang truy cáē­p trang qua https", + "unable_to_create": "Không tháģƒ táēĄo workflow", "unable_to_create_admin_account": "Không tháģƒ táēĄo tài khoáēŖn quáēŖn tráģ‹ viÃĒn", "unable_to_create_api_key": "Không tháģƒ táēĄo khÃŗa API máģ›i", "unable_to_create_library": "Không tháģƒ táēĄo thư viáģ‡n", @@ -1024,6 +1041,7 @@ "unable_to_delete_exclusion_pattern": "Không tháģƒ xÃŗa quy táē¯c loáēĄi tráģĢ", "unable_to_delete_shared_link": "Không tháģƒ xÃŗa liÃĒn káēŋt chia sáēģ", "unable_to_delete_user": "Không tháģƒ xÃŗa ngưáģi dÚng", + "unable_to_delete_workflow": "Không tháģƒ xÃŗa workflow", "unable_to_download_files": "Không tháģƒ táēŖi xuáģ‘ng táģ‡p", "unable_to_edit_exclusion_pattern": "Không tháģƒ cháģ‰nh sáģ­a quy táē¯c loáēĄi tráģĢ", "unable_to_empty_trash": "Không tháģƒ dáģn sáēĄch thÚng rÃĄc", @@ -1063,6 +1081,7 @@ "unable_to_scan_library": "Không tháģƒ quÊt thư viáģ‡n", "unable_to_set_feature_photo": "Không tháģƒ Ä‘áēˇt áēŖnh náģ•i báē­t", "unable_to_set_profile_picture": "Không tháģƒ Ä‘áēˇt áēŖnh đáēĄi diáģ‡n", + "unable_to_set_rating": "Không tháģƒ Ä‘áēˇt Ä‘ÃĄnh giÃĄ", "unable_to_submit_job": "Không tháģƒ gáģ­i tÃĄc váģĨ", "unable_to_trash_asset": "Không tháģƒ chuyáģƒn áēŖnh vào thÚng rÃĄc", "unable_to_unlink_account": "Không tháģƒ háģ§y liÃĒn káēŋt tài khoáēŖn", @@ -1074,6 +1093,7 @@ "unable_to_update_settings": "Không tháģƒ cáē­p nháē­t cài đáēˇt", "unable_to_update_timeline_display_status": "Không tháģƒ cáē­p nháē­t tráēĄng thÃĄi hiáģƒn tháģ‹ dÃ˛ng tháģi gian", "unable_to_update_user": "Không tháģƒ cáē­p nháē­t ngưáģi dÚng", + "unable_to_update_workflow": "Không tháģƒ cáē­p nháē­t workflow", "unable_to_upload_file": "Không tháģƒ táēŖi táģ‡p lÃĒn" }, "exclusion_pattern": "MáēĢu ngoáēĄi láģ‡", @@ -1111,7 +1131,7 @@ "failed_to_authenticate": "XÃĄc tháģąc tháēĨt báēĄi", "failed_to_load_assets": "Không táēŖi đưáģŖc táģ‡p", "failed_to_load_folder": "Không táēŖi đưáģŖc thư máģĨc", - "favorite": "ÄÃŖ thích", + "favorite": "Thích", "favorite_action_prompt": "{count} Ä‘ÃŖ thÃĒm vào ÄÃŖ thích", "favorite_or_unfavorite_photo": "Thích hoáēˇc báģ thích áēŖnh", "favorites": "ÄÃŖ thích", @@ -1120,14 +1140,15 @@ "features": "Tính năng", "features_in_development": "Tính năng đang đưáģŖc phÃĄt triáģƒn", "features_setting_description": "QuáēŖn lÃŊ cÃĄc tính năng app", - "file_name": "TÃĒn táģ‡p", "file_name_or_extension": "TÃĒn hoáēˇc pháē§n máģŸ ráģ™ng táē­p tin", "file_size": "Kích cáģĄ táģ‡p tin", "filename": "TÃĒn táģ‡p", "filetype": "LoáēĄi táģ‡p", "filter": "Báģ™ láģc", + "filter_description": "Điáģu kiáģ‡n đáģƒ láģc táģ‡p máģĨc tiÃĒu", "filter_people": "Láģc ngưáģi", "filter_places": "Láģc đáģ‹a điáģƒm", + "filters": "Báģ™ láģc", "find_them_fast": "TÃŦm nhanh báēąng tÃĒn váģ›i tÃŦm kiáēŋm", "first": "Đáē§u tiÃĒn", "fix_incorrect_match": "Sáģ­a láģ—i trÚng kháģ›p không chính xÃĄc", @@ -1136,13 +1157,14 @@ "folders": "Thư máģĨc", "folders_feature_description": "Duyáģ‡t áēŖnh và video theo thư máģĨc trÃĒn háģ‡ tháģ‘ng táģ‡p", "forgot_pin_code_question": "QuÃĒn mÃŖ PIN?", - "forward": "Tiáēŋn váģ trưáģ›c", + "forward": "Tiáēŋn táģ›i", "full_path": "Đưáģng dáēĢn đáē§y đáģ§: {path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "Tính năng này táēŖi cÃĄc tài nguyÃĒn bÃĒn ngoài táģĢ Google đáģƒ hoáēĄt đáģ™ng.", "general": "Chung", "geolocation_instruction_location": "NháēĨn vào máģ™t táģ‡p cÃŗ táģa đáģ™ GPS đáģƒ sáģ­ dáģĨng váģ‹ trí cáģ§a nÃŗ hoáēˇc cháģn váģ‹ trí tráģąc tiáēŋp táģĢ báēŖn đáģ“", "get_help": "Nháē­n tráģŖ giÃēp", + "get_people_error": "Láģ—i khi láēĨy thông tin ngưáģi", "get_wifiname_error": "Không tháģƒ láēĨy tÃĒn Wi-Fi. HÃŖy đáēŖm báēŖo báēĄn Ä‘ÃŖ cáēĨp cÃĄc quyáģn cáē§n thiáēŋt và đưáģŖc káēŋt náģ‘i váģ›i máēĄng Wi-Fi", "getting_started": "Báē¯t đáē§u", "go_back": "Quay láēĄi", @@ -1175,6 +1197,7 @@ "hide_named_person": "áē¨n ngưáģi {name}", "hide_password": "áē¨n máē­t kháēŠu", "hide_person": "áē¨n ngưáģi", + "hide_schema": "áē¨n lưáģŖc đáģ“", "hide_text_recognition": "áē¨n nháē­n dáēĄng văn báēŖn", "hide_unnamed_people": "áē¨n nháģ¯ng ngưáģi không tÃĒn", "home_page_add_to_album_conflicts": "ÄÃŖ thÃĒm {added} táģ‡p vào album {album}. {failed} táģ‡p Ä‘ÃŖ cÃŗ sáēĩn trong album.", @@ -1247,6 +1270,8 @@ "ios_debug_info_processing_ran_at": "QuÃĄ trÃŦnh xáģ­ lÃŊ Ä‘ÃŖ cháēĄy vào {dateTime}", "items_count": "{count, plural, one {# máģĨc} other {# máģĨc}}", "jobs": "TÃĄc váģĨ", + "json_editor": "BiÃĒn táē­p JSON", + "json_error": "Láģ—i JSON", "keep": "Giáģ¯", "keep_all": "Giáģ¯ táēĨt cáēŖ", "keep_this_delete_others": "Giáģ¯ táģ‡p này, xÃŗa cÃĄc táģ‡p khÃĄc", @@ -1292,7 +1317,7 @@ "local_asset_cast_failed": "Không tháģƒ chiáēŋu náģ™i dung chưa đưáģŖc táēŖi lÃĒn mÃĄy cháģ§", "local_assets": "Táģ‡p trÃĒn thiáēŋt báģ‹", "local_id": "ID cáģĨc báģ™", - "local_media_summary": "TÃŗm táē¯t phÆ°ÆĄng tiáģ‡n thiáēŋt báģ‹", + "local_media_summary": "Mô táēŖ phÆ°ÆĄng tiáģ‡n trÃĒn thiáēŋt báģ‹", "local_network": "MáēĄng náģ™i báģ™", "local_network_sheet_info": "App sáēŊ káēŋt náģ‘i váģ›i mÃĄy cháģ§ qua URL này khi sáģ­ dáģĨng máēĄng Wi-Fi đưáģŖc cháģ‰ Ä‘áģ‹nh", "location": "Đáģ‹a điáģƒm", @@ -1416,11 +1441,13 @@ "monthly_title_text_date_format": "MMMM y", "more": "ThÃĒm", "move": "Di chuyáģƒn", + "move_down": "Di chuyáģƒn xuáģ‘ng", "move_off_locked_folder": "Di chuyáģƒn ra kháģi thư máģĨc KhÃŗa", "move_to": "Chuyáģƒn đáēŋn", "move_to_lock_folder_action_prompt": "{count} Ä‘ÃŖ đưáģŖc thÃĒm vào thư máģĨc KhÃŗa", "move_to_locked_folder": "Di chuyáģƒn đáēŋn thư máģĨc KhÃŗa", "move_to_locked_folder_confirmation": "áēĸnh và video này sáēŊ báģ‹ xÃŗa kháģi cÃĄc album, cháģ‰ cÃŗ tháģƒ xem đưáģŖc trong thư máģĨc KhÃŗa", + "move_up": "Di chuyáģƒn lÃĒn", "moved_to_archive": "ÄÃŖ di chuyáģƒn {count, plural, one {# táģ‡p} other {# táģ‡p}} đáēŋn lưu tráģ¯", "moved_to_library": "ÄÃŖ di chuyáģƒn {count, plural, one {# táģ‡p} other {# táģ‡p}} đáēŋn thư viáģ‡n", "moved_to_trash": "ÄÃŖ chuyáģƒn vào thÚng rÃĄc", @@ -1430,6 +1457,7 @@ "my_albums": "Album cáģ§a tôi", "name": "TÃĒn", "name_or_nickname": "TÃĒn hoáēˇc biáģ‡t danh", + "name_required": "Báē¯t buáģ™c nháē­p tÃĒn", "navigate": "Điáģu hưáģ›ng", "navigate_to_time": "Xem Tháģi gian", "network_requirement_photos_upload": "DÚng dáģ¯ liáģ‡u di đáģ™ng sao lưu áēŖnh", @@ -1454,6 +1482,7 @@ "next": "Tiáēŋp theo", "next_memory": "Káģˇ niáģ‡m tiáēŋp theo", "no": "Không", + "no_actions_added": "Chưa cÃŗ hành đáģ™ng nào đưáģŖc thÃĒm vào", "no_albums_message": "TáēĄo album đáģƒ sáē¯p xáēŋp áēŖnh và video cáģ§a báēĄn", "no_albums_with_name_yet": "CÃŗ váēģ như báēĄn chưa cÃŗ báēĨt káģŗ album nào váģ›i tÃĒn này.", "no_albums_yet": "CÃŗ váēģ như báēĄn chưa cÃŗ báēĨt káģŗ album nào.", @@ -1463,11 +1492,13 @@ "no_cast_devices_found": "Không tÃŦm tháēĨy thiáēŋt báģ‹ chiáēŋu", "no_checksum_local": "Không cÃŗ checksum kháēŖ dáģĨng - không tháģƒ truy xuáēĨt táģ‡p trÃĒn thiáēŋt báģ‹", "no_checksum_remote": "Không cÃŗ checksum kháēŖ dáģĨng - không tháģƒ truy xuáēĨt táģ‡p trÃĒn mÃĸy", + "no_configuration_needed": "Không cáē§n cáēĨu hÃŦnh", "no_devices": "Không cÃŗ thiáēŋt báģ‹ Ä‘Æ°áģŖc cáēĨp quyáģn", "no_duplicates_found": "Không tÃŦm tháēĨy cÃĄc máģĨc trÚng láēˇp.", "no_exif_info_available": "Không cÃŗ thông tin exif", "no_explore_results_message": "TáēŖi thÃĒm áēŖnh lÃĒn đáģƒ khÃĄm phÃĄ báģ™ sưu táē­p cáģ§a báēĄn.", "no_favorites_message": "ThÃĒm áēŖnh yÃĒu thích đáģƒ nhanh chÃŗng tÃŦm tháēĨy nháģ¯ng báģŠc áēŖnh và video đáēšp nháēĨt cáģ§a báēĄn", + "no_filters_added": "Chưa cÃŗ báģ™ láģc nào đưáģŖc thÃĒm vào", "no_libraries_message": "TáēĄo máģ™t thư viáģ‡n bÃĒn ngoài đáģƒ xem áēŖnh và video cáģ§a báēĄn", "no_local_assets_found": "Không tÃŦm tháēĨy táģ‡p trÃĒn thiáēŋt báģ‹ nào váģ›i checksum này", "no_location_set": "Chưa cÃŗ đáģ‹a điáģƒm đưáģŖc đáēˇt", @@ -1485,7 +1516,6 @@ "not_available": "Thiáēŋu", "not_in_any_album": "Không thuáģ™c album nào", "not_selected": "Không đưáģŖc cháģn", - "note_apply_storage_label_to_previously_uploaded assets": "Lưu ÃŊ: Đáģƒ ÃĄp dáģĨng NhÃŖn lưu tráģ¯ cho cÃĄc áēŖnh Ä‘ÃŖ táēŖi lÃĒn trưáģ›c Ä‘Ãŗ, hÃŖy cháēĄy", "notes": "Lưu ÃŊ", "nothing_here_yet": "Chưa cÃŗ náģ™i dung nào", "notification_permission_dialog_content": "Đáģƒ báē­t thông bÃĄo, chuyáģƒn táģ›i Cài đáēˇt và cháģn cho phÊp.", @@ -1563,6 +1593,7 @@ "people": "Máģi ngưáģi", "people_edits_count": "ÄÃŖ cháģ‰nh sáģ­a {count, plural, one {# ngưáģi} other {# ngưáģi}}", "people_feature_description": "Duyáģ‡t áēŖnh và video đưáģŖc xáēŋp nhÃŗm theo ngưáģi", + "people_selected": "{count, plural, one {# ngưáģi Ä‘ÃŖ cháģn} other {# ngưáģi Ä‘ÃŖ cháģn}}", "people_sidebar_description": "Hiáģƒn tháģ‹ máģĨc Máģi ngưáģi trong thanh bÃĒn", "permanent_deletion_warning": "CáēŖnh bÃĄo xÃŗa vÄŠnh viáģ…n", "permanent_deletion_warning_setting_description": "Hiáģƒn tháģ‹ cáēŖnh bÃĄo khi xÃŗa vÄŠnh viáģ…n áēŖnh", @@ -1587,6 +1618,8 @@ "person_age_years": "{years, plural, other {# năm}} tuáģ•i", "person_birthdate": "Sinh vào {date}", "person_hidden": "{name}{hidden, select, true { (Ä‘ÃŖ áēŠn)} other {}}", + "person_recognized": "Ngưáģi đưáģŖc nháē­n diáģ‡n", + "person_selected": "Ngưáģi Ä‘ÃŖ cháģn", "photo_shared_all_users": "CÃŗ váēģ như báēĄn Ä‘ÃŖ chia sáēģ áēŖnh cáģ§a mÃŦnh váģ›i táēĨt cáēŖ ngưáģi dÚng hoáēˇc báēĄn không cÃŗ ngưáģi dÚng nào đáģƒ chia sáēģ.", "photos": "áēĸnh", "photos_and_videos": "áēĸnh & Video", @@ -1667,10 +1700,12 @@ "purchase_settings_server_activated": "KhÃŗa sáēŖn pháēŠm mÃĄy cháģ§ Ä‘Æ°áģŖc quáēŖn lÃŊ báģŸi quáēŖn tráģ‹ viÃĒn", "query_asset_id": "Truy váēĨn ID táģ‡p", "queue_status": "Xáēŋp hàng {count}/{total}", + "rate_asset": "Asset ÄÃĄnh giÃĄ", "rating": "Xáēŋp háēĄng sao", "rating_clear": "XÃŗa xáēŋp háēĄng", "rating_count": "{count, plural, one {# sao} other {# sao}}", "rating_description": "Hiáģƒn tháģ‹ xáēŋp háēĄng EXIF trong báēŖng thông tin", + "rating_set": "ÄÃĄnh giÃĄ đáēˇt thành {rating, plural, one {# sao} other {# sao}}", "reaction_options": "TÚy cháģn pháēŖn áģŠng", "read_changelog": "Đáģc nháē­t kÃŊ thay đáģ•i", "readonly_mode_disabled": "ÄÃŖ táē¯t cháēŋ đáģ™ cháģ‰-xem", @@ -1681,7 +1716,7 @@ "reassigned_assets_to_new_person": "ÄÃŖ gÃĄn láēĄi {count, plural, one {# áēŖnh} other {# áēŖnh}} cho máģ™t ngưáģi máģ›i", "reassing_hint": "GÃĄn cÃĄc áēŖnh Ä‘ÃŖ cháģn cho máģ™t ngưáģi hiáģ‡n cÃŗ", "recent": "Gáē§n đÃĸy", - "recent-albums": "Album gáē§n đÃĸy", + "recent_albums": "Album gáē§n đÃĸy", "recent_searches": "TÃŦm kiáēŋm gáē§n đÃĸy", "recently_added": "ThÃĒm gáē§n đÃĸy", "recently_added_page_title": "Máģ›i thÃĒm gáē§n đÃĸy", @@ -1700,7 +1735,7 @@ "regenerating_thumbnails": "Đang táēĄo láēĄi áēŖnh thu nháģ", "remote": "TrÃĒn mÃĸy", "remote_assets": "Táģ‡p trÃĒn mÃĸy", - "remote_media_summary": "TÃŗm táē¯t phÆ°ÆĄng tiáģ‡n trÃĒn mÃĸy", + "remote_media_summary": "Mô táēŖ phÆ°ÆĄng tiáģ‡n trÃĒn mÃĄy cháģ§", "remove": "XÃŗa", "remove_assets_album_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa {count, plural, one {# táģ‡p} other {# táģ‡p}} kháģi album?", "remove_assets_shared_link_confirmation": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa {count, plural, one {# táģ‡p} other {# táģ‡p}} kháģi liÃĒn káēŋt chia sáēģ này?", @@ -1820,15 +1855,15 @@ "search_page_view_all_button": "Xem táēĨt cáēŖ", "search_page_your_activity": "HoáēĄt đáģ™ng cáģ§a báēĄn", "search_page_your_map": "BáēŖn đáģ“ cáģ§a báēĄn", - "search_people": "TÃŦm kiáēŋm ngưáģi", + "search_people": "TÃŦm ngưáģi", "search_places": "TÃŦm kiáēŋm đáģ‹a điáģƒm", "search_rating": "TÃŦm kiáēŋm theo xáēŋp háēĄngâ€Ļ", "search_result_page_new_search_hint": "TÃŦm kiáēŋm máģ›i", "search_settings": "TÃŦm kiáēŋm cài đáēˇt", - "search_state": "TÃŦm kiáēŋm táģ‰nh...", + "search_state": "TÃŦm táģ‰nh...", "search_suggestion_list_smart_search_hint_1": "TÃŦm kiáēŋm thông minh đưáģŖc báē­t máēˇc đáģ‹nh, đáģƒ tÃŦm kiáēŋm metadata hÃŖy sáģ­ dáģĨng cÃē phÃĄp ", "search_suggestion_list_smart_search_hint_2": "m:cáģĨm-táģĢ-tÃŦm-kiáēŋm-cáģ§a-báēĄn", - "search_tags": "TÃŦm kiáēŋm tháēģ...", + "search_tags": "TÃŦm tháēģ...", "search_timezone": "TÃŦm kiáēŋm mÃēi giáģ...", "search_type": "Kiáģƒu tÃŦm kiáēŋm", "search_your_photos": "TÃŦm áēŖnh cáģ§a báēĄn", @@ -1836,17 +1871,22 @@ "second": "GiÃĸy", "see_all_people": "Xem táēĨt cáēŖ máģi ngưáģi", "select": "Cháģn", + "select_album": "Cháģn album", "select_album_cover": "Cháģn áēŖnh bÃŦa album", + "select_albums": "Cháģn cÃĄc album", "select_all": "Cháģn táēĨt cáēŖ", "select_all_duplicates": "Cháģn táēĨt cáēŖ cÃĄc báēŖn trÚng láēˇp", "select_all_in": "Cháģn táēĨt cáēŖ trong {group}", "select_avatar_color": "Cháģn màu áēŖnh đáēĄi diáģ‡n", + "select_count": "{count, plural, one {Cháģn #} other {Cháģn #}}", "select_face": "Cháģn khuôn máēˇt", "select_featured_photo": "Cháģn áēŖnh náģ•i báē­t", "select_from_computer": "Cháģn táģĢ mÃĄy tính", "select_keep_all": "Cháģn giáģ¯ táēĨt cáēŖ", "select_library_owner": "Cháģn cháģ§ sáģŸ háģ¯u thư viáģ‡n", "select_new_face": "Cháģn khuôn máēˇt máģ›i", + "select_people": "Cháģn ngưáģi", + "select_person": "Cháģn ngưáģi", "select_person_to_tag": "Cháģn ngưáģi đáģƒ gáē¯n tháēģ", "select_photos": "Cháģn áēŖnh", "select_trash_all": "Cháģn xÃŗa táēĨt cáēŖ", @@ -1882,7 +1922,7 @@ "setting_image_viewer_preview_title": "TáēŖi áēŖnh xem trưáģ›c", "setting_image_viewer_title": "áēĸnh", "setting_languages_apply": "Áp dáģĨng", - "setting_languages_subtitle": "Ngôn ngáģ¯ app", + "setting_languages_subtitle": "Thay đáģ•i ngôn ngáģ¯ áģŠng dáģĨng", "setting_notifications_notify_failures_grace_period": "Thông bÃĄo láģ—i sao lưu náģn: {duration}", "setting_notifications_notify_hours": "{count} giáģ", "setting_notifications_notify_immediately": "ngay láē­p táģŠc", @@ -1982,6 +2022,7 @@ "show_password": "Hiáģƒn tháģ‹ máē­t kháēŠu", "show_person_options": "Hiáģ‡n tÚy cháģn ngưáģi", "show_progress_bar": "Hiáģƒn tháģ‹ thanh tiáēŋn trÃŦnh", + "show_schema": "Hiáģ‡n lưáģŖc đáģ“", "show_search_options": "Hiáģ‡n tÚy cháģn tÃŦm kiáēŋm", "show_shared_links": "Hiáģƒn tháģ‹ cÃĄc liÃĒn káēŋt đưáģŖc chia sáēģ", "show_slideshow_transition": "Hiáģƒn tháģ‹ hiáģ‡u áģŠng chuyáģƒn tiáēŋp", @@ -2109,6 +2150,13 @@ "trash_page_select_assets_btn": "Cháģn táģ‡p", "trash_page_title": "ThÚng rÃĄc ({count})", "trashed_items_will_be_permanently_deleted_after": "CÃĄc máģĨc Ä‘ÃŖ xÃŗa sáēŊ báģ‹ xÃŗa vÄŠnh viáģ…n sau {days, plural, one {# ngày} other {# ngày}}.", + "trigger": "Kích hoáēĄt", + "trigger_asset_uploaded": "Táģ‡p Ä‘ÃŖ đưáģŖc táēŖi lÃĒn", + "trigger_asset_uploaded_description": "Sáģą kiáģ‡n này đưáģŖc kích hoáēĄt khi máģ™t táģ‡p máģ›i đưáģŖc táēŖi lÃĒn", + "trigger_description": "Máģ™t sáģą kiáģ‡n kháģŸi đáē§u workflow", + "trigger_person_recognized": "Ngưáģi đưáģŖc nháē­n diáģ‡n", + "trigger_person_recognized_description": "ĐưáģŖc kích hoáēĄt khi phÃĄt hiáģ‡n tháēĨy máģ™t ngưáģi", + "trigger_type": "Kiáģƒu kích hoáēĄt", "troubleshoot": "Kháē¯c pháģĨc sáģą cáģ‘", "type": "LoáēĄi", "unable_to_change_pin_code": "Thay đáģ•i mÃŖ PIN tháēĨt báēĄi", @@ -2118,7 +2166,7 @@ "unarchive_action_prompt": "{count} Ä‘ÃŖ báģ kháģi Lưu tráģ¯", "unarchived_count": "{count, plural, other {ÄÃŖ báģ lưu tráģ¯ # máģĨc}}", "undo": "Hoàn tÃĄc", - "unfavorite": "Báģ yÃĒu thích", + "unfavorite": "Báģ thích", "unfavorite_action_prompt": "{count} Ä‘ÃŖ báģ kháģi ÄÃŖ thích", "unhide_person": "Hiáģ‡n ngưáģi", "unknown": "Không xÃĄc đáģ‹nh", @@ -2139,13 +2187,14 @@ "unstack": "Háģ§y xáēŋp nhÃŗm", "unstack_action_prompt": "{count} Ä‘ÃŖ báģ nhÃŗm", "unstacked_assets_count": "ÄÃŖ háģ§y xáēŋp nhÃŗm {count, plural, one {# táģ‡p} other {# táģ‡p}}", + "unsupported_field_type": "LoáēĄi trưáģng không đưáģŖc háģ— tráģŖ", "untagged": "Chưa gáē¯n tháēģ", + "untitled_workflow": "Workflow chưa đáēˇt tÃĒn", "up_next": "Tiáēŋp theo", "update_location_action_prompt": "Cáē­p nháē­t đáģ‹a điáģƒm cáģ§a {count} táģ‡p Ä‘ÃŖ cháģn váģ›i:", "updated_at": "ÄÃŖ cáē­p nháē­t", "updated_password": "ÄÃŖ cáē­p nháē­t máē­t kháēŠu", "upload": "TáēŖi lÃĒn", - "upload_action_prompt": "{count} cháģ Ä‘áģƒ táēŖi lÃĒn", "upload_concurrency": "TáēŖi lÃĒn đáģ“ng tháģi", "upload_details": "Chi tiáēŋt táēŖi lÃĒn", "upload_dialog_info": "BáēĄn cÃŗ muáģ‘n sao lưu nháģ¯ng táģ‡p Ä‘ÃŖ cháģn lÃĒn mÃĄy cháģ§ không?", @@ -2185,6 +2234,7 @@ "utilities": "Tiáģ‡n ích", "validate": "XÃĄc minh", "validate_endpoint_error": "Vui lÃ˛ng nháē­p URL háģŖp láģ‡", + "validation_error": "Láģ—i xÃĄc tháģąc", "variables": "CÃĄc tham sáģ‘", "version": "PhiÃĒn báēŖn", "version_announcement_closing": "BáēĄn cáģ§a báēĄn, Alex", @@ -2196,6 +2246,7 @@ "video_hover_setting_description": "PhÃĄt đoáēĄn video xem trưáģ›c khi di chuáģ™t qua máģĨc. Ngay cáēŖ khi táē¯t cháģŠc năng này, váēĢn cÃŗ tháģƒ báē¯t đáē§u phÃĄt video báēąng cÃĄch di chuáģ™t qua biáģƒu tưáģŖng phÃĄt.", "videos": "Video", "videos_count": "{count, plural, one {# Video} other {# Video}}", + "videos_only": "Cháģ‰ video", "view": "Xem", "view_album": "Xem Album", "view_all": "Xem táēĨt cáēŖ", @@ -2213,9 +2264,11 @@ "view_stack": "Xem nhÃŗm áēŖnh", "view_user": "Xem Ngưáģi dÚng", "viewer_remove_from_stack": "XÃŗa kháģi nhÃŗm", - "viewer_stack_use_as_main_asset": "Đáēˇt làm báģ™ táģ‡p chính", + "viewer_stack_use_as_main_asset": "Đáēˇt làm áēŖnh náģ•i báē­t", "viewer_unstack": "Háģ§y xáēŋp nhÃŗm", "visibility_changed": "ÄÃŖ thay đáģ•i tráēĄng thÃĄi hiáģƒn tháģ‹ cho {count, plural, one {# ngưáģi} other {# ngưáģi}}", + "visual": "Tráģąc quan", + "visual_builder": "TáēĄo tráģąc quan", "waiting": "Đang cháģ", "waiting_count": "Đang cháģ: {count}", "warning": "CáēŖnh bÃĄo", @@ -2224,13 +2277,26 @@ "welcome_to_immich": "Chào máģĢng đáēŋn váģ›i Immich", "width": "Chiáģu ráģ™ng", "wifi_name": "TÃĒn Wi-Fi", - "workflow": "Workflow", + "workflow_delete_prompt": "BáēĄn cÃŗ cháē¯c muáģ‘n xÃŗa luáģ“ng công viáģ‡c này?", + "workflow_deleted": "ÄÃŖ xÃŗa luáģ“ng công viáģ‡c", + "workflow_description": "Mô táēŖ luáģ“ng công viáģ‡c", + "workflow_info": "Thông tin luáģ“ng công viáģ‡c", + "workflow_json": "JSON cáģ§a luáģ“ng công viáģ‡c", + "workflow_json_help": "Cháģ‰nh sáģ­a cáēĨu hÃŦnh luáģ“ng công viáģ‡c áģŸ Ä‘áģ‹nh dáēĄng JSON. CÃĄc thay đáģ•i sáēŊ đưáģŖc đáģ“ng báģ™ hÃŗa váģ›i trÃŦnh táēĄo tráģąc quan.", + "workflow_name": "TÃĒn luáģ“ng công viáģ‡c", + "workflow_navigation_prompt": "BáēĄn cÃŗ cháē¯c muáģ‘n ráģi đi mà không lưu láēĄi cÃĄc thay đáģ•i cáģ§a mÃŦnh?", + "workflow_summary": "Mô táēŖ luáģ“ng công viáģ‡c", + "workflow_update_success": "ÄÃŖ cáē­p nháē­t luáģ“ng công viáģ‡c thành công", + "workflow_updated": "ÄÃŖ cáē­p nháē­t Luáģ“ng công viáģ‡c", + "workflows": "Luáģ“ng công viáģ‡c", + "workflows_help_text": "Luáģ“ng công viáģ‡c táģą Ä‘áģ™ng hÃŗa cÃĄc hành đáģ™ng trÃĒn táē­p tin cáģ§a báēĄn dáģąa trÃĒn cÃĄc trÃŦnh kích hoáēĄt và báģ™ láģc", "wrong_pin_code": "MÃŖ PIN không đÃēng", "year": "Năm", "years_ago": "{years, plural, one {# năm} other {# năm}} trưáģ›c", "yes": "Đáģ“ng ÃŊ", "you_dont_have_any_shared_links": "BáēĄn không cÃŗ liÃĒn káēŋt chia sáēģ nào", "your_wifi_name": "TÃĒn Wi-Fi cáģ§a báēĄn", + "zero_to_clear_rating": "nháēĨn 0 đáģƒ xÃŗa Ä‘ÃĄnh giÃĄ áēŖnh", "zoom_image": "Thu phÃŗng áēŖnh", - "zoom_to_bounds": "Thu phÃŗng đáēŋn giáģ›i háēĄn" + "zoom_to_bounds": "Thu phÃŗng váģĢa khung" } diff --git a/i18n/yue_Hant.json b/i18n/yue_Hant.json index e88c5da1b0..372816da2a 100644 --- a/i18n/yue_Hant.json +++ b/i18n/yue_Hant.json @@ -2,6 +2,98 @@ "about": "關æ–ŧ", "account": "å¸ŗč™Ÿ", "account_settings": "å¸ŗč™Ÿč¨­åŽš", + "acknowledge": "äē†č§Ŗ", "action": "動äŊœ", - "week": "星期" + "action_common_update": "更新", + "action_description": "é‡å°į¯Šé¸åžŒå˜…čŗ‡æēåŸˇčĄŒå˜…一įŗģ列動äŊœ", + "actions": "動äŊœ", + "active": "æ­Ŗåœ¨č™•į†", + "active_count": "æ­Ŗåœ¨č™•į†īŧš{count}", + "activity": "æ´ģ動", + "activity_changed": "æ´ģ動厞{enabled, select, true {啟動} other {停æ­ĸ}}", + "add": "加", + "add_a_description": "加一個描čŋ°", + "add_a_location": "加一個äŊįŊŽ", + "add_a_name": "加一個姓名", + "add_a_title": "åŠ ä¸€å€‹æ¨™éĄŒ", + "add_action": "加動äŊœ", + "add_action_description": "éģžæ“ŠäģĨ加動äŊœ", + "add_assets": "åŠ čŗ‡æē", + "add_birthday": "åŠ ä¸€å€‹į”Ÿæ—Ĩ", + "add_endpoint": "加į̝éģž", + "add_exclusion_pattern": "加å…Ĩį¯Šé¸æĸäģļ", + "add_filter": "加過æŋžå™¨", + "add_filter_description": "éģžæ“ŠäģĨ加一個過æŋžæĸäģļ", + "add_location": "加äŊįŊŽ", + "add_more_users": "åŠ æ›´å¤šį”¨æˆļ", + "add_partner": "加äŧ™äŧ´", + "add_path": "åŠ čˇ¯åž‘", + "add_photos": "加多åŧĩᛏቇ", + "add_tag": "åŠ æ¨™įą¤", + "add_to": "åŠ č‡ŗâ€Ļ", + "add_to_album": "åŠ č‡ŗį›¸į°ŋ", + "add_to_album_bottom_sheet_added": "åˇ˛åŠ č‡ŗ{album}", + "add_to_album_bottom_sheet_already_exists": "厞圍 {album} 中", + "add_to_album_bottom_sheet_some_local_assets": "į„Ąæŗ•åŠ éƒ¨åˆ†æœŦæŠŸčŗ‡æēč‡ŗį›¸į°ŋ", + "add_to_album_toggle": "選擇{album}ᛏį°ŋ", + "add_to_albums": "åŠ č‡ŗį›¸į°ŋ", + "add_to_albums_count": "加 ({count}) å€‹é …į›Žč‡ŗį›¸į°ŋ", + "add_to_bottom_bar": "åŠ č‡ŗ", + "add_to_shared_album": "åŠ č‡ŗå…ąäēĢᛏį°ŋ", + "add_url": "加įļ˛å€", + "add_workflow_step": "åĸžåŠ åˇĨäŊœæ­Ĩ驟", + "added_to_favorites": "åˇ˛åŠ č‡ŗæœ€æ„›", + "added_to_favorites_count": "厞加{count, number} å€‹é …į›Žč‡ŗæœ€æ„›", + "admin": { + "admin_user": "įŽĄį†å“Ąį”¨æˆļ", + "authentication_settings": "éŠ—č­‰č¨­åŽš", + "authentication_settings_description": "įŽĄį†å¯†įĸŧ、OAuth 同å…ļäģ–éŠ—č­‰č¨­åŽš", + "background_task_job": "čƒŒæ™¯æ“äŊœ", + "backup_database": "åģēįĢ‹čŗ‡æ–™åēĢ備äģŊ", + "backup_database_enable_description": "å•Ÿį”¨čŗ‡æ–™åēĢ備äģŊ", + "backup_keep_last_amount": "äŋį•™å…ˆå‰å‚™äģŊ嘅數量", + "backup_onboarding_1_description": "äŋ‚雲įĢ¯æˆ–č€…å…ļäģ–å¯ĻéĢ”åœ°æ–šåģēįĢ‹å˜…å‚™äģŊ副æœŦ。", + "backup_onboarding_2_description": "å„˛å­˜äŋ‚å””åŒčŖįŊŽå˜…æœŦ地副æœŦ。包åĢä¸ģčĻå˜…æĒ”æĄˆåŒåŸ‹å–ēæœŦ抟嘅備äģŊ。", + "backup_onboarding_3_description": "čŗ‡æ–™åŒ…åĢ原始文äģļīŧŒį¸Ŋå…ąå‚™äģŊ嘅æŦĄæ•¸ã€‚å‘ĸ個包æ‹Ŧ1äģŊį•°åœ°å˜…å‚™äģŊ同埋2äģŊæœŦ抟副æœŦ。", + "backup_onboarding_footer": "有關å…ļäģ–Immich備äģŊå˜…čŗ‡æ–™īŧŒčĢ‹åƒč€ƒã€‚", + "backup_onboarding_parts_title": "一個3-2-1備äģŊ包æ‹Ŧīŧš", + "backup_onboarding_title": "備äģŊ", + "backup_settings": "čŗ‡æ–™åēĢ備äģŊå˜…č¨­åŽš", + "backup_settings_description": "įŽĄį†čŗ‡æ–™åēĢ備äģŊå˜…č¨­åŽšã€‚", + "cleared_jobs": "åˇ˛æ¸…é™¤{job}嘅åˇĨäŊœ", + "config_set_by_file": "䞝åŽļå˜…č¨­åŽšäŋ‚į”ąč¨­åޚæĒ”æĄˆč¨‚įĢ‹å˜…", + "confirm_delete_library": "äŊ äŋ‚å””äŋ‚įĸē厚čĻéŸé™¤{library}å˜…å¤–éƒ¨čŗ‡æ–™åēĢīŧŸ", + "confirm_delete_library_assets": "äŊ äŋ‚å””äŋ‚įĸē厚čĻéŸé™¤å‘ĸ個外部åĒ’éĢ”åēĢīŧŸImmich將會鏟除 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}īŧŒä¸”į„Ąæŗ•æ’¤å›žã€‚æĒ”æĄˆäģį„ļ會čĸĢäŋį•™å–ēįĄŦįĸŸå…Ĩéĸ。", + "confirm_email_below": "čĢ‹å–ēåē•下čŧ¸å…Ĩ{email}厞įĸēčĒ", + "confirm_reprocess_all_faces": "äŊ įĸē厚čĻé‡æ–°č™•į†æ‰€æœ‰å˜…éĸ螌īŧŸå‘ĸå€‹éŽį¨‹äēĻéƒŊæœƒæ¸…é™¤åˇ˛å‘Ŋ名嘅äēēį‰Šã€‚" + }, + "main_menu": "ä¸ģ選喎", + "maintenance_action_restore": "é‚„åŽŸįˇŠæ•¸æ“šåēĢ", + "onboarding_user_welcome_description": "æˆ‘å“‹č€ŒåŽļ開始喇īŧ", + "onboarding_welcome_user": "æ­ĄčŋŽīŧŒ{user}", + "online": "厞䏊᎚", + "only_favorites": "åĒéĄ¯į¤ē最愛", + "open": "開", + "open_in_map_view": "į”¨åœ°åœ–é–‹", + "open_in_openstreetmap": "ᔍ OpenStreetMap 開", + "open_the_search_filters": "開搜尋過æŋžå™¨", + "options": "選項", + "or": "æˆ–č€…", + "organize_into_albums": "åŸˇæˆį›¸į°ŋ", + "setting_notifications_notify_seconds": "{count} į§’", + "warning": "č­Ļ告", + "week": "星期", + "welcome": "æ­ĄčŋŽ", + "welcome_to_immich": "æ­ĄčŋŽäŊŋᔍ Immich", + "width": "å¯Ŧ", + "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": "äŊ å˜… Wi-Fi åį¨ą", + "zero_to_clear_rating": "按0äģĨæ¸…é™¤čŗ‡æēčŠ•į´š", + "zoom_image": "į¸Žæ”žį›¸į‰‡", + "zoom_to_bounds": "į¸Žæ”žč‡ŗé‚Šį•Œ" } diff --git a/i18n/zh_SIMPLIFIED.json b/i18n/zh_Hans.json similarity index 66% rename from i18n/zh_SIMPLIFIED.json rename to i18n/zh_Hans.json index 6e16116d32..2e7960bffd 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_Hans.json @@ -1,496 +1,523 @@ { "about": "å…ŗäēŽ", - "account": "č´Ļæˆˇ", - "account_settings": "č´ĻæˆˇčŽžįŊŽ", + "account": "č´Ļåˇ", + "account_settings": "č´ĻåˇčŽžįŊŽ", "acknowledge": "厞įŸĨ悉", "action": "操äŊœ", "action_common_update": "更新", + "action_description": "å¯šį­›é€‰å‡ēįš„čĩ„äē§æ‰§čĄŒįš„一į섿“äŊœ", "actions": "操äŊœ", - "active": "æ­Ŗåœ¨å¤„į†", + "active": "čŋ›čĄŒä¸­", "active_count": "æ´ģ动: {count}", "activity": "æ´ģ动", - "activity_changed": "æ´ģ劍厞{enabled, select, true {吝ᔍ} other {åœį”¨}}", + "activity_changed": "æ´ģ动įŠļ态{enabled, select, true {厞吝ᔍ} other {厞įρᔍ}}", "add": "æˇģ加", "add_a_description": "æˇģ加描čŋ°", "add_a_location": "æˇģ加äŊįŊŽ", - "add_a_name": "æˇģåŠ åį§°", + "add_a_name": "æˇģ加äēē名", "add_a_title": "æˇģ加标éĸ˜", + "add_action": "æˇģ加操äŊœ", + "add_action_description": "į‚šå‡ģäģĨæˇģ加čĻæ‰§čĄŒįš„æ“äŊœ", + "add_assets": "æˇģ加čĩ„äē§", "add_birthday": "æˇģåŠ į”Ÿæ—Ĩ", - "add_endpoint": "æˇģåŠ æœåŠĄå™¨ URL", + "add_endpoint": "æˇģ加įĢ¯į‚š", "add_exclusion_pattern": "æˇģåŠ æŽ’é™¤č§„åˆ™", - "add_location": "æˇģåŠ åœ°į‚š", + "add_filter": "æˇģåŠ į­›é€‰æĄäģļ", + "add_filter_description": "į‚šå‡ģæˇģåŠ į­›é€‰æĄäģļ", + "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}”", - "add_to_album_bottom_sheet_already_exists": "åˇ˛åœ¨į›¸å†Œâ€œ {album} ” 中", - "add_to_album_bottom_sheet_some_local_assets": "某ä盿œŦ地čĩ„äē§æ— æŗ•æˇģåŠ åˆ°į›¸å†Œ", - "add_to_album_toggle": "é€‰æ‹Šį›¸å†Œ {album}", + "add_to_album_bottom_sheet_added": "厞æˇģåŠ č‡ŗ {album}", + "add_to_album_bottom_sheet_already_exists": "厞圍 {album} 中", + "add_to_album_bottom_sheet_some_local_assets": "部分æœŦ地čĩ„äē§æ— æŗ•æˇģåŠ åˆ°į›¸å†Œ", + "add_to_album_toggle": "切æĸ {album} įš„é€‰ä¸­įŠļ态", "add_to_albums": "æˇģåŠ åˆ°į›¸å†Œ", - "add_to_albums_count": "æˇģåŠ åˆ°į›¸å†Œīŧˆ{count}ä¸Ēīŧ‰", + "add_to_albums_count": "æˇģåŠ åˆ°į›¸å†Œ ({count})", "add_to_bottom_bar": "æˇģ加到", "add_to_shared_album": "æˇģåŠ åˆ°å…ąäēĢį›¸å†Œ", - "add_upload_to_stack": "上äŧ éĄšį›Žč‡ŗå †å ", + "add_upload_to_stack": "æˇģ加上äŧ č‡ŗå †æ ˆ", "add_url": "æˇģ加 URL", - "added_to_archive": "æˇģ加到åŊ’æĄŖ", - "added_to_favorites": "æˇģ加到æ”ļ藏", - "added_to_favorites_count": "æˇģ加{count, number}éĄšåˆ°æ”ļ藏", + "add_workflow_step": "æˇģ加åˇĨäŊœæĩæ­ĨéǤ", + "added_to_archive": "æˇģåŠ č‡ŗå­˜æĄŖ", + "added_to_favorites": "厞æˇģ加到æ”ļ藏", + "added_to_favorites_count": "厞将 {count, number} 饚æˇģ加到æ”ļ藏", "admin": { - "add_exclusion_pattern_description": "æˇģåŠ æŽ’é™¤č§„åˆ™ã€‚æ”¯æŒäŊŋᔍ *、** 和 ? 通配įŦĻ。比åĻ‚čρåŋŊį•ĨäģģäŊ•名ä¸ē “Raw” įš„æ–‡äģļå¤šä¸­įš„æ‰€æœ‰æ–‡äģļīŧŒč¯ˇäŊŋᔍ “**/Raw/**”īŧ›čρåŋŊį•Ĩ所有äģĨ “.tif” įģ“å°žįš„æ–‡äģļīŧŒč¯ˇäŊŋᔍ “**/*.tif”īŧ›čρåŋŊį•Ĩįģå¯ščˇ¯åž„īŧŒč¯ˇäŊŋᔍ “/path/to/ignore/**”。", + "add_exclusion_pattern_description": "æˇģåŠ æŽ’é™¤æ¨Ąåŧīŧˆæ”¯æŒ  * ,  ** ,  ?  通配įŦĻīŧ‰ã€‚äž‹åĻ‚īŧšåŋŊį•Ĩ \"Raw\" į›ŽåŊ•蝎ᔍ  \"**/Raw/**\" īŧ›åŋŊį•Ĩ \".tif\" 文äģļ蝎ᔍ  \"**/*.tif\" īŧ›åŋŊį•Ĩįģå¯ščˇ¯åž„蝎ᔍ  \"/path/to/ignore/**\" 。", "admin_user": "įŽĄį†å‘˜į”¨æˆˇ", - "asset_offline_description": "įŖį›˜ä¸Šåˇ˛æ‰žä¸åˆ°æ­¤å¤–éƒ¨åē“éĄšį›ŽīŧŒåˇ˛å°†å…ļį§ģč‡ŗå›žæ”ļįĢ™ã€‚åĻ‚æžœæ–‡äģļ厞圍åē“中į§ģ动īŧŒč¯ˇæŖ€æŸĨæ—ļ间įēŋ中是åĻ有寚åē”éĄšį›Žã€‚čρæĸå¤æ­¤éĄšį›ŽīŧŒč¯ˇįĄŽäŋ Immich 可äģĨčŽŋ问äģĨ下文äģļčˇ¯åž„åšļæ‰§čĄŒâ€œæ‰Ģ描åē“”äģģåŠĄã€‚", + "asset_offline_description": "æœĒ扞到č¯Ĩ外部čĩ„äē§å瓿–‡äģļīŧŒåˇ˛å°†å…ļį§ģč‡ŗå›žæ”ļįĢ™ã€‚åĻ‚æžœæ–‡äģ￘¯åœ¨åē“内čĸĢį§ģ动īŧŒč¯ˇåœ¨æ—ļ间įēŋ中æŸĨ扞寚åē”įš„æ–°čĩ„äē§ã€‚åĻ‚éœ€æĸ复此čĩ„äē§īŧŒč¯ˇįĄŽäŋ Immich 可čŽŋé—Žä¸‹æ–šįš„æ–‡äģļčˇ¯åž„īŧŒåšļ重新æ‰Ģ描č¯Ĩčĩ„äē§åē“。", "authentication_settings": "čŽ¤č¯čŽžįŊŽ", "authentication_settings_description": "įŽĄį†å¯†į ã€OAuth 和å…ļåŽƒčŽ¤č¯čŽžįŊŽ", - "authentication_settings_disable_all": "įĄŽåŽščρįĻį”¨æ‰€æœ‰įš„į™ģåŊ•æ–šåŧīŧŸč¯Ĩ操äŊœå°†åŽŒå…¨įρæ­ĸį™ģåŊ•。", - "authentication_settings_reenable": "åĻ‚éœ€å†æŦĄå¯į”¨īŧŒäŊŋᔍ æœåŠĄå™¨æŒ‡äģ¤ã€‚", + "authentication_settings_disable_all": "æ‚¨įĄŽåŽščρįĻį”¨æ‰€æœ‰į™ģåŊ•æ–šåŧå—īŧŸį™ģåŊ•功čƒŊå°†åŽŒå…¨å¤ąæ•ˆã€‚", + "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å¯į”¨īŧŒč¯ˇäŊŋᔍ æœåŠĄå™¨å‘Ŋäģ¤ã€‚", "background_task_job": "后台äģģåŠĄ", "backup_database": "创åģēæ•°æŽåē“备äģŊ", - "backup_database_enable_description": "å¯į”¨æ•°æŽåē“å¯ŧå‡ē备äģŊ", - "backup_keep_last_amount": "čρäŋį•™įš„åŽ†å˛å¯ŧå‡ē数量", - "backup_onboarding_1_description": "äē‘įĢ¯æˆ–å…ļäģ–į‰Šį†äŊįŊŽįš„åŧ‚地副æœŦ。", - "backup_onboarding_2_description": "åœ¨ä¸åŒčŽžå¤‡ä¸Šįš„æœŦ地副æœŦ。čŋ™åŒ…æ‹Ŧä¸ģ文äģļ及å…ļæœŦ地备äģŊ。", - "backup_onboarding_3_description": "æ‚¨įš„æ•°æŽīŧˆåŒ…æ‹Ŧ原始文äģļīŧ‰įš„æ€ģ副æœŦ数。å…ļ中包æ‹Ŧ 1 äģŊåŧ‚地副æœŦ和 2 äģŊæœŦ地副æœŦ。", - "backup_onboarding_description": "åģēčŽŽé‡‡į”¨3-2-1备äģŊį­–į•ĨæĨäŋæŠ¤æ‚¨įš„æ•°æŽã€‚您åē”č¯Ĩäŋį•™åˇ˛ä¸Šäŧ į…§į‰‡/视éĸ‘äģĨ及 Immich 数捎åē“įš„å‰¯æœŦīŧŒäģĨčŽˇåž—å…¨éĸįš„å¤‡äģŊč§Ŗå†ŗæ–šæĄˆã€‚", - "backup_onboarding_footer": "æœ‰å…ŗå¤‡äģŊ Immich įš„æ›´å¤šäŋĄæ¯īŧŒč¯ˇå‚é˜…æ–‡æĄŖã€‚", - "backup_onboarding_parts_title": "3-2-1 备äģŊ包æ‹Ŧīŧš", + "backup_database_enable_description": "å¯į”¨æ•°æŽåē“备äģŊ", + "backup_keep_last_amount": "äŋį•™įš„åŽ†å˛å¤‡äģŊ数量", + "backup_onboarding_1_description": "åŧ‚地备äģŊīŧŒäž‹åĻ‚å­˜å‚¨åœ¨äē‘įĢ¯æˆ–åĻ一ä¸Ēį‰Šį†äŊįŊŽã€‚", + "backup_onboarding_2_description": "æœŦåœ°å¤ščŽžå¤‡å‰¯æœŦã€‚åŗåœ¨ä¸åŒčŽžå¤‡ä¸Šäŋå­˜ä¸ģ文äģļ及å…ļæœŦ地备äģŊ。", + "backup_onboarding_3_description": "æ•°æŽįš„æ€ģ副æœŦ数īŧŒåŒ…åĢ原始文äģļ。䞋åĻ‚īŧš1 äģŊåŧ‚地备äģŊ和 2 äģŊæœŦ地副æœŦ。", + "backup_onboarding_description": "åģēčŽŽé‡‡į”¨ 3-2-1 备äģŊį­–į•Ĩ æĨäŋæŠ¤æ‚¨įš„æ•°æŽã€‚ä¸ēäē†åŽžįŽ°å…¨éĸįš„å¤‡äģŊæ–šæĄˆīŧŒæ‚¨åē”åŊ“äŋå­˜ä¸Šäŧ įš„ᅧቇ/视éĸ‘副æœŦäģĨ及 Immich 数捎åē“。", + "backup_onboarding_footer": "æœ‰å…ŗå¤‡äģŊ Immich įš„æ›´å¤šäŋĄæ¯īŧŒč¯ˇå‚阅 æ–‡æĄŖã€‚", + "backup_onboarding_parts_title": "3-2-1 备äģŊį­–į•Ĩ包æ‹Ŧīŧš", "backup_onboarding_title": "备äģŊ", - "backup_settings": "数捎åē“å¯ŧå‡ē莞įŊŽ", + "backup_settings": "数捎åē“备äģŊ莞įŊŽ", "backup_settings_description": "įŽĄį†æ•°æŽåē“备äģŊ莞įŊŽã€‚", - "cleared_jobs": "åˇ˛æ¸…į†äģģåŠĄīŧš{job}", - "config_set_by_file": "åŊ“前配įŊŽåˇ˛é€ščŋ‡é…įŊŽæ–‡äģļ莞įŊŽ", - "confirm_delete_library": "是åĻįĄŽåŽšåˆ é™¤å›žåē““{library}”īŧŸ", - "confirm_delete_library_assets": "įĄŽåŽščĻåˆ é™¤č¯Ĩ回åē“吗īŧŸčŋ™å°†åˆ é™¤æ‰€æœ‰åŒ…åĢ在 Immich ä¸­įš„{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}īŧŒä¸”æ— æŗ•æ’¤é”€ã€‚äŊ†æ–‡äģļäģå°†äŋį•™åœ¨įŖį›˜ä¸­ã€‚", - "confirm_email_below": "č¯ˇčž“å…Ĩ“{email}”äģĨčŋ›čĄŒįĄŽčޤ", - "confirm_reprocess_all_faces": "įĄŽåŽščĻå¯šå…¨éƒ¨į…§į‰‡é‡æ–°čŋ›čĄŒéĸéƒ¨č¯†åˆĢ吗īŧŸčŋ™å°†åŒæ—ᅬ…é™¤æ‰€æœ‰åˇ˛å‘Ŋ名äēēį‰Šã€‚", - "confirm_user_password_reset": "įĄŽåŽščĻé‡įŊŽį”¨æˆˇâ€œ{user}â€įš„å¯†į å—īŧŸ", - "confirm_user_pin_code_reset": "įĄŽåŽščĻé‡įŊŽį”¨æˆˇâ€œ{user}â€įš„PIN᠁吗īŧŸ", - "copy_config_to_clipboard_description": "将åŊ“前įŗģįģŸé…įŊŽäŊœä¸ēJSONå¯ščąĄå¤åˆļ到å‰Ēč´´æŋ", + "cleared_jobs": "åˇ˛æ¸…é™¤ {job} įš„äģģåŠĄ", + "config_set_by_file": "åŊ“前配įŊŽį”ąé…įŊŽæ–‡äģļčŽžåŽš", + "confirm_delete_library": "įĄŽåŽščĻåˆ é™¤čĩ„äē§åē“ \"{library}\" 吗īŧŸ", + "confirm_delete_library_assets": "įĄŽåŽščĻåˆ é™¤æ­¤čĩ„äē§åē“吗īŧŸæ­¤æ“äŊœå°†äģŽ Immich 中删除 {count, plural, one {# ä¸Ē兺联čĩ„äē§} other {全部 # ä¸Ē兺联čĩ„äē§}}īŧŒä¸”æ— æŗ•æ’¤é”€ã€‚æŗ¨æ„īŧšæ–‡äģļäģå°†äŋį•™åœ¨įŖį›˜ä¸Šã€‚", + "confirm_email_below": "ä¸ēįĄŽčŽ¤æ“äŊœīŧŒč¯ˇåœ¨ä¸‹æ–ščž“å…Ĩ \"{email}\"", + "confirm_reprocess_all_faces": "įĄŽåŽščĻé‡æ–°å¤„į†æ‰€æœ‰äēē脏吗īŧŸæ­¤æ“äŊœå°†æ¸…除厞å‘Ŋåįš„äēēį‰Šã€‚", + "confirm_user_password_reset": "įĄŽåŽščĻé‡įŊŽ {user} įš„å¯†į å—īŧŸ", + "confirm_user_pin_code_reset": "įĄŽåŽščĻé‡įŊŽ {user} įš„ PIN ᠁吗īŧŸ", + "copy_config_to_clipboard_description": "将åŊ“前įŗģįģŸé…įŊŽäŊœä¸ē JSON å¯ščąĄå¤åˆļ到å‰Ēč´´æŋ", "create_job": "创åģēäģģåŠĄ", "cron_expression": "Cron 襨螞åŧ", - "cron_expression_description": "äŊŋᔍ Cron æ ŧåŧčŽžįŊŽæ‰Ģ描间隔。更多č¯Ļįģ†äŋĄæ¯č¯ˇå‚阅 Crontab Guru", + "cron_expression_description": "äŊŋᔍ Cron æ ŧåŧčŽžįŊŽæ‰Ģ描间隔。更多äŋĄæ¯č¯ˇå‚č€ƒ Crontab Guru į­‰įŊ‘įĢ™", "cron_expression_presets": "Cron 襨螞åŧéĸ„莞", "disable_login": "įρᔍį™ģåŊ•", - "duplicate_detection_job_description": "å¯šį…§į‰‡čŋ›čĄŒæœē器å­Ļäš å¤„į†æĨæŖ€æĩ‹į›¸äŧŧéĄšį›ŽīŧŒäžčĩ–äēŽæ™ēčƒŊ搜į´ĸ", - "exclusion_pattern_description": "æŽ’é™¤č§„åˆ™å…čŽ¸åœ¨æ‰Ģ描回å瓿—ļåŋŊį•Ĩ文äģļ和文äģļ多。åĻ‚æžœæœ‰åŒ…åĢä¸æƒŗå¯ŧå…Ĩįš„æ–‡äģļįš„æ–‡äģļ多īŧŒäž‹åĻ‚ RAW 文äģļīŧŒæŽ’é™¤č§„åˆ™å°†éžå¸¸æœ‰į”¨ã€‚", - "export_config_as_json_description": "将åŊ“前įŗģįģŸé…įŊŽä¸‹čŊŊä¸ēJSON文äģļ", - "external_libraries_page_description": "įŽĄį†å¤–éƒ¨åē“éĄĩéĸ", + "duplicate_detection_job_description": "čŋčĄŒæœē器å­Ļäš æĨæŖ€æĩ‹į›¸äŧŧ回像īŧŒæ­¤åŠŸčƒŊ䞝čĩ–äēŽæ™ēčƒŊ搜į´ĸ", + "exclusion_pattern_description": "æŽ’é™¤č§„åˆ™å…čŽ¸æ‚¨åœ¨æ‰Ģ描čĩ„äē§å瓿—ļåŋŊį•Ĩį‰šåŽšįš„æ–‡äģļ和文äģļ多。åĻ‚æžœæ‚¨æœ‰æŸäē›åŒ…åĢ不希望å¯ŧå…Ĩįš„æ–‡äģļīŧˆäž‹åĻ‚ RAW æ ŧåŧæ–‡äģļīŧ‰įš„æ–‡äģļ多īŧŒæ­¤åŠŸčƒŊå°†éžå¸¸æœ‰į”¨ã€‚", + "export_config_as_json_description": "将åŊ“前įŗģįģŸé…įŊŽä¸‹čŊŊä¸ē JSON 文äģļ", + "external_libraries_page_description": "įŽĄį†å¤–éƒ¨čĩ„äē§åē“", "face_detection": "äēēč„¸æŖ€æĩ‹", - "face_detection_description": "äŊŋᔍæœē器å­Ļäš æŖ€æĩ‹éĄšį›Žä¸­įš„äēē脸īŧˆč§†éĸ‘åĒæŖ€æĩ‹å…ļįŧŠį•Ĩå›žä¸­įš„äēē脸īŧ‰ã€‚é€‰æ‹Šâ€œåˆˇæ–°â€å°†äŧšīŧˆé‡æ–°īŧ‰å¤„į†æ‰€æœ‰éĄšį›Žã€‚é€‰æ‹Šâ€œé‡įŊŽâ€čŋ˜äŧšæ¸…é™¤æ‰€æœ‰åŊ“前éĸ部数捎。选拊“įŧēå¤ąâ€å°†å°šæœĒå¤„į†įš„éĄšį›Žčŋ›čĄŒæŽ’é˜Ÿå¤„į†ã€‚äēēč„¸æŖ€æĩ‹åŽŒæˆåŽīŧŒæŖ€æĩ‹åˆ°įš„äēēč„¸å°†æŽ’é˜Ÿčŋ›čĄŒéĸéƒ¨č¯†åˆĢīŧŒå°†åރäģŦ分įģ„åˆ°įŽ°æœ‰įš„æˆ–æ–°įš„äēēį‰Šä¸­ã€‚", - "facial_recognition_job_description": "å°†æŖ€æĩ‹åˆ°įš„äēēč„¸æŒ‰į…§äēēį‰Šåˆ†įģ„。čŋ™ä¸€æ­Ĩ将在äēēč„¸æŖ€æĩ‹åŽŒæˆåŽæ‰§čĄŒã€‚é€‰æ‹Šâ€œé‡įŊŽâ€å°†äŧšīŧˆé‡æ–°īŧ‰åˆ†į섿‰€æœ‰äēēč„¸ã€‚é€‰æ‹Šâ€œįŧēå¤ąâ€å°†å°šæœĒåˆ†é…įš„äēē脸įŊŽäēŽé˜Ÿåˆ—中。", - "failed_job_command": "{command}å‘Ŋä줿‰§čĄŒå¤ąč´Ĩįš„äģģåŠĄīŧš{job}", - "force_delete_user_warning": "č­Ļ告īŧščŋ™å°†įĢ‹åŗį§ģé™¤į”¨æˆˇäģĨ及å…￉€æœ‰éĄšį›Žã€‚č¯Ĩ操äŊœæ— æŗ•撤销且文äģļæ— æŗ•æĸ复。", + "face_detection_description": "äŊŋᔍæœē器å­Ļäš æŖ€æĩ‹åŊąåƒä¸­įš„äēē脸īŧŒå¯šäēŽč§†éĸ‘äģ…处ᐆå…ļįŧŠį•Ĩå›žã€‚â€œåˆˇæ–°â€äŧšé‡æ–°å¤„į†æ‰€æœ‰åŊąåƒīŧ›â€œé‡įŊŽâ€äŧ𿏅除åŊ“前所有äēēč„¸æ•°æŽīŧ›â€œįŧēå¤ąâ€åˆ™äģ…å°†æœĒæ›žå¤„į†čŋ‡įš„åŊąåƒåŠ å…Ĩ队列。åŊ““äēēč„¸æŖ€æĩ‹â€åŽŒæˆåŽīŧŒįŗģįģŸäŧšå°†æ–°æŖ€æĩ‹åˆ°įš„äēēč„¸æ”žå…Ĩ“äēē脏蝆åˆĢ”队列īŧŒäģĨ将å…ļåŊ’įąģåˆ°įŽ°æœ‰æˆ–æ–°åģēįš„äēēį‰Šåˆ†įģ„中。", + "facial_recognition_job_description": "å°†æŖ€æĩ‹åˆ°įš„äēē脸åŊ’įąģä¸ēä¸åŒįš„äēēį‰ŠīŧŒæ­¤æ­ĨéĒ¤éœ€åœ¨â€œäēēč„¸æŖ€æĩ‹â€åŽŒæˆåŽčŋčĄŒã€‚“重įŊŽâ€äŧšīŧˆé‡æ–°īŧ‰čšįąģ所有äēēč„¸ã€‚â€œįŧēå¤ąâ€åˆ™å°†å°šæœĒįĄŽåŽšæ˜¯č°įš„äēēč„¸åŠ å…ĨåŊ’įąģ队列。", + "failed_job_command": "å‘Ŋäģ¤ {command} åœ¨æ‰§čĄŒäģģåŠĄ {job} æ—ļå¤ąč´Ĩ", + "force_delete_user_warning": "č­Ļ告īŧšæ­¤æ“äŊœå°†įĢ‹åŗåˆ é™¤č¯Ĩį”¨æˆˇåŠå…ļ所有čĩ„äē§ã€‚此操äŊœä¸å¯æ’¤é”€īŧŒä¸”æ–‡äģļæ— æŗ•æĸ复。", "image_format": "æ ŧåŧ", - "image_format_description": "WebP 文äģļäŊ“į§¯čžƒ JPEG 文äģļæ›´å°īŧŒäŊ†įŧ–į é€ŸåēĻ螃æ…ĸ。", - "image_fullsize_description": "åŽģé™¤å…ƒæ•°æŽįš„å…¨å°ē寸回像īŧŒæ”žå¤§æ—ļäŊŋᔍ", + "image_format_description": "WebP æ ŧåŧįš„æ–‡äģļäŊ“į§¯æ¯” JPEG 更小īŧŒäŊ†įŧ–į é€ŸåēĻ螃æ…ĸ。", + "image_fullsize_description": "厞å‰ĨįĻģå…ƒæ•°æŽįš„å…¨å°ē寸回像īŧŒæ”žå¤§æŸĨįœ‹æ—ļäŊŋᔍ", "image_fullsize_enabled": "吝ᔍ免å°ēå¯¸å›žåƒį”Ÿæˆ", - "image_fullsize_enabled_description": "į”ŸæˆéžįŊ‘įģœå‹åĨŊæ ŧåŧįš„å…¨å°ēå¯¸å›žåƒã€‚å¯į”¨ “éϖ选åĩŒå…Ĩåŧéĸ„č§ˆ ”后īŧŒå°†į›´æŽĨäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆč€Œæ— éœ€čŊŦæĸ。不åŊąå“ JPEG į­‰įŊ‘įģœå‹åĨŊæ ŧåŧã€‚", - "image_fullsize_quality_description": "全å°ēå¯¸å›žåƒč´¨é‡äģŽ 1 到 100。čļŠé̘čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļčžƒå¤§ã€‚", + "image_fullsize_enabled_description": "ä¸ē非įŊ‘éĄĩ友åĨŊæ ŧåŧį”Ÿæˆå…¨å°ēå¯¸å›žåƒã€‚å¯į”¨â€œäŧ˜å…ˆäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆâ€åŽīŧŒå°†į›´æŽĨäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆč€Œæ— éœ€čŊŦæĸã€‚æ­¤čŽžįŊŽä¸åŊąå“ JPEG į­‰įŊ‘éĄĩ友åĨŊæ ŧåŧã€‚", + "image_fullsize_quality_description": "全å°ēå¯¸å›žåƒč´¨é‡īŧˆ1-100īŧ‰ã€‚æ•°å€ŧčļŠé̘į”ģč´¨čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļ也čļŠå¤§ã€‚", "image_fullsize_title": "全å°ēå¯¸å›žåƒčŽžįŊŽ", - "image_prefer_embedded_preview": "åĩŒå…Ĩåŧéĸ„č§ˆ", - "image_prefer_embedded_preview_setting_description": "äŧ˜å…ˆäŊŋᔍ RAW į…§į‰‡įš„åĩŒå…Ĩåŧéĸ„č§ˆäŊœä¸ēå›žåƒå¤„į†įš„čž“å…Ĩ。可äģĨ提升某äē›åŊąåƒįš„éĸœč‰˛å‡†įĄŽåēĻīŧŒäŊ†åĩŒå…Ĩåŧéĸ„č§ˆįš„č´¨é‡å–å†ŗäēŽį›¸æœēīŧŒå›žåƒå¯čƒŊ压įŧŠå¤ąįœŸæ›´ä¸Ĩ重。", - "image_prefer_wide_gamut": "åšŋč‰˛åŸŸ", - "image_prefer_wide_gamut_setting_description": "寚įŧŠį•Ĩ回äŊŋᔍ Display P3。čŋ™å¯äģĨ更åĨŊ地äŋį•™åŽŊč‰˛åŸŸå›žåƒįš„é˛œč‰ŗåēĻīŧŒäŊ†åœ¨æ—§čŽžå¤‡å’Œæ—§į‰ˆæĩč§ˆå™¨ä¸Šå›žåƒå¯čƒŊäŧšæ˜žåž—ä¸åŒã€‚sRGB 回像åē”äŋå­˜ä¸ē sRGB äģĨéŋ免éĸœč‰˛åį§ģ。", - "image_preview_description": "å‰ĨįĻģå…ƒæ•°æŽįš„ä¸­å°ē寸回像īŧŒį”¨äēŽå•ä¸€éĄšį›ŽæŸĨįœ‹å’Œæœē器å­Ļäš ", - "image_preview_quality_description": "éĸ„č§ˆč´¨é‡äģŽ 1 到 100。čļŠé̘čļŠåĨŊīŧŒäŊ†äŧšäē§į”Ÿæ›´å¤§įš„æ–‡äģļīŧŒåšļ且äŧšé™äŊŽįŗģįģŸįš„响åē”čƒŊåŠ›ã€‚čŽžįŊŽčžƒäŊŽįš„å€ŧ可čƒŊäŧšåŊąå“æœē器å­Ļäš įš„č´¨é‡ã€‚", - "image_preview_title": "éĸ„č§ˆčŽžįŊŽ", + "image_prefer_embedded_preview": "äŧ˜å…ˆäŊŋᔍåĩŒå…Ĩåŧéĸ„č§ˆ", + "image_prefer_embedded_preview_setting_description": "äŊŋᔍ RAW 文äģļ内åĩŒå…Ĩįš„éĸ„č§ˆå›žåƒīŧˆåĻ‚æœ‰īŧ‰äŊœä¸ē后įģ­å¤„į†įš„åŸēįĄ€ã€‚čŋ™čƒŊä¸ēéƒ¨åˆ†å›žåƒį”Ÿæˆæ›´å‡†įĄŽįš„č‰˛åŊŠīŧŒäŊ†åĩŒå…Ĩåŧéĸ„č§ˆå›žåƒįš„č´¨é‡å–å†ŗäēŽį›¸æœēåž‹åˇīŧŒä¸”可čƒŊ因有损压įŧŠåŒ…åĢæ›´å¤šįš„äŧĒåŊąã€‚", + "image_prefer_wide_gamut": "äŧ˜å…ˆäŊŋᔍåšŋč‰˛åŸŸ", + "image_prefer_wide_gamut_setting_description": "įŧŠį•Ĩ回äŊŋᔍ Display P3 色åŊŠįŠē间。čŋ™čƒŊ更åĨŊ地äŋį•™åšŋč‰˛åŸŸå›žåƒįš„č‰˛åŊŠé˛œč‰ŗåēĻīŧŒäŊ†åœ¨äŊŋį”¨æ—§į‰ˆæĩč§ˆå™¨įš„č€æ—§čŽžå¤‡ä¸ŠīŧŒå¯čƒŊå¯ŧč‡´č‰˛åˇŽã€‚sRGB 回像将äŋæŒä¸ē sRGBīŧŒäģĨéŋå…č‰˛åŊŠåį§ģ。", + "image_preview_description": "厞å‰ĨįĻģå…ƒæ•°æŽįš„ä¸­į­‰å°ē寸回像īŧŒį”¨äēŽå•åš…åŊąåƒåą•į¤ē及æœē器å­Ļäš ", + "image_preview_quality_description": "éĸ„č§ˆå›žč´¨é‡īŧˆ1-100īŧ‰ã€‚æ•°å€ŧčļŠé̘į”ģč´¨čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļčļŠå¤§īŧŒä¸”可čƒŊ降äŊŽåē”į”¨å“åē”速åēĻã€‚čŽžįŊŽčŋ‡äŊŽįš„æ•°å€ŧ可čƒŊåŊąå“æœē器å­Ļäš īŧˆč¯†åˆĢīŧ‰įš„å‡†įĄŽåēĻ。", + "image_preview_title": "éĸ„č§ˆå›žčŽžįŊŽ", + "image_progressive": "渐čŋ›åŧįŧ–᠁", + "image_progressive_description": "寚 JPEG 回像čŋ›čĄŒæ¸čŋ›åŧįŧ–᠁īŧŒäģĨæå‡å›žį‰‡åŠ čŊŊäŊ“éĒŒã€‚æ­¤åŧ€å…ŗå¯š WebP 无效。", "image_quality": "质量", "image_resolution": "åˆ†čž¨įŽ‡", - "image_resolution_description": "更éĢ˜įš„åˆ†čž¨įŽ‡å¯äģĨäŋį•™æ›´å¤šįģ†čŠ‚īŧŒäŊ†įŧ–᠁æ—ļ间更é•ŋīŧŒæ–‡äģļäŊ“į§¯æ›´å¤§īŧŒč€Œä¸”äŧšé™äŊŽįŗģįģŸįš„响åē”速åēĻ。", - "image_settings": "å›žį‰‡čŽžįŊŽ", + "image_resolution_description": "螃éĢ˜įš„åˆ†čž¨įŽ‡čƒŊäŋį•™æ›´å¤šå›žåƒįģ†čŠ‚īŧŒäŊ†įŧ–᠁æ—ļ间更é•ŋã€į”Ÿæˆįš„æ–‡äģļæ›´å¤§īŧŒä¸”可čƒŊå¯ŧ致åē”į”¨å“åē”变æ…ĸ。", + "image_settings": "å›žåƒčŽžįŊŽ", "image_settings_description": "įŽĄį†į”Ÿæˆå›žåƒįš„č´¨é‡å’Œåˆ†čž¨įŽ‡", - "image_thumbnail_description": "å‰ĨįĻģå…ƒæ•°æŽįš„å°įŧŠį•Ĩ回īŧŒį”¨äēŽæĩč§ˆä¸ģæ—ļ间įēŋᭉᅧቇįģ„", - "image_thumbnail_quality_description": "įŧŠį•Ĩå›žč´¨é‡äģŽ 1 到 100。čļŠé̘čļŠåĨŊīŧŒäŊ†äŧšäē§į”Ÿæ›´å¤§įš„æ–‡äģļīŧŒåšļ且äŧšé™äŊŽįŗģįģŸįš„响åē”čƒŊ力。", + "image_thumbnail_description": "厞å‰ĨįĻģå…ƒæ•°æŽįš„å°åž‹įŧŠį•Ĩ回īŧŒį”¨äēŽåˆ—čĄ¨åą•į¤ē多åŧ į…§į‰‡īŧŒäž‹åĻ‚ä¸ģæ—ļ间įēŋ", + "image_thumbnail_quality_description": "įŧŠį•Ĩå›žč´¨é‡īŧˆ1-100īŧ‰ã€‚æ•°å€ŧčļŠé̘į”ģč´¨čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļčļŠå¤§īŧŒä¸”可čƒŊ降äŊŽåē”į”¨å“åē”速åēĻ。", "image_thumbnail_title": "įŧŠį•Ĩå›žčŽžįŊŽ", - "import_config_from_json_description": "通čŋ‡ä¸Šäŧ JSON配įŊŽæ–‡äģļå¯ŧå…ĨįŗģįģŸé…įŊŽ", - "job_concurrency": "{job}äģģåŠĄåšļ发", + "import_config_from_json_description": "通čŋ‡ä¸Šäŧ  JSON 配įŊŽæ–‡äģļå¯ŧå…ĨįŗģįģŸé…įŊŽ", + "job_concurrency": "{job} åšļ发数", "job_created": "äģģåŠĄåˇ˛åˆ›åģē", - "job_not_concurrency_safe": "æ­¤äģģåŠĄåšļ发åšļ不厉全。", + "job_not_concurrency_safe": "č¯ĨäģģåŠĄä¸æ”¯æŒåšļ发操äŊœã€‚", "job_settings": "äģģåŠĄčŽžįŊŽ", - "job_settings_description": "įŽĄį†äģģåŠĄåšļ发", - "jobs_delayed": "{jobCount, plural, other {#饚äģģåŠĄåˇ˛æŽ¨čŋŸ}}", - "jobs_failed": "{jobCount, plural, other {#éĄšå¤ąč´Ĩ}}", - "jobs_over_time": "单äŊæ—ļ间äģģåŠĄæ•°", - "library_created": "åˇ˛åˆ›åģē回åē“īŧš{library}", - "library_deleted": "回åē“åˇ˛åˆ é™¤", - "library_details": "回åē“č¯Ļ情", - "library_folder_description": "指厚čρå¯ŧå…Ĩįš„æ–‡äģļ多。将寚č¯Ĩ文äģļ多īŧˆåŒ…æ‹Ŧ子文äģļ多īŧ‰čŋ›čĄŒå›žåƒå’Œč§†éĸ‘æ‰Ģ描。", - "library_remove_exclusion_pattern_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤æŽ’é™¤č§„åˆ™å—īŧŸ", - "library_remove_folder_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤å¯ŧå…Ĩ文äģļ多吗īŧŸ", + "job_settings_description": "įŽĄį†äģģåŠĄåšļ发数", + "jobs_delayed": "{jobCount, plural, other {# ä¸ĒåģļčŋŸ}}", + "jobs_failed": "{jobCount, plural, other {# ä¸Ēå¤ąč´Ĩ}}", + "jobs_over_time": "äģģåŠĄåŠ¨æ€", + "library_created": "åˇ˛åˆ›åģēčĩ„äē§åē“īŧš{library}", + "library_deleted": "čĩ„äē§åē“åˇ˛åˆ é™¤", + "library_details": "čĩ„äē§åē“č¯Ļ情", + "library_folder_description": "指厚一ä¸Ēå¯ŧå…Ĩ文äģļ多。įŗģįģŸå°†æ‰Ģ描č¯Ĩ文äģļ多及å…ļ所有子文äģļå¤šä¸­įš„å›žį‰‡å’Œč§†éĸ‘。", + "library_remove_exclusion_pattern_prompt": "įĄŽåŽščρį§ģé™¤æ­¤æŽ’é™¤č§„åˆ™å—īŧŸ", + "library_remove_folder_prompt": "įĄŽåŽščρį§ģ除此å¯ŧå…Ĩ文äģļ多吗īŧŸ", "library_scanning": "厚期æ‰Ģ描", - "library_scanning_description": "配įŊŽåŽšæœŸæ‰Ģ描回åē“", - "library_scanning_enable_description": "å¯į”¨åŽšæœŸæ‰Ģ描回åē“", - "library_settings": "外部回åē“", - "library_settings_description": "įŽĄį†å¤–éƒ¨å›žåē“莞įŊŽ", - "library_tasks_description": "æ‰Ģ描外部åē“īŧŒæŸĨ扞新åĸžæˆ–äŋŽæ”šįš„éĄšį›Ž", - "library_updated": "åˇ˛æ›´æ–°įš„å›žåē“", - "library_watching_enable_description": "į›‘æŽ§å¤–éƒ¨å›žå瓿–‡äģļ变化", - "library_watching_settings": "į›‘æŽ§å›žåē“[厞éĒŒæ€§]", - "library_watching_settings_description": "č‡ĒåŠ¨į›‘æŽ§æ–‡äģļ变化", + "library_scanning_description": "配įŊŽåŽšæœŸæ‰Ģ描", + "library_scanning_enable_description": "åŧ€å¯åŽšæœŸæ‰Ģ描", + "library_settings": "外部čĩ„äē§åē“", + "library_settings_description": "įŽĄį†å¤–éƒ¨čĩ„äē§åē“莞įŊŽ", + "library_tasks_description": "æ‰Ģ描外部čĩ„äē§åē“äģĨæŸĨ扞新åĸžå’Œå˜æ›´įš„æ–‡äģļ", + "library_updated": "čĩ„äē§åē“åˇ˛æ›´æ–°", + "library_watching_enable_description": "į›‘æŽ§å¤–éƒ¨čĩ„äē§åē“įš„æ–‡äģļ变更", + "library_watching_settings": "čĩ„äē§åē“į›‘æŽ§ [厞éĒŒæ€§åŠŸčƒŊ]", + "library_watching_settings_description": "č‡ĒåŠ¨į›‘æŽ§æ–‡äģļ变更", "logging_enable_description": "吝ᔍæ—Ĩåŋ—čްåŊ•", - "logging_level_description": "吝ᔍæ—ļīŧŒčρäŊŋį”¨įš„æ—Ĩåŋ—įē§åˆĢ。", + "logging_level_description": "å¯į”¨åŽīŧŒæ‰€é‡‡į”¨įš„æ—Ĩåŋ—įē§åˆĢ。", "logging_settings": "æ—Ĩåŋ—", "machine_learning_availability_checks": "å¯į”¨æ€§æŖ€æŸĨ", "machine_learning_availability_checks_description": "č‡ĒåŠ¨æŖ€æĩ‹åšļäŧ˜å…ˆé€‰æ‹Šå¯į”¨įš„æœē器å­Ļäš æœåŠĄå™¨", "machine_learning_availability_checks_enabled": "å¯į”¨å¯į”¨æ€§æŖ€æŸĨ", "machine_learning_availability_checks_interval": "æŖ€æŸĨ间隔", - "machine_learning_availability_checks_interval_description": "å¯į”¨æ€§æŖ€æŸĨäš‹é—´įš„é—´éš”īŧˆæ¯Ģį§’īŧ‰", - "machine_learning_availability_checks_timeout": "č¯ˇæą‚čļ…æ—ļ", - "machine_learning_availability_checks_timeout_description": "ᔍäēŽå¯į”¨æ€§æŖ€æŸĨįš„čļ…æ—ļæ—ļ间īŧˆæ¯Ģį§’īŧ‰", + "machine_learning_availability_checks_interval_description": "两æŦĄå¯į”¨æ€§æŖ€æŸĨäš‹é—´įš„æ—ļ间间隔īŧˆæ¯Ģį§’īŧ‰", + "machine_learning_availability_checks_timeout": "č¯ˇæą‚čļ…æ—ļæ—ļ间", + "machine_learning_availability_checks_timeout_description": "å¯į”¨æ€§æŖ€æŸĨįš„č¯ˇæą‚čļ…æ—ļæ—ļ间īŧˆæ¯Ģį§’īŧ‰", "machine_learning_clip_model": "CLIP æ¨Ąåž‹", - "machine_learning_clip_model_description": "蝎äēŽ æ­¤å¤„æŸĨįœ‹æ”¯æŒįš„ CLIP æ¨Ąåž‹åį§°ã€‚æŗ¨æ„īŧŒæ›´æĸæ¨Ąåž‹åŽéœ€čĻå¯šæ‰€æœ‰å›žį‰‡é‡æ–°čŋčĄŒâ€œæ™ēčƒŊ搜į´ĸ”äģģåŠĄã€‚", + "machine_learning_clip_model_description": "在 此处 列å‡ēįš„ CLIP æ¨Ąåž‹åį§°ã€‚č¯ˇæŗ¨æ„īŧŒæ›´æ”šæ¨Ąåž‹åŽīŧŒåŋ…éĄģ重新čŋčĄŒæ‰€æœ‰å›žį‰‡įš„“æ™ēčƒŊ搜į´ĸ”äģģåŠĄã€‚", "machine_learning_duplicate_detection": "é‡å¤éĄšæŖ€æĩ‹", - "machine_learning_duplicate_detection_enabled": "å¯į”¨é‡å¤æŖ€æĩ‹", - "machine_learning_duplicate_detection_enabled_description": "åĻ‚æžœįρᔍīŧŒåŽŒå…¨į›¸åŒįš„éĄšį›Žäģå°†čĸĢåŽģ重。", - "machine_learning_duplicate_detection_setting_description": "äŊŋᔍ CLIP 向量匚配īŧˆå…ŗé”Žč¯į›¸äŧŧåēĻīŧ‰æĨæŸĨ扞可čƒŊįš„é‡å¤éĄš", + "machine_learning_duplicate_detection_enabled": "å¯į”¨é‡å¤éĄšæŖ€æĩ‹", + "machine_learning_duplicate_detection_enabled_description": "č‹Ĩå…ŗé—­æ­¤åŠŸčƒŊīŧŒåŽŒå…¨į›¸åŒįš„čĩ„äē§äģäŧščĸĢåŽģé‡å¤„į†ã€‚", + "machine_learning_duplicate_detection_setting_description": "åˆŠį”¨ CLIP åĩŒå…Ĩå‘é‡č¯†åˆĢæŊœåœ¨įš„é‡å¤éĄš", "machine_learning_enabled": "吝ᔍæœē器å­Ļäš ", - "machine_learning_enabled_description": "åĻ‚æžœįρᔍīŧŒæ— čŽēäģĨ下åĻ‚äŊ•莞įŊŽīŧŒæ‰€æœ‰æœē器å­Ļ䚠功čƒŊ将čĸĢįĻį”¨ã€‚", + "machine_learning_enabled_description": "č‹Ĩå…ŗé—­æ­¤å¤„æ€ģåŧ€å…ŗīŧŒæ‰€æœ‰æœē器å­Ļäš į›¸å…ŗį‰šæ€§å°†å…¨éƒ¨åœį”¨īŧŒä¸‹æ–šå…ˇäŊ“莞įŊŽæ— æ•ˆã€‚", "machine_learning_facial_recognition": "äēē脏蝆åˆĢ", - "machine_learning_facial_recognition_description": "æŖ€æĩ‹ã€č¯†åˆĢåšļå°†å›žåƒä¸­įš„äēēč„¸åˆ†įģ„", + "machine_learning_facial_recognition_description": "æŖ€æĩ‹ã€č¯†åˆĢåšļč‡Ē动åŊ’įąģå›žį‰‡ä¸­įš„äēē脸", "machine_learning_facial_recognition_model": "äēē脏蝆åˆĢæ¨Ąåž‹", - "machine_learning_facial_recognition_model_description": "æœē器å­Ļäš æ¨Ąåž‹æŒ‰č§„æ¨Ąå¤§å°é™åēæŽ’åˆ—ã€‚æ›´å¤§įš„æ¨Ąåž‹é€ŸåēĻæ›´æ…ĸīŧŒå į”¨įš„内存更多īŧŒäŊ†æ•ˆæžœæ›´åĨŊã€‚č¯ˇæŗ¨æ„īŧŒåœ¨æ›´æĸæ¨Ąåž‹åŽīŧŒåŋ…éĄģ寚所有回像重新čŋčĄŒäēēč„¸æŖ€æĩ‹ã€‚", + "machine_learning_facial_recognition_model_description": "æ¨Ąåž‹æŒ‰å°ē寸降åēæŽ’åˆ—ã€‚čžƒå¤§įš„æ¨Ąåž‹čŋčĄŒé€ŸåēĻ螃æ…ĸä¸”å į”¨æ›´å¤šå†…å­˜īŧŒäŊ†æ•ˆæžœæ›´åĨŊã€‚č¯ˇæŗ¨æ„īŧŒæ›´æĸæ¨Ąåž‹åŽīŧŒåŋ…éĄģ重新čŋčĄŒæ‰€æœ‰å›žį‰‡įš„“äēēč„¸æŖ€æĩ‹â€äģģåŠĄã€‚", "machine_learning_facial_recognition_setting": "吝ᔍäēē脏蝆åˆĢ", - "machine_learning_facial_recognition_setting_description": "åĻ‚æžœįĻį”¨æ­¤åŠŸčƒŊīŧŒå›žį‰‡å°†ä¸äŧščĸĢįŧ–᠁åšļᔍäēŽäēē脏蝆åˆĢīŧŒäšŸä¸äŧšåœ¨æŽĸį´ĸéĄĩéĸ昞į¤ēäēēį‰Šåˆ—čĄ¨ã€‚", + "machine_learning_facial_recognition_setting_description": "č‹Ĩå…ŗé—­æ­¤åŠŸčƒŊīŧŒå›žį‰‡å°†ä¸äŧščŋ›čĄŒäēē脏蝆åˆĢįŧ–᠁īŧŒä¸”“æŽĸį´ĸ”éĄĩéĸįš„â€œäēēį‰Šâ€æŋå—å°†æ— æŗ•æ˜žį¤ē内厚。", "machine_learning_max_detection_distance": "æœ€å¤§æŖ€æĩ‹čˇįĻģ", - "machine_learning_max_detection_distance_description": "两åŧ å›žį‰‡čĸĢ莤ä¸ēæ˜¯é‡å¤įš„æœ€å¤§čˇįĻģčŒƒå›´æ˜¯0.001到0.1ã€‚čžƒéĢ˜įš„å€ŧå°†æŖ€æĩ‹å‡ēæ›´å¤šįš„é‡å¤å›žį‰‡īŧŒäŊ†å¯čƒŊå¯ŧ致蝝æŠĨ。", + "machine_learning_max_detection_distance_description": "两åŧ å›žį‰‡čĸĢ视ä¸ēé‡å¤éĄšįš„æœ€å¤§čˇįĻģīŧŒå–å€ŧčŒƒå›´ä¸ē 0.001 - 0.1。数å€ŧčļŠé̘īŧŒæŖ€æĩ‹å‡ēįš„é‡å¤éĄščļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤īŧˆäž‹åĻ‚å°†ä¸åŒįš„äēē蝆åˆĢä¸ē同一äēēīŧ‰ã€‚", "machine_learning_max_recognition_distance": "æœ€å¤§č¯†åˆĢ距įĻģ", - "machine_learning_max_recognition_distance_description": "将此阈å€ŧčŽžåŽšåœ¨0到2之间īŧŒå¯äģĨäŧ˜åŒ–įŗģįģŸįš„蝆åˆĢį˛žåēĻ。选拊一ä¸Ē螃äŊŽįš„阈å€ŧīŧŒæœ‰åŠŠäēŽäŋæŒäēēč„¸įš„į‹Ŧį‰šæ€§īŧŒéŋå…é”™č¯¯åœ°å°†ä¸¤ä¸Ēä¸åŒįš„äēē蝆åˆĢä¸ē同一äēēã€‚į›¸åīŧŒé€‚åŊ“提é̘阈å€ŧ可äģĨ减少将同一äēēč¯¯åˆ†ä¸ē多ä¸Ēäēēč„¸įš„æƒ…å†ĩ。在čŋ™ä¸Ē选拊čŋ‡į¨‹ä¸­īŧŒæˆ‘äģŦ倞向äēŽæ›´äŊŽįš„阈å€ŧīŧŒå› ä¸ē合åšļ错蝝蝆åˆĢįš„äēē脸čĻæ¯”åˆ†įĻģ同一ä¸Ēäēēč„¸ä¸­įš„å¤šä¸Ēäēēæ›´įŽ€å•ã€‚", - "machine_learning_min_detection_score": "最äŊŽæŖ€æĩ‹åˆ†æ•°", - "machine_learning_min_detection_score_description": "æŖ€æĩ‹åˆ°äēēč„¸įš„æœ€å°įŊŽäŋĄåˆ†æ•°ä¸ē0-1ã€‚čžƒäŊŽįš„å€ŧå°†æŖ€æĩ‹åˆ°æ›´å¤šäēē脸īŧŒäŊ†å¯čƒŊå¯ŧ致蝝æŠĨ。", - "machine_learning_min_recognized_faces": "蝆åˆĢįš„æœ€å°‘äēēč„¸æ•°", - "machine_learning_min_recognized_faces_description": "创åģē一ä¸Ēäē翉€éœ€č¯†åˆĢįš„æœ€å°‘äēēč„¸æ•°é‡ã€‚æé̘čŋ™ä¸Ēå€ŧ可äģĨäŊŋäēē脏蝆åˆĢæ›´į˛žįĄŽīŧŒäŊ†äšŸåĸžåŠ äē†äēē脸æœĒčƒŊčĸĢåˆ†é…åˆ°į›¸å¯šåē”äēēį‰Šįš„å¯čƒŊ性。", - "machine_learning_ocr": "文æœŦ蝆åˆĢ", - "machine_learning_ocr_description": "äŊŋᔍæœē器å­Ļäš č¯†åˆĢå›žį‰‡ä¸­įš„æ–‡æœŦ", - "machine_learning_ocr_enabled": "å¯į”¨æ–‡æœŦ蝆åˆĢ", - "machine_learning_ocr_enabled_description": "åĻ‚æžœįρᔍīŧŒåˆ™ä¸äŧšå¯šå›žåƒįŧ–᠁äģĨᔍäēŽæ–‡æœŦ蝆åˆĢ。", - "machine_learning_ocr_max_resolution": "最éĢ˜åˆ†čž¨įŽ‡", - "machine_learning_ocr_max_resolution_description": "é̘äēŽæ­¤åˆ†čž¨įŽ‡įš„éĸ„č§ˆå°†č°ƒæ•´å¤§å°īŧŒåŒæ—ļäŋæŒįēĩæ¨Ē比。更éĢ˜įš„å€ŧæ›´å‡†įĄŽīŧŒäŊ†å¤„ᐆæ—ļ间更é•ŋīŧŒå į”¨æ›´å¤šå†…存。", - "machine_learning_ocr_min_detection_score": "最äŊŽæŖ€æĩ‹åˆ†æ•°", - "machine_learning_ocr_min_detection_score_description": "čĻæŖ€æĩ‹įš„æ–‡æœŦįš„æœ€å°įŊŽäŋĄåēĻ分数ä¸ē0-1ã€‚čžƒäŊŽįš„å€ŧå°†æŖ€æĩ‹åˆ°æ›´å¤šįš„æ–‡æœŦīŧŒäŊ†å¯čƒŊäŧšå¯ŧ致蝝æŠĨ。", - "machine_learning_ocr_min_recognition_score": "最äŊŽč¯†åˆĢ分数", - "machine_learning_ocr_min_score_recognition_description": "æŖ€æĩ‹åˆ°įš„æ–‡æœŦįš„æœ€å°įŊŽäŋĄåēĻ垗分ä¸ē0-1ã€‚čžƒäŊŽįš„å€ŧ将蝆åˆĢæ›´å¤šįš„æ–‡æœŦīŧŒäŊ†å¯čƒŊäŧšå¯ŧ致蝝æŠĨ。", - "machine_learning_ocr_model": "文æœŦ蝆åˆĢæ¨Ąåž‹", - "machine_learning_ocr_model_description": "æœåŠĄå™¨æ¨Ąåž‹æ¯”į§ģåŠ¨æ¨Ąåž‹æ›´å‡†įĄŽīŧŒäŊ†éœ€čĻæ›´é•ŋįš„æ—ļ间æĨå¤„į†å’ŒäŊŋį”¨æ›´å¤šįš„å†…å­˜ã€‚", + "machine_learning_max_recognition_distance_description": "两åŧ äēē脸čĸĢ视ä¸ē同一ä¸Ēäēēįš„æœ€å¤§čˇįĻģīŧŒå–å€ŧčŒƒå›´ä¸ē 0 - 2ã€‚č°ƒäŊŽč¯Ĩå€ŧ可éŋ免将两ä¸Ēäēēč¯¯æ ‡ä¸ē同一äēēīŧŒč°ƒéĢ˜åˆ™å¯éŋ免将同一ä¸Ēäēēč¯¯æ ‡ä¸ē两ä¸Ēäēēã€‚č¯ˇæŗ¨æ„īŧŒäē‹åŽåˆåšļ两ä¸Ēäēēį‰Šæ¯”æ‹†åˆ†ä¸€ä¸Ēäēēį‰Šæ›´åŽšæ˜“īŧŒå› æ­¤åœ¨å¯čƒŊįš„æƒ…å†ĩ下īŧŒåģē莎äŧ˜å…ˆčŽžįŊŽčžƒäŊŽįš„阈å€ŧ。", + "machine_learning_min_detection_score": "最äŊŽæŖ€æĩ‹é˜ˆå€ŧ", + "machine_learning_min_detection_score_description": "äēēč„¸æŖ€æĩ‹įš„æœ€äŊŽįŊŽäŋĄåēĻ分数īŧŒå–å€ŧčŒƒå›´ä¸ē 0-1。数å€ŧčļŠäŊŽīŧŒæŖ€æĩ‹åˆ°įš„äēē脸čļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤īŧˆäž‹åĻ‚å°†éžäēē脸åŒēåŸŸč¯†åˆĢä¸ēäēē脸īŧ‰ã€‚", + "machine_learning_min_recognized_faces": "æœ€å°č¯†åˆĢ数量", + "machine_learning_min_recognized_faces_description": "创åģēäēēį‰Šæ‰€éœ€įš„æœ€å°‘äēēč„¸æ•°é‡ã€‚č°ƒéĢ˜æ­¤å€ŧ可提升äēē脏蝆åˆĢįš„į˛žå‡†åēĻīŧŒäŊ†äŧšåĸžåŠ äēēč„¸æ— æŗ•čĸĢ分配įģ™äēēį‰Šįš„éŖŽé™Šã€‚", + "machine_learning_ocr": "æ–‡å­—č¯†åˆĢīŧˆOCRīŧ‰", + "machine_learning_ocr_description": "åˆŠį”¨æœē器å­Ļäš æŠ€æœ¯č¯†åˆĢå›žį‰‡ä¸­įš„æ–‡æœŦ内厚", + "machine_learning_ocr_enabled": "å¯į”¨æ–‡å­—č¯†åˆĢīŧˆOCRīŧ‰", + "machine_learning_ocr_enabled_description": "č‹ĨįρᔍīŧŒå°†ä¸äŧšå°č¯•蝆åˆĢå›žį‰‡ä¸­įš„æ–‡å­—ã€‚", + "machine_learning_ocr_max_resolution": "æœ€å¤§åˆ†čž¨įŽ‡", + "machine_learning_ocr_max_resolution_description": "čļ…čŋ‡æ­¤åˆ†čž¨įŽ‡įš„éĸ„č§ˆå›žå°†æŒ‰æ¯”äž‹č°ƒæ•´å¤§å°ã€‚æ•°å€ŧčļŠé̘īŧŒæ•ˆæžœčļŠį˛žå‡†īŧŒäŊ†å¤„ᐆæ—ļ间更é•ŋä¸”æ›´å į”¨å†…å­˜ã€‚", + "machine_learning_ocr_min_detection_score": "最äŊŽæŖ€æĩ‹é˜ˆå€ŧ", + "machine_learning_ocr_min_detection_score_description": "文æœŦæŖ€æĩ‹įš„æœ€äŊŽįŊŽäŋĄåēĻ分数īŧˆ0-1īŧ‰ã€‚æ•°å€ŧčļŠäŊŽīŧŒæŖ€æĩ‹åˆ°įš„æ–‡æœŦčļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤ã€‚", + "machine_learning_ocr_min_recognition_score": "最äŊŽč¯†åˆĢ阈å€ŧ", + "machine_learning_ocr_min_score_recognition_description": "åˇ˛æŖ€æĩ‹æ–‡æœŦįš„æœ€äŊŽįŊŽäŋĄåēĻ分数īŧˆ0-1īŧ‰ã€‚æ•°å€ŧčļŠäŊŽīŧŒč¯†åˆĢå‡ēįš„æ–‡æœŦčļŠå¤šīŧŒäŊ†å¯čƒŊå‡ēįŽ°č¯¯åˆ¤ã€‚", + "machine_learning_ocr_model": "OCR æ¨Ąåž‹", + "machine_learning_ocr_model_description": "æœåŠĄå™¨įĢ¯æ¨Ąåž‹æ¯”į§ģ动įĢ¯æ¨Ąåž‹æ›´į˛žå‡†īŧŒäŊ†å¤„ᐆ耗æ—ļ更é•ŋä¸”æ›´å į”¨å†…å­˜ã€‚", "machine_learning_settings": "æœē器å­Ļ䚠莞įŊŽ", - "machine_learning_settings_description": "įŽĄį†æœē器å­Ļ䚠功čƒŊå’ŒčŽžįŊŽ", + "machine_learning_settings_description": "įŽĄį†æœē器å­Ļ䚠功čƒŊåŠį›¸å…ŗčŽžįŊŽ", "machine_learning_smart_search": "æ™ēčƒŊ搜į´ĸ", - "machine_learning_smart_search_description": "äŊŋᔍ CLIP äģĨ文搜回、æ™ēčƒŊ搜回", + "machine_learning_smart_search_description": "äŊŋᔍ CLIP åĩŒå…Ĩ向量čŋ›čĄŒč¯­äš‰åŒ–å›žį‰‡æœį´ĸ", "machine_learning_smart_search_enabled": "吝ᔍæ™ēčƒŊ搜į´ĸ", - "machine_learning_smart_search_enabled_description": "åĻ‚æžœįρᔍīŧŒåˆ™ä¸äŧšå¯šå›žåƒįŧ–᠁äģĨᔍäēŽæ™ēčƒŊ搜į´ĸ。", - "machine_learning_url_description": "æœē器å­Ļäš æœåŠĄå™¨įš„ URL。åĻ‚æžœæäž›å¤šä¸Ē URLīŧŒåˆ™å°†æŒ‰äžæŦĄå°č¯•čŋžæŽĨ每ä¸ĒæœåŠĄå™¨īŧŒį›´åˆ°æœ‰ä¸€ä¸ĒæœåŠĄå™¨æˆåŠŸå“åē”ä¸ēæ­ĸ。不响åē”įš„æœåŠĄå™¨å°†čĸĢæš‚æ—ļåŋŊį•ĨīŧŒį›´åˆ°åރäģŦé‡æ–°č”æœē。", - "maintenance_settings": "įģ´æŠ¤æ¨Ąåŧ", - "maintenance_settings_description": "将ImmichįŊŽäēŽįģ´æŠ¤æ¨Ąåŧã€‚", - "maintenance_start": "åŧ€å¯įģ´æŠ¤æ¨Ąåŧ", - "maintenance_start_error": "åŧ€å¯įģ´æŠ¤æ¨Ąåŧå¤ąč´Ĩ。", - "manage_concurrency": "įŽĄį†äģģåŠĄåšļ发", - "manage_concurrency_description": "å¯ŧčˆĒ到äģģåŠĄéĄĩéĸäģĨįŽĄį†äģģåŠĄåšļ发性", + "machine_learning_smart_search_enabled_description": "č‹ĨįρᔍīŧŒå›žį‰‡å°†ä¸äŧščĸĢįŧ–᠁äģĨᔍäēŽæ™ēčƒŊ搜į´ĸ。", + "machine_learning_url_description": "æœē器å­Ļäš æœåŠĄå™¨įš„ URL。č‹Ĩ提䞛多ä¸Ē URLīŧŒįŗģįģŸå°†æŒ‰äģŽå‰åž€åŽįš„éĄēåēé€ä¸Ēå°č¯•čŋžæŽĨīŧŒį›´č‡ŗæœ‰æœåŠĄå™¨æˆåŠŸå“åē”ä¸ēæ­ĸ。æœĒčƒŊ响åē”įš„æœåŠĄå™¨å°†čĸĢæš‚æ—ļåŋŊį•ĨīŧŒį›´č‡ŗå…￁ĸ复在įēŋ。", + "maintenance_delete_backup": "删除备äģŊ", + "maintenance_delete_backup_description": "此文äģļ将čĸĢæ°¸äš…删除。", + "maintenance_delete_error": "删除备äģŊå¤ąč´Ĩ。", + "maintenance_restore_backup": "æĸ复备äģŊ", + "maintenance_restore_backup_description": "Immich 数捎将čĸĢæ¸…除īŧŒåšļäģŽé€‰åŽšįš„å¤‡äģŊ中æĸ复。在įģ§įģ­äš‹å‰īŧŒå°†å…ˆåˆ›åģē一ä¸ĒåŊ“å‰æ•°æŽįš„å¤‡äģŊ。", + "maintenance_restore_backup_different_version": "此备äģŊæ˜¯į”ąä¸åŒį‰ˆæœŦįš„ Immich 创åģēįš„īŧ", + "maintenance_restore_backup_unknown_version": "æ— æŗ•įĄŽåŽšå¤‡äģŊį‰ˆæœŦ。", + "maintenance_restore_database_backup": "æĸ复数捎åē“备äģŊ", + "maintenance_restore_database_backup_description": "äŊŋᔍ备äģŊ文äģļ将数捎åē“回æģšåˆ°čžƒæ—Šįš„įŠļ态", + "maintenance_settings": "į촿Ф", + "maintenance_settings_description": "吝ᔍ Immich įģ´æŠ¤æ¨Ąåŧã€‚", + "maintenance_start": "切æĸ到įģ´æŠ¤æ¨Ąåŧ", + "maintenance_start_error": "įģ´æŠ¤æ¨Ąåŧå¯åŠ¨å¤ąč´Ĩ。", + "maintenance_upload_backup": "上äŧ æ•°æŽåē“备äģŊ文äģļ", + "maintenance_upload_backup_error": "æ— æŗ•ä¸Šäŧ å¤‡äģŊīŧŒåŽƒæ˜¯ .sql 或 .sql.gz æ ŧåŧįš„æ–‡äģļ吗īŧŸ", + "manage_concurrency": "įŽĄį†åšļ发数量", + "manage_concurrency_description": "前垀äģģåŠĄéĄĩéĸäģĨįŽĄį†äģģåŠĄåšļ发数量", "manage_log_settings": "įŽĄį†æ—Ĩåŋ—莞įŊŽ", - "map_dark_style": "æˇąč‰˛æ¨Ąåŧ", + "map_dark_style": "æˇąč‰˛éŖŽæ ŧ", "map_enable_description": "å¯į”¨åœ°å›žåŠŸčƒŊ", "map_gps_settings": "地回与 GPS 莞įŊŽ", - "map_gps_settings_description": "įŽĄį†åœ°å›žä¸Ž GPSīŧˆåå‘åœ°į†įŧ–᠁īŧ‰čŽžįŊŽ", - "map_implications": "地回功čƒŊ䞝čĩ–äēŽå¤–部地回į“Ļį‰‡æœåŠĄīŧˆtiles.immich.cloudīŧ‰", - "map_light_style": "æĩ…č‰˛æ¨Ąåŧ", - "map_manage_reverse_geocoding_settings": "įŽĄį†åå‘åœ°į†įŧ–į čŽžįŊŽ", - "map_reverse_geocoding": "åå‘åœ°į†įŧ–᠁", - "map_reverse_geocoding_enable_description": "å¯į”¨åå‘åœ°į†įŧ–᠁", - "map_reverse_geocoding_settings": "åå‘åœ°į†įŧ–į čŽžįŊŽ", + "map_gps_settings_description": "įŽĄį†åœ°å›žä¸Ž GPSīŧˆé€†åœ°į†įŧ–᠁īŧ‰čŽžįŊŽ", + "map_implications": "地回功čƒŊ䞝čĩ–äēŽå¤–部į“Ļį‰‡æœåŠĄ (tiles.immich.cloud)", + "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_style_description": "地回ä¸ģéĸ˜ style.json įš„ URL", - "memory_cleanup_job": "清įŠē回åŋ†", + "map_style_description": "style.json 地回ä¸ģéĸ˜įš„ URL", + "memory_cleanup_job": "æ¸…į†å›žåŋ†æ•°æŽ", "memory_generate_job": "į”Ÿæˆå›žåŋ†", "metadata_extraction_job": "提取元数捎", - "metadata_extraction_job_description": "äģŽæ¯ä¸ĒéĄšį›Žä¸­æå–å…ƒæ•°æŽäŋĄæ¯īŧŒåĻ‚ GPS、äēēč„¸å’Œåˆ†čž¨įŽ‡", + "metadata_extraction_job_description": "äģŽæ¯ä¸Ēčĩ„äē§ä¸­æå–元数捎äŋĄæ¯īŧŒäž‹åĻ‚ GPS、äēēč„¸å’Œåˆ†čž¨įŽ‡", "metadata_faces_import_setting": "吝ᔍäēē脸å¯ŧå…Ĩ", - "metadata_faces_import_setting_description": "äģŽå›žį‰‡įš„ EXIF å’Œčž…åŠŠå…ƒæ•°æŽä¸­å¯ŧå…Ĩäēē脸", + "metadata_faces_import_setting_description": "äģŽå›žį‰‡ EXIF 数捎和附å¸Ļ文äģļ中å¯ŧå…Ĩäēē脸äŋĄæ¯", "metadata_settings": "å…ƒæ•°æŽčŽžįŊŽ", "metadata_settings_description": "įŽĄį†å…ƒæ•°æŽčŽžįŊŽ", "migration_job": "čŋį§ģ", - "migration_job_description": "å°†éĄšį›Žå’Œäēē脏蝆åˆĢįš„įŧŠį•Ĩ回čŋį§ģåˆ°æœ€æ–°įš„æ–‡äģļ多į쓿ž„", - "nightly_tasks_cluster_faces_setting_description": "å¯šæ–°æŖ€æĩ‹åˆ°įš„éĸ部čŋ›čĄŒéĸéƒ¨č¯†åˆĢ", - "nightly_tasks_cluster_new_faces_setting": "įž¤į섿–°äēē脸", + "migration_job_description": "将åĒ’äŊ“æ–‡äģļ和äēēč„¸įš„įŧŠį•Ĩ回čŋį§ģåˆ°æœ€æ–°įš„æ–‡äģļ多į쓿ž„", + "nightly_tasks_cluster_faces_setting_description": "å¯šæ–°æŖ€æĩ‹åˆ°įš„äēē脸čŋčĄŒäēē脏蝆åˆĢ", + "nightly_tasks_cluster_new_faces_setting": "聚įąģæ–°æŖ€æĩ‹åˆ°įš„äēē脸", "nightly_tasks_database_cleanup_setting": "数捎å瓿¸…ᐆäģģåŠĄ", "nightly_tasks_database_cleanup_setting_description": "æ¸…į†æ•°æŽåē“中čŋ‡æœŸįš„æ—§æ•°æŽ", "nightly_tasks_generate_memories_setting": "į”Ÿæˆå›žåŋ†", - "nightly_tasks_generate_memories_setting_description": "äģŽéĄšį›Žä¸­į”Ÿæˆæ–°įš„回åŋ†", + "nightly_tasks_generate_memories_setting_description": "åŸēäēŽåĒ’äŊ“æ–‡äģļį”Ÿæˆæ–°įš„å›žåŋ†", "nightly_tasks_missing_thumbnails_setting": "į”Ÿæˆįŧēå¤ąįš„įŧŠį•Ĩ回", - "nightly_tasks_missing_thumbnails_setting_description": "ä¸ēį”ŸæˆįŧŠį•Ĩ回队列无įŧŠį•Ĩå›žįš„éĄšį›Ž", - "nightly_tasks_settings": "夜间äģģåŠĄčŽžįŊŽ", - "nightly_tasks_settings_description": "įŽĄį†å¤œé—´äģģåŠĄ", + "nightly_tasks_missing_thumbnails_setting_description": "将无įŧŠį•Ĩå›žįš„åĒ’äŊ“æ–‡äģļ加å…Ĩ队列äģĨį”ŸæˆįŧŠį•Ĩ回", + "nightly_tasks_settings": "每æ—ĨäģģåŠĄčŽžįŊŽ", + "nightly_tasks_settings_description": "įŽĄį†æ¯æ—ĨäģģåŠĄ", "nightly_tasks_start_time_setting": "åŧ€å§‹æ—ļ间", - "nightly_tasks_start_time_setting_description": "æœåŠĄå™¨åŧ€å§‹čŋčĄŒå¤œé—´äģģåŠĄįš„æ—ļ间", + "nightly_tasks_start_time_setting_description": "æœåŠĄå™¨åŧ€å§‹æ‰§čĄŒæ¯æ—ĨäģģåŠĄįš„æ—ļ间", "nightly_tasks_sync_quota_usage_setting": "同æ­Ĩ配éĸäŊŋį”¨æƒ…å†ĩ", - "nightly_tasks_sync_quota_usage_setting_description": "栚捎åŊ“前äŊŋį”¨æƒ…å†ĩæ›´æ–°į”¨æˆˇå­˜å‚¨é…éĸ", - "no_paths_added": "æ— åˇ˛æˇģåŠ čˇ¯åž„", - "no_pattern_added": "æ— åˇ˛æˇģåŠ č§„åˆ™", - "note_apply_storage_label_previous_assets": "提į¤ēīŧščĻå°†å­˜å‚¨æ ‡į­žåē”ᔍäēŽäš‹å‰ä¸Šäŧ įš„éĄšį›ŽīŧŒéœ€čρčŋčĄŒ", - "note_cannot_be_changed_later": "æŗ¨æ„īŧšæ­¤éĄšä¸€æ—ĻčŽžåŽšīŧŒäģĨåŽæ— æŗ•æ›´æ”šīŧ", + "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": "发äģļäēēé‚ŽįŽąīŧŒäž‹åĻ‚īŧšâ€œåŧ ä¸‰<12345@qq.com>â€ã€‚č¯ˇįĄŽäŋäŊŋį”¨å…čŽ¸æ‚¨å‘é€į”ĩ子邎äģļįš„åœ°å€ã€‚", - "notification_email_host_description": "æœåŠĄå™¨åœ°å€īŧˆäž‹åĻ‚īŧšsmtp.qq.comīŧ‰", + "notification_email_from_address_description": "发äģļäēēé‚ŽįŽąåœ°å€īŧŒäž‹åĻ‚īŧšâ€œImmich į…§į‰‡æœåŠĄå™¨ â€ã€‚č¯ˇįĄŽäŋäŊŋį”¨æ‚¨æœ‰æƒäŊŋį”¨įš„é‚ŽįŽąåœ°å€čŋ›čĄŒå‘送。", + "notification_email_host_description": "邮äģᅵåŠĄå™¨ä¸ģæœēīŧˆäž‹åĻ‚īŧšsmtp.immich.appīŧ‰", "notification_email_ignore_certificate_errors": "åŋŊį•Ĩ蝁äšĻ错蝝", - "notification_email_ignore_certificate_errors_description": "åŋŊį•Ĩ TLS 蝁äšĻéĒŒč¯é”™č¯¯īŧˆä¸åģē莎īŧ‰", - "notification_email_password_description": "与邮äģᅵåŠĄå™¨čŋ›čĄŒčēĢäģŊénj蝁æ—ļäŊŋį”¨įš„å¯†į ", - "notification_email_port_description": "邮äģᅵåŠĄå™¨įĢ¯åŖīŧˆäž‹åĻ‚ 25、465 或 587īŧ‰", + "notification_email_ignore_certificate_errors_description": "åŋŊį•Ĩ TLS 蝁äšĻéĒŒč¯é”™č¯¯īŧˆä¸æŽ¨čīŧ‰", + "notification_email_password_description": "čŋžæŽĨ邮äģᅵåŠĄå™¨čŋ›čĄŒčēĢäģŊénj蝁æ—ļäŊŋį”¨įš„å¯†į ", + "notification_email_port_description": "邮äģᅵåŠĄå™¨įĢ¯åŖīŧˆäž‹åĻ‚īŧš25、465 或 587īŧ‰", "notification_email_secure": "SMTPS", "notification_email_secure_description": "äŊŋᔍSMTPSīŧˆåŸēäēŽTLSįš„SMTPīŧ‰", "notification_email_sent_test_email_button": "发送æĩ‹č¯•邎äģļåšļäŋå­˜", - "notification_email_setting_description": "发送邎äģļ通įŸĨ莞įŊŽ", + "notification_email_setting_description": "邮äģļ通įŸĨå‘é€čŽžįŊŽ", "notification_email_test_email": "发送æĩ‹č¯•邎äģļ", - "notification_email_test_email_failed": "发送æĩ‹č¯•邎äģļå¤ąč´ĨīŧŒč¯ˇæŖ€æŸĨæ‚¨čž“å…Ĩįš„äŋĄæ¯", - "notification_email_test_email_sent": "厞向 {email} 发送äē†ä¸€å°æĩ‹č¯•邎äģļīŧŒč¯ˇæŗ¨æ„æŸĨæ”ļ。", - "notification_email_username_description": "与邮äģᅵåŠĄå™¨čŋ›čĄŒčēĢäģŊénj蝁æ—ļäŊŋį”¨įš„į”¨æˆˇå", + "notification_email_test_email_failed": "发送æĩ‹č¯•邎äģļå¤ąč´ĨīŧŒč¯ˇæŖ€æŸĨæ‚¨įš„é…įŊŽäŋĄæ¯", + "notification_email_test_email_sent": "厞向 {email} 发送æĩ‹č¯•邎äģļīŧŒč¯ˇæŸĨæ”ļ。", + "notification_email_username_description": "čŋžæŽĨ邮äģᅵåŠĄå™¨čŋ›čĄŒčēĢäģŊénj蝁æ—ļäŊŋį”¨įš„į”¨æˆˇå", "notification_enable_email_notifications": "å¯į”¨é‚Žäģļ通įŸĨ", "notification_settings": "通įŸĨ莞įŊŽ", - "notification_settings_description": "įŽĄį†é€šįŸĨ莞įŊŽīŧŒåŒ…æ‹Ŧ邮äģļ", + "notification_settings_description": "įŽĄį†é€šįŸĨ莞įŊŽīŧŒåŒ…æ‹Ŧ邮äģļ通įŸĨ", "oauth_auto_launch": "č‡Ē动启动", - "oauth_auto_launch_description": "在į™ģåŊ•éĄĩéĸč‡Ē动启动 OAuth į™ģåŊ•", + "oauth_auto_launch_description": "čŋ›å…Ĩį™ģåŊ•éĄĩéĸæ—ļīŧŒč‡Ē动åŧ€å§‹ OAuth į™ģåŊ•æĩį¨‹", "oauth_auto_register": "č‡ĒåŠ¨æŗ¨å†Œ", - "oauth_auto_register_description": "äŊŋᔍ OAuth į™ģåŊ•后č‡ĒåŠ¨æŗ¨å†Œä¸ēæ–°į”¨æˆˇ", - "oauth_button_text": "按钎文æœŦ", - "oauth_client_secret_description": "åĻ‚æžœ OAuth 提䞛商不支持 PKCEīŧˆį”¨äēŽäģŖį ä礿ĸįš„č¯æ˜Žå¯†é’Ĩīŧ‰īŧŒåˆ™ä¸ēåŋ…åĄĢ饚", + "oauth_auto_register_description": "į”¨æˆˇé€ščŋ‡ OAuth į™ģåŊ•后īŧŒč‡Ē动ä¸ēå…ļæŗ¨å†Œæ–°č´Ļæˆˇ", + "oauth_button_text": "按钎文字", + "oauth_client_secret_description": "æœē密åŽĸæˆˇį̝åŋ…åĄĢīŧŒæˆ–å…Ŧå…ąåŽĸæˆˇį̝č‹Ĩ不支持 PKCEīŧˆäģŖį ä礿ĸč¯æ˜Žå¯†é’Ĩīŧ‰æ—ļåŋ…åĄĢ。", "oauth_enable_description": "äŊŋᔍ OAuth į™ģåŊ•", "oauth_mobile_redirect_uri": "į§ģ动įĢ¯é‡åŽšå‘ URI", "oauth_mobile_redirect_uri_override": "į§ģ动įĢ¯é‡åŽšå‘ URI čφᛖ", - "oauth_mobile_redirect_uri_override_description": "åŊ“ OAuth æäž›å•†ä¸å…čŽ¸äŊŋᔍį§ģ动 URI æ—ļ吝ᔍīŧŒåĻ‚â€œ{callback}”", + "oauth_mobile_redirect_uri_override_description": "åŊ“ OAuth æäž›å•†ä¸å…čŽ¸äŊŋᔍį§ģ动į̝ URIīŧˆäž‹åĻ‚ “{callback}”īŧ‰æ—ļ吝ᔍ", "oauth_role_claim": "č§’č‰˛åŖ°æ˜Ž", "oauth_role_claim_description": "æ šæŽæ­¤åŖ°æ˜Žįš„å­˜åœ¨č‡Ē动授äēˆįŽĄį†å‘˜čŽŋé—Žæƒé™ã€‚åŖ°æ˜Žå¯äģĨ是“user”īŧˆį”¨æˆˇīŧ‰æˆ–“admin”īŧˆįŽĄį†å‘˜īŧ‰ã€‚", "oauth_settings": "OAuth", "oauth_settings_description": "įŽĄį† OAuth į™ģåŊ•莞įŊŽ", - "oauth_settings_more_details": "å…ŗäēŽæ­¤åŠŸčƒŊįš„æ›´å¤šč¯Ļįģ†äŋĄæ¯īŧŒč¯ˇæŸĨįœ‹į›¸å…ŗæ–‡æĄŖã€‚", + "oauth_settings_more_details": "æœ‰å…ŗæ­¤åŠŸčƒŊįš„æ›´å¤šč¯Ļ情īŧŒč¯ˇå‚阅 į›¸å…ŗæ–‡æĄŖã€‚", "oauth_storage_label_claim": "å­˜å‚¨æ ‡į­žåŖ°æ˜Ž", - "oauth_storage_label_claim_description": "č‡ĒåŠ¨å°†į”¨æˆˇįš„å­˜å‚¨æ ‡į­žčŽžįŊŽä¸ēæ­¤éĄšįš„å€ŧ。", + "oauth_storage_label_claim_description": "č‡ĒåŠ¨å°†į”¨æˆˇįš„å­˜å‚¨æ ‡į­žčŽžįŊŽä¸ēč¯ĨåŖ°æ˜Žįš„å€ŧ。", "oauth_storage_quota_claim": "存储配éĸåŖ°æ˜Ž", - "oauth_storage_quota_claim_description": "č‡ĒåŠ¨å°†į”¨æˆˇįš„å­˜å‚¨é…éĸčŽžįŊŽä¸ēæ­¤éĄšįš„å€ŧ。", + "oauth_storage_quota_claim_description": "č‡ĒåŠ¨å°†į”¨æˆˇįš„å­˜å‚¨é…éĸčŽžįŊŽä¸ēč¯ĨåŖ°æ˜Žįš„å€ŧ。", "oauth_storage_quota_default": "éģ˜čŽ¤å­˜å‚¨é…éĸīŧˆGiBīŧ‰", - "oauth_storage_quota_default_description": "æœĒæäž›åŖ°æ˜Žæ—ļäŊŋį”¨įš„é…éĸīŧˆGiBīŧ‰ã€‚", + "oauth_storage_quota_default_description": "åŊ“æœĒæäž›åŖ°æ˜Žæ—ļīŧŒå°†äŊŋį”¨įš„é…éĸīŧˆGiBīŧ‰ã€‚", "oauth_timeout": "č¯ˇæą‚čļ…æ—ļ", - "oauth_timeout_description": "č¯ˇæą‚čļ…æ—ļīŧˆæ¯Ģį§’īŧ‰", - "ocr_job_description": "äŊŋᔍæœē器å­Ļäš č¯†åˆĢå›žį‰‡ä¸­įš„æ–‡æœŦ", + "oauth_timeout_description": "č¯ˇæą‚čļ…æ—ļæ—ļ间īŧˆæ¯Ģį§’īŧ‰", + "ocr_job_description": "åˆŠį”¨æœē器å­Ļäš æŠ€æœ¯č¯†åˆĢå›žåƒä¸­įš„æ–‡æœŦ", "password_enable_description": "äŊŋį”¨é‚ŽįŽąå’Œå¯†į į™ģåŊ•", "password_settings": "坆᠁į™ģåŊ•", "password_settings_description": "įŽĄį†å¯†į į™ģåŊ•莞įŊŽ", - "paths_validated_successfully": "æ‰€æœ‰čˇ¯åž„éĒŒč¯æˆåŠŸ", - "person_cleanup_job": "æ¸…į†äēēį‰Š", + "paths_validated_successfully": "æ‰€æœ‰čˇ¯åž„å‡åˇ˛æˆåŠŸénj蝁", + "person_cleanup_job": "äēēå‘˜æ¸…į†", "queue_details": "队列č¯Ļ情", "queues": "äģģåŠĄé˜Ÿåˆ—", - "queues_page_description": "įŽĄį†äŊœä¸šé˜Ÿåˆ—éĄĩéĸ", + "queues_page_description": "įŽĄį†äģģåŠĄé˜Ÿåˆ—éĄĩéĸ", "quota_size_gib": "配éĸå¤§å°īŧˆGiBīŧ‰", - "refreshing_all_libraries": "åˆˇæ–°æ‰€æœ‰å›žåē“", - "registration": "æŗ¨å†ŒįŽĄį†å‘˜", - "registration_description": "į”ąäēŽæ‚¨æ˜¯įŗģįģŸä¸Šįš„įŦŦ一ä¸Ēį”¨æˆˇīŧŒæ‚¨å°†čĸĢæŒ‡åޚä¸ēįŽĄį†å‘˜åšļč´Ÿč´ŖįŽĄį†äģģåŠĄīŧŒį”ąæ‚¨æĨ创åģēæ–°įš„į”¨æˆˇã€‚", - "remove_failed_jobs": "åˆ é™¤å¤ąč´Ĩįš„äŊœä¸š", - "require_password_change_on_login": "čĻæą‚į”¨æˆˇéĻ–æŦĄį™ģåŊ•æ—ļæ›´æ”šå¯†į ", - "reset_settings_to_default": "æĸ复éģ˜čŽ¤čŽžįŊŽ", - "reset_settings_to_recent_saved": "æĸ复到最čŋ‘äŋå­˜įš„莞įŊŽ", - "scanning_library": "æ‰Ģ描回åē“", + "refreshing_all_libraries": "åˆˇæ–°æ‰€æœ‰åē“", + "registration": "įŽĄį†å‘˜æŗ¨å†Œ", + "registration_description": "į”ąäēŽæ‚¨æ˜¯įŗģįģŸįš„įŦŦ一äŊį”¨æˆˇīŧŒįŗģįģŸå°†č‡Ē动ä¸ēæ‚¨åˆ†é…įŽĄį†å‘˜æƒé™ã€‚æ‚¨éœ€čĻč´Ÿč´Ŗį›¸å…ŗįš„įŽĄį†äģģåŠĄīŧŒåŽįģ­įš„å…ļäģ–į”¨æˆˇäšŸå°†į”ąæ‚¨æĨ创åģē。", + "remove_failed_jobs": "į§ģé™¤å¤ąč´ĨäģģåŠĄ", + "require_password_change_on_login": "åŧēåˆļį”¨æˆˇéĻ–æŦĄį™ģåŊ•æ—ļäŋŽæ”šå¯†į ", + "reset_settings_to_default": "å°†čŽžįŊŽé‡įŊŽä¸ēéģ˜čޤå€ŧ", + "reset_settings_to_recent_saved": "å°†čŽžįŊŽé‡įŊŽä¸ē上æŦĄäŋå­˜įš„å€ŧ", + "scanning_library": "æ­Ŗåœ¨æ‰Ģ描čĩ„æ–™åē“", "search_jobs": "搜į´ĸäģģåŠĄâ€Ļ", "send_welcome_email": "发送æŦĸčŋŽé‚Žäģļ", "server_external_domain_settings": "外部域名", - "server_external_domain_settings_description": "å…ąäēĢ链æŽĨ域名īŧŒåŒ…æ‹Ŧ http(s)://", - "server_public_users": "å…Ŧå…ąį”¨æˆˇ", - "server_public_users_description": "å°†į”¨æˆˇæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒäŧšåˆ—å‡ēæ‰€æœ‰į”¨æˆˇīŧˆå§“åå’Œé‚ŽįŽąīŧ‰ã€‚įĻį”¨åŽīŧŒį”¨æˆˇåˆ—čĄ¨å°†äģ…å¯šįŽĄį†å‘˜į”¨æˆˇå¯į”¨ã€‚", + "server_external_domain_settings_description": "å…Ŧåŧ€åˆ†äēĢ链æŽĨįš„åŸŸåīŧŒéœ€åŒ…åĢ http(s)://", + "server_public_users": "į”¨æˆˇå…Ŧåŧ€", + "server_public_users_description": "åœ¨å°†į”¨æˆˇæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒæ‰€æœ‰į”¨æˆˇīŧˆå§“åå’Œé‚ŽįŽąīŧ‰éƒŊäŧščĸĢ列å‡ē。č‹Ĩå…ŗé—­æ­¤åŠŸčƒŊīŧŒį”¨æˆˇåˆ—čĄ¨å°†äģ…å¯šįŽĄį†å‘˜å¯č§ã€‚", "server_settings": "æœåŠĄå™¨čŽžįŊŽ", "server_settings_description": "įŽĄį†æœåŠĄå™¨čŽžįŊŽ", "server_stats_page_description": "įŽĄį†æœåŠĄå™¨įģŸčŽĄéĄĩéĸ", - "server_welcome_message": "æŦĸčŋŽæļˆæ¯", - "server_welcome_message_description": "昞į¤ē在į™ģåŊ•éĄĩéĸä¸Šįš„æļˆæ¯ã€‚", + "server_welcome_message": "æŦĸčŋŽäŋĄæ¯", + "server_welcome_message_description": "一æŽĩ昞į¤ē在į™ģåŊ•éĄĩéĸįš„æļˆæ¯ã€‚", "settings_page_description": "įŽĄį†å‘˜čŽžįŊŽéĄĩéĸ", - "sidecar_job": "čž…åŠŠå…ƒæ•°æŽ", - "sidecar_job_description": "äģŽæ–‡äģļįŗģįģŸä¸­å‘įŽ°æˆ–åŒæ­Ĩčž…åŠŠå…ƒæ•°æŽ", - "slideshow_duration_description": "昞į¤ē每åŧ å›žåƒįš„į§’æ•°", - "smart_search_job_description": "å¯šéĄšį›Žčŋ›čĄŒæœē器å­Ļäš å¤„į†äģĨᔍäēŽæ™ēčƒŊ搜į´ĸ", - "storage_template_date_time_description": "äŊŋį”¨éĄšį›Žįš„åˆ›åģēæ—ļé—´æˆŗäŊœä¸ēæ—Ĩ期æ—ļ间äŋĄæ¯", - "storage_template_date_time_sample": "é‡‡æ ˇæ—ļ间 {date}", - "storage_template_enable_description": "å¯į”¨å­˜å‚¨æ¨Ąæŋ", - "storage_template_hash_verification_enabled": "å“ˆå¸Œæ ĄéĒŒåˇ˛å¯į”¨", - "storage_template_hash_verification_enabled_description": "å¯į”¨å“ˆå¸Œæ ĄénjīŧŒåĻ‚æžœæ‚¨ä¸įŸĨé“æ­¤éĄšįš„äŊœį”¨č¯ˇä¸čρįĻį”¨æ­¤åŠŸčƒŊ", - "storage_template_migration": "å­˜å‚¨æ¨ĄæŋčŊŦæĸ", - "storage_template_migration_description": "åē”ᔍåŊ“å‰įš„{template}到䚋前上äŧ įš„éĄšį›Ž", - "storage_template_migration_info": "å­˜å‚¨æ¨Ąæŋäŧšå°†æ‰€æœ‰æ‰Šåą•名čŊŦæĸä¸ēå°å†™ã€‚æ¨ĄæŋäŋŽæ”šåĒäŧšäŊœį”¨äēŽæ–°įš„éĄšį›ŽīŧŒåĻ‚éœ€åē”į”¨æ­¤æ¨Ąæŋ到䚋前上äŧ įš„éĄšį›ŽīŧŒč¯ˇčŋčĄŒ{job}。", - "storage_template_migration_job": "å­˜å‚¨æ¨ĄæŋčŊŦæĸäģģåŠĄ", - "storage_template_more_details": "å…ŗäēŽæœŦ功čƒŊįš„æ›´å¤šįģ†čŠ‚īŧŒč¯ˇå‚č§å­˜å‚¨æ¨Ąæŋ及å…ļåŽžįŽ°æ–šåŧ", - "storage_template_onboarding_description_v2": "å¯į”¨åŽīŧŒč¯Ĩ功čƒŊå°†æ šæŽį”¨æˆˇåŽšäš‰įš„æ¨Ąæŋč‡ĒåŠ¨æ•´į†æ–‡äģļã€‚æœ‰å…ŗč¯Ļįģ†äŋĄæ¯īŧŒč¯ˇå‚阅 æ–‡æĄŖã€‚", - "storage_template_path_length": "čˇ¯åž„įš„å­—įŦĻé•ŋåēĻ及限åˆļīŧš{length, number}/{limit, number}", + "sidecar_job": "é™„åąžå…ƒæ•°æŽ", + "sidecar_job_description": "äģŽæ–‡äģļįŗģįģŸä¸­å‘įŽ°æˆ–åŒæ­Ĩé™„åąžå…ƒæ•°æŽ", + "slideshow_duration_description": "每åŧ å›žį‰‡æ˜žį¤ēįš„į§’æ•°", + "smart_search_job_description": "寚čĩ„äē§čŋčĄŒæœē器å­Ļäš äģĨ支持æ™ēčƒŊ搜į´ĸ", + "storage_template_date_time_description": "čĩ„äē§įš„创åģēæ—ļé—´æˆŗį”¨äēŽæ—Ĩ期æ—ļ间äŋĄæ¯", + "storage_template_date_time_sample": "į¤ē例æ—ļ间īŧš{date}", + "storage_template_enable_description": "å¯į”¨å­˜å‚¨æ¨Ąæŋåŧ•擎", + "storage_template_hash_verification_enabled": "å¯į”¨å“ˆå¸Œæ Ąénj", + "storage_template_hash_verification_enabled_description": "åŧ€å¯å“ˆå¸Œæ Ąénj功čƒŊ。č‹Ĩ不清æĨšå…ŗé—­įš„后果īŧŒč¯ˇå‹ŋå…ŗé—­", + "storage_template_migration": "å­˜å‚¨æ¨Ąæŋčŋį§ģ", + "storage_template_migration_description": "将åŊ“前 {template} åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„čĩ„äē§", + "storage_template_migration_info": "å­˜å‚¨æ¨Ąæŋäŧšå°†æ‰€æœ‰æ–‡äģ￉Šåą•名čŊŦæĸä¸ēå°å†™ã€‚æ¨Ąæŋ更攚äģ…寚新上äŧ įš„čĩ„äē§į”Ÿæ•ˆã€‚č‹ĨčĻå°†æ¨Ąæŋ回æē¯åē”ᔍäēŽåˇ˛ä¸Šäŧ įš„čĩ„äē§īŧŒč¯ˇčŋčĄŒ {job}。", + "storage_template_migration_job": "å­˜å‚¨æ¨Ąæŋčŋį§ģäģģåŠĄ", + "storage_template_more_details": "æœ‰å…ŗæ­¤åŠŸčƒŊįš„æ›´å¤šč¯Ļįģ†äŋĄæ¯īŧŒč¯ˇå‚阅 å­˜å‚¨æ¨Ąæŋ 及å…ļ åĢ义", + "storage_template_onboarding_description_v2": "å¯į”¨åŽīŧŒæ­¤åŠŸčƒŊå°†æ šæŽį”¨æˆˇåŽšäš‰įš„æ¨Ąæŋč‡ĒåŠ¨æ•´į†æ–‡äģļ。更多äŋĄæ¯īŧŒč¯ˇå‚阅 æ–‡æĄŖã€‚", + "storage_template_path_length": "čŋ‘äŧŧčˇ¯åž„é•ŋåēĻ限åˆļīŧš{length, number}/{limit, number}", "storage_template_settings": "å­˜å‚¨æ¨Ąæŋ", - "storage_template_settings_description": "įŽĄį†ä¸Šäŧ éĄšį›Žæ–‡äģļ多į쓿ž„和文äģļ名", - "storage_template_user_label": "{label}æ˜¯į”¨æˆˇįš„å­˜å‚¨æ ‡į­ž", + "storage_template_settings_description": "įŽĄį†ä¸Šäŧ čĩ„äē§æ–‡äģļ多į쓿ž„和文äģļ名", + "storage_template_user_label": "{label}ä¸ēį”¨æˆˇįš„å­˜å‚¨æ ‡į­ž", "system_settings": "įŗģįģŸčŽžįŊŽ", - "tag_cleanup_job": "æ¸…į†æ ‡į­ž", - "template_email_available_tags": "可äģĨåœ¨æ¨Ąæŋ中äŊŋᔍäģĨ下变量īŧš{tags}", - "template_email_if_empty": "åĻ‚æžœæ¨Ąæŋä¸ēįŠēīŧŒåˆ™äŊŋᔍéģ˜čŽ¤æ¨Ąæŋ。", - "template_email_invite_album": "į›¸å†Œé‚€č¯ˇæ¨Ąæŋ", + "tag_cleanup_job": "æ ‡į­žæ¸…į†", + "template_email_available_tags": "您可äģĨåœ¨æ¨Ąæŋ中äŊŋᔍäģĨ下变量īŧš{tags}", + "template_email_if_empty": "åĻ‚æžœæ¨Ąæŋä¸ēįŠēīŧŒåˆ™äŊŋᔍéģ˜čŽ¤é‚ŽįŽąã€‚", + "template_email_invite_album": "é‚€č¯ˇį›¸å†Œæ¨Ąæŋ", "template_email_preview": "éĸ„č§ˆ", "template_email_settings": "邮äģᅪĄæŋ", - "template_email_update_album": "į›¸å†Œæ›´æ–°æ¨Ąæŋ", + "template_email_update_album": "æ›´æ–°į›¸å†Œæ¨Ąæŋ", "template_email_welcome": "æŦĸčŋŽé‚ŽäģᅪĄæŋ", "template_settings": "通įŸĨæ¨Ąæŋ", - "template_settings_description": "įŽĄį†č‡Ē厚䚉通įŸĨæ¨Ąæŋ", + "template_settings_description": "įŽĄį†é€šįŸĨįš„č‡ĒåŽšäš‰æ¨Ąæŋ", "theme_custom_css_settings": "č‡Ē厚䚉 CSS", - "theme_custom_css_settings_description": "可äģĨ通čŋ‡ CSS č‡Ē厚䚉 Immich å¤–č§‚ã€‚", + "theme_custom_css_settings_description": "CSS å…čŽ¸č‡Ē厚䚉 Immich į•ŒéĸčŽžčŽĄã€‚", "theme_settings": "ä¸ģéĸ˜čŽžįŊŽ", "theme_settings_description": "č‡Ē厚䚉 Immich Web į•Œéĸ", "thumbnail_generation_job": "į”ŸæˆįŧŠį•Ĩ回", - "thumbnail_generation_job_description": "ä¸ē每ä¸ĒéĄšį›Žį”Ÿæˆä¸åŒå°ēå¯¸įš„įŧŠį•Ĩ回īŧŒåšļä¸ē每ä¸Ēäēēį‰Šį”ŸæˆįŧŠį•Ĩ回", - "transcoding_acceleration_api": "加速器 API", - "transcoding_acceleration_api_description": "čŋ™ä¸Ē API 将äŧšä¸Žæ‚¨įš„čŽžå¤‡čŋ›čĄŒäē¤äē’īŧŒäģĨ加速čŊŦ᠁čŋ‡į¨‹ã€‚æ­¤čŽžįŊŽä¸ē“å°ŊåŠ›č€Œä¸ē”——åĻ‚æžœčŊŦį å¤ąč´ĨīŧŒå°†äŧšå›žé€€åˆ°čŊ¯äģļčŊŦį ã€‚VP9 是åĻåˇĨäŊœå–冺äēŽæ‚¨įš„įĄŦäģļ配įŊŽã€‚", - "transcoding_acceleration_nvenc": "NVENCīŧˆéœ€čρ NVIDIA GPUīŧ‰", + "thumbnail_generation_job_description": "ä¸ē每ä¸Ēčĩ„äē§į”Ÿæˆä¸åŒå°ēå¯¸įš„įŧŠį•Ĩ回īŧŒåšļä¸ē每ä¸Ēäēēį‰Šį”ŸæˆįŧŠį•Ĩ回", + "transcoding_acceleration_api": "įĄŦäģļ加速 API", + "transcoding_acceleration_api_description": "ᔍäēŽä¸ŽčŽžå¤‡äē¤äē’äģĨ加速čŊŦį įš„ API。č¯Ĩ莞įŊŽé‡‡į”¨â€œå°ŊåŠ›č€Œä¸ēâ€į­–į•Ĩīŧšč‹ĨįĄŦäģļåŠ é€Ÿå¤ąč´ĨīŧŒįŗģįģŸå°†č‡Ē动回退到čŊ¯äģļčŊŦį ã€‚VP9 įŧ–į įš„æ”¯æŒæƒ…å†ĩå–å†ŗäēŽæ‚¨įš„įĄŦäģļ配įŊŽã€‚", + "transcoding_acceleration_nvenc": "NVENCīŧˆéœ€čρ NVIDIA æ˜žåĄīŧ‰", "transcoding_acceleration_qsv": "Quick Syncīŧˆéœ€čρ Intel 7äģŖåŠäģĨä¸Šįš„ CPUīŧ‰", "transcoding_acceleration_rkmpp": "RKMPPīŧˆäģ…适ᔍäēŽ Rockchip SOCsīŧ‰", - "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "æ”¯æŒįš„éŸŗéĸ‘įŧ–觪᠁噍", - "transcoding_accepted_audio_codecs_description": "选拊不需čρčŊŦį įš„éŸŗéĸ‘įŧ–č§Ŗį å™¨ã€‚äģ…ᔍäēŽį‰šåŽšįš„čŊŦ᠁᭖į•Ĩ。", - "transcoding_accepted_containers": "æ”¯æŒįš„åŽšå™¨", - "transcoding_accepted_containers_description": "选拊å“Ēäē›åŽšå™¨æ ŧåŧä¸éœ€čĻé‡æ–°æˇˇåˆä¸ē MP4。äģ…适ᔍäēŽį‰šåŽšįš„čŊŦ᠁᭖į•Ĩ。", - "transcoding_accepted_video_codecs": "æ”¯æŒįš„č§†éĸ‘įŧ–觪᠁噍", - "transcoding_accepted_video_codecs_description": "选拊不需čρčŊŦį įš„č§†éĸ‘įŧ–č§Ŗį å™¨ã€‚äģ…ᔍäēŽį‰šåŽšįš„čŊŦ᠁᭖į•Ĩ。", + "transcoding_acceleration_vaapi": "视éĸ‘加速 API", + "transcoding_accepted_audio_codecs": "æ”¯æŒįš„éŸŗéĸ‘įŧ–᠁æ ŧåŧ", + "transcoding_accepted_audio_codecs_description": "选拊无需čŊŦį įš„éŸŗéĸ‘įŧ–᠁æ ŧåŧã€‚äģ…åœ¨į‰šåŽšįš„čŊŦ᠁᭖į•Ĩä¸‹į”Ÿæ•ˆã€‚", + "transcoding_accepted_containers": "æ”¯æŒįš„åŽšå™¨æ ŧåŧ", + "transcoding_accepted_containers_description": "é€‰æ‹Šæ— éœ€é‡æ–°å°čŖ…ä¸ē MP4 įš„åŽšå™¨æ ŧåŧã€‚äģ…åœ¨į‰šåŽšįš„čŊŦ᠁᭖į•Ĩä¸‹į”Ÿæ•ˆã€‚", + "transcoding_accepted_video_codecs": "æ”¯æŒįš„č§†éĸ‘įŧ–᠁æ ŧåŧ", + "transcoding_accepted_video_codecs_description": "选拊无需čŊŦį įš„č§†éĸ‘įŧ–᠁æ ŧåŧã€‚äģ…åœ¨į‰šåŽšįš„čŊŦ᠁᭖į•Ĩä¸‹į”Ÿæ•ˆã€‚", "transcoding_advanced_options_description": "å¤§å¤šæ•°į”¨æˆˇä¸éœ€čĻæ›´æ”šįš„é€‰éĄš", - "transcoding_audio_codec": "韺éĸ‘įŧ–觪᠁噍", - "transcoding_audio_codec_description": "Opus 是最éĢ˜č´¨é‡įš„é€‰æ‹ŠīŧŒäŊ†ä¸Žæ—§čŽžå¤‡æˆ–čŊ¯äģļįš„å…ŧåŽšæ€§čžƒäŊŽã€‚", - "transcoding_bitrate_description": "视éĸ‘čļ…čŋ‡æœ€å¤§į įŽ‡æˆ–æ ŧåŧä¸å…ŧ厚", - "transcoding_codecs_learn_more": "čρäē†č§Ŗæ­¤å¤„äŊŋį”¨įš„æœ¯č¯­č¯Ļ情īŧŒč¯ˇå‚见 FFmpeg æ–‡æĄŖīŧšH.264 įŧ–č§Ŗį ã€HEVC įŧ–觪᠁ 和 VP9 įŧ–č§Ŗį ã€‚", + "transcoding_audio_codec": "韺éĸ‘įŧ–᠁æ ŧåŧ", + "transcoding_audio_codec_description": "Opus æ˜¯éŸŗč´¨æœ€éĢ˜įš„é€‰éĄšīŧŒäŊ†åœ¨č€æ—§čŽžå¤‡æˆ–čŊ¯äģļä¸Šįš„å…ŧåŽšæ€§čžƒåˇŽã€‚", + "transcoding_bitrate_description": "视éĸ‘į įŽ‡é̘äēŽæœ€å¤§é™åˆļīŧŒæˆ–æ ŧåŧä¸åœ¨æŽĨå—åˆ—čĄ¨ä¸­", + "transcoding_codecs_learn_more": "č‹Ĩčρäē†č§Ŗæ­¤å¤„äŊŋį”¨įš„æœ¯č¯­č¯Ļ情īŧŒč¯ˇæŸĨ阅 FFmpeg æ–‡æĄŖä¸­įš„ H.264 įŧ–į ã€HEVC įŧ–᠁ 和 VP9 įŧ–į ã€‚", "transcoding_constant_quality_mode": "æ’åŽšč´¨é‡æ¨Ąåŧ", - "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更åĨŊīŧŒäŊ†éƒ¨åˆ†įĄŦäģļåŠ é€ŸčŽžå¤‡ä¸æ”¯æŒčŋ™į§æ¨Ąåŧã€‚åŊ“äŊŋᔍåŸēäēŽč´¨é‡įš„įŧ–᠁æ—ļīŧŒæ­¤é€‰éĄšå°†ä¸ēéĻ–é€‰æŒ‡åŽšįš„æ¨Ąåŧã€‚į”ąäēŽ NVENC 不支持 ICQīŧŒé€‰æ‹Š NVENC æ—ļ将åŋŊį•Ĩæ­¤é€‰éĄšã€‚", + "transcoding_constant_quality_mode_description": "ICQ 比 CQP 效果更åĨŊīŧŒäŊ†éƒ¨åˆ†įĄŦäģļåŠ é€ŸčŽžå¤‡ä¸æ”¯æŒæ­¤æ¨Ąåŧã€‚吝ᔍč¯Ĩé€‰éĄšåŽīŧŒåœ¨åŸēäēŽč´¨é‡įš„įŧ–᠁䏭将äŧ˜å…ˆäŊŋį”¨æŒ‡åŽšįš„æ¨Ąåŧã€‚į”ąäēŽ NVENCīŧˆNVIDIA æ˜žåĄįŧ–᠁噍īŧ‰ä¸æ”¯æŒ ICQīŧŒå› æ­¤č¯Ĩ莞įŊŽå¯šå…ļ无效。", "transcoding_constant_rate_factor": "æ’åŽšį įŽ‡įŗģ数īŧˆ-crfīŧ‰", - "transcoding_constant_rate_factor_description": "视éĸ‘č´¨é‡įē§åˆĢ。H.264下晎遍将å…ļ莞ä¸ē 23īŧŒHEVC ä¸ē 28īŧŒVP9 ä¸ē 31īŧŒAV1 ä¸ē 35。数å€ŧčļŠäŊŽīŧŒåˆ™į”ģéĸ质量čļŠåĨŊīŧŒäŊ†äē§į”Ÿįš„æ–‡äģļäŊ“į§¯æ›´å¤§ã€‚", - "transcoding_disabled_description": "不čĻå¯šäģģäŊ•视éĸ‘čŋ›čĄŒčŊŦ᠁īŧŒåœ¨æŸäē›åŽĸæˆˇįĢ¯ä¸Šå¯čƒŊäŧšæ— æŗ•æ’­æ”ž", + "transcoding_constant_rate_factor_description": "视éĸ‘č´¨é‡į­‰įē§ã€‚典型å€ŧä¸ēīŧšH.264 äŊŋᔍ 23īŧŒHEVC äŊŋᔍ 28īŧŒVP9 äŊŋᔍ 31īŧŒAV1 äŊŋᔍ 35。数å€ŧčļŠäŊŽč´¨é‡čļŠåĨŊīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļ也čļŠå¤§ã€‚", + "transcoding_disabled_description": "不čŊŦ᠁äģģäŊ•视éĸ‘īŧŒå¯čƒŊäŧšå¯ŧč‡´éƒ¨åˆ†åŽĸæˆˇįĢ¯æ— æŗ•æ’­æ”ž", "transcoding_encoding_options": "įŧ–į é€‰éĄš", "transcoding_encoding_options_description": "莞įŊŽįŧ–᠁视éĸ‘įš„įŧ–č§Ŗį å™¨ã€åˆ†čž¨įŽ‡ã€č´¨é‡å’Œå…ļäģ–选饚", "transcoding_hardware_acceleration": "įĄŦäģļ加速", "transcoding_hardware_acceleration_description": "厞éĒŒæ€§åŠŸčƒŊīŧšé€ŸåēĻæ›´åŋĢīŧŒäŊ†åœ¨į›¸åŒį įŽ‡ä¸‹č´¨é‡äŧšé™äŊŽ", "transcoding_hardware_decoding": "įĄŦäģļ觪᠁", - "transcoding_hardware_decoding_setting_description": "吝ᔍį̝到įĢ¯åŠ é€ŸīŧŒč€Œä¸äģ…äģ…æ˜¯åŠ é€Ÿįŧ–į ã€‚å¯čƒŊåšļä¸é€‚į”¨äēŽæ‰€æœ‰č§†éĸ‘。", + "transcoding_hardware_decoding_setting_description": "吝ᔍį̝到įĢ¯åŠ é€ŸīŧŒč€Œä¸äģ…äģ…å°†åŠ é€Ÿį”¨äēŽįŧ–į ã€‚å¯čƒŊåšļä¸é€‚į”¨äēŽæ‰€æœ‰č§†éĸ‘。", "transcoding_max_b_frames": "最大B帧数", - "transcoding_max_b_frames_description": "螃éĢ˜įš„å€ŧ可äģĨ提éĢ˜åŽ‹įŧŠæ•ˆįއīŧŒäŊ†äŧšå‡æ…ĸįŧ–į é€ŸåēĻ。可čƒŊä¸Žæ—§čŽžå¤‡ä¸Šįš„įĄŦäģļ加速不å…ŧ厚。0襨į¤ē将įρᔍB帧īŧŒ-1襨į¤ē将č‡ĒåŠ¨čŽžįŊŽæ­¤å‚数。", + "transcoding_max_b_frames_description": "螃éĢ˜įš„å€ŧ可äģĨ提éĢ˜åŽ‹įŧŠæ•ˆįއīŧŒäŊ†äŧšå‡æ…ĸįŧ–į é€ŸåēĻ。可čƒŊä¸Žæ—§čŽžå¤‡ä¸Šįš„įĄŦäģļ加速不å…ŧ厚。0襨į¤ēįρᔍB帧īŧŒ-1襨į¤ēįŗģįģŸč‡ĒåŠ¨čŽžįŊŽå‚数。", "transcoding_max_bitrate": "最éĢ˜į įŽ‡", "transcoding_max_bitrate_description": "莞įŊŽæœ€å¤§æ¯”į‰šįŽ‡å¯åœ¨å¯ščž“å‡ē质量åŊąå“čžƒå°įš„æƒ…å†ĩ下īŧŒäŊŋ文äģļäŊ“į§¯æ›´ä¸ē可控。720p 下īŧŒVP9 或 HEVC 晎遍将å…ļ莞ä¸ē 2600 kbit/sīŧŒH.264 则ä¸ē 4500 kbit/s。åĻ‚æžœæ­¤éĄščŽžįŊŽä¸ē 0īŧŒåˆ™ä¸é™åˆļæœ€å¤§æ¯”į‰šįŽ‡ã€‚åŊ“æ˛Ąæœ‰æŒ‡åŽšå•äŊæ—ļīŧŒå‡čŽžkīŧˆäģŖčĄ¨kbit/sīŧ‰īŧ›å› æ­¤īŧŒ5000、5000k和5MīŧˆMbit/sīŧ‰æ˜¯į­‰æ•ˆįš„。", "transcoding_max_keyframe_interval": "æœ€å¤§å…ŗé”Žå¸§é—´éš”", - "transcoding_max_keyframe_interval_description": "莞įŊŽå…ŗé”Žå¸§äš‹é—´įš„æœ€å¤§å¸§čˇįĻģã€‚čžƒäŊŽįš„å€ŧäŧšé™äŊŽåŽ‹įŧŠæ•ˆįއīŧŒäŊ†å¯äģĨ提éĢ˜æœį´ĸ速åēĻīŧŒåšļ且可čƒŊ在åŋĢ速čŋåŠ¨įš„åœē景中提é̘į”ģč´¨ã€‚0 襨į¤ē将č‡ĒåŠ¨čŽžįŊŽæ­¤å‚数。", + "transcoding_max_keyframe_interval_description": "莞įŊŽå…ŗé”Žå¸§äš‹é—´įš„æœ€å¤§é—´éš”ã€‚čžƒäŊŽįš„å€ŧäŧšé™äŊŽåŽ‹įŧŠæ•ˆįއīŧŒäŊ†å¯äģĨ加åŋĢæ‹–动čŋ›åēĻæĄæ—ļįš„čˇŗčŊŦ速åēĻīŧŒåšļ且可čƒŊ在åŋĢ速čŋåŠ¨įš„åœē景中提é̘į”ģč´¨ã€‚0 襨į¤ēįŗģįģŸč‡ĒåŠ¨čŽžįŊŽã€‚", "transcoding_optimal_description": "视éĸ‘čļ…čŋ‡į›Žæ ‡åˆ†čž¨įŽ‡æˆ–æ ŧåŧä¸æ”¯æŒ", "transcoding_policy": "čŊŦ᠁᭖į•Ĩ", "transcoding_policy_description": "莞įŊŽč§†éĸ‘čŊŦ᠁æ—ļæœē", "transcoding_preferred_hardware_device": "éϖ选įĄŦäģļčŽžå¤‡", - "transcoding_preferred_hardware_device_description": "äģ…适ᔍäēŽ VAAPI 和 QSVã€‚čŽžįŊŽį”¨äēŽįĄŦäģļčŊŦį įš„ dri čŠ‚į‚šã€‚", + "transcoding_preferred_hardware_device_description": "äģ…适ᔍäēŽ VAAPI 和 QSVã€‚čŽžįŊŽį”¨äēŽįĄŦäģļčŊŦį įš„ DRI čŽžå¤‡čŠ‚į‚šã€‚", "transcoding_preset_preset": "éĸ„莞īŧˆ-presetīŧ‰", - "transcoding_preset_preset_description": "压įŧŠé€ŸåēĻã€‚čžƒæ…ĸįš„éĸ„莞äŧšäē§į”Ÿæ›´å°įš„æ–‡äģļīŧŒåšļåœ¨į›Žæ ‡į‰šåŽšæ¯”į‰šįŽ‡æ—ļ提éĢ˜č´¨é‡ã€‚VP9蝎åŋŊį•ĨfasteräģĨä¸Šįš„é€ŸåēĻ。", + "transcoding_preset_preset_description": "压įŧŠé€ŸåēĻ。éĸ„čŽžé€ŸåēĻč…ĸīŧŒį”Ÿæˆįš„æ–‡äģļčļŠå°īŧ›åœ¨čŽžåŽšį‰šåŽšį įŽ‡æ—ļīŧŒčŋ˜čƒŊ提升į”ģč´¨ã€‚VP9 įŧ–᠁噍äŧšåŋŊį•Ĩīŧˆä¸æ”¯æŒīŧ‰é̘äēŽâ€œfaster”速åēĻįš„é€‰éĄšã€‚", "transcoding_reference_frames": "å‚č€ƒå¸§", - "transcoding_reference_frames_description": "在压įŧŠįģ™åޚ叧æ—ļå‚č€ƒįš„å¸§æ•°ã€‚čžƒéĢ˜įš„å€ŧ可äģĨ提éĢ˜åŽ‹įŧŠæ•ˆįއīŧŒäŊ†äŧšå‡æ…ĸįŧ–į é€ŸåēĻ。0 襨į¤ē将č‡ĒåŠ¨čŽžįŊŽæ­¤å‚数。", - "transcoding_required_description": "äģ…限不å…ŧ厚æ ŧåŧįš„视éĸ‘", + "transcoding_reference_frames_description": "在压įŧŠæŒ‡åޚ叧æ—ļīŧŒæ‰€å‚č€ƒįš„å¸§æ•°é‡ã€‚æ•°å€ŧčļŠé̘īŧŒåŽ‹įŧŠæ•ˆįއčļŠé̘īŧŒäŊ†äŧšé™äŊŽįŧ–į é€ŸåēĻ。0 襨į¤ēį”ąįŗģįģŸč‡ĒåŠ¨čŽžįŊŽã€‚", + "transcoding_required_description": "äģ…非标准æ ŧåŧįš„视éĸ‘", "transcoding_settings": "视éĸ‘čŊŦį čŽžįŊŽ", - "transcoding_settings_description": "įŽĄį†čρčŊŦį įš„č§†éĸ‘å’Œå¤„į†æ–šåŧ", + "transcoding_settings_description": "įŽĄį†éœ€čρčŊŦį įš„č§†éĸ‘čŒƒå›´īŧŒäģĨåŠå…ˇäŊ“įš„å¤„į†æ–šåŧ", "transcoding_target_resolution": "į›Žæ ‡åˆ†čž¨įŽ‡", - "transcoding_target_resolution_description": "更éĢ˜įš„åˆ†čž¨įŽ‡å¯äģĨäŋį•™æ›´å¤šįģ†čŠ‚īŧŒäŊ†įŧ–᠁æ—ļ间更é•ŋīŧŒæ–‡äģļäŊ“į§¯æ›´å¤§īŧŒä¸”可čƒŊ降äŊŽåē”ᔍፋåēįš„响åē”速åēĻ。", - "transcoding_temporal_aq": "æ—ļ间č‡Ē适åē”量化", - "transcoding_temporal_aq_description": "äģ…适ᔍäēŽ NVENC。æ—ļ间č‡Ē适åē”量化提é̘äē†é̘įģ†čŠ‚ã€äŊŽåŠ¨æ€åœēæ™¯įš„č´¨é‡ã€‚å¯čƒŊä¸Žæ—§čŽžå¤‡ä¸å…ŧ厚。", + "transcoding_target_resolution_description": "更éĢ˜įš„åˆ†čž¨įŽ‡č™Ŋį„ļčƒŊäŋį•™æ›´å¤šį”ģéĸįģ†čŠ‚īŧŒäŊ†äŧšåģļé•ŋįŧ–᠁æ—ļ间、åĸžå¤§æ–‡äģļäŊ“᧝īŧŒåšļ可čƒŊå¯ŧ致åē”į”¨å“åē”变æ…ĸ。", + "transcoding_temporal_aq": "æ—ļ间域č‡Ē适åē”量化", + "transcoding_temporal_aq_description": "äģ…适ᔍäēŽ NVENC。æ—ļ间域č‡Ē适åē”量化可提升é̘įģ†čŠ‚ã€äŊŽčŋåЍåœēæ™¯įš„į”ģč´¨ã€‚å¯čƒŊä¸Žčžƒæ—§įš„čŽžå¤‡ä¸å…ŧ厚。", "transcoding_threads": "įēŋį¨‹æ•°", - "transcoding_threads_description": "čŽžåŽšå€ŧčļŠé̘īŧŒįŧ–į é€ŸåēĻčļŠåŋĢīŧŒį•™įģ™å…ļ厃äģģåŠĄīŧˆDocker 外åŽŋä¸ģæœēįš„äģģåŠĄį­‰īŧ‰įš„čŽĄįŽ—čƒŊ力čļŠå°‘。此å€ŧ不åē”大äēŽ CPU æ ¸åŋƒįš„æ•°é‡ã€‚0 襨į¤ē最大限åēĻ地提éĢ˜åˆŠį”¨įŽ‡ã€‚", + "transcoding_threads_description": "数å€ŧčļŠé̘īŧŒįŧ–į é€ŸåēĻčļŠåŋĢīŧŒäŊ†åœ¨čŋčĄŒæ—ļäŧšå‡å°‘æœåŠĄå™¨å¤„į†å…ļäģ–äģģåŠĄįš„äŊ™é‡ã€‚č¯Ĩ数å€ŧ不åē”čļ…čŋ‡ CPU æ ¸åŋƒæ•°ã€‚莞ä¸ē 0 可最大化čĩ„æēåˆŠį”¨įŽ‡ã€‚", "transcoding_tone_mapping": "č‰˛č°ƒæ˜ å°„", - "transcoding_tone_mapping_description": "在将 HDR 视éĸ‘čŊŦæĸä¸ē SDR æ—ļīŧŒčŊ¯äģļäŧšå°č¯•å°Ŋ可čƒŊäŋæŒå…ļč§‚æ„Ÿã€‚æ¯į§įŽ—æŗ•åœ¨éĸœč‰˛ã€įģ†čŠ‚å’ŒäēŽåēĻæ–šéĸ做å‡ēäē†ä¸åŒįš„æƒčĄĄã€‚Hable įŽ—æŗ•äŋį•™įģ†čŠ‚īŧŒMobius įŽ—æŗ•äŋį•™éĸœč‰˛īŧŒč€Œ Reinhard įŽ—æŗ•äŋį•™äēŽåēĻ。", + "transcoding_tone_mapping_description": "旨在将 HDR 视éĸ‘čŊŦæĸä¸ē SDR æ—ļīŧŒå°Ŋ量äŋį•™åŽŸæœ‰įš„č§†č§‰æ•ˆæžœã€‚æ¯į§įŽ—æŗ•éƒŊåœ¨č‰˛åŊŠã€įģ†čŠ‚å’ŒäēŽåēĻ䚋间做å‡ēäē†ä¸åŒįš„å–čˆīŧšHable įŽ—æŗ•äž§é‡äŋį•™įģ†čŠ‚īŧŒMobius įŽ—æŗ•äž§é‡äŋį•™č‰˛åŊŠīŧŒč€Œ Reinhard įŽ—æŗ•åˆ™äž§é‡äŋį•™äēŽåēĻ。", "transcoding_transcode_policy": "čŊŦ᠁᭖į•Ĩ", - "transcoding_transcode_policy_description": "视éĸ‘čŊŦ᠁᭖į•Ĩ。HDR 视éĸ‘将始įģˆčŋ›čĄŒčŊŦ᠁īŧˆé™¤éžįρᔍäē†čŊŦ᠁功čƒŊīŧ‰ã€‚", + "transcoding_transcode_policy_description": "čŽžåŽšč§†éĸ‘äŊ•æ—ļåē”čŋ›čĄŒčŊŦį įš„į­–į•Ĩ。HDR 视éĸ‘å§‹įģˆäŧščĸĢčŊŦ᠁īŧˆé™¤éžåˇ˛åŽŒå…¨įρᔍčŊŦ᠁功čƒŊīŧ‰ã€‚", "transcoding_two_pass_encoding": "ä猿ŦĄįŧ–᠁", - "transcoding_two_pass_encoding_setting_description": "分两æŦĄčŋ›čĄŒčŊŦ᠁īŧŒäģĨį”Ÿæˆæ›´åĨŊįš„įŧ–᠁视éĸ‘。åŊ“å¯į”¨æœ€å¤§æ¯”į‰šįŽ‡īŧˆä¸Ž H.264 和 HEVC ååŒå¤„į†æ—ļ所需īŧ‰æ—ļīŧŒæ­¤æ¨ĄåŧäŊŋᔍåŸēäēŽæœ€å¤§æ¯”į‰šįŽ‡įš„æ¯”į‰šįŽ‡čŒƒå›´īŧŒåšļåŋŊį•Ĩ CRF。寚äēŽ VP9īŧŒåĻ‚æžœįρᔍä熿œ€å¤§æ¯”į‰šįŽ‡īŧŒåˆ™å¯äģĨäŊŋᔍ CRFīŧˆæŗ¨īŧšCRFīŧŒå…¨į§°ä¸ēconstant rate factorīŧŒæ˜¯æŒ‡äŋč¯â€œä¸€åŽšč´¨é‡â€īŧŒæ™ēčƒŊåˆ†é…į įŽ‡īŧŒåŒ…æ‹ŦåŒä¸€å¸§å†…åˆ†é…į įŽ‡ã€å¸§é—´åˆ†é…į įŽ‡īŧ‰ã€‚", - "transcoding_video_codec": "视éĸ‘įŧ–觪᠁噍", - "transcoding_video_codec_description": "VP9 å…ˇæœ‰čžƒéĢ˜įš„æ•ˆįŽ‡å’ŒįŊ‘įģœå…ŧ厚性īŧŒäŊ†éœ€čĻæ›´é•ŋįš„æ—ļ间čŋ›čĄŒčŊŦį ã€‚HEVC 性čƒŊä¸Žäš‹į›¸äŧŧīŧŒäŊ†įŊ‘įģœå…ŧåŽšæ€§čžƒäŊŽã€‚H.264 čŊŦ᠁åŋĢé€Ÿä¸”å…ˇæœ‰åšŋæŗ›įš„å…ŧ厚性īŧŒäŊ†äē§į”Ÿįš„æ–‡äģļäŊ“į§¯čžƒå¤§ã€‚AV1 是最éĢ˜æ•ˆįš„įŧ–觪᠁噍īŧŒäŊ†åœ¨čžƒæ—§įš„čŽžå¤‡ä¸Šå…ŧåŽšæ€§čžƒåˇŽã€‚", - "trash_enabled_description": "å¯į”¨å›žæ”ļįĢ™", - "trash_number_of_days": "夊数", - "trash_number_of_days_description": "čĸĢæ°¸äš…删除䚋前īŧŒéĄšį›Žåœ¨å›žæ”ļį̙䏭äŋį•™įš„夊数", + "transcoding_two_pass_encoding_setting_description": "采ᔍ䏤æŦĄįŧ–į æ¨ĄåŧäģĨį”Ÿæˆč´¨é‡æ›´äŧ˜įš„视éĸ‘。åŊ“åŧ€å¯æœ€å¤§į įއ限åˆļæ—ļīŧˆH.264 和 HEVC įŧ–᠁æ ŧåŧåŋ…éĄģåŧ€å¯æ­¤é€‰éĄšæ‰čƒŊį”Ÿæ•ˆīŧ‰īŧŒč¯Ĩæ¨ĄåŧäŧšäžæŽæœ€å¤§į įŽ‡čŽžåŽšä¸€ä¸Ēį įŽ‡čŒƒå›´īŧŒåšļåŋŊį•Ĩ CRF 莞įŊŽã€‚寚äēŽ VP9 įŧ–᠁īŧŒč‹Ĩå…ŗé—­æœ€å¤§į įŽ‡é™åˆļīŧŒåˆ™å¯äģĨäŊŋᔍ CRF 莞įŊŽã€‚", + "transcoding_video_codec": "视éĸ‘įŧ–᠁æ ŧåŧ", + "transcoding_video_codec_description": "VP9 įŧ–į æ•ˆįŽ‡é̘īŧŒä¸”在įŊ‘éĄĩį̝å…ŧ厚性åĨŊīŧŒäŊ†čŊŦ᠁耗æ—ļ螃é•ŋ。HEVCīŧˆH.265īŧ‰æ€§čƒŊä¸Žäš‹į›¸äŧŧīŧŒäŊ†åœ¨įŊ‘éĄĩįĢ¯įš„å…ŧåŽšæ€§čžƒåˇŽã€‚H.264 å…ŧ厚性极åšŋ且čŊŦį é€ŸåēĻåŋĢīŧŒäŊ†į”Ÿæˆįš„æ–‡äģļäŊ“᧝čĻå¤§åž—å¤šã€‚AV1 æ˜¯æ•ˆįŽ‡æœ€éĢ˜įš„įŧ–᠁æ ŧåŧīŧŒäŊ†åœ¨æ—§čŽžå¤‡ä¸Šįŧē䚏支持。", + "trash_enabled_description": "å¯į”¨å›žæ”ļįĢ™åŠŸčƒŊ", + "trash_number_of_days": "äŋį•™å¤Šæ•°", + "trash_number_of_days_description": "文äģļ在回æ”ļį̙䏭äŋį•™å¤šå°‘夊后čĸĢæ°¸äš…删除", "trash_settings": "回æ”ļįĢ™čŽžįŊŽ", "trash_settings_description": "įŽĄį†å›žæ”ļįĢ™čŽžįŊŽ", - "unlink_all_oauth_accounts": "č§Ŗé™¤æ‰€æœ‰ä¸Ž OAuth å¸æˆˇįš„é“žæŽĨ", - "unlink_all_oauth_accounts_description": "在čŋį§ģč‡ŗæ–°įš„æœåŠĄæäž›å•†å‰īŧŒč¯ˇä¸čρåŋ˜čްčĻå…ˆč§Ŗé™¤æ‰€æœ‰ä¸Ž OAuth å¸æˆˇįš„é“žæŽĨ。", - "unlink_all_oauth_accounts_prompt": "您是åĻįĄŽčŽ¤čĻč§Ŗé™¤æ‰€æœ‰ä¸Ž OAuth å¸æˆˇįš„é“žæŽĨ? æ‰€æœ‰į›¸å…ŗįš„äŊŋᔍ者čēĢäģŊäŧščĸĢ重设īŧŒåšļ且不čƒŊčĸĢčŋ˜åŽŸã€‚", - "user_cleanup_job": "æ¸…į†į”¨æˆˇ", - "user_delete_delay": "{user}įš„č´ĻæˆˇåŠéĄšį›Žå°†åœ¨{delay, plural, one {#夊} other {#夊}}后č‡Ē动永䚅删除。", + "unlink_all_oauth_accounts": "č§Ŗé™¤æ‰€æœ‰ OAuth å¸æˆˇįš„é“žæŽĨ", + "unlink_all_oauth_accounts_description": "在čŋį§ģåˆ°æ–°æœåŠĄå•†äš‹å‰īŧŒč¯ˇčŽ°åž—č§Ŗé™¤æ‰€æœ‰ OAuth č´Ļæˆˇįš„å…ŗč”ã€‚", + "unlink_all_oauth_accounts_prompt": "æ‚¨įĄŽåŽščĻč§Ŗé™¤æ‰€æœ‰ OAuth č´Ļæˆˇįš„å…ŗč”å—īŧŸæ­¤æ“äŊœå°†é‡įŊŽæ¯ä¸Ēį”¨æˆˇįš„čēĢäģŊčŽ¤č¯ IDīŧŒä¸”æ— æŗ•æ’¤é”€ã€‚", + "user_cleanup_job": "į”¨æˆˇæ¸…į†", + "user_delete_delay": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†åœ¨{delay, plural, one {#夊} other {#夊}}后č‡Ē动永䚅删除。", "user_delete_delay_settings": "åģᅵŸåˆ é™¤", - "user_delete_delay_settings_description": "åˆ é™¤åŽæ°¸äš…åˆ é™¤į”¨æˆˇå¸æˆˇå’Œčĩ„äē§įš„å¤Šæ•°ã€‚į”¨æˆˇåˆ é™¤äŊœä¸šäŧšåœ¨åˆå¤œæŖ€æŸĨ是åĻæœ‰į”¨æˆˇå¯äģĨ删除。寚č¯Ĩ莞įŊŽįš„æ›´æ”šå°†åœ¨ä¸‹æŦĄæ‰§čĄŒæ—ļį”Ÿæ•ˆã€‚", - "user_delete_immediately": "{user}įš„č´ĻæˆˇåŠéĄšį›Žå°†įĢ‹åŗæ°¸äš…åˆ é™¤ã€‚", - "user_delete_immediately_checkbox": "įĢ‹åŗåˆ é™¤æŖ€į´ĸåˆ°įš„į”¨æˆˇåŠéĄšį›Ž", + "user_delete_delay_settings_description": "į§ģ除后多少夊īŧŒæ°¸äš…åˆ é™¤į”¨æˆˇįš„č´ĻæˆˇåŠčĩ„äē§ã€‚į”¨æˆˇåˆ é™¤äģģåŠĄå°†åœ¨åˆå¤œčŋčĄŒīŧŒäģĨæŖ€æŸĨ是åĻæœ‰åž…åˆ é™¤įš„į”¨æˆˇã€‚æ­¤čŽžįŊŽįš„æ›´æ”šå°†åœ¨ä¸‹æŦĄäģģåŠĄæ‰§čĄŒæ—ļį”Ÿæ•ˆã€‚", + "user_delete_immediately": "{user}įš„č´ĻæˆˇåŠčĩ„äē§å°†čĸĢįĢ‹åŗåŠ å…Ĩ永䚅删除队列。", + "user_delete_immediately_checkbox": "å°†į”¨æˆˇåŠå…ļčĩ„äē§åŠ å…ĨįĢ‹åŗåˆ é™¤é˜Ÿåˆ—", "user_details": "į”¨æˆˇč¯Ļ情", "user_management": "į”¨æˆˇįŽĄį†", - "user_password_has_been_reset": "č¯Ĩį”¨æˆˇįš„å¯†į čĸĢ重įŊŽīŧš", - "user_password_reset_description": "č¯ˇå‘į”¨æˆˇæäž›ä¸´æ—ļ坆᠁īŧŒåšļ告įŸĨäģ–äģŦ下æŦĄį™ģåŊ•æ—ļ需čĻæ›´æ”šå¯†į ã€‚", + "user_password_has_been_reset": "į”¨æˆˇįš„å¯†į åˇ˛é‡įŊŽīŧš", + "user_password_reset_description": "蝎将䏴æ—ļå¯†į æäž›įģ™į”¨æˆˇīŧŒåšļ告įŸĨäģ–äģŦ需在下æŦĄį™ģåŊ•æ—ļæ›´æ”šå¯†į ã€‚", "user_restore_description": "č´Ļæˆˇâ€œ{user}”将čĸĢæĸ复。", - "user_restore_scheduled_removal": "æĸå¤į”¨æˆˇ - čŽĄåˆ’äēŽ{date, date, long}删除", + "user_restore_scheduled_removal": "æĸå¤į”¨æˆˇ - 原厚äēŽ {date, date, long} įš„åˆ é™¤čŽĄåˆ’åˇ˛å–æļˆ", "user_settings": "į”¨æˆˇčŽžįŊŽ", "user_settings_description": "įŽĄį†į”¨æˆˇčŽžįŊŽ", "user_successfully_removed": "į”¨æˆˇ {email} åˇ˛æˆåŠŸåˆ é™¤ã€‚", "users_page_description": "įŽĄį†į”¨æˆˇéĄĩéĸ", - "version_check_enabled_description": "å¯į”¨į‰ˆæœŦæŖ€æĩ‹", + "version_check_enabled_description": "æŖ€æŸĨčŊ¯äģļæ–°į‰ˆæœŦ", "version_check_implications": "į‰ˆæœŦæŖ€æŸĨ功čƒŊ䞝čĩ–äēŽä¸Ž github.com įš„åŽšæœŸé€šäŋĄ", - "version_check_settings": "į‰ˆæœŦæŖ€æŸĨ", - "version_check_settings_description": "å¯į”¨æˆ–įĻį”¨æ–°į‰ˆæœŦ通įŸĨ", - "video_conversion_job": "视éĸ‘čŊŦ᠁", + "version_check_settings": "æ–°į‰ˆæœŦæŖ€æŸĨ", + "version_check_settings_description": "吝ᔍ/įĻį”¨æ–°į‰ˆæœŦ通įŸĨ", + "video_conversion_job": "čŊŦ᠁视éĸ‘", "video_conversion_job_description": "å¯šč§†éĸ‘čŋ›čĄŒčŊŦ᠁īŧŒäģĨå…ŧåŽšæ›´å¤šįš„æĩč§ˆå™¨å’ŒčŽžå¤‡" }, "admin_email": "įŽĄį†å‘˜é‚ŽįŽą", "admin_password": "įŽĄį†å‘˜å¯†į ", "administration": "įŗģįģŸįŽĄį†", "advanced": "é̘įē§", - "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é€‰éĄšå¯åœ¨åŒæ­Ĩčŋ‡į¨‹ä¸­æ šæŽå¤‡į”¨æĄäģļį­›é€‰éĄšį›Žã€‚äģ…åŊ“您在åē”ᔍፋåēæŖ€æĩ‹æ‰€æœ‰į›¸å†Œå‡é‡åˆ°é—Žéĸ˜æ—￉å°č¯•此功čƒŊ。", - "advanced_settings_enable_alternate_media_filter_title": "äŊŋį”¨å¤‡į”¨įš„čŽžå¤‡į›¸å†ŒåŒæ­Ĩį­›é€‰æĄäģļ[厞éĒŒæ€§]", + "advanced_settings_clear_image_cache": "清įŠē回像įŧ“å­˜", + "advanced_settings_clear_image_cache_error": "æ— æŗ•æ¸…įŠē回像įŧ“å­˜", + "advanced_settings_clear_image_cache_success": "æˆåŠŸæ¸…į† {size}", + "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é€‰éĄšå¯æ šæŽå…ļäģ–æĄäģļį­›é€‰åŒæ­ĨæœŸé—´įš„åĒ’äŊ“。äģ…在åē”į”¨æ— æŗ•æŖ€æĩ‹åˆ°æ‰€æœ‰į›¸å†Œæ—ļå°č¯•æ­¤é€‰éĄšã€‚", + "advanced_settings_enable_alternate_media_filter_title": "[厞éĒŒæ€§] äŊŋį”¨å¤‡į”¨čŽžå¤‡į›¸å†Œį­›é€‰æ–šåŧ", "advanced_settings_log_level_title": "æ—Ĩåŋ—į­‰įē§: {level}", - "advanced_settings_prefer_remote_subtitle": "在某äē›čŽžå¤‡ä¸ŠīŧŒäģŽæœŦåœ°įš„éĄšį›ŽåŠ čŊŊįŧŠį•Ĩå›žįš„é€ŸåēĻ非常æ…ĸã€‚å¯į”¨æ­¤é€‰éĄšäģĨ加čŊŊčŋœį¨‹éĄšį›Žã€‚", - "advanced_settings_prefer_remote_title": "äŧ˜å…ˆčŋœį¨‹éĄšį›Ž", - "advanced_settings_proxy_headers_subtitle": "厚䚉äģŖį†æ ‡å¤´īŧŒåē”ᔍäēŽ Immich įš„æ¯æŦĄįŊ‘įģœč¯ˇæą‚", - "advanced_settings_proxy_headers_title": "č‡Ē厚䚉äģŖį†æ ‡å¤´[厞éĒŒæ€§]", - "advanced_settings_readonly_mode_subtitle": "吝ᔍåĒč¯ģæ¨ĄåŧīŧŒåœ¨č¯Ĩæ¨Ąåŧä¸‹åĒčƒŊæŸĨįœ‹į…§į‰‡īŧŒå¤šé€‰ã€å…ąäēĢã€æŠ•åąã€åˆ é™¤į­‰æ“äŊœéƒŊčĸĢįĻį”¨ã€‚äģŽä¸ģåąåš•é€ščŋ‡į”¨æˆˇå¤´åƒå¯į”¨/įρᔍåĒč¯ģ", + "advanced_settings_prefer_remote_subtitle": "éƒ¨åˆ†čŽžå¤‡č¯ģ取æœŦ地čĩ„æēįŧŠį•Ĩå›žįš„é€ŸåēĻæžæ…ĸ。åŧ€å¯æ­¤čŽžįŊŽå¯æ”šä¸ē加čŊŊčŋœį¨‹å›žį‰‡ã€‚", + "advanced_settings_prefer_remote_title": "äŧ˜å…ˆäŊŋᔍčŋœį¨‹å›žį‰‡", + "advanced_settings_proxy_headers_subtitle": "厚䚉 Immich 每æŦĄįŊ‘įģœč¯ˇæą‚åē”附å¸Ļįš„äģŖį†å¤´äŋĄæ¯", + "advanced_settings_proxy_headers_title": "č‡Ē厚䚉äģŖį†å¤´äŋĄæ¯ [厞éĒŒæ€§]", + "advanced_settings_readonly_mode_subtitle": "吝ᔍåĒč¯ģæ¨ĄåŧīŧŒåœ¨æ­¤æ¨Ąåŧä¸‹äģ…可æŸĨįœ‹į…§į‰‡īŧŒå¤šé€‰ã€åˆ†äēĢã€æŠ•åąã€åˆ é™¤į­‰åŠŸčƒŊ将全部įĻį”¨ã€‚å¯é€ščŋ‡ä¸ģåąåš•ä¸Šįš„į”¨æˆˇå¤´åƒåŧ€å¯/å…ŗé—­åĒč¯ģæ¨Ąåŧ", "advanced_settings_readonly_mode_title": "åĒč¯ģæ¨Ąåŧ", - "advanced_settings_self_signed_ssl_subtitle": "莺čŋ‡å¯šæœåŠĄå™¨ įš„ SSL 蝁äšĻénj蝁īŧˆč¯Ĩé€‰éĄšé€‚į”¨äēŽäŊŋᔍč‡Ēį­žåč¯äšĻįš„æœåŠĄå™¨īŧ‰ã€‚", - "advanced_settings_self_signed_ssl_title": "å…čŽ¸č‡Ēį­žå SSL 蝁äšĻ[厞éĒŒæ€§]", - "advanced_settings_sync_remote_deletions_subtitle": "在įŊ‘éĄĩä¸Šæ‰§čĄŒæ“äŊœæ—ļīŧŒč‡Ē动删除或čŋ˜åŽŸč¯ĨčŽžå¤‡ä¸­įš„éĄšį›Ž", - "advanced_settings_sync_remote_deletions_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_subtitle": "吝ᔍéĸå¤–įš„æ•…éšœæŽ’æŸĨ功čƒŊ", "advanced_settings_troubleshooting_title": "故障排除", "age_months": "{months, plural, one {#ä¸Ē月} other {#ä¸Ē月}}", "age_year_months": "1垁{months, plural, one {#ä¸Ē月} other {#ä¸Ē月}}", "age_years": "{years, plural, other {#垁}}", "album": "į›¸å†Œ", - "album_added": "čĸĢæˇģåŠ åˆ°į›¸å†Œ", + "album_added": "į›¸å†Œæˇģ加成功", "album_added_notification_setting_description": "åŊ“您čĸĢæˇģåŠ åˆ°å…ąäēĢį›¸å†Œæ—ļīŧŒæŽĨæ”ļé‚ŽįŽąé€šįŸĨ", - "album_cover_updated": "į›¸å†Œå°éĸåˇ˛æ›´æ–°", - "album_delete_confirmation": "įĄŽåŽščĻåˆ é™¤į›¸å†Œâ€œ{album}”吗īŧŸ", - "album_delete_confirmation_description": "åĻ‚æžœč¯Ĩį›¸å†Œæ˜¯å…ąäēĢįš„īŧŒå…ļäģ–į”¨æˆˇå°†æ— æŗ•å†čŽŋ闎厃。", + "album_cover_updated": "封éĸåˇ˛æ›´æ–°", + "album_delete_confirmation": "įĄŽåŽščĻåˆ é™¤į›¸å†Œ “{album}” 吗īŧŸ", + "album_delete_confirmation_description": "åĻ‚æžœæ­¤į›¸å†Œåˇ˛čĸĢå…ąäēĢīŧŒå…ļäģ–į”¨æˆˇäšŸå°†æ— æŗ•å†čŽŋ闎厃。", "album_deleted": "į›¸å†Œåˇ˛åˆ é™¤", "album_info_card_backup_album_excluded": "åˇ˛æŽ’é™¤", - "album_info_card_backup_album_included": "厞选䏭", + "album_info_card_backup_album_included": "åˇ˛åŒ…åĢ", "album_info_updated": "į›¸å†ŒäŋĄæ¯åˇ˛æ›´æ–°", "album_leave": "退å‡ēį›¸å†ŒīŧŸ", - "album_leave_confirmation": "įĄŽåŽščρ退å‡ēį›¸å†Œâ€œ{album}”吗īŧŸ", + "album_leave_confirmation": "įĄŽåŽščρ退å‡ēį›¸å†Œ “{album}” 吗īŧŸ", "album_name": "į›¸å†Œåį§°", - "album_options": "į›¸å†ŒčŽžįŊŽ", + "album_options": "į›¸å†Œé€‰éĄš", "album_remove_user": "į§ģé™¤į”¨æˆˇīŧŸ", - "album_remove_user_confirmation": "įĄŽåŽščρį§ģ除“{user}”吗īŧŸ", - "album_search_not_found": "æœĒ扞到įŦĻ合搜į´ĸæĄäģļįš„į›¸å†Œ", - "album_share_no_users": "įœ‹čĩˇæĨæ‚¨åˇ˛ä¸Žæ‰€æœ‰į”¨æˆˇå…ąäēĢä熿­¤į›¸å†ŒīŧŒæˆ–č€…æ‚¨æ šæœŦæ˛Ąæœ‰äģģäŊ•į”¨æˆˇå¯å…ąäēĢ。", - "album_summary": "į›¸å†Œæ‘˜čρ", - "album_updated": "į›¸å†Œæœ‰æ›´æ–°", - "album_updated_setting_description": "åŊ“å…ąäēĢį›¸å†Œæœ‰æ–°éĄšį›Žæ—ļæŽĨæ”ļ邮äģļ通įŸĨ", - "album_user_left": "įĻģåŧ€â€œ{album}”", - "album_user_removed": "厞į§ģ除“{user}”", + "album_remove_user_confirmation": "įĄŽåŽščρį§ģ除 “{user}” 吗īŧŸ", + "album_search_not_found": "æœĒ扞到与搜į´ĸæĄäģļåŒšé…įš„į›¸å†Œ", + "album_selected": "į›¸å†Œåˇ˛é€‰ä¸­", + "album_share_no_users": "įœ‹čĩˇæĨæ‚¨åˇ˛å°†æ­¤į›¸å†Œå…ąäēĢį왿‰€æœ‰į”¨æˆˇīŧŒæˆ–č€…æ‚¨æ˛Ąæœ‰å¯å…ąäēĢįš„į”¨æˆˇã€‚", + "album_summary": "į›¸å†ŒæĻ‚č§ˆ", + "album_updated": "į›¸å†Œåˇ˛æ›´æ–°", + "album_updated_setting_description": "åŊ“å…ąäēĢį›¸å†Œæœ‰æ–°å†…åŽšæ—ļīŧŒæŽĨæ”ļ邮äģļ通įŸĨ", + "album_upload_assets": "äģŽæ‚¨įš„į”ĩ脑上äŧ æ–‡äģļåšļæˇģåŠ åˆ°į›¸å†Œ", + "album_user_left": "厞退å‡ē “{album}”", + "album_user_removed": "厞į§ģ除 “{user}”", "album_viewer_appbar_delete_confirm": "įĄŽåŽščρäģŽč´Ļæˆˇä¸­åˆ é™¤æ­¤į›¸å†Œå—īŧŸ", "album_viewer_appbar_share_err_delete": "åˆ é™¤į›¸å†Œå¤ąč´Ĩ", "album_viewer_appbar_share_err_leave": "退å‡ēå…ąäēĢå¤ąč´Ĩ", - "album_viewer_appbar_share_err_remove": "äģŽį›¸å†Œä¸­į§ģ除æ—ļå‡ēįŽ°é”™č¯¯", + "album_viewer_appbar_share_err_remove": "äģŽį›¸å†Œį§ģ除内厚æ—ļå‡ēįŽ°é—Žéĸ˜", "album_viewer_appbar_share_err_title": "äŋŽæ”šį›¸å†Œæ ‡éĸ˜å¤ąč´Ĩ", "album_viewer_appbar_share_leave": "退å‡ēį›¸å†Œ", - "album_viewer_appbar_share_to": "å…ąäēĢįģ™", + "album_viewer_appbar_share_to": "分äēĢįģ™", "album_viewer_page_share_add_users": "邀蝎äģ–äēē", - "album_with_link_access": "æ‹Ĩ有此铞æŽĨįš„äģģäŊ•äēē均可æŸĨįœ‹æœŦį›¸å†Œä¸­įš„į…§į‰‡å’Œäēēį‰Šã€‚", + "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": "可与å…ļäģ–į”¨æˆˇå…ąäēĢįš„éĄšį›Žæ”ļč—ã€‚", + "albums_default_sort_order_description": "创åģēæ–°į›¸å†Œæ—ļīŧŒåŊąåƒįš„初始排åēæ–šåŧã€‚", + "albums_feature_description": "可与å…ļäģ–į”¨æˆˇå…ąäēĢįš„į…§į‰‡/内厚合集。", "albums_on_device_count": "čŽžå¤‡ä¸Šįš„į›¸å†Œīŧˆ{count} ä¸Ēīŧ‰", + "albums_selected": "{count, plural, one {# ä¸Ēį›¸å†Œåˇ˛é€‰æ‹Š} other {# ä¸Ēį›¸å†Œåˇ˛é€‰æ‹Š}}", "all": "全部", "all_albums": "æ‰€æœ‰į›¸å†Œ", "all_people": "全部äēēį‰Š", + "all_photos": "æ‰€æœ‰į…§į‰‡", "all_videos": "æ‰€æœ‰č§†éĸ‘", "allow_dark_mode": "å…čŽ¸æˇąč‰˛æ¨Ąåŧ", "allow_edits": "å…čŽ¸įŧ–čž‘", @@ -498,6 +525,9 @@ "allow_public_user_to_upload": "å…čŽ¸æ‰€æœ‰į”¨æˆˇä¸Šäŧ ", "allowed": "å…čŽ¸", "alt_text_qr_code": "äēŒįģ´į å›žį‰‡", + "always_keep": "始įģˆäŋį•™", + "always_keep_photos_hint": "åŧ€å¯â€œé‡Šæ”žįŠē间”后īŧŒäģäŧšäŋį•™æ‰€æœ‰į…§į‰‡åœ¨æœŦčŽžå¤‡ä¸Šã€‚", + "always_keep_videos_hint": "åŧ€å¯â€œé‡Šæ”žįŠē间”后īŧŒäģäŧšäŋį•™æ‰€æœ‰č§†éĸ‘在æœŦčŽžå¤‡ä¸Šã€‚", "anti_clockwise": "逆æ—ļ针", "api_key": "API 密é’Ĩ", "api_key_description": "č¯Ĩåē”ᔍ坆é’ĨåĒäŧšæ˜žį¤ē一æŦĄã€‚č¯ˇįĄŽäŋåœ¨å…ŗé—­įĒ—åŖå‰å¤åˆļ下æĨ。", @@ -507,178 +537,183 @@ "app_bar_signout_dialog_content": "æ‚¨įĄŽåŽščρ退å‡ē吗īŧŸ", "app_bar_signout_dialog_ok": "是", "app_bar_signout_dialog_title": "退å‡ēį™ģåŊ•", - "app_download_links": "APP下čŊŊ链æŽĨ", + "app_download_links": "åē”ᔍ䏋čŊŊ链æŽĨ", "app_settings": "åē”į”¨čŽžįŊŽ", "app_stores": "åē”ᔍ商åē—", - "app_update_available": "åē”ᔍፋåēæ›´æ–°å¯į”¨", - "appears_in": "å‡ēįŽ°äēŽ", - "apply_count": "åē”ᔍ ({count, number}ä¸Ēčĩ„äē§)", + "app_update_available": "åē”į”¨æ›´æ–°åˇ˛å‘å¸ƒ", + "appears_in": "æ”ļåŊ•äēŽ", + "apply_count": "åē”ᔍ ({count, number})", "archive": "åŊ’æĄŖ", "archive_action_prompt": "厞将 {count} 饚æˇģ加到åŊ’æĄŖ", "archive_or_unarchive_photo": "åŊ’æĄŖæˆ–å–æļˆåŊ’æĄŖį…§į‰‡", - "archive_page_no_archived_assets": "æœĒ扞到åŊ’æĄŖéĄšį›Ž", + "archive_page_no_archived_assets": "æœĒæ‰žåˆ°åˇ˛åŊ’æĄŖįš„åŊąåƒ", "archive_page_title": "åŊ’æĄŖīŧˆ{count}īŧ‰", "archive_size": "åŊ’æĄŖå¤§å°", - "archive_size_description": "配įŊŽä¸‹čŊŊåŊ’æĄŖå¤§å°īŧˆGiBīŧ‰", + "archive_size_description": "配įŊŽä¸‹čŊŊįš„åŊ’æĄŖå¤§å°īŧˆGiBīŧ‰", "archived": "厞åŊ’æĄŖ", "archived_count": "{count, plural, other {厞åŊ’æĄŖ # 饚}}", - "are_these_the_same_person": "äģ–äģŦ是同一äŊäēē吗īŧŸ", - "are_you_sure_to_do_this": "įĄŽåŽšæ‰§čĄŒæ­¤æ“äŊœīŧŸ", - "asset_action_delete_err_read_only": "æ— æŗ•åˆ é™¤åĒč¯ģéĄšį›ŽīŧŒčˇŗčŋ‡", - "asset_action_share_err_offline": "æ— æŗ•čŽˇå–įĻģįēŋéĄšį›ŽīŧŒčˇŗčŋ‡", + "are_these_the_same_person": "čŋ™æ˜¯åŒä¸€ä¸Ēäēē吗īŧŸ", + "are_you_sure_to_do_this": "įĄŽåŽščĻæ‰§čĄŒæ­¤æ“äŊœīŧŸ", + "array_field_not_fully_supported": "数įģ„å­—æŽĩ需čĻæ‰‹åŠ¨čŋ›čĄŒ JSON įŧ–čž‘", + "asset_action_delete_err_read_only": "æ— æŗ•åˆ é™¤åĒč¯ģčĩ„æēīŧŒåˇ˛čˇŗčŋ‡", + "asset_action_share_err_offline": "æ— æŗ•čŽˇå–įĻģįēŋčĩ„æēīŧŒåˇ˛čˇŗčŋ‡", "asset_added_to_album": "厞æˇģåŠ č‡ŗį›¸å†Œ", "asset_adding_to_album": "æ­Ŗåœ¨æˇģåŠ č‡ŗį›¸å†Œâ€Ļ", - "asset_description_updated": "éĄšį›Žæčŋ°åˇ˛æ›´æ–°", - "asset_filename_is_offline": "éĄšį›Žâ€œ{filename}â€åˇ˛įĻģįēŋ", - "asset_has_unassigned_faces": "éĄšį›Žä¸­æœ‰æœĒåˆ†é…įš„äēē脸", - "asset_hashing": "å“ˆå¸Œæ Ąénj䏭â€Ļ", - "asset_list_group_by_sub_title": "分į섿–šåŧ", + "asset_created": "čĩ„æēåˇ˛åˆ›åģē", + "asset_description_updated": "čĩ„æēæčŋ°åˇ˛æ›´æ–°", + "asset_filename_is_offline": "čĩ„æēâ€œ{filename}â€åˇ˛įĻģįēŋ", + "asset_has_unassigned_faces": "čĩ„æēåŒ…åĢæœĒåˆ†é…įš„äēē脸", + "asset_hashing": "æ­Ŗåœ¨čŽĄįŽ—å“ˆå¸Œå€ŧâ€Ļ", + "asset_list_group_by_sub_title": "分įģ„䞝捎", "asset_list_layout_settings_dynamic_layout_title": "åŠ¨æ€å¸ƒåą€", "asset_list_layout_settings_group_automatically": "č‡Ē动", - "asset_list_layout_settings_group_by": "éĄšį›Žåˆ†į섿–šåŧ", - "asset_list_layout_settings_group_by_month_day": "月和æ—Ĩ", + "asset_list_layout_settings_group_by": "čĩ„æēåˆ†įģ„䞝捎", + "asset_list_layout_settings_group_by_month_day": "月äģŊ + æ—Ĩ期", "asset_list_layout_sub_title": "å¸ƒåą€", "asset_list_settings_subtitle": "ᅧቇįŊ‘æ ŧå¸ƒåą€čŽžįŊŽ", "asset_list_settings_title": "ᅧቇįŊ‘æ ŧ", - "asset_offline": "éĄšį›Žč„ąæœē", - "asset_offline_description": "įŖį›˜ä¸Šåˇ˛æ‰žä¸åˆ°č¯Ĩå¤–éƒ¨éĄšį›Žã€‚č¯ˇč”įŗģæ‚¨įš„ Immich įŽĄį†å‘˜å¯ģæą‚å¸ŽåŠŠã€‚", - "asset_restored_successfully": "åˇ˛æˆåŠŸæĸå¤æ‰€æœ‰éĄšį›Ž", + "asset_not_found_on_device_android": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩčĩ„æē", + "asset_not_found_on_device_ios": "čŽžå¤‡ä¸ŠæœĒ扞到č¯Ĩčĩ„æēã€‚åĻ‚æžœæ‚¨äŊŋᔍäē† iCloudīŧŒå¯čƒŊæ˜¯į”ąäēŽ iCloud 中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", + "asset_not_found_on_icloud": "iCloud 中æœĒ扞到č¯Ĩčĩ„æēã€‚可čƒŊæ˜¯į”ąäēŽ iCloud 中存储äē†é”™č¯¯įš„æ–‡äģļå¯ŧ致čĩ„æēæ— æŗ•čŽŋ问", + "asset_offline": "čĩ„æēįĻģįēŋ", + "asset_offline_description": "įŖį›˜ä¸ŠæœĒ扞到此外部čĩ„æēã€‚蝎联įŗģæ‚¨įš„ Immich įŽĄį†å‘˜å¯ģæą‚å¸ŽåŠŠã€‚", + "asset_restored_successfully": "čĩ„æēæĸ复成功", "asset_skipped": "厞莺čŋ‡", - "asset_skipped_in_trash": "åˇ˛å›žæ”ļ", - "asset_trashed": "čĩ„äē§åˇ˛čĸĢ删除", - "asset_troubleshoot": "čĩ„äē§æ•…障排除", + "asset_skipped_in_trash": "在回æ”ļį̙䏭", + "asset_trashed": "čĩ„æēåˇ˛į§ģč‡ŗå›žæ”ļįĢ™", + "asset_troubleshoot": "čĩ„æēč¯Šæ–­", "asset_uploaded": "厞䏊äŧ ", "asset_uploading": "上äŧ ä¸­â€Ļ", - "asset_viewer_settings_subtitle": "įŽĄį†å›žå瓿ĩč§ˆå™¨čŽžįŊŽ", + "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_albums_count": "厞æˇģ加 {assetTotal, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}到 {albumTotal, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}", - "assets_cannot_be_added_to_album_count": "æ— æŗ•æˇģ加 {count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}} åˆ°į›¸å†Œä¸­", - "assets_cannot_be_added_to_albums": "æ— æŗ•æˇģ加 {count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}} åˆ°į›¸å†Œ", - "assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}", - "assets_deleted_permanently": "{count} ä¸ĒéĄšį›Žåˇ˛čĸĢæ°¸äš…删除", - "assets_deleted_permanently_from_server": "åˇ˛æ°¸äš…į§ģ除 {count} ä¸ĒéĄšį›Ž", - "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} ä¸ĒéĄšį›Žåˇ˛æ”žå…Ĩ回æ”ļįĢ™", - "assets_were_part_of_album_count": "{count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}}厞įģåœ¨į›¸å†Œä¸­", - "assets_were_part_of_albums_count": "{count, plural, one {ä¸ĒéĄšį›Ž} other {ä¸ĒéĄšį›Ž}} åˇ˛åœ¨į›¸å†Œä¸­", + "assets": "čĩ„æē", + "assets_added_count": "厞æˇģ加{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_added_to_album_count": "åˇ˛å‘į›¸å†Œæˇģ加{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_added_to_albums_count": "厞向 {albumTotal, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}æˇģ加 {assetTotal, plural, one {# ä¸Ēčĩ„æē} other {# ä¸Ēčĩ„æē}}", + "assets_cannot_be_added_to_album_count": "æ— æŗ•å‘į›¸å†Œæˇģ加{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}", + "assets_cannot_be_added_to_albums": "æ— æŗ•å‘äģģäŊ•一ä¸Ēį›¸å†Œæˇģ加 {count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}", + "assets_count": "{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_deleted_permanently": "åˇ˛æ°¸äš…åˆ é™¤ {count} ä¸Ēčĩ„æē", + "assets_deleted_permanently_from_server": "åˇ˛æ°¸äš…į§ģ除 {count} ä¸Ēčĩ„äē§", + "assets_downloaded_failed": "{count, plural, one {厞䏋čŊŊ#ä¸Ē文äģļ - {error} ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ} other {厞䏋čŊŊ#ä¸Ē文äģļ - {error} ä¸Ē文äģļ下čŊŊå¤ąč´Ĩ}}", + "assets_downloaded_successfully": "{count, plural, one {åˇ˛æˆåŠŸä¸‹čŊŊ # ä¸Ē文äģļ} other {åˇ˛æˆåŠŸä¸‹čŊŊ # ä¸Ē文äģļ}}", + "assets_moved_to_trash_count": "厞将{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}į§ģ动到回æ”ļįĢ™", + "assets_permanently_deleted_count": "åˇ˛æ°¸äš…åˆ é™¤{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_removed_count": "厞į§ģ除{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_removed_permanently_from_device": "厞äģŽæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤ {count} ä¸Ēčĩ„æē", + "assets_restore_confirmation": "æ‚¨įĄŽåŽščρæĸ复回æ”ļįĢ™ä¸­įš„æ‰€æœ‰čĩ„æēå—īŧŸæ­¤æ“äŊœæ— æŗ•撤销īŧč¯ˇæŗ¨æ„īŧŒäģģäŊ•įĻģįēŋčĩ„æēæ— æŗ•通čŋ‡æ­¤æ–šåŧæĸ复。", + "assets_restored_count": "厞æĸ复{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}", + "assets_restored_successfully": "åˇ˛æˆåŠŸæĸ复{count}ä¸Ēčĩ„æē", + "assets_trashed": "{count} ä¸Ēčĩ„æēį§ģč‡ŗå›žæ”ļįĢ™", + "assets_trashed_count": "厞将{count, plural, one {#ä¸Ēčĩ„æē} other {#ä¸Ēčĩ„æē}}į§ģč‡ŗå›žæ”ļįĢ™", + "assets_trashed_from_server": "Immich æœåŠĄå™¨ä¸Šåˇ˛į§ģ除 {count} ä¸Ēčĩ„æē", + "assets_were_part_of_album_count": "{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}}厞圍č¯Ĩį›¸å†Œä¸­", + "assets_were_part_of_albums_count": "{count, plural, one {ä¸Ēčĩ„æē} other {ä¸Ēčĩ„æē}} 厞存圍äēŽčŋ™äē›į›¸å†Œä¸­", "authorized_devices": "åˇ˛æŽˆæƒčŽžå¤‡", - "automatic_endpoint_switching_subtitle": "čŋžæŽĨ指厚 Wi-Fi æ—ļäŊŋᔍæœŦ地įŊ‘įģœīŧŒåĻ则äŊŋį”¨å¤–éƒ¨įŊ‘įģœ", + "automatic_endpoint_switching_subtitle": "åœ¨å¯į”¨æ—ļ通čŋ‡æŒ‡åŽšįš„ Wi-Fi čŋ›čĄŒæœŦ地čŋžæŽĨīŧŒå…ļäģ–äŊįŊŽåˆ™äŊŋᔍæ›ŋäģŖįŊ‘įģœčŋžæŽĨ", "automatic_endpoint_switching_title": "č‡Ē动切æĸ URL", "autoplay_slideshow": "č‡Ē动播攞åšģၝቇ", "back": "čŋ”回", - "back_close_deselect": "čŋ”å›žã€å…ŗé—­æˆ–åé€‰", - "background_backup_running_error": "后台备äģŊæ­Ŗåœ¨čŋčĄŒīŧŒæ— æŗ•启动手动备äģŊ", + "back_close_deselect": "čŋ”å›žã€å…ŗé—­æˆ–å–æļˆé€‰æ‹Š", + "background_backup_running_error": "后台备äģŊæ­Ŗåœ¨čŋčĄŒä¸­īŧŒæ— æŗ•启动手动备äģŊ", "background_location_permission": "后台厚äŊæƒé™", - "background_location_permission_content": "ä¸ēįĄŽäŋåŽå°čŋčĄŒæ—ļč‡Ē动切æĸįŊ‘įģœīŧŒéœ€æŽˆäēˆ Immich *始įģˆå…čŽ¸į˛žįĄŽåŽšäŊ* 权限īŧŒäģĨ蝆åˆĢ Wi-Fi įŊ‘įģœåį§°", - "background_options": "čƒŒæ™¯é€‰éĄš", + "background_location_permission_content": "ä¸ēäē†åœ¨åŽå°čŋčĄŒæ—ļåŽžįŽ°įŊ‘įģœåˆ‡æĸīŧŒImmich åŋ…éĄģ始į숿‹Ĩæœ‰į˛žįĄŽäŊįŊŽčŽŋ闎权限īŧŒäģĨäžŋåē”ᔍčƒŊ够č¯ģ取 Wi-Fi įŊ‘įģœįš„åį§°", + "background_options": "åŽå°é€‰éĄš", "backup": "备äģŊ", "backup_album_selection_page_albums_device": "čŽžå¤‡ä¸Šįš„į›¸å†Œīŧˆ{count}īŧ‰", - "backup_album_selection_page_albums_tap": "单å‡ģ选中īŧŒåŒå‡ģ取æļˆ", - "backup_album_selection_page_assets_scatter": "éĄšį›Žäŧšåˆ†æ•Ŗåœ¨å¤šä¸Ēį›¸å†Œä¸­ã€‚å› æ­¤īŧŒå¯äģĨ在备äģŊčŋ‡į¨‹ä¸­åŒ…åĢæˆ–æŽ’é™¤į›¸å†Œã€‚", + "backup_album_selection_page_albums_tap": "单å‡ģ包åĢīŧŒåŒå‡ģ排除", + "backup_album_selection_page_assets_scatter": "čĩ„æēæ–‡äģļ可čƒŊåˆ†æ•Ŗåœ¨å¤šä¸Ēį›¸å†Œä¸­ã€‚å› æ­¤īŧŒåœ¨å¤‡äģŊčŋ‡į¨‹ä¸­īŧŒæ‚¨å¯äģĨ选拊包åĢæˆ–æŽ’é™¤į‰šåŽšįš„į›¸å†Œã€‚", "backup_album_selection_page_select_albums": "é€‰æ‹Šį›¸å†Œ", "backup_album_selection_page_selection_info": "选拊äŋĄæ¯", - "backup_album_selection_page_total_assets": "æ€ģ莥", + "backup_album_selection_page_total_assets": "唯一čĩ„æēæ€ģ莥", "backup_albums_sync": "备äģŊį›¸å†ŒåŒæ­Ĩ", "backup_all": "全部", - "backup_background_service_backup_failed_message": "备äģŊå¤ąč´ĨīŧŒæ­Ŗåœ¨é‡č¯•â€Ļ", - "backup_background_service_complete_notification": "čĩ„äē§å¤‡äģŊ厌成", - "backup_background_service_connection_failed_message": "čŋžæŽĨæœåŠĄå™¨å¤ąč´ĨīŧŒæ­Ŗåœ¨é‡č¯•â€Ļ", - "backup_background_service_current_upload_notification": "æ­Ŗåœ¨ä¸Šäŧ  {filename}", - "backup_background_service_default_notification": "æ­Ŗåœ¨æŖ€æŸĨæ–°éĄšį›Žâ€Ļ", - "backup_background_service_error_title": "备äģŊå¤ąč´Ĩ", - "backup_background_service_in_progress_notification": "æ­Ŗåœ¨å¤‡äģŊæ‚¨įš„čĩ„äē§â€Ļ", - "backup_background_service_upload_failure_notification": "{filename}上äŧ å¤ąč´Ĩ", + "backup_background_service_backup_failed_message": "čĩ„æēå¤‡äģŊå¤ąč´Ĩã€‚æ­Ŗåœ¨é‡č¯•â€Ļ", + "backup_background_service_complete_notification": "čĩ„æēå¤‡äģŊ厌成", + "backup_background_service_connection_failed_message": "æ— æŗ•čŋžæŽĨåˆ°æœåŠĄå™¨ã€‚æ­Ŗåœ¨é‡č¯•â€Ļ", + "backup_background_service_current_upload_notification": "æ­Ŗåœ¨ä¸Šäŧ  “{filename}”", + "backup_background_service_default_notification": "æ­Ŗåœ¨æŖ€æŸĨ新čĩ„æēâ€Ļ", + "backup_background_service_error_title": "备äģŊ错蝝", + "backup_background_service_in_progress_notification": "æ­Ŗåœ¨å¤‡äģŊæ‚¨įš„čĩ„æēâ€Ļ", + "backup_background_service_upload_failure_notification": "“{filename}”上äŧ å¤ąč´Ĩ", "backup_controller_page_albums": "备äģŊį›¸å†Œ", - "backup_controller_page_background_app_refresh_disabled_content": "čρäŊŋį”¨åŽå°å¤‡äģŊ功čƒŊīŧŒč¯ˇåœ¨â€œčŽžįŊŽâ€>â€œå¸¸č§„â€>“后台åē”į”¨åˆˇæ–°â€ä¸­å¯į”¨åŽå°åē”ᔍፋåēåˆˇæ–°ã€‚", - "backup_controller_page_background_app_refresh_disabled_title": "后台åē”į”¨åˆˇæ–°åˇ˛įρᔍ", + "backup_controller_page_background_app_refresh_disabled_content": "åœ¨â€œčŽžįŊŽâ€>â€œé€šį”¨â€>“后台 App åˆˇæ–°â€ä¸­å¯į”¨æ­¤åŠŸčƒŊīŧŒäģĨäŊŋį”¨åŽå°å¤‡äģŊ。", + "backup_controller_page_background_app_refresh_disabled_title": "后台 App åˆˇæ–°åˇ˛å…ŗé—­", "backup_controller_page_background_app_refresh_enable_button_text": "å‰åž€čŽžįŊŽ", - "backup_controller_page_background_battery_info_link": "怎䚈做", - "backup_controller_page_background_battery_info_message": "ä¸ēäē†čŽˇåž—æœ€äŊŗįš„后台备äģŊäŊ“énjīŧŒč¯ˇįρᔍäģģäŊ•限åˆļ Immich 后台æ´ģåŠ¨įš„į”ĩæą äŧ˜åŒ–。\n\nį”ąäēŽčŋ™æ˜¯čŽžå¤‡į›¸å…ŗįš„īŧŒå› æ­¤č¯ˇæŸĨæ‰žčŽžå¤‡åˆļé€ å•†æäž›įš„äŋĄæ¯čŋ›čĄŒæ“äŊœã€‚", + "backup_controller_page_background_battery_info_link": "åą•į¤ē操äŊœæ­ĨéǤ", + "backup_controller_page_background_battery_info_message": "ä¸ēčŽˇåž—æœ€äŊŗįš„后台备äģŊäŊ“énjīŧŒč¯ˇåœ¨įŗģįģŸčŽžįŊŽä¸­įĻį”¨é’ˆå¯š Immich įš„äģģäŊ•į”ĩæą äŧ˜åŒ–限åˆļ。\n\nį”ąäēŽč¯Ĩ莞įŊŽå› čŽžå¤‡č€Œåŧ‚īŧŒč¯ˇæŸĨč¯ĸæ‚¨čŽžå¤‡åˆļé€ å•†įš„å…ˇäŊ“čĻæą‚ã€‚", "backup_controller_page_background_battery_info_ok": "我įŸĨ道äē†", "backup_controller_page_background_battery_info_title": "į”ĩæą äŧ˜åŒ–", - "backup_controller_page_background_charging": "äģ…å……į”ĩæ—ļ", - "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_charging": "äģ…在充į”ĩæ—ļ", + "backup_controller_page_background_configure_error": "åŽå°æœåŠĄé…įŊŽå¤ąč´Ĩ", + "backup_controller_page_background_delay": "åģļčŋŸæ–°æ–‡äģļ备äģŊīŧš{duration}", + "backup_controller_page_background_description": "åŧ€å¯åŽå°æœåŠĄīŧŒåŗå¯åœ¨æ— éœ€æ‰“åŧ€ App įš„æƒ…å†ĩ下īŧŒč‡Ē动备äģŊ所有新文äģļ", + "backup_controller_page_background_is_off": "后台č‡Ē动备äģŊæœĒåŧ€å¯", "backup_controller_page_background_is_on": "后台č‡Ē动备äģŊ厞åŧ€å¯", "backup_controller_page_background_turn_off": "å…ŗé—­åŽå°æœåŠĄ", "backup_controller_page_background_turn_on": "åŧ€å¯åŽå°æœåŠĄ", - "backup_controller_page_background_wifi": "äģ… Wi-Fi", + "backup_controller_page_background_wifi": "äģ…在 Wi-Fi 下", "backup_controller_page_backup": "备äģŊ", - "backup_controller_page_backup_selected": "厞选䏭īŧš ", + "backup_controller_page_backup_selected": "厞选īŧš ", "backup_controller_page_backup_sub": "厞备äģŊįš„į…§į‰‡å’Œč§†éĸ‘", "backup_controller_page_created": "创åģēæ—ļ间īŧš{date}", - "backup_controller_page_desc_backup": "打åŧ€å‰å°å¤‡äģŊīŧŒäģĨåœ¨į¨‹åēčŋčĄŒæ—ļč‡Ē动备äģŊæ–°éĄšį›Žã€‚", + "backup_controller_page_desc_backup": "åŧ€å¯å‰å°å¤‡äģŊīŧŒæ‰“åŧ€ App åŗč‡Ē动上äŧ æ–°æ–‡äģļ。", "backup_controller_page_excluded": "åˇ˛æŽ’é™¤īŧš ", "backup_controller_page_failed": "å¤ąč´Ĩīŧˆ{count}īŧ‰", - "backup_controller_page_filename": "文äģļåį§°īŧš{filename} [{size}]", + "backup_controller_page_filename": "文äģļ名īŧš{filename} [{size}]", "backup_controller_page_id": "IDīŧš{id}", "backup_controller_page_info": "备äģŊäŋĄæ¯", - "backup_controller_page_none_selected": "æœĒ选拊", + "backup_controller_page_none_selected": "暂æœĒ选拊", "backup_controller_page_remainder": "削äŊ™", - "backup_controller_page_remainder_sub": "所选数捎中尚æœĒ备äģŊįš„æ•°æŽ", + "backup_controller_page_remainder_sub": "åˇ˛é€‰éĄšä¸­å°šæœĒ备äģŊįš„į…§į‰‡å’Œč§†éĸ‘", "backup_controller_page_server_storage": "æœåŠĄå™¨å­˜å‚¨", "backup_controller_page_start_backup": "åŧ€å§‹å¤‡äģŊ", - "backup_controller_page_status_off": "前台č‡Ē动备äģŊ厞兺闭", - "backup_controller_page_status_on": "前台č‡Ē动备äģŊ厞åŧ€å¯", - "backup_controller_page_storage_format": "{used}/{total} 厞äŊŋᔍ", - "backup_controller_page_to_backup": "čρ备äģŊįš„į›¸å†Œ", - "backup_controller_page_total_sub": "é€‰ä¸­į›¸å†Œä¸­æ‰€æœ‰ä¸é‡å¤įš„č§†éĸ‘和回像", + "backup_controller_page_status_off": "æœĒåŧ€å¯å‰å°č‡Ē动备äģŊ", + "backup_controller_page_status_on": "前台č‡Ē动备äģŊåˇ˛æ‰“åŧ€", + "backup_controller_page_storage_format": "厞ᔍ {used}īŧˆå…ą {total}īŧ‰", + "backup_controller_page_to_backup": "垅备äģŊįš„į›¸å†Œ", + "backup_controller_page_total_sub": "包åĢæ‰€é€‰į›¸å†Œå†…å…¨éƒ¨å”¯ä¸€įš„į…§į‰‡å’Œč§†éĸ‘", "backup_controller_page_turn_off": "å…ŗé—­å‰å°å¤‡äģŊ", "backup_controller_page_turn_on": "åŧ€å¯å‰å°å¤‡äģŊ", - "backup_controller_page_uploading_file_info": "æ­Ŗåœ¨ä¸Šäŧ ä¸­įš„æ–‡äģļäŋĄæ¯", - "backup_err_only_album": "不čƒŊį§ģé™¤å”¯ä¸€įš„ä¸€ä¸Ēį›¸å†Œ", + "backup_controller_page_uploading_file_info": "æ­Ŗåœ¨ä¸Šäŧ æ–‡äģļäŋĄæ¯", + "backup_err_only_album": "æ— æŗ•åˆ é™¤å”¯ä¸€įš„į›¸å†Œ", "backup_error_sync_failed": "同æ­Ĩå¤ąč´Ĩã€‚æ— æŗ•å¤„į†å¤‡äģŊ。", - "backup_info_card_assets": "饚", + "backup_info_card_assets": "čĩ„äē§", "backup_manual_cancelled": "åˇ˛å–æļˆ", "backup_manual_in_progress": "上äŧ æ­Ŗåœ¨čŋ›čĄŒä¸­īŧŒč¯ˇį¨åŽå†č¯•", "backup_manual_success": "成功", "backup_manual_title": "上äŧ įŠļ态", "backup_options": "备äģŊ选项", "backup_options_page_title": "备äģŊ选项", - "backup_setting_subtitle": "įŽĄį†åŽå°å’Œå‰å°ä¸Šäŧ čŽžįŊŽ", + "backup_setting_subtitle": "įŽĄį†åŽå°ä¸Žå‰å°ä¸Šäŧ čŽžįŊŽ", "backup_settings_subtitle": "įŽĄį†ä¸Šäŧ čŽžįŊŽ", - "backup_upload_details_page_more_details": "į‚šå‡ģäē†č§Ŗč¯Ļ情", + "backup_upload_details_page_more_details": "į‚šå‡ģæŸĨįœ‹č¯Ļ情", "backward": "后退", - "biometric_auth_enabled": "į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯åˇ˛å¯į”¨", - "biometric_locked_out": "您čĸĢé”åŽšåœ¨į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯äš‹å¤–", - "biometric_no_options": "æ˛Ąæœ‰å¯į”¨įš„į”Ÿį‰Šč¯†åˆĢ选项", - "biometric_not_available": "į”Ÿį‰Šč¯†åˆĢčēĢäģŊéĒŒč¯åœ¨æ­¤čŽžå¤‡ä¸Šä¸å¯į”¨", + "biometric_auth_enabled": "į”Ÿį‰Šč¯†åˆĢčŽ¤č¯åˇ˛å¯į”¨", + "biometric_locked_out": "æ‚¨åˇ˛čĸĢ锁厚īŧŒæ— æŗ•äŊŋį”¨į”Ÿį‰Šč¯†åˆĢčŽ¤č¯", + "biometric_no_options": "æ— å¯į”¨įš„į”Ÿį‰Šč¯†åˆĢ选项", + "biometric_not_available": "æœŦčŽžå¤‡ä¸æ”¯æŒį”Ÿį‰Šč¯†åˆĢčŽ¤č¯", "birthdate_saved": "å‡ēį”Ÿæ—Ĩ期äŋå­˜æˆåŠŸ", - "birthdate_set_description": "å‡ēį”Ÿæ—ĨæœŸį”¨äēŽčŽĄįŽ—į…§į‰‡ä¸­č¯Ĩäēēį‰Šåœ¨æ‹į…§æ—ļįš„åš´éž„ã€‚", - "blurred_background": "čƒŒæ™¯æ¨ĄįŗŠ", - "bugs_and_feature_requests": "Bug 与功čƒŊč¯ˇæą‚", + "birthdate_set_description": "å‡ēį”Ÿæ—ĨæœŸį”¨äēŽčŽĄįŽ—æ‹æ‘„æ­¤į…§į‰‡æ—ļæ­¤äēēįš„åš´éž„ã€‚", + "blurred_background": "čƒŒæ™¯č™šåŒ–", + "bugs_and_feature_requests": "问éĸ˜ä¸ŽåŠŸčƒŊåģē莎", "build": "构åģēį‰ˆæœŦ", "build_image": "é•œåƒį‰ˆæœŦ", - "bulk_delete_duplicates_confirmation": "æ‚¨įĄŽåŽščĻæ‰šé‡åˆ é™¤{count, plural, one {#ä¸Ēé‡å¤éĄšį›Ž} other {#ä¸Ēé‡å¤éĄšį›Ž}}吗īŧŸčŋ™å°†äŋį•™æ¯ä¸Ēįģ„ä¸­æœ€å¤§įš„éĄšį›Žåšļ永䚅删除所有å…ļåŽƒé‡å¤éĄšį›Žã€‚æŗ¨æ„īŧšč¯Ĩ操äŊœæ— æŗ•čĸĢæ’¤æļˆīŧ", - "bulk_keep_duplicates_confirmation": "æ‚¨įĄŽåŽščρäŋį•™{count, plural, one {#ä¸Ēé‡å¤éĄšį›Ž} other {#ä¸Ēé‡å¤éĄšį›Ž}}吗īŧŸčŋ™å°†æ¸…įŠēæ‰€æœ‰é‡å¤čŽ°åŊ•īŧŒäŊ†ä¸äŧšåˆ é™¤äģģäŊ•内厚。", - "bulk_trash_duplicates_confirmation": "æ‚¨įĄŽåŽščĻæ‰šé‡åˆ é™¤{count, plural, one {#ä¸Ēé‡å¤éĄšį›Ž} other {#ä¸Ēé‡å¤éĄšį›Ž}}吗īŧŸčŋ™å°†äŋį•™æ¯įģ„ä¸­æœ€å¤§įš„éĄšį›Žåšļ删除所有å…ļåŽƒé‡å¤éĄšį›Žã€‚", + "bulk_delete_duplicates_confirmation": "æ‚¨įĄŽåŽščĻæ‰šé‡åˆ é™¤{count, plural, one {#ä¸Ēé‡å¤éĄš} other {#ä¸Ēé‡å¤éĄš}}吗īŧŸčŋ™å°†äŋį•™æ¯įģ„中äŊ“į§¯æœ€å¤§įš„æ–‡äģļīŧŒåšļ永䚅删除å…ļäŊ™æ‰€æœ‰é‡å¤éĄšã€‚č¯Ĩ操äŊœæ— æŗ•čĸĢæ’¤æļˆīŧ", + "bulk_keep_duplicates_confirmation": "æ‚¨įĄŽåŽščρäŋį•™{count, plural, one {#ä¸Ēé‡å¤éĄš} other {#ä¸Ēé‡å¤éĄš}}吗īŧŸčŋ™å°†æ ‡čŽ°æ‰€æœ‰é‡å¤įģ„ä¸ē厞觪冺īŧŒä¸”不äŧšåˆ é™¤äģģäŊ•æ–‡äģļ。", + "bulk_trash_duplicates_confirmation": "æ‚¨įĄŽåŽščĻæ‰šé‡å°†{count, plural, one {#ä¸Ēé‡å¤éĄš} other {#ä¸Ēé‡å¤éĄš}}į§ģč‡ŗå›žæ”ļį̙吗īŧŸčŋ™å°†äŋį•™æ¯įģ„中äŊ“į§¯æœ€å¤§įš„æ–‡äģļīŧŒåšļ将å…ļäŊ™æ‰€æœ‰é‡å¤éĄšį§ģč‡ŗå›žæ”ļįĢ™ã€‚", "buy": "č´­äš° Immich", "cache_settings_clear_cache_button": "清除įŧ“å­˜", - "cache_settings_clear_cache_button_title": "清除åē”ᔍįŧ“å­˜ã€‚åœ¨é‡æ–°į”Ÿæˆįŧ“存䚋前īŧŒå°†æ˜žč‘—åŊąå“åē”į”¨įš„æ€§čƒŊ。", + "cache_settings_clear_cache_button_title": "æ¸…į†åē”ᔍįŧ“存。在įŧ“存重åģ翜Ÿé—´īŧŒåē”į”¨įš„čŋčĄŒé€ŸåēĻäŧšæ˜Žæ˜žå˜æ…ĸ。", "cache_settings_duplicated_assets_clear_button": "清除", - "cache_settings_duplicated_assets_subtitle": "åē”ᔍፋåēåŋŊį•Ĩįš„į…§į‰‡å’Œč§†éĸ‘", - "cache_settings_duplicated_assets_title": "é‡å¤éĄšį›Žīŧˆ{count}īŧ‰", + "cache_settings_duplicated_assets_subtitle": "åŋŊį•Ĩåˆ—čĄ¨ä¸­įš„åĒ’äŊ“æ–‡äģļ", + "cache_settings_duplicated_assets_title": "重复čĩ„äē§īŧˆ{count}īŧ‰", "cache_settings_statistics_album": "回åē“įŧŠį•Ĩ回", - "cache_settings_statistics_full": "厌整回像", + "cache_settings_statistics_full": "原回", "cache_settings_statistics_shared": "å…ąäēĢį›¸å†ŒįŧŠį•Ĩ回", "cache_settings_statistics_thumbnail": "įŧŠį•Ĩ回", - "cache_settings_statistics_title": "įŧ“å­˜äŊŋį”¨æƒ…å†ĩ", - "cache_settings_subtitle": "控åˆļ Immich app įš„įŧ“å­˜čĄŒä¸ē", + "cache_settings_statistics_title": "įŧ“å­˜å į”¨æƒ…å†ĩ", + "cache_settings_subtitle": "įŽĄį† Immich 手æœēįĢ¯įš„įŧ“å­˜", "cache_settings_tile_subtitle": "莞įŊŽæœŦåœ°å­˜å‚¨čĄŒä¸ē", "cache_settings_tile_title": "æœŦ地存储", "cache_settings_title": "įŧ“å­˜čŽžįŊŽ", @@ -711,17 +746,31 @@ "change_password_form_password_mismatch": "å¯†į ä¸åŒšé…", "change_password_form_reenter_new_password": "再æŦĄčž“å…Ĩæ–°å¯†į ", "change_pin_code": "äŋŽæ”šPIN᠁", + "change_trigger": "更攚č§Ļå‘æĄäģļ", + "change_trigger_prompt": "æ‚¨įĄŽåŽščĻæ›´æ”šč§Ļå‘æĄäģļ吗īŧŸčŋ™å°†åˆ é™¤æ‰€æœ‰įŽ°æœ‰æ“äŊœå’Œį­›é€‰ã€‚", "change_your_password": "äŋŽæ”šæ‚¨įš„å¯†į ", "changed_visibility_successfully": "æ›´æ”šå¯č§æ€§æˆåŠŸ", "charging": "充į”ĩ", "charging_requirement_mobile_backup": "后台备äģŊ需čĻčŽžå¤‡å¤„äēŽå……į”ĩįŠļ态", "check_corrupt_asset_backup": "æŖ€æŸĨ备äģŊ是åĻ损坏", "check_corrupt_asset_backup_button": "æ‰§čĄŒæŖ€æŸĨ", - "check_corrupt_asset_backup_description": "äģ…在čŋžæŽĨ到 Wi-Fi åšļåŽŒæˆæ‰€æœ‰éĄšį›Žå¤‡äģŊåŽæ‰§čĄŒæ­¤æŖ€æŸĨ。č¯Ĩčŋ‡į¨‹å¯čƒŊ需čĻå‡ åˆ†é’Ÿã€‚", + "check_corrupt_asset_backup_description": "äģ…在čŋžæŽĨ到 Wi-Fi åšļ厌成所有čĩ„äē§å¤‡äģŊåŽæ‰§čĄŒæ­¤æŖ€æŸĨ。č¯Ĩčŋ‡į¨‹å¯čƒŊ需čĻå‡ åˆ†é’Ÿã€‚", "check_logs": "æŖ€æŸĨæ—Ĩåŋ—", "checksum": "æ ĄéĒŒå’Œ", "choose_matching_people_to_merge": "é€‰æ‹ŠåŒšé…įš„äēēčŋ›čĄŒåˆåšļ", "city": "城市", + "cleanup_confirm_description": "Immichå‘įŽ°{count}ä¸Ēčĩ„äē§īŧˆåœ¨{date}䚋前创åģēīŧ‰åˇ˛åމ免备äģŊåˆ°æœåŠĄå™¨ã€‚æ˜¯åĻäģŽæ­¤čŽžå¤‡ä¸­åˆ é™¤æœŦ地副æœŦīŧŸ", + "cleanup_confirm_prompt_title": "äģŽæ­¤čŽžå¤‡åˆ é™¤īŧŸ", + "cleanup_deleted_assets": "将{count}ä¸Ēčĩ„äē§į§ģåŠ¨åˆ°čŽžå¤‡å›žæ”ļįĢ™", + "cleanup_deleting": "į§ģč‡ŗå›žæ”ļįĢ™...", + "cleanup_found_assets": "扞到{count}ä¸Ē备äģŊčĩ„äē§", + "cleanup_found_assets_with_size": "扞到 {count} ä¸Ē厞备äģŊįš„æ–‡äģļ ({size})", + "cleanup_icloud_shared_albums_excluded": "iCloudå…ąäēĢį›¸å†ŒčĸĢæŽ’é™¤åœ¨æ‰Ģ描䚋外", + "cleanup_no_assets_found": "æœĒ扞到įŦĻ合上čŋ°æĄäģļįš„æ–‡äģļ。释攞įŠē间功čƒŊåĒčƒŊį§ģ除厞备äģŊåˆ°æœåŠĄå™¨įš„æ–‡äģļ", + "cleanup_preview_title": "čĻåˆ é™¤įš„čĩ„äē§īŧˆ{count}ä¸Ēīŧ‰", + "cleanup_step3_description": "æ‰Ģ描įŦĻ合您æ—Ĩ期和äŋį•™čŽžįŊŽįš„厞备äģŊ文äģļ。", + "cleanup_step4_summary": "将äģŽæœŦæœēį§ģ除 {count} ä¸Ē文äģļīŧˆåˆ›åģēäēŽ {date} 䚋前īŧ‰ã€‚ᅧቇäģå¯åœ¨ Immich åē”ᔍ䏭æŸĨįœ‹ã€‚", + "cleanup_trash_hint": "čĻåŽŒå…¨å›žæ”ļ存储įŠē间īŧŒč¯ˇæ‰“åŧ€įŗģįģŸåē“åē”ᔍፋåēåšļ清įŠē回æ”ļįĢ™", "clear": "清įŠē", "clear_all": "清įŠē全部", "clear_all_recent_searches": "清除所有最čŋ‘搜į´ĸ", @@ -733,6 +782,8 @@ "client_cert_import": "å¯ŧå…Ĩ", "client_cert_import_success_msg": "åŽĸæˆˇį̝蝁äšĻ厞å¯ŧå…Ĩ", "client_cert_invalid_msg": "æ— æ•ˆįš„č¯äšĻ文äģ￈–坆᠁错蝝", + "client_cert_password_message": "输å…Ĩ蝁äšĻįš„å¯†į ", + "client_cert_password_title": "蝁äšĻ坆᠁", "client_cert_remove_msg": "åŽĸæˆˇį̝蝁äšĻ厞į§ģ除", "client_cert_subtitle": "äģ…æ”¯æŒPKCS12īŧˆ.p12、.pfxīŧ‰æ ŧåŧã€‚蝁äšĻå¯ŧå…Ĩ/删除äģ…在į™ģåŊ•å‰å¯į”¨", "client_cert_title": "SSL åŽĸæˆˇį̝蝁äšĻ[厞éĒŒæ€§]", @@ -787,31 +838,40 @@ "create_album": "创åģēį›¸å†Œ", "create_album_page_untitled": "æœĒå‘Ŋ名", "create_api_key": "创åģē API Key", - "create_library": "创åģē回åē“", + "create_first_workflow": "创åģēįŦŦ一ä¸ĒåˇĨäŊœæĩ", + "create_library": "创åģēčĩ„äē§åē“", "create_link": "创åģē链æŽĨ", "create_link_to_share": "创åģēå…ąäēĢ链æŽĨ", "create_link_to_share_description": "čŽˇåž—æ­¤é“žæŽĨįš„äēē均可æŸĨįœ‹æ‰€é€‰į…§į‰‡", "create_new": "新åģē", "create_new_person": "创åģēæ–°äēēį‰Š", - "create_new_person_hint": "æŒ‡æ´žåˇ˛é€‰æ‹ŠéĄšį›Žåˆ°æ–°įš„äēēį‰Š", + "create_new_person_hint": "æŒ‡æ´žåˇ˛é€‰æ‹Ščĩ„äē§åˆ°æ–°įš„äēēį‰Š", "create_new_user": "创åģēæ–°į”¨æˆˇ", - "create_shared_album_page_share_add_assets": "æˇģåŠ éĄšį›Ž", - "create_shared_album_page_share_select_photos": "é€‰æ‹ŠéĄšį›Ž", + "create_shared_album_page_share_add_assets": "æˇģ加čĩ„äē§", + "create_shared_album_page_share_select_photos": "选拊čĩ„äē§", "create_shared_link": "创åģēå…ąäēĢ链æŽĨ", "create_tag": "创åģēæ ‡į­ž", "create_tag_description": "创åģē一ä¸Ēæ–°æ ‡į­žã€‚å¯šäēŽåĩŒåĨ—æ ‡į­žīŧŒč¯ˇčž“å…Ĩæ ‡į­žįš„åŽŒæ•´čˇ¯åž„īŧŒåŒ…æ‹Ŧæ­Ŗæ–œæ īŧˆ/īŧ‰ã€‚", "create_user": "创åģēį”¨æˆˇ", + "create_workflow": "创åģēåˇĨäŊœæĩ", "created": "åˇ˛åˆ›åģē", - "created_at": "åˇ˛åˆ›åģē", + "created_at": "创åģēæ—ļ间", "creating_linked_albums": "æ­Ŗåœ¨åˆ›åģēį›¸å†Œé“žæŽĨâ€Ļ", "crop": "誁å‰Ē", + "crop_aspect_ratio_fixed": "å›ē厚įēĩæ¨Ē比", + "crop_aspect_ratio_free": "č‡Ēį”ąįēĩæ¨Ē比", + "crop_aspect_ratio_original": "原始įēĩæ¨Ē比", "curated_object_page_title": "äē‹į‰Š", "current_device": "åŊ“å‰čŽžå¤‡", "current_pin_code": "åŊ“前PIN᠁", "current_server_address": "åŊ“å‰æœåŠĄå™¨åœ°å€", + "custom_date": "č‡Ē厚䚉æ—Ĩ期", "custom_locale": "č‡Ē厚䚉地åŒē", "custom_locale_description": "æ—Ĩ期和数字昞į¤ēæ ŧåŧčˇŸéšč¯­č¨€å’Œåœ°åŒē", "custom_url": "č‡Ē厚䚉URL", + "cutoff_date_description": "äŋį•™æœ€čŋ‘įš„į…§į‰‡â€Ļ", + "cutoff_day": "{count, plural, one {夊} other {夊}}", + "cutoff_year": "{count, plural, one {åš´} other {åš´}}", "daily_title_text_date": "MMM dd (E)", "daily_title_text_date_year": "YYYYåš´M月Dæ—Ĩ (E)", "dark": "æˇąč‰˛", @@ -829,7 +889,7 @@ "deduplication_criteria_1": "回像大小īŧˆå­—节īŧ‰", "deduplication_criteria_2": "EXIF æ•°æŽčŽĄæ•°", "deduplication_info": "é‡å¤æ•°æŽåˆ é™¤æą‡æ€ģ", - "deduplication_info_description": "čρč‡Ē动éĸ„é€‰éĄšį›Žåšļæ‰šé‡åˆ é™¤é‡å¤éĄšīŧŒæˆ‘äģŦäŧšč€ƒč™‘īŧš", + "deduplication_info_description": "čρč‡Ē动éĸ„选čĩ„äē§åšļæ‰šé‡åˆ é™¤é‡å¤éĄšīŧŒæˆ‘äģŦäŧšč€ƒč™‘īŧš", "default_locale": "éģ˜čޤ地åŒē", "default_locale_description": "æ šæŽæ‚¨įš„æĩč§ˆå™¨åœ°åŒē莞įŊŽæ—Ĩ期和数字昞į¤ēæ ŧåŧ", "delete": "删除", @@ -837,7 +897,7 @@ "delete_action_prompt": "åˇ˛åˆ é™¤ {count} 饚", "delete_album": "åˆ é™¤į›¸å†Œ", "delete_api_key_prompt": "是åĻįĄŽčŽ¤åˆ é™¤æ­¤ API 密é’ĨīŧŸ", - "delete_dialog_alert": "čŋ™äē›éĄšį›Žå°†äģŽ Immich å’Œæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤", + "delete_dialog_alert": "čŋ™äē›čĩ„äē§å°†äģŽ Immich å’Œæ‚¨įš„čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤", "delete_dialog_alert_local": "čŋ™äē›éĄšį›Žå°†äģŽæ‚¨įš„į§ģåŠ¨čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤īŧŒäŊ†äģį„ļ可äģĨäģŽ Immich æœåŠĄå™¨ä¸­å†æŦĄčŽˇå–", "delete_dialog_alert_local_non_backed_up": "éƒ¨åˆ†éĄšį›Žčŋ˜æœĒ备äģŊ臺 Immich æœåŠĄå™¨īŧŒå°†äģŽæ‚¨įš„į§ģåŠ¨čŽžå¤‡ä¸­æ°¸äš…åˆ é™¤", "delete_dialog_alert_remote": "čŋ™äē›éĄšį›Žå°†äģŽ Immich æœåŠĄå™¨ä¸­æ°¸äš…åˆ é™¤", @@ -846,7 +906,7 @@ "delete_duplicates_confirmation": "įĄŽåŽščĻæ°¸äš…åˆ é™¤čŋ™äē›é‡å¤éĄšå—īŧŸ", "delete_face": "删除äēē脸", "delete_key": "删除密é’Ĩ", - "delete_library": "删除回åē“", + "delete_library": "删除čĩ„äē§åē“", "delete_link": "删除铞æŽĨ", "delete_local_action_prompt": "åˇ˛åˆ é™¤æœŦåœ°éĄšį›Ž{count}饚", "delete_local_dialog_ok_backed_up_only": "äģ…åˆ é™¤åˇ˛å¤‡äģŊéĄšį›Ž", @@ -860,14 +920,15 @@ "delete_tag_confirmation_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤â€œ{tagName}â€æ ‡į­žå—īŧŸ", "delete_user": "åˆ é™¤į”¨æˆˇ", "deleted_shared_link": "å…ąäēĢ链æŽĨåˇ˛åˆ é™¤", - "deletes_missing_assets": "åˆ é™¤įŖį›˜ä¸­ä¸ĸå¤ąįš„éĄšį›Ž", + "deletes_missing_assets": "åˆ é™¤įŖį›˜ä¸­ä¸ĸå¤ąįš„čĩ„äē§", "description": "描čŋ°", "description_input_hint_text": "æˇģ加描čŋ°...", "description_input_submit_error": "更新描čŋ°æ—ļå‡ē错īŧŒč¯ˇæŖ€æŸĨæ—Ĩåŋ—äģĨčŽˇå–æ›´å¤šč¯Ļįģ†äŋĄæ¯", "deselect_all": "取æļˆå…¨é€‰", "details": "č¯Ļ情", "direction": "斚向", - "disabled": "厞įρᔍ", + "disable": "įρᔍ", + "disabled": "įρᔍ", "disallow_edits": "ä¸å…čŽ¸įŧ–čž‘", "discord": "Discord į¤žåŒē", "discover": "å‘įŽ°", @@ -882,7 +943,7 @@ "documentation": "å¸ŽåŠŠæ–‡æĄŖ", "done": "厌成", "download": "下čŊŊ", - "download_action_prompt": "æ­Ŗåœ¨ä¸‹čŊŊ {count} ä¸ĒéĄšį›Ž", + "download_action_prompt": "æ­Ŗåœ¨ä¸‹čŊŊ {count} ä¸Ēčĩ„äē§", "download_canceled": "下čŊŊåˇ˛å–æļˆ", "download_complete": "下čŊŊ厌成", "download_enqueue": "厞加å…Ĩ下čŊŊ队列", @@ -892,6 +953,7 @@ "download_include_embedded_motion_videos": "内åĩŒč§†éĸ‘", "download_include_embedded_motion_videos_description": "将厞å†ĩį…§į‰‡ä¸­įš„å†…åĩŒč§†éĸ‘äŊœä¸ē单į‹Ŧ文äģļįēŗå…Ĩ", "download_notfound": "æ— æŗ•æ‰žåˆ°ä¸‹čŊŊ", + "download_original": "下čŊŊ原始文äģļ", "download_paused": "下čŊŊåˇ˛æš‚åœ", "download_settings": "下čŊŊ", "download_settings_description": "įŽĄį†éĄšį›Žä¸‹čŊŊį›¸å…ŗčŽžįŊŽ", @@ -901,6 +963,7 @@ "download_waiting_to_retry": "į­‰åž…é‡č¯•", "downloading": "下čŊŊ中", "downloading_asset_filename": "下čŊŊéĄšį›Žâ€œ{filename}”", + "downloading_from_icloud": "äģŽiCloud下čŊŊ", "downloading_media": "æ­Ŗåœ¨ä¸‹čŊŊåĒ’äŊ“", "drop_files_to_upload": "拖攞文äģļäģĨ上äŧ ", "duplicates": "é‡å¤éĄš", @@ -929,11 +992,22 @@ "edit_tag": "įŧ–čž‘æ ‡į­ž", "edit_title": "įŧ–čž‘æ ‡éĸ˜", "edit_user": "įŧ–čž‘į”¨æˆˇ", + "edit_workflow": "įŧ–čž‘åˇĨäŊœæĩ", "editor": "įŧ–čž‘å™¨", "editor_close_without_save_prompt": "此更攚不äŧščĸĢäŋå­˜", "editor_close_without_save_title": "å…ŗé—­įŧ–čž‘å™¨īŧŸ", - "editor_crop_tool_h2_aspect_ratios": "é•ŋåŽŊ比", - "editor_crop_tool_h2_rotation": "旋čŊŦ", + "editor_confirm_reset_all_changes": "æ‚¨įĄŽåŽščĻé‡įŊŽæ‰€æœ‰æ›´æ”šå—īŧŸ", + "editor_discard_edits_confirm": "攞åŧƒįŧ–čž‘", + "editor_discard_edits_prompt": "您有æœĒäŋå­˜įš„æ›´æ”šīŧŒįĄŽåޚčĻæ”žåŧƒå—īŧŸ", + "editor_discard_edits_title": "įĄŽåŽšæ”žåŧƒįŧ–čž‘å—īŧŸ", + "editor_edits_applied_error": "åē”ᔍįŧ–čž‘å¤ąč´Ĩ", + "editor_edits_applied_success": "įŧ–čž‘åˇ˛æˆåŠŸåē”ᔍ", + "editor_flip_horizontal": "æ°´åšŗįŋģčŊŦ", + "editor_flip_vertical": "åž‚į›´įŋģčŊŦ", + "editor_orientation": "斚向", + "editor_reset_all_changes": "重įŊŽæ›´æ”š", + "editor_rotate_left": "逆æ—ļ针旋čŊŦ90°", + "editor_rotate_right": "éĄēæ—ļ针旋čŊŦ90åēĻ", "email": "é‚ŽįŽą", "email_notifications": "邮äģļ通įŸĨ", "empty_folder": "此文äģļ多ä¸ēįŠē", @@ -952,11 +1026,14 @@ "error_change_sort_album": "æ›´æ”šį›¸å†ŒæŽ’åēå¤ąč´Ĩ", "error_delete_face": "删除äēēč„¸å¤ąč´Ĩ", "error_getting_places": "čŽˇå–äŊįŊŽæ—ļå‡ē错", + "error_loading_albums": "加čŊŊį›¸å†Œå¤ąč´Ĩ", "error_loading_image": "加čŊŊå›žį‰‡æ—ļå‡ē错", - "error_loading_partners": "加čŊŊ同äŧ´æ—ļå‡ē错īŧš{error}", + "error_loading_partners": "加čŊŊ协äŊœč€…æ—ļå‡ē错īŧš{error}", + "error_retrieving_asset_information": "čŽˇå–čĩ„äē§äŋĄæ¯æ—ļå‡ē错", "error_saving_image": "错蝝īŧš{error}", "error_tag_face_bounding_box": "æ ‡čŽ°äēē脸å‡ē错 - æ— æŗ•čŽˇå–äēēč„¸æĄ†åæ ‡", "error_title": "错蝝 - åĨŊ像å‡ēäē†é—Žéĸ˜", + "error_while_navigating": "莺čŊŦ到文äģļæ—ļå‡ē错", "errors": { "cannot_navigate_next_asset": "æ— æŗ•å¯ŧčˆĒ到下一ä¸ĒéĄšį›Ž", "cannot_navigate_previous_asset": "æ— æŗ•å¯ŧčˆĒ到上一ä¸ĒéĄšį›Ž", @@ -1000,7 +1077,7 @@ "unable_to_add_assets_to_shared_link": "æ— æŗ•æˇģåŠ éĄšį›Žåˆ°å…ąäēĢ链æŽĨ", "unable_to_add_comment": "æ— æŗ•æˇģåŠ č¯„čŽē", "unable_to_add_exclusion_pattern": "æ— æŗ•æˇģåŠ æŽ’é™¤č§„åˆ™", - "unable_to_add_partners": "æ— æŗ•æˇģ加同äŧ´", + "unable_to_add_partners": "æ— æŗ•æˇģ加协äŊœč€…", "unable_to_add_remove_archive": "æ— æŗ•{archived, select, true {äģŽåŊ’æĄŖä¸­į§ģ除} other {æˇģåŠ éĄšį›Žåˆ°åŊ’æĄŖ}}", "unable_to_add_remove_favorites": "æ— æŗ•{favorite, select, true {æˇģåŠ éĄšį›Žåˆ°æ”ļ藏} other {äģŽæ”ļ藏中į§ģ除}}", "unable_to_archive_unarchive": "æ— æŗ•{archived, select, true {åŊ’æĄŖ} other {取æļˆåŊ’æĄŖ}}", @@ -1014,9 +1091,10 @@ "unable_to_complete_oauth_login": "æ— æŗ•åŽŒæˆ OAuth į™ģåŊ•", "unable_to_connect": "æ— æŗ•čŋžæŽĨ", "unable_to_copy_to_clipboard": "æ— æŗ•å¤åˆļ到å‰Ē切æŋīŧŒč¯ˇįĄŽäŋæ‚¨åœ¨äŊŋᔍhttpsčŽŋ问æœŦéĄĩ", + "unable_to_create": "æ— æŗ•åˆ›åģēåˇĨäŊœæĩ", "unable_to_create_admin_account": "æ— æŗ•åˆ›åģēįŽĄį†å‘˜č´Ļæˆˇ", "unable_to_create_api_key": "æ— æŗ•åˆ›åģēæ–°įš„ API 密é’Ĩ", - "unable_to_create_library": "æ— æŗ•åˆ›åģē回åē“", + "unable_to_create_library": "æ— æŗ•åˆ›åģēčĩ„äē§åē“", "unable_to_create_user": "æ— æŗ•åˆ›åģēį”¨æˆˇ", "unable_to_delete_album": "æ— æŗ•åˆ é™¤į›¸å†Œ", "unable_to_delete_asset": "æ— æŗ•åˆ é™¤éĄšį›Ž", @@ -1024,6 +1102,7 @@ "unable_to_delete_exclusion_pattern": "æ— æŗ•åˆ é™¤æŽ’é™¤č§„åˆ™", "unable_to_delete_shared_link": "æ— æŗ•åˆ é™¤å…ąäēĢ链æŽĨ", "unable_to_delete_user": "æ— æŗ•åˆ é™¤į”¨æˆˇ", + "unable_to_delete_workflow": "æ— æŗ•åˆ é™¤åˇĨäŊœæĩ", "unable_to_download_files": "æ— æŗ•ä¸‹čŊŊ文äģļ", "unable_to_edit_exclusion_pattern": "æ— æŗ•įŧ–čž‘æŽ’é™¤č§„åˆ™", "unable_to_empty_trash": "æ— æŗ•æ¸…įŠē回æ”ļįĢ™", @@ -1044,9 +1123,9 @@ "unable_to_remove_album_users": "æ— æŗ•äģŽį›¸å†Œä¸­į§ģé™¤į”¨æˆˇ", "unable_to_remove_api_key": "æ— æŗ•į§ģ除 API 密é’Ĩ", "unable_to_remove_assets_from_shared_link": "æ— æŗ•äģŽå…ąäēĢ链æŽĨ中į§ģé™¤éĄšį›Ž", - "unable_to_remove_library": "æ— æŗ•į§ģ除回åē“", - "unable_to_remove_partner": "æ— æŗ•į§ģ除同äŧ´", - "unable_to_remove_reaction": "æ— æŗ•į§ģ除回åē”", + "unable_to_remove_library": "æ— æŗ•į§ģ除čĩ„äē§åē“", + "unable_to_remove_partner": "æ— æŗ•į§ģ除协äŊœč€…", + "unable_to_remove_reaction": "æ— æŗ•åˆ é™¤å›žå¤", "unable_to_reset_password": "æ— æŗ•é‡įŊŽå¯†į ", "unable_to_reset_pin_code": "æ— æŗ•é‡įŊŽPIN᠁", "unable_to_resolve_duplicate": "æ— æŗ•č§Ŗå†ŗé‡å¤éĄš", @@ -1060,22 +1139,25 @@ "unable_to_save_profile": "æ— æŗ•äŋå­˜é…įŊŽæ–‡äģļ", "unable_to_save_settings": "æ— æŗ•äŋå­˜čŽžįŊŽ", "unable_to_scan_libraries": "æ— æŗ•æ‰Ģ描åē“", - "unable_to_scan_library": "æ— æŗ•æ‰Ģ描åē“", + "unable_to_scan_library": "æ— æŗ•æ‰Ģ描čĩ„äē§åē“", "unable_to_set_feature_photo": "æ— æŗ•čŽžįŊŽäēēį‰Šå¤´åƒ", "unable_to_set_profile_picture": "æ— æŗ•čŽžįŊŽä¸Ēäēēčĩ„æ–™å›žį‰‡", + "unable_to_set_rating": "æ— æŗ•čŽžįŊŽæ˜Ÿįē§", "unable_to_submit_job": "æ— æŗ•æäē¤äģģåŠĄ", "unable_to_trash_asset": "æ— æŗ•æ”žå…Ĩ回æ”ļįĢ™", "unable_to_unlink_account": "æ— æŗ•å–æļˆč´Ļæˆˇé“žæŽĨ", "unable_to_unlink_motion_video": "æ— æŗ•å–æļˆé“žæŽĨåŠ¨æ€č§†éĸ‘", "unable_to_update_album_cover": "æ— æŗ•æ›´æ–°į›¸å†Œå°éĸ", "unable_to_update_album_info": "æ— æŗ•æ›´æ–°į›¸å†ŒäŋĄæ¯", - "unable_to_update_library": "æ— æŗ•æ›´æ–°åē“", + "unable_to_update_library": "æ— æŗ•æ›´æ–°čĩ„äē§åē“", "unable_to_update_location": "æ— æŗ•æ›´æ–°äŊįŊŽ", "unable_to_update_settings": "æ— æŗ•æ›´æ–°čŽžįŊŽ", "unable_to_update_timeline_display_status": "æ— æŗ•æ›´æ–°æ—ļ间čŊ´æ˜žį¤ēįŠļ态", "unable_to_update_user": "æ— æŗ•æ›´æ–°į”¨æˆˇ", + "unable_to_update_workflow": "æ— æŗ•æ›´æ–°åˇĨäŊœæĩ", "unable_to_upload_file": "æ— æŗ•ä¸Šäŧ æ–‡äģļ" }, + "errors_text": "错蝝", "exclusion_pattern": "æŽ’é™¤č§„åˆ™", "exif": "Exif äŋĄæ¯", "exif_bottom_sheet_description": "æˇģ加描čŋ°...", @@ -1091,7 +1173,7 @@ "experimental_settings_new_asset_list_title": "å¯į”¨åŽžéĒŒæ€§į…§į‰‡įŊ‘æ ŧ", "experimental_settings_subtitle": "äŊŋį”¨éŖŽé™Šč‡Ē负īŧ", "experimental_settings_title": "厞éĒŒæ€§åŠŸčƒŊ", - "expire_after": "æœ‰æ•ˆæœŸč‡ŗ", + "expire_after": "čŋ‡æœŸæ—ļ间", "expired": "厞čŋ‡æœŸ", "expires_date": "čŋ‡æœŸäēŽ {date}", "explore": "æŽĸį´ĸ", @@ -1100,8 +1182,8 @@ "export_as_json": "å¯ŧå‡ēä¸ē JSON", "export_database": "å¯ŧå‡ē数捎åē“", "export_database_description": "å¯ŧå‡ē SQLite 数捎åē“", - "extension": "æ‰Šåą•", - "external": "å¤–éƒ¨įš„", + "extension": "æ‰Šåą•å", + "external": "外部", "external_libraries": "外部回åē“", "external_network": "外部įŊ‘įģœ", "external_network_sheet_info": "åŊ“æœĒčŋžæŽĨåˆ°æŒ‡åŽšįš„ Wi-Fi įŊ‘į윿—ļīŧŒåē”ᔍፋåēå°†é€ščŋ‡ä¸‹æ–šįŦŦ一ä¸Ē可čŋžé€šįš„ URL čŽŋé—ŽæœåŠĄå™¨", @@ -1120,14 +1202,17 @@ "features": "功čƒŊ", "features_in_development": "åŧ€å‘ä¸­įš„åŠŸčƒŊ", "features_setting_description": "įŽĄį† App 功čƒŊ", - "file_name": "文äģļ名", - "file_name_or_extension": "文äģļ名", + "file_name_or_extension": "文äģļåæˆ–æ‰Šåą•å", + "file_name_text": "文äģļ名", + "file_name_with_value": "文äģļ名īŧš{file_name}", "file_size": "大小", "filename": "文äģļ名", "filetype": "文äģļįąģ型", "filter": "æģ¤é•œ", - "filter_people": "čŋ‡æģ¤äēēį‰Š", + "filter_description": "į›Žæ ‡éĄšį›Žį­›é€‰æĄäģļ", + "filter_people": "᭛选äēēį‰Š", "filter_places": "į­›é€‰åœ°į‚š", + "filters": "᭛选噍", "find_them_fast": "æŒ‰åį§°åŋĢ速搜į´ĸ", "first": "įŦŦ一ä¸Ē", "fix_incorrect_match": "äŋŽå¤ä¸æ­ŖįĄŽįš„匚配", @@ -1137,17 +1222,21 @@ "folders_feature_description": "在文äģļå¤šč§†å›žä¸­æĩč§ˆæ–‡äģļįŗģįģŸä¸Šįš„į…§į‰‡å’Œč§†éĸ‘", "forgot_pin_code_question": "åŋ˜čŽ°æ‚¨įš„PIN᠁äē†īŧŸ", "forward": "向前", + "free_up_space": "释攞įŠē间", + "free_up_space_description": "将厞备äģŊįš„į…§į‰‡å’Œč§†éĸ‘į§ģč‡ŗčŽžå¤‡å›žæ”ļįĢ™äģĨ释攞įŠēé—´ã€‚æœåŠĄå™¨ä¸Šįš„å‰¯æœŦ将äŋæŒåŽ‰å…¨ã€‚", + "free_up_space_settings_subtitle": "é‡Šæ”žčŽžå¤‡å­˜å‚¨įŠē间", "full_path": "åŽŒæ•´čˇ¯åž„īŧš{path}", "gcast_enabled": "Google Cast æŠ•åą", "gcast_enabled_description": "č¯Ĩ功čƒŊ需čρ加čŊŊæĨč‡Ē Google įš„å¤–éƒ¨čĩ„æēã€‚", "general": "é€šį”¨", "geolocation_instruction_location": "į‚šå‡ģå¸Ļ有GPSåæ ‡įš„čĩ„äē§äģĨäŊŋᔍå…ļäŊįŊŽīŧŒæˆ–į›´æŽĨäģŽåœ°å›žä¸Šé€‰æ‹ŠäŊįŊŽ", "get_help": "čŽˇå–å¸ŽåŠŠ", + "get_people_error": "čŽˇå–äēēį‰Šé”™č¯¯", "get_wifiname_error": "æ— æŗ•čŽˇå– Wi-Fi åį§°ã€‚įĄŽäŋåˇ˛æŽˆäēˆåŋ…čĻįš„æƒé™īŧŒåšļ厞čŋžæŽĨ到 Wi-Fi įŊ‘įģœ", "getting_started": "å…Ĩ门", "go_back": "čŋ”回", "go_to_folder": "čŋ›å…Ĩ文äģļ多", - "go_to_search": "前垀搜į´ĸ", + "go_to_search": "搜į´ĸ", "gps": "有GPSäŋĄæ¯", "gps_missing": "无GPSäŋĄæ¯", "grant_permission": "čŽˇå–æƒé™", @@ -1162,7 +1251,7 @@ "has_quota": "配éĸå¤§å°", "hash_asset": "å“ˆå¸ŒéĄšį›Ž", "hashed_assets": "åˇ˛å“ˆå¸Œįš„éĄšį›Ž", - "hashing": "æ­Ŗåœ¨å“ˆå¸Œ", + "hashing": "æ­Ŗåœ¨čŋ›čĄŒå“ˆå¸ŒæŖ€énj", "header_settings_add_header_tip": "æˇģ加标头", "header_settings_field_validator_msg": "莞įŊŽä¸å¯ä¸ēįŠē", "header_settings_header_name_input": "æ ‡å¤´åį§°", @@ -1175,22 +1264,23 @@ "hide_named_person": "隐藏äēēį‰Šâ€œ{name}”", "hide_password": "éšč—å¯†į ", "hide_person": "隐藏äēēį‰Š", + "hide_schema": "隐藏æžļ构", "hide_text_recognition": "éšč—æ–‡æœŦ蝆åˆĢ", "hide_unnamed_people": "隐藏æœĒå‘Ŋåįš„äēēį‰Š", "home_page_add_to_album_conflicts": "åˇ˛å‘į›¸å†Œ {album} 中æˇģ加 {added} éĄšã€‚å…ļ中 {failed} éĄšåœ¨į›¸å†Œä¸­åˇ˛å­˜åœ¨ã€‚", - "home_page_add_to_album_err_local": "暂不čƒŊ将æœŦåœ°éĄšį›ŽæˇģåŠ åˆ°į›¸å†Œä¸­īŧŒčˇŗčŋ‡", + "home_page_add_to_album_err_local": "æš‚æ— æŗ•å°†æœŦåœ°éĄšį›ŽæˇģåŠ åˆ°į›¸å†Œä¸­īŧŒčˇŗčŋ‡", "home_page_add_to_album_success": "åˇ˛å‘į›¸å†Œ {album} 中æˇģ加 {added} éĄšã€‚", - "home_page_album_err_partner": "æš‚æ— æŗ•å°†åŒäŧ´įš„éĄšį›ŽæˇģåŠ åˆ°į›¸å†ŒīŧŒčˇŗčŋ‡", + "home_page_album_err_partner": "æš‚æ— æŗ•å°†åäŊœč€…įš„éĄšį›ŽæˇģåŠ åˆ°į›¸å†ŒīŧŒčˇŗčŋ‡", "home_page_archive_err_local": "æš‚æ— æŗ•åŊ’æĄŖæœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", - "home_page_archive_err_partner": "æ— æŗ•å­˜æĄŖåŒäŧ´įš„éĄšį›ŽīŧŒčˇŗčŋ‡", + "home_page_archive_err_partner": "æ— æŗ•å­˜æĄŖåäŊœč€…įš„éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_building_timeline": "æ­Ŗåœ¨į”Ÿæˆæ—ļ间įēŋ", - "home_page_delete_err_partner": "æ— æŗ•åˆ é™¤åŒäŧ´įš„éĄšį›ŽīŧŒčˇŗčŋ‡", + "home_page_delete_err_partner": "æ— æŗ•åˆ é™¤åäŊœč€…įš„éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_delete_remote_err_local": "čŋœį¨‹éĄšį›Žåˆ é™¤æ¨ĄåŧīŧŒčˇŗčŋ‡æœŦåœ°éĄšį›Ž", - "home_page_favorite_err_local": "暂不čƒŊæ”ļ藏æœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", - "home_page_favorite_err_partner": "æš‚æ— æŗ•æ”ļč—åŒäŧ´įš„éĄšį›ŽīŧŒčˇŗčŋ‡", + "home_page_favorite_err_local": "æš‚æ— æŗ•æ”ļ藏æœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", + "home_page_favorite_err_partner": "æš‚æ— æŗ•æ”ļč—åäŊœč€…įš„éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_first_time_notice": "åĻ‚æžœčŋ™æ˜¯æ‚¨įŦŦ一æŦĄäŊŋᔍč¯Ĩåē”ᔍፋåēīŧŒč¯ˇįĄŽäŋé€‰æ‹Šä¸€ä¸Ēčρ备äģŊįš„æœŦåœ°į›¸å†ŒīŧŒäģĨäžŋ可äģĨ在æ—ļ间įēŋ中éĸ„č§ˆč¯Ĩį›¸å†Œä¸­įš„į…§į‰‡å’Œč§†éĸ‘", "home_page_locked_error_local": "æ— æŗ•å°†æœŦåœ°éĄšį›Žį§ģ动到锁厚文äģļ多īŧŒčˇŗčŋ‡", - "home_page_locked_error_partner": "æ— æŗ•å°†åŒäŧ´įš„éĄšį›Žį§ģ动到锁厚文äģļ多īŧŒčˇŗčŋ‡", + "home_page_locked_error_partner": "æ— æŗ•å°†åäŊœč€…įš„éĄšį›Žį§ģ动到锁厚文äģļ多īŧŒčˇŗčŋ‡", "home_page_share_err_local": "æš‚æ— æŗ•é€ščŋ‡é“žæŽĨå…ąäēĢæœŦåœ°éĄšį›ŽīŧŒčˇŗčŋ‡", "home_page_upload_err_limit": "一æŦĄæœ€å¤šåĒčƒŊ上äŧ  30 ä¸ĒéĄšį›ŽīŧŒčˇŗčŋ‡", "host": "æœåŠĄå™¨", @@ -1219,19 +1309,19 @@ "immich_web_interface": "Immich Web į•Œéĸ", "import_from_json": "äģŽ JSON å¯ŧå…Ĩ", "import_path": "å¯ŧå…Ĩčˇ¯åž„", - "in_albums": "在{count, plural, one {#ä¸Ēį›¸å†Œ} other {#ä¸Ēį›¸å†Œ}}中", + "in_albums": "在{count, plural, one {# ä¸Ēį›¸å†Œ} other {# ä¸Ēį›¸å†Œ}}中", "in_archive": "在åŊ’æĄŖä¸­", "in_year": "{year}åš´", "in_year_selector": "在", "include_archived": "包æ‹Ŧ厞åŊ’æĄŖ", "include_shared_albums": "包æ‹Ŧå…ąäēĢį›¸å†Œ", - "include_shared_partner_assets": "包æ‹Ŧ同äŧ´å…ąäēĢéĄšį›Ž", + "include_shared_partner_assets": "包æ‹Ŧ协äŊœč€…å…ąäēĢéĄšį›Ž", "individual_share": "ä¸Ēäēē分äēĢ", "individual_shares": "ä¸Ēäēē分äēĢ", "info": "äŋĄæ¯", "interval": { "day_at_onepm": "每夊下午 1 į‚š", - "hours": "每 {hours, plural, one {小æ—ļ} other {{hours, number} 小æ—ļ}}", + "hours": "每隔 {hours, plural, one {小æ—ļ} other {{hours, number} 小æ—ļ}}", "night_at_midnight": "每晚 0 į‚š", "night_at_twoam": "每晚凌晨 2 į‚š" }, @@ -1247,9 +1337,18 @@ "ios_debug_info_processing_ran_at": "čŋčĄŒå¤„ᐆ {dateTime}", "items_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}", "jobs": "äģģåŠĄ", + "json_editor": "JSONįŧ–čž‘å™¨", + "json_error": "JSON错蝝", "keep": "äŋį•™", + "keep_albums": "äŋį•™į›¸å†Œ", + "keep_albums_count": "äŋį•™ {count} {count, plural, one {ä¸Ēį›¸å†Œ} other {ä¸Ēį›¸å†Œ}}", "keep_all": "全部äŋį•™", + "keep_description": "选拊释攞įŠē间æ—ļäŋį•™åœ¨čŽžå¤‡ä¸Šįš„å†…åŽšã€‚", + "keep_favorites": "äŋį•™æ”ļč—å¤š", + "keep_on_device": "äŋį•™åœ¨čŽžå¤‡ä¸Š", + "keep_on_device_hint": "选拊čρäŋį•™åœ¨æœŦčŽžå¤‡ä¸Šįš„éĄšį›Ž", "keep_this_delete_others": "äŋį•™æ­¤éĄšīŧŒå…ļäŊ™åˆ é™¤", + "keeping": "äŋį•™: {items}", "kept_this_deleted_others": "äŋį•™č¯ĨéĄšį›Žåšļ删除 {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}", "keyboard_shortcuts": "é”Žį›˜åŋĢæˇé”Ž", "language": "蝭荀", @@ -1268,10 +1367,10 @@ "lens_model": "é•œå¤´åž‹åˇ", "let_others_respond": "å…čŽ¸äģ–äēē回åē”", "level": "į­‰įē§", - "library": "回åē“", + "library": "čĩ„äē§åē“", "library_add_folder": "æˇģ加文äģļ多", "library_edit_folder": "įŧ–čž‘æ–‡äģļ多", - "library_options": "回åē“选饚", + "library_options": "čĩ„äē§åē“选饚", "library_page_device_albums": "čŽžå¤‡ä¸Šįš„į›¸å†Œ", "library_page_new_album": "新åģēį›¸å†Œ", "library_page_sort_asset_count": "éĄšį›Žæ•°é‡", @@ -1289,7 +1388,7 @@ "loading": "加čŊŊ中", "loading_search_results_failed": "加čŊŊ搜į´ĸį쓿žœå¤ąč´Ĩ", "local": "æœŦ地", - "local_asset_cast_failed": "æ— æŗ•æŠ•æ”žæœĒ上äŧ č‡ŗæœåŠĄå™¨įš„éĄšį›Ž", + "local_asset_cast_failed": "æ— æŗ•æŠ•åąæœĒ上äŧ č‡ŗæœåŠĄå™¨įš„éĄšį›Ž", "local_assets": "æœŦåœ°éĄšį›Ž", "local_id": "æœŦ地 ID", "local_media_summary": "æœŦ地åĒ’äŊ“摘čρ", @@ -1343,11 +1442,29 @@ "loop_videos_description": "å¯į”¨åœ¨č¯Ļįģ†äŋĄæ¯ä¸­č‡Ē动åžĒįŽ¯æ’­æ”žč§†éĸ‘。", "main_branch_warning": "您åŊ“前äŊŋį”¨įš„æ˜¯åŧ€å‘į‰ˆīŧ›æˆ‘äģŦåŧē჈åģēčŽŽæ‚¨äŊŋį”¨æ­Ŗåŧå‘čĄŒį‰ˆīŧˆreleaseį‰ˆīŧ‰īŧ", "main_menu": "ä¸ģčœå•", + "maintenance_action_restore": "æ­Ŗåœ¨æĸ复数捎åē“", "maintenance_description": "Immich厞čŋ›å…Ĩįģ´æŠ¤æ¨Ąåŧã€‚", "maintenance_end": "退å‡ēįģ´æŠ¤æ¨Ąåŧ", "maintenance_end_error": "退å‡ēįģ´æŠ¤æ¨Ąåŧå¤ąč´Ĩ。", "maintenance_logged_in_as": "åŊ“前äģĨ{user}čēĢäģŊį™ģåŊ•", - "maintenance_title": "暂æ—ļä¸å¯į”¨", + "maintenance_restore_from_backup": "äģŽå¤‡äģŊ中æĸ复", + "maintenance_restore_library": "æĸå¤æ‚¨įš„čĩ„äē§åē“", + "maintenance_restore_library_confirm": "åĻ‚æžœäģĨ上äŋĄæ¯æ— č¯¯īŧŒč¯ˇįģ§įģ­čŋ›čĄŒå¤‡äģŊæĸ复īŧ", + "maintenance_restore_library_description": "æ­Ŗåœ¨æĸ复数捎åē“", + "maintenance_restore_library_folder_has_files": "{folder} 包åĢ {count} ä¸Ē文äģļ多", + "maintenance_restore_library_folder_no_files": "{folder} įŧē少文äģļīŧ", + "maintenance_restore_library_folder_pass": "可č¯ģ且可写", + "maintenance_restore_library_folder_read_fail": "不可č¯ģ", + "maintenance_restore_library_folder_write_fail": "不可写", + "maintenance_restore_library_hint_missing_files": "您可čƒŊä¸ĸå¤ąäē†é‡čĻæ–‡äģļ", + "maintenance_restore_library_hint_regenerate_later": "您可äģĨåœ¨čŽžįŊŽä¸­į¨åŽé‡æ–°į”Ÿæˆčŋ™äē›å†…厚", + "maintenance_restore_library_hint_storage_template_missing_files": "æ­Ŗåœ¨äŊŋį”¨å­˜å‚¨æ¨ĄæŋīŧŸæ‚¨å¯čƒŊä¸ĸå¤ąä熿–‡äģļ", + "maintenance_restore_library_loading": "æ­Ŗåœ¨åŠ čŊŊåŽŒæ•´æ€§æŖ€æŸĨ与启发åŧåˆ†æžâ€Ļ", + "maintenance_task_backup": "æ­Ŗåœ¨åˆ›åģēįŽ°æœ‰æ•°æŽåē“įš„å¤‡äģŊâ€Ļ", + "maintenance_task_migrations": "æ­Ŗåœ¨čŋčĄŒæ•°æŽåē“čŋį§ģâ€Ļ", + "maintenance_task_restore": "æ­Ŗåœ¨æĸå¤é€‰åŽšįš„å¤‡äģŊâ€Ļ", + "maintenance_task_rollback": "æĸå¤å¤ąč´ĨīŧŒæ­Ŗåœ¨å›žæģšåˆ°čŋ˜åŽŸį‚šâ€Ļ", + "maintenance_title": "įŗģį쟿š‚æ—ļä¸å¯į”¨", "make": "å“į‰Œ", "manage_geolocation": "įŽĄį†åæ ‡äŊįŊŽ", "manage_media_access_rationale": "æ­ŖįĄŽå¤„į†å°†čĩ„äē§į§ģč‡ŗåžƒåœžæĄļåšļ将å…ļäģŽåžƒåœžæĄļ中æĸ复需čĻæ­¤čŽ¸å¯ã€‚", @@ -1355,7 +1472,7 @@ "manage_media_access_subtitle": "å…čŽ¸Immichåē”ᔍፋåēįŽĄį†å’Œį§ģ动åĒ’äŊ“æ–‡äģļ。", "manage_media_access_title": "åĒ’äŊ“įŽĄį†čŽŋ问", "manage_shared_links": "įŽĄį†å…ąäēĢ链æŽĨ", - "manage_sharing_with_partners": "įŽĄį†ä¸ŽåŒäŧ´įš„å…ąäēĢ", + "manage_sharing_with_partners": "įŽĄį†ä¸ŽåäŊœč€…įš„å…ąäēĢ", "manage_the_app_settings": "įŽĄį†åē”į”¨čŽžįŊŽ", "manage_your_account": "įŽĄį†æ‚¨įš„č´Ļæˆˇ", "manage_your_api_keys": "įŽĄį†æ‚¨įš„ API 密é’Ĩ", @@ -1380,7 +1497,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": "įŧŠå°äģĨæŸĨįœ‹éĄšį›Ž", @@ -1390,7 +1507,7 @@ "matches": "匚配", "matching_assets": "匚配čĩ„äē§", "media_type": "åĒ’äŊ“įąģ型", - "memories": "回åŋ†", + "memories": "é‚Ŗåš´ä슿—Ĩ", "memories_all_caught_up": "åˇ˛å…¨éƒ¨įœ‹åŽŒ", "memories_check_back_tomorrow": "æ˜Žå¤Šå†įœ‹", "memories_setting_description": "įŽĄį†å›žåŋ†ä¸­įš„内厚", @@ -1408,19 +1525,24 @@ "minimize": "最小化", "minute": "分", "minutes": "分钟", + "mirror_horizontal": "æ°´åšŗ", + "mirror_vertical": "åž‚į›´", "missing": "įŧēå¤ą", - "mobile_app": "手æœēAPP", + "mobile_app": "į§ģ动į̝APP", "mobile_app_download_onboarding_note": "下čŊŊį§ģ动åē”ᔍäģĨčŽŋ问čŋ™äē›é€‰éĄš", "model": "åž‹åˇ", "month": "月", "monthly_title_text_date_format": "y MMMM", "more": "更多", "move": "į§ģ动", + "move_down": "向下į§ģ动", "move_off_locked_folder": "į§ģå‡ē锁厚文äģļ多", "move_to": "į§ģ动到", + "move_to_device_trash": "į§ģč‡ŗčŽžå¤‡å›žæ”ļįĢ™", "move_to_lock_folder_action_prompt": "厞将 {count} 饚æˇģ加到锁厚文äģļ多", "move_to_locked_folder": "į§ģ动到锁厚文äģļ多", "move_to_locked_folder_confirmation": "čŋ™äē›į…§į‰‡å’Œč§†éĸ‘å°†äģŽæ‰€æœ‰į›¸å†Œä¸­į§ģ除īŧŒåĒčƒŊ在锁厚文äģļ多中æŸĨįœ‹", + "move_up": "向上į§ģ动", "moved_to_archive": "厞åŊ’æĄŖ {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}", "moved_to_library": "厞į§ģ动 {count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}} 到回åē“", "moved_to_trash": "åˇ˛æ”žå…Ĩ回æ”ļįĢ™", @@ -1430,6 +1552,7 @@ "my_albums": "æˆ‘įš„į›¸å†Œ", "name": "åį§°", "name_or_nickname": "åį§°æˆ–æ˜ĩį§°", + "name_required": "åį§°æ˜¯åŋ…åĄĢ饚", "navigate": "å¯ŧčˆĒ", "navigate_to_time": "å¯ŧčˆĒ臺æ—ļ间", "network_requirement_photos_upload": "äŊŋį”¨čœ‚įĒæ•°æŽå¤‡äģŊᅧቇ", @@ -1454,20 +1577,24 @@ "next": "下一ä¸Ē", "next_memory": "下一ä¸Ē", "no": "åĻ", + "no_actions_added": "尚æœĒæˇģ加动äŊœ", + "no_albums_found": "æœĒæ‰žåˆ°į›¸å†Œ", "no_albums_message": "创åģēį›¸å†ŒäģĨæ•´į†į…§į‰‡å’Œč§†éĸ‘", "no_albums_with_name_yet": "螌äŧŧ您čŋ˜æ˛Ąæœ‰æ­¤åå­—įš„į›¸å†Œã€‚", "no_albums_yet": "螌äŧŧ您čŋ˜æ˛Ąæœ‰åˆ›åģēį›¸å†Œã€‚", "no_archived_assets_message": "åŊ’æĄŖį…§į‰‡å’Œč§†éĸ‘äģĨäžŋåœ¨į…§į‰‡č§†å›žä¸­éšč—åŽƒäģŦ", - "no_assets_message": "į‚šå‡ģ上äŧ æ‚¨įš„įŦŦ一åŧ į…§į‰‡", + "no_assets_message": "į‚šå‡ģ此处上äŧ äŊ įš„įŦŦ一åŧ į…§į‰‡", "no_assets_to_show": "æ˛Ąæœ‰čĻæ˜žį¤ēįš„čĩ„äē§", "no_cast_devices_found": "æœĒæ‰žåˆ°æŠ•æ”žčŽžå¤‡", "no_checksum_local": "æ˛Ąæœ‰å¯į”¨įš„æ ĄéĒŒå’Œ-æ— æŗ•čŽˇå–æœŦ地čĩ„äē§", "no_checksum_remote": "æ˛Ąæœ‰å¯į”¨įš„æ ĄéĒŒå’Œ-æ— æŗ•čŽˇå–čŋœį¨‹čĩ„äē§", + "no_configuration_needed": "不需čĻé…įŊŽ", "no_devices": "æ— æŽˆæƒčŽžå¤‡", "no_duplicates_found": "æœĒå‘įŽ°é‡å¤éĄšã€‚", "no_exif_info_available": "æ˛Ąæœ‰å¯į”¨įš„ EXIF äŋĄæ¯", "no_explore_results_message": "上äŧ æ›´å¤šį…§į‰‡æĨæŽĸį´ĸ。", "no_favorites_message": "æˇģ加到æ”ļč—å¤šīŧŒåŋĢ速æŸĨ扞最äŊŗå›žį‰‡å’Œč§†éĸ‘", + "no_filters_added": "尚æœĒæˇģåŠ į­›é€‰", "no_libraries_message": "创åģē外部回å瓿ĨæŸĨįœ‹æ‚¨įš„į…§į‰‡å’Œč§†éĸ‘", "no_local_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéĒŒå’Œįš„æœŦ地čĩ„äē§", "no_location_set": "æœĒ莞įŊŽåœ°į‚š", @@ -1481,11 +1608,11 @@ "no_results_description": "å°č¯•äŊŋį”¨åŒäš‰č¯æˆ–æ›´é€šį”¨įš„å…ŗé”Žč¯", "no_shared_albums_message": "创åģēį›¸å†ŒäģĨå…ąäēĢį…§į‰‡å’Œč§†éĸ‘", "no_uploads_in_progress": "æ˛Ąæœ‰æ­Ŗåœ¨čŋ›čĄŒįš„上äŧ ", + "none": "无", "not_allowed": "ä¸å…čŽ¸", "not_available": "ä¸é€‚į”¨", "not_in_any_album": "不在äģģäŊ•į›¸å†Œä¸­", "not_selected": "æœĒ选拊", - "note_apply_storage_label_to_previously_uploaded assets": "提į¤ēīŧščĻå°†å­˜å‚¨æ ‡į­žåē”ᔍäēŽäš‹å‰ä¸Šäŧ įš„éĄšį›ŽīŧŒéœ€čρčŋčĄŒ", "notes": "提į¤ē", "nothing_here_yet": "čŋ™é‡Œäģ€äšˆéƒŊæ˛Ąæœ‰", "notification_permission_dialog_content": "čĻå¯į”¨é€šįŸĨīŧŒč¯ˇčŊŦåˆ°â€œčŽžįŊŽâ€īŧŒåšļé€‰æ‹Šâ€œå…čŽ¸â€ã€‚", @@ -1517,7 +1644,7 @@ "open": "打åŧ€", "open_in_map_view": "åœ¨åœ°å›žč§†å›žä¸­æ‰“åŧ€", "open_in_openstreetmap": "在 OpenStreetMap 中打åŧ€", - "open_the_search_filters": "打åŧ€æœį´ĸčŋ‡æģ¤å™¨", + "open_the_search_filters": "打åŧ€æœį´ĸ᭛选", "options": "选项", "or": "或", "organize_into_albums": "æ•´į†æˆį›¸å†Œ", @@ -1531,20 +1658,20 @@ "owned": "æˆ‘įš„", "owner": "æ‰€æœ‰č€…", "page": "éĄĩéĸ", - "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": "需čρ坆᠁", @@ -1563,6 +1690,7 @@ "people": "äēēį‰Š", "people_edits_count": "{count, plural, one {#ä¸Ēäēēį‰Š} other {#ä¸Ēäēēį‰Š}}厞įŧ–čž‘", "people_feature_description": "按äēēį‰Šåˆ†įģ„čŋ›čĄŒæĩč§ˆį…§į‰‡å’Œč§†éĸ‘", + "people_selected": "{count, plural, one {åˇ˛é€‰æ‹Š # äēē} other {åˇ˛é€‰æ‹Š # äēē}}", "people_sidebar_description": "åœ¨äž§čžšæ ä¸­æ˜žį¤ē“äēēį‰Šâ€é“žæŽĨ", "permanent_deletion_warning": "永䚅删除č­Ļ告", "permanent_deletion_warning_setting_description": "åŊ“æ°¸äš…åˆ é™¤éĄšį›Žæ—ļ昞į¤ēč­Ļ告", @@ -1587,11 +1715,14 @@ "person_age_years": "{years, plural, other {# 垁}}", "person_birthdate": "å‡ēį”ŸäēŽ{date}", "person_hidden": "{name}{hidden, select, true {īŧˆåˇ˛éšč—īŧ‰} other {}}", + "person_recognized": "蝆åˆĢå‡ēįš„äēēį‰Š", + "person_selected": "é€‰æ‹Šįš„äēēį‰Š", "photo_shared_all_users": "įœ‹čĩˇæĨæ‚¨åˇ˛ä¸Žæ‰€æœ‰į”¨æˆˇå…ąäēĢä熿­¤į›¸å†ŒīŧŒæˆ–č€…æ‚¨æ šæœŦæ˛Ąæœ‰äģģäŊ•į”¨æˆˇå¯å…ąäēĢ。", "photos": "ᅧቇ", "photos_and_videos": "ᅧቇ & 视éĸ‘", "photos_count": "{count, plural, one {{count, number}åŧ į…§į‰‡} other {{count, number}åŧ į…§į‰‡}}", "photos_from_previous_years": "čŋ‡åž€įš„ä슿˜”įžŦ间", + "photos_only": "äģ…ᅧቇ", "pick_a_location": "选拊äŊįŊŽ", "pick_custom_range": "č‡ĒåŽšäš‰čŒƒå›´", "pick_date_range": "选拊æ—ĨæœŸčŒƒå›´", @@ -1618,8 +1749,8 @@ "preview": "éĸ„č§ˆ", "previous": "上一ä¸Ē", "previous_memory": "上一ä¸Ē", - "previous_or_next_day": "前一夊/后一夊", - "previous_or_next_month": "下ä¸Ē月/上ä¸Ē月", + "previous_or_next_day": "昨夊/明夊", + "previous_or_next_month": "上ä¸Ē月/下ä¸Ē月", "previous_or_next_photo": "下一åŧ /上一åŧ ", "previous_or_next_year": "明嚴/åŽģåš´", "primary": "éĻ–čρ", @@ -1656,7 +1787,7 @@ "purchase_panel_info_2": "į”ąäēŽæˆ‘äģŦæ‰ŋč¯ē不æˇģ加äģ˜č´šåŠŸčƒŊīŧŒæ­¤æŦĄč´­äš°ä¸äŧšä¸ē您提䞛 Immich įš„äģģäŊ•éĸå¤–功čƒŊ。我äģŦ䞝靠像您čŋ™æ ˇįš„į”¨æˆˇæĨ支持 Immich įš„æŒįģ­åŧ€å‘。", "purchase_panel_title": "支持čŋ™ä¸ĒéĄšį›Ž", "purchase_per_server": "æ¯å°æœåŠĄå™¨", - "purchase_per_user": "每äŊį”¨æˆˇ", + "purchase_per_user": "每ä¸Ēį”¨æˆˇ", "purchase_remove_product_key": "į§ģ除äē§å“å¯†é’Ĩ", "purchase_remove_product_key_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤äē§å“å¯†é’Ĩ吗īŧŸ", "purchase_remove_server_product_key": "į§ģé™¤æœåŠĄå™¨äē§å“å¯†é’Ĩ", @@ -1667,11 +1798,13 @@ "purchase_settings_server_activated": "æœåŠĄå™¨äē§å“å¯†é’Ĩæ­Ŗåœ¨į”ąįŽĄį†å‘˜įŽĄį†", "query_asset_id": "æŸĨč¯ĸčĩ„äē§ID", "queue_status": "排队中 {count}/{total}", + "rate_asset": "čĩ„äē§æ˜Ÿįē§", "rating": "星įē§", "rating_clear": "删除星įē§", "rating_count": "{count, plural, one {#星} other {#星}}", "rating_description": "在äŋĄæ¯éĸæŋä¸­åą•į¤ē EXIF 星įē§", - "reaction_options": "回åē”选饚", + "rating_set": "åˇ˛čŽžįŊŽä¸ē {rating, plural, one {# 星} other {# 星}}", + "reaction_options": "å›žå¤é€‰éĄš", "read_changelog": "阅č¯ģ更新æ—Ĩåŋ—", "readonly_mode_disabled": "åĒč¯ģæ¨Ąåŧåˇ˛įρᔍ", "readonly_mode_enabled": "åĒč¯ģæ¨Ąåŧåˇ˛å¯į”¨", @@ -1681,7 +1814,7 @@ "reassigned_assets_to_new_person": "重新指洞{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˆ°æ–°įš„äēēį‰Š", "reassing_hint": "æŒ‡æ´žé€‰æ‹Šįš„éĄšį›Žåˆ°åˇ˛å­˜åœ¨įš„äēēį‰Š", "recent": "最čŋ‘", - "recent-albums": "最čŋ‘įš„į›¸å†Œ", + "recent_albums": "最čŋ‘įš„į›¸å†Œ", "recent_searches": "最čŋ‘搜į´ĸ", "recently_added": "čŋ‘期æˇģ加", "recently_added_page_title": "最čŋ‘æˇģ加", @@ -1694,14 +1827,14 @@ "refresh_thumbnails": "åˆˇæ–°įŧŠį•Ĩ回", "refreshed": "åˇ˛åˆˇæ–°", "refreshes_every_file": "重新æ‰Ģææ‰€æœ‰įŽ°æœ‰æ–‡äģļ和新文äģļ", - "refreshing_encoded_video": "æ­Ŗåœ¨åˆˇæ–°åˇ˛įŧ–᠁视éĸ‘", - "refreshing_faces": "æ­Ŗåœ¨éĸéƒ¨é‡æ–°č¯†åˆĢ", - "refreshing_metadata": "æ­Ŗåœ¨åˆˇæ–°å…ƒæ•°æŽ", - "regenerating_thumbnails": "æ­Ŗåœ¨é‡æ–°į”ŸæˆįŧŠį•Ĩ回", + "refreshing_encoded_video": "åˆˇæ–°åˇ˛įŧ–᠁视éĸ‘", + "refreshing_faces": "åˆˇæ–°éĸéƒ¨č¯†åˆĢ", + "refreshing_metadata": "åˆˇæ–°å…ƒæ•°æŽ", + "regenerating_thumbnails": "é‡æ–°į”ŸæˆįŧŠį•Ĩ回", "remote": "čŋœį¨‹", "remote_assets": "čŋœį¨‹éĄšį›Ž", "remote_media_summary": "čŋœį¨‹åĒ’äŊ“摘čρ", - "remove": "į§ģ除", + "remove": "æ“Ļ除", "remove_assets_album_confirmation": "įĄŽåŽščρäģŽå›žåē“中į§ģ除{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}īŧŸ", "remove_assets_shared_link_confirmation": "įĄŽåŽščρäģŽå…ąäēĢ链æŽĨ中į§ģ除{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}īŧŸ", "remove_assets_title": "į§ģé™¤éĄšį›ŽīŧŸ", @@ -1770,9 +1903,11 @@ "saved_settings": "厞äŋå­˜čŽžįŊŽ", "say_something": "č¯´į‚šäģ€äšˆ", "scaffold_body_error_occurred": "å‘į”Ÿé”™č¯¯", + "scan": "æ‰Ģ描", "scan_all_libraries": "æ‰Ģ描所有回åē“", "scan_library": "æ‰Ģ描", "scan_settings": "æ‰ĢæčŽžįŊŽ", + "scanning": "æ‰Ģ描中", "scanning_for_album": "æ‰Ģæį›¸å†Œä¸­...", "search": "搜į´ĸ", "search_albums": "搜į´ĸį›¸å†Œ", @@ -1802,6 +1937,7 @@ "search_filter_media_type_title": "选拊åĒ’äŊ“įąģ型", "search_filter_ocr": "通čŋ‡æ–‡æœŦ蝆åˆĢ搜į´ĸ", "search_filter_people_title": "选拊äēēį‰Š", + "search_filter_star_rating": "星įē§č¯„分", "search_for": "æŸĨ扞", "search_for_existing_person": "æŸĨæ‰žåˇ˛æœ‰äēēį‰Š", "search_no_more_result": "无更多į쓿žœ", @@ -1836,17 +1972,23 @@ "second": "į§’", "see_all_people": "æŸĨįœ‹æ‰€æœ‰äēēį‰Š", "select": "选拊", + "select_album": "é€‰æ‹Šį›¸å†Œ", "select_album_cover": "é€‰æ‹Šį›¸å†Œå°éĸ", + "select_albums": "é€‰æ‹Šį›¸å†Œ", "select_all": "全选", "select_all_duplicates": "é€‰æ‹Šæ‰€æœ‰é‡å¤éĄš", "select_all_in": "选拊 {group} ä¸­įš„æ‰€æœ‰å†…åŽš", "select_avatar_color": "选拊头像éĸœč‰˛", + "select_count": "{count, plural, one {选拊 # 饚} other {选拊 # 饚}}", + "select_cutoff_date": "选拊æˆĒæ­ĸæ—Ĩ期", "select_face": "选拊äēē脸", "select_featured_photo": "选拊ä¸Ē性头像", "select_from_computer": "äģŽčŽĄįŽ—æœē中选拊", "select_keep_all": "全部äŋį•™", "select_library_owner": "选拊回å瓿‰€æœ‰č€…", "select_new_face": "选拊新äēē脸", + "select_people": "选拊äēēį‰Š", + "select_person": "选拊äēēį‰Š", "select_person_to_tag": "选拊čĻæ ‡čŽ°įš„äēēį‰Š", "select_photos": "é€‰æ‹Šį…§į‰‡", "select_trash_all": "全部删除", @@ -1965,7 +2107,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": "按äŊ ⇧ Shift é”Žæ°¸äš…åˆ é™¤éĄšį›Ž", "show_album_options": "昞į¤ēį›¸å†Œé€‰éĄš", "show_albums": "昞į¤ēį›¸å†Œ", @@ -1982,6 +2124,7 @@ "show_password": "昞į¤ē坆᠁", "show_person_options": "昞į¤ēäēēį‰Šé€‰éĄš", "show_progress_bar": "昞į¤ēčŋ›åēĻæĄ", + "show_schema": "昞į¤ēæžļ构", "show_search_options": "昞į¤ē搜į´ĸ选项", "show_shared_links": "昞į¤ēå…ąäēĢ链æŽĨ", "show_slideshow_transition": "昞į¤ēåšģၝቇčŋ‡æ¸Ąæ•ˆæžœ", @@ -1999,6 +2142,8 @@ "skip_to_folders": "莺čŊŦ到文äģļ多", "skip_to_tags": "莺čŊŦåˆ°æ ‡į­ž", "slideshow": "åšģį¯į‰‡æ”žæ˜ ", + "slideshow_repeat": "重复åšģၝቇ", + "slideshow_repeat_description": "åšģၝቇį쓿ŸåŽåžĒįŽ¯æ’­æ”ž", "slideshow_settings": "æ”žæ˜ čŽžįŊŽ", "sort_albums_by": "į›¸å†ŒæŽ’åēäžæŽ...", "sort_created": "创åģēæ—Ĩ期", @@ -2075,6 +2220,7 @@ "theme_setting_theme_subtitle": "选拊åē”ᔍä¸ģéĸ˜", "theme_setting_three_stage_loading_subtitle": "三æŽĩåŧåŠ čŊŊ可čƒŊäŧšæå‡åŠ čŊŊ性čƒŊīŧŒäŊ†å¯čƒŊäŧšå¯ŧč‡´æ›´éĢ˜įš„įŊ‘įģœč´ŸčŊŊ", "theme_setting_three_stage_loading_title": "吝ᔍ䏉æŽĩåŧåŠ čŊŊ", + "then": "į„ļ后", "they_will_be_merged_together": "éĄšį›Žå°†äŧšåˆåšļ到一čĩˇ", "third_party_resources": "įŦŦ三斚čĩ„æē", "time": "æ—ļ间", @@ -2109,6 +2255,13 @@ "trash_page_select_assets_btn": "é€‰æ‹ŠéĄšį›Ž", "trash_page_title": "回æ”ļįĢ™ ({count})", "trashed_items_will_be_permanently_deleted_after": "回æ”ļįĢ™ä¸­įš„éĄšį›Žå°†åœ¨{days, plural, one {#夊} other {#夊}}后čĸĢæ°¸äš…删除。", + "trigger": "č§Ļå‘æĄäģļ", + "trigger_asset_uploaded": "éĄšį›Žåˇ˛ä¸Šäŧ ", + "trigger_asset_uploaded_description": "åŊ“上äŧ æ–°éĄšį›Žæ—ļč§Ļ发", + "trigger_description": "启动åˇĨäŊœæĩįš„äē‹äģļ", + "trigger_person_recognized": "äēēį‰Šåˇ˛č¯†åˆĢ", + "trigger_person_recognized_description": "åŊ“æŖ€æĩ‹åˆ°äēēį‰Šæ—ļč§Ļ发", + "trigger_type": "č§Ļ发įąģ型", "troubleshoot": "故障排除", "type": "įąģ型", "unable_to_change_pin_code": "æ— æŗ•äŋŽæ”šPIN᠁", @@ -2123,6 +2276,7 @@ "unhide_person": "昞į¤ēäēēį‰Š", "unknown": "æœĒįŸĨ", "unknown_country": "æœĒįŸĨįš„å›ŊåŽļ", + "unknown_date": "æœĒįŸĨæ—Ĩ期", "unknown_year": "æœĒįŸĨåš´äģŊ", "unlimited": "无限åˆļ", "unlink_motion_video": "取æļˆé“žæŽĨåŠ¨æ€č§†éĸ‘", @@ -2139,17 +2293,19 @@ "unstack": "取æļˆå †å ", "unstack_action_prompt": "{count} ä¸ĒæœĒ堆叠", "unstacked_assets_count": "{count, plural, one {#ä¸ĒéĄšį›Ž} other {#ä¸ĒéĄšį›Ž}}åˇ˛å–æļˆå †å ", + "unsupported_field_type": "ä¸æ”¯æŒįš„å­—æŽĩįąģ型", "untagged": "æ— æ ‡į­ž", + "untitled_workflow": "无标éĸ˜åˇĨäŊœæĩ", "up_next": "下一ä¸Ē", "update_location_action_prompt": "更新 {count} ä¸Ē所选čĩ„äē§įš„äŊįŊŽīŧš", - "updated_at": "åˇ˛æ›´æ–°", + "updated_at": "最后更新æ—ļ间", "updated_password": "æ›´æ–°å¯†į ", "upload": "上äŧ ", - "upload_action_prompt": "有{count}ä¸Ē垅上äŧ ", "upload_concurrency": "上äŧ åšļ发", "upload_details": "上äŧ č¯Ļ情", "upload_dialog_info": "是åĻčĻå°†æ‰€é€‰éĄšį›Žå¤‡äģŊåˆ°æœåŠĄå™¨īŧŸ", "upload_dialog_title": "上äŧ éĄšį›Ž", + "upload_error_with_count": "{count, plural, one {# ä¸ĒéĄšį›Ž} other {# ä¸ĒéĄšį›Ž}}上äŧ é”™č¯¯", "upload_errors": "上äŧ åŽŒæˆīŧŒå‡ēįŽ°{count, plural, one {#ä¸Ē错蝝} other {#ä¸Ē错蝝}}īŧŒåˆˇæ–°éĄĩéĸäģĨæŸĨįœ‹æ–°ä¸Šäŧ įš„éĄšį›Žã€‚", "upload_finished": "上äŧ åŽŒæˆ", "upload_progress": "削äŊ™{remaining, number} - 厞处ᐆ {processed, number}/{total, number}", @@ -2185,6 +2341,7 @@ "utilities": "åŽžį”¨åˇĨå…ˇ", "validate": "énj蝁", "validate_endpoint_error": "č¯ˇčž“å…Ĩæœ‰æ•ˆįš„ URL", + "validation_error": "éĒŒč¯é”™č¯¯", "variables": "变量", "version": "į‰ˆæœŦ", "version_announcement_closing": "æ‚¨įš„æœ‹å‹īŧŒAlex", @@ -2196,6 +2353,7 @@ "video_hover_setting_description": "åŊ“éŧ æ ‡æ‚Ŧåœåœ¨éĄšį›Žä¸Šæ—ļæ’­æ”žč§†éĸ‘įŧŠį•Ĩå›žã€‚åŗäŊŋįρᔍä熿­¤åŠŸčƒŊīŧŒäšŸå¯äģĨ通čŋ‡å°†éŧ æ ‡æ‚Ŧ停在播攞回标上æĨåŧ€å§‹æ’­æ”žã€‚", "videos": "视éĸ‘", "videos_count": "{count, plural, one {#ä¸Ē视éĸ‘} other {#ä¸Ē视éĸ‘}}", + "videos_only": "äģ…视éĸ‘", "view": "æŸĨįœ‹", "view_album": "æŸĨįœ‹į›¸å†Œ", "view_all": "æŸĨįœ‹å…¨éƒ¨", @@ -2216,7 +2374,9 @@ "viewer_stack_use_as_main_asset": "äŊœä¸ēä¸ģéĄšį›ŽäŊŋᔍ", "viewer_unstack": "取æļˆå †å ", "visibility_changed": "{count, plural, one {#ä¸Ēäēēį‰Š} other {#ä¸Ēäēēį‰Š}}įš„å¯č§æ€§åˇ˛äŋŽæ”š", - "waiting": "准备处ᐆ", + "visual": "å¯č§†åŒ–", + "visual_builder": "å¯č§†åŒ–į”Ÿæˆå™¨", + "waiting": "į­‰åž…å¤„į†", "waiting_count": "į­‰åž…: {count}", "warning": "č­Ļ告", "week": "周", @@ -2224,13 +2384,26 @@ "welcome_to_immich": "æŦĸčŋŽäŊŋᔍ Immich", "width": "åŽŊåēĻ", "wifi_name": "Wi-Fi åį§°", - "workflow": "åˇĨäŊœæĩ", + "workflow_delete_prompt": "æ‚¨įĄŽåŽščĻåˆ é™¤æ­¤åˇĨäŊœæĩå—īŧŸ", + "workflow_deleted": "åˇĨäŊœæĩåˇ˛åˆ é™¤", + "workflow_description": "åˇĨäŊœæĩæčŋ°", + "workflow_info": "åˇĨäŊœæĩäŋĄæ¯", + "workflow_json": "åˇĨäŊœæĩJSON", + "workflow_json_help": "äģĨJSONæ ŧåŧįŧ–čž‘åˇĨäŊœæĩé…įŊŽã€‚变动äŧšåŒæ­Ĩåˆ°å¯č§†åŒ–į”Ÿæˆå™¨ã€‚", + "workflow_name": "åˇĨäŊœæĩåį§°", + "workflow_navigation_prompt": "äŊ įĄŽåŽšä¸äŋå­˜č€Œé€€å‡ēīŧŸ", + "workflow_summary": "åˇĨäŊœæĩæ‘˜čρ", + "workflow_update_success": "åˇĨäŊœæĩæˆåŠŸæ›´æ–°", + "workflow_updated": "åˇĨäŊœæĩåˇ˛æ›´æ–°", + "workflows": "åˇĨäŊœæĩ", + "workflows_help_text": "åˇĨäŊœæĩå¯æ šæŽč§Ļå‘å’Œį­›é€‰æĄäģļč‡ĒåŠ¨æ‰§čĄŒéĄšį›Žæ“äŊœ", "wrong_pin_code": "é”™č¯¯įš„PIN᠁", "year": "åš´", "years_ago": "{years, plural, one {#åš´} other {#åš´}}前", "yes": "是", "you_dont_have_any_shared_links": "æ‚¨æ˛Ąæœ‰äģģäŊ•å…ąäēĢ链æŽĨ", "your_wifi_name": "æ‚¨įš„ Wi-Fi åį§°", + "zero_to_clear_rating": "按0清除čĩ„äē§æ˜Ÿįē§", "zoom_image": "įŧŠæ”žå›žåƒ", "zoom_to_bounds": "įŧŠæ”žåˆ°čžšį•Œ" } diff --git a/i18n/zh_Hant.json b/i18n/zh_Hant.json index bd4073d52c..60dae6ed22 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.json @@ -3,125 +3,134 @@ "account": "å¸ŗč™Ÿ", "account_settings": "å¸ŗč™Ÿč¨­åŽš", "acknowledge": "äē†č§Ŗ", - "action": "操äŊœ", + "action": "動äŊœ", "action_common_update": "更新", - "actions": "é€˛čĄŒå‹•äŊœ", + "action_description": "å°į¯Šé¸åžŒįš„é …į›ŽåŸˇčĄŒä¸€įĩ„å‹•äŊœ", + "actions": "動äŊœ", "active": "處ᐆ䏭", "active_count": "處ᐆ䏭īŧš{count}", "activity": "動態", - "activity_changed": "å‹•æ…‹åˇ˛{enabled, select, true {開啟} other {關閉}}", + "activity_changed": "å‹•æ…‹åˇ˛{enabled, select, true {å•Ÿį”¨} other {åœį”¨}}", "add": "加å…Ĩ", - "add_a_description": "新åĸžæčŋ°", + "add_a_description": "新åĸžčĒĒæ˜Ž", "add_a_location": "新åĸžåœ°éģž", - "add_a_name": "加å…Ĩ姓名", + "add_a_name": "新åĸžå§“名", "add_a_title": "新åĸžæ¨™éĄŒ", + "add_action": "新åĸžå‹•äŊœ", + "add_action_description": "按一下äģĨ新åĸžčĻåŸˇčĄŒįš„å‹•äŊœ", + "add_assets": "新åĸžé …į›Ž", "add_birthday": "新åĸžį”Ÿæ—Ĩ", "add_endpoint": "新åĸžį̝éģž", - "add_exclusion_pattern": "加å…Ĩį¯Šé¸æĸäģļ", + "add_exclusion_pattern": "新åĸžæŽ’é™¤æ¨Ąåŧ", + "add_filter": "新åĸžį¯Šé¸å™¨", + "add_filter_description": "按一下äģĨ新åĸžį¯Šé¸æĸäģļ", "add_location": "新åĸžåœ°éģž", "add_more_users": "新åĸžå…ļäģ–äŊŋᔍ者", - "add_partner": "新åĸžčĻĒæœ‹åĨŊ友", + "add_partner": "新åĸžčĻĒ友", "add_path": "新åĸžčˇ¯åž‘", - "add_photos": "加å…Ĩᅧቇ", + "add_photos": "加å…Ĩᛏቇ", "add_tag": "加å…Ĩæ¨™įą¤", - "add_to": "加å…Ĩ到â€Ļ", + "add_to": "加å…Ĩ臺â€Ļ", "add_to_album": "加å…Ĩåˆ°į›¸į°ŋ", - "add_to_album_bottom_sheet_added": "新åĸžåˆ° {album}", + "add_to_album_bottom_sheet_added": "åˇ˛æ–°åĸžč‡ŗ {album}", "add_to_album_bottom_sheet_already_exists": "厞圍 {album} 中", - "add_to_album_bottom_sheet_some_local_assets": "į„Ąæŗ•å°‡æŸä盿œŦæŠŸčŗ‡į”ĸ新åĸžåˆ°į›¸į°ŋ", - "add_to_album_toggle": "é¸æ“‡į›¸į°ŋ{album}", + "add_to_album_bottom_sheet_some_local_assets": "į„Ąæŗ•å°‡éƒ¨åˆ†æœŦæŠŸé …į›Žæ–°åĸžč‡ŗį›¸į°ŋ", + "add_to_album_toggle": "é¸å–į›¸į°ŋ {album}", "add_to_albums": "加å…Ĩᛏį°ŋ", "add_to_albums_count": "將 ({count}) å€‹é …į›ŽåŠ å…Ĩᛏį°ŋ", "add_to_bottom_bar": "新åĸžåˆ°", - "add_to_shared_album": "åŠ åˆ°å…ąäēĢᛏį°ŋ", + "add_to_shared_album": "新åĸžč‡ŗå…ąäēĢᛏį°ŋ", "add_upload_to_stack": "新åĸžä¸Šå‚ŗåˆ°å †į–Š", "add_url": "新åĸž URL", + "add_workflow_step": "新åĸžåˇĨäŊœæĩį¨‹æ­Ĩ驟", "added_to_archive": "į§ģč‡ŗå°å­˜", "added_to_favorites": "加å…Ĩæ”ļ藏", - "added_to_favorites_count": "將 {count, number} å€‹é …į›ŽåŠ å…Ĩæ”ļ藏", + "added_to_favorites_count": "厞將 {count, number} å€‹é …į›ŽåŠ å…Ĩæ”ļ藏", "admin": { - "add_exclusion_pattern_description": "新åĸžæŽ’除æĸäģļ。支援äŊŋį”¨ã€Œ*」、「 **」、「?」䞆扞尋įŦĻ合čĻå‰‡įš„å­—ä¸˛ã€‚åĻ‚æžœčρ圍äģģäŊ•名į‚ē「Rawã€įš„į›ŽéŒ„å…§æŽ’é™¤æ‰€æœ‰įŦĻ合æĸäģļįš„æĒ”æĄˆīŧŒčĢ‹äŊŋį”¨ã€Œ**/Raw/**」。åĻ‚æžœčĻæŽ’é™¤æ‰€æœ‰ã€Œ.tif」įĩå°žįš„æĒ”æĄˆīŧŒčĢ‹äŊŋį”¨ã€Œ**/*.tif」。åĻ‚æžœčĻæŽ’é™¤æŸå€‹įĩ•å°čˇ¯åž‘īŧŒčĢ‹äŊŋį”¨ã€Œ/path/to/ignore/**」。", + "add_exclusion_pattern_description": "新åĸžæŽ’é™¤æ¨Ąåŧã€‚支援äŊŋᔍ *、** 與 ? 進行čŦį”¨å­—å…ƒæ¯”å° (Globbing)。č‹ĨčρåŋŊį•ĨäģģäŊ•名į‚ē「Rawã€į›ŽéŒ„ä¸­įš„æ‰€æœ‰æĒ”æĄˆīŧŒčĢ‹äŊŋį”¨ã€Œ**/Raw/**」īŧ›č‹ĨčρåŋŊį•Ĩ所有äģĨ「.tif」įĩå°žįš„æĒ”æĄˆīŧŒčĢ‹äŊŋį”¨ã€Œ**/*.tif」īŧ›č‹ĨčρåŋŊį•Ĩį‰šåŽšįš„įĩ•å°čˇ¯åž‘īŧŒčĢ‹äŊŋį”¨ã€Œ/path/to/ignore/**」。", "admin_user": "įŽĄį†å“Ą", - "asset_offline_description": "此外部åĒ’éĢ”åēĢé …į›Žåˇ˛į„Ąæŗ•åœ¨įŖįĸŸä¸Šæ‰žåˆ°īŧŒä¸Ļ厞į§ģč‡ŗåžƒåœžæĄļ。č‹Ĩ芲æĒ”æĄˆæ˜¯åœ¨åĒ’éĢ”åēĢ內į§ģ動īŧŒčĢ‹åœ¨æ™‚é–“čģ¸ä¸­æĒĸčĻ–æ–°įš„å°æ‡‰é …į›Žã€‚č‹ĨčĻé‚„åŽŸæ­¤é …į›ŽīŧŒčĢ‹įĸēäŋä¸‹æ–šįš„æĒ”æĄˆčˇ¯åž‘å¯äž› Immich 存取īŧŒä¸Ļ重新掃描åĒ’éĢ”åēĢ。", + "asset_offline_description": "此外部åĒ’éĢ”åēĢé …į›Žåˇ˛į„Ąæŗ•åœ¨įŖįĸŸä¸Šæ‰žåˆ°īŧŒä¸Ļ厞į§ģč‡ŗåžƒåœžæĄļ。č‹Ĩ芲æĒ”æĄˆæ˜¯åœ¨åĒ’éĢ”åēĢ內į§ģ動īŧŒčĢ‹åœ¨æ™‚é–“čģ¸ä¸­æŸĨįœ‹æ–°įš„å°æ‡‰é …į›Žã€‚č‹ĨčĻé‚„åŽŸæ­¤é …į›ŽīŧŒčĢ‹įĸēäŋä¸‹æ–šįš„æĒ”æĄˆčˇ¯åž‘å¯äž› Immich 存取īŧŒä¸Ļ重新掃描åĒ’éĢ”åēĢ。", "authentication_settings": "éŠ—č­‰č¨­åŽš", "authentication_settings_description": "įŽĄį†å¯†įĸŧ、OAuth 與å…ļäģ–éŠ—č­‰č¨­åŽš", - "authentication_settings_disable_all": "įĸē厚čĻåœį”¨æ‰€æœ‰į™ģå…Ĩæ–šåŧå—ŽīŧŸé€™æ¨ŖæœƒåŽŒå…¨į„Ąæŗ•į™ģå…Ĩ。", - "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å•Ÿį”¨īŧŒčĢ‹äŊŋᔍ äŧ翜å™¨æŒ‡äģ¤ ã€‚", + "authentication_settings_disable_all": "您įĸē厚čĻåœį”¨æ‰€æœ‰į™ģå…Ĩæ–šåŧå—ŽīŧŸé€™å°‡å°Žč‡´åŽŒå…¨į„Ąæŗ•į™ģå…Ĩ。", + "authentication_settings_reenable": "åĻ‚éœ€é‡æ–°å•Ÿį”¨īŧŒčĢ‹äŊŋᔍ äŧ翜å™¨æŒ‡äģ¤ã€‚", "background_task_job": "čƒŒæ™¯åˇĨäŊœ", "backup_database": "åģēįĢ‹čŗ‡æ–™åēĢ備äģŊ", "backup_database_enable_description": "å•Ÿį”¨čŗ‡æ–™åēĢ備äģŊ", "backup_keep_last_amount": "äŋį•™å…ˆå‰å‚™äģŊįš„æ•¸é‡", "backup_onboarding_1_description": "åœ¨é›˛įĢ¯æˆ–å…ļäģ–å¯ĻéĢ”äŊįŊŽįš„į•°åœ°å‚™äģŊ副æœŦ。", - "backup_onboarding_2_description": "å„˛å­˜åœ¨ä¸åŒčŖįŊŽä¸Šįš„æœŦ抟副æœŦ。這包æ‹Ŧä¸ģčρæĒ”æĄˆåŠå…ļæœŦ抟備äģŊ。", + "backup_onboarding_2_description": "å„˛å­˜åœ¨ä¸åŒčŖįŊŽä¸Šįš„æœŦ抟副æœŦ。這包åĢä¸ģčρæĒ”æĄˆåŠå…ļæœŦ抟備äģŊ。", "backup_onboarding_3_description": "æ‚¨čŗ‡æ–™įš„į¸Ŋ備äģŊäģŊ數īŧŒåŒ…æ‹Ŧ原始æĒ”æĄˆåœ¨å…§ã€‚é€™åŒ…æ‹Ŧ 1 äģŊį•°åœ°å‚™äģŊ與 2 äģŊæœŦ抟副æœŦ。", - "backup_onboarding_description": "åģēč­°æŽĄį”¨ 3-2-1 備äģŊį­–į•Ĩ 來äŋč­ˇæ‚¨įš„čŗ‡æ–™ã€‚æ‚¨æ‡‰äŋį•™åˇ˛ä¸Šå‚ŗįš„ᅧቇ/åŊąį‰‡å‰¯æœŦīŧŒäģĨ及 Immich čŗ‡æ–™åēĢīŧŒäģĨåģēįĢ‹åŽŒæ•´įš„å‚™äģŊæ–šæĄˆã€‚", + "backup_onboarding_description": "åģēč­°æŽĄį”¨ 3-2-1 備äģŊį­–į•Ĩ 來äŋč­ˇæ‚¨įš„čŗ‡æ–™ã€‚æ‚¨æ‡‰äŋį•™åˇ˛ä¸Šå‚ŗįš„ᛏቇ/åŊąį‰‡å‰¯æœŦīŧŒäģĨ及 Immich čŗ‡æ–™åēĢīŧŒäģĨåģēįĢ‹åŽŒæ•´įš„å‚™äģŊæ–šæĄˆã€‚", "backup_onboarding_footer": "更多備äģŊ Immich čŗ‡č¨ŠīŧŒčĢ‹åƒč€ƒčĒĒæ˜Žæ–‡äģļ。", "backup_onboarding_parts_title": "éĩåžžå‚™äģŊ原則 3-2-1īŧš", "backup_onboarding_title": "備äģŊ", "backup_settings": "čŗ‡æ–™åēĢ備äģŊč¨­åŽš", "backup_settings_description": "įŽĄį†čŗ‡æ–™åēĢ備äģŊč¨­åŽšã€‚", "cleared_jobs": "厞åˆĒ除「{job}」äģģ務", - "config_set_by_file": "į›Žå‰įš„č¨­åŽšæ˜¯į”ąč¨­åŽšæĒ”設åޚ", + "config_set_by_file": "į›Žå‰įš„č¨­åŽšæ˜¯į”ąč¨­åŽšæĒ”æ‰€č¨­åޚ", "confirm_delete_library": "您įĸē厚čρåˆĒ除外部åĒ’éĢ”åēĢ {library} 嗎īŧŸ", - "confirm_delete_library_assets": "您įĸē厚čρåˆĒ除此外部åĒ’éĢ”åēĢ嗎īŧŸé€™å°‡åžž Immich 中åˆĒ除 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}} īŧŒä¸”į„Ąæŗ•åžŠåŽŸã€‚æĒ”æĄˆäģæœƒäŋį•™åœ¨įĄŦįĸŸä¸­ã€‚", + "confirm_delete_library_assets": "您įĸē厚čρåˆĒ除此åĒ’éĢ”åēĢ嗎īŧŸé€™å°‡åžž Immich 中åˆĒ除 {count, plural, one {# å€‹é …į›Ž} other {所有 # å€‹é …į›Ž}}ä¸”į„Ąæŗ•åžŠåŽŸã€‚æĒ”æĄˆäģæœƒäŋį•™åœ¨įŖįĸŸä¸­ã€‚", "confirm_email_below": "čĢ‹åœ¨åē•下čŧ¸å…Ĩ {email} äģĨįĸēčĒ", "confirm_reprocess_all_faces": "您įĸē厚čĻé‡æ–°č™•į†æ‰€æœ‰č‡‰å­”å—ŽīŧŸé€™æœƒæ¸…除厞å‘Ŋåįš„äēēį‰Šã€‚", "confirm_user_password_reset": "您įĸē厚čĻé‡č¨­ {user} įš„å¯†įĸŧ嗎īŧŸ", "confirm_user_pin_code_reset": "įĸē厚čĻé‡č¨­ {user} įš„ PIN įĸŧ嗎īŧŸ", - "copy_config_to_clipboard_description": "將į•ļ前įŗģįĩąé…å¯˜äŊœį‚ēJSONå°čąĄč¤‡čŖŊ到å‰Ēč˛ŧæŋ", + "copy_config_to_clipboard_description": "å°‡į›Žå‰įŗģįĩąč¨­åޚäģĨ JSON į‰Šäģļæ ŧåŧč¤‡čŖŊ到å‰Ēč˛ŧį°ŋ", "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": "䞝靠æ™ēæ…§æœå°‹ã€‚å°é …į›ŽåŸˇčĄŒæŠŸå™¨å­¸įŋ’䞆åĩæ¸Ŧᛏäŧŧåœ–į‰‡", - "exclusion_pattern_description": "排除čĻå‰‡å¯čŽ“æ‚¨åœ¨æŽƒæåĒ’éĢ”åēĢæ™‚åŋŊį•Ĩį‰šåŽšįš„æĒ”æĄˆå’Œčŗ‡æ–™å¤žã€‚é€™åœ¨æ‚¨æœ‰äē›čŗ‡æ–™å¤žåŒ…åĢä¸æƒŗåŒ¯å…Ĩįš„æĒ”æĄˆīŧˆäž‹åĻ‚ RAW æĒ”īŧ‰æ™‚į‰šåˆĨæœ‰į”¨ã€‚", - "export_config_as_json_description": "將į•ļ前įŗģįĩąé…å¯˜ä¸‹čŧ‰į‚ēJSONæĒ”æĄˆ", - "external_libraries_page_description": "įŽĄį†å¤–éƒ¨åēĢ頁éĸ", + "exclusion_pattern_description": "æŽ’é™¤æ¨Ąåŧå¯čŽ“æ‚¨åœ¨æŽƒæåĒ’éĢ”åēĢæ™‚åŋŊį•Ĩį‰šåŽšæĒ”æĄˆčˆ‡čŗ‡æ–™å¤žã€‚č‹Ĩ某äē›čŗ‡æ–™å¤žåŒ…åĢæ‚¨ä¸æƒŗåŒ¯å…Ĩįš„æĒ”æĄˆīŧˆäž‹åĻ‚ RAW æĒ”īŧ‰īŧŒæ­¤åŠŸčƒŊå°‡éžå¸¸æœ‰į”¨ã€‚", + "export_config_as_json_description": "å°‡į›Žå‰įŗģįĩąč¨­åŽšä¸‹čŧ‰į‚ē JSON æĒ”æĄˆ", + "external_libraries_page_description": "įŽĄį†å¤–éƒ¨åĒ’éĢ”åēĢ頁éĸ", "face_detection": "臉孔åĩæ¸Ŧ", - "face_detection_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’åĩæ¸ŦåĒ’éĢ”æĒ”æĄˆä¸­įš„č‡‰å­”ã€‚å°æ–ŧåŊąį‰‡īŧŒåƒ…æœƒåˆ†æžį¸Žåœ–ã€‚ã€Œé‡æ–°æ•´į†ã€æœƒīŧˆé‡æ–°īŧ‰č™•į†æ‰€æœ‰åĒ’éĢ”æĒ”æĄˆã€‚ã€Œé‡č¨­ã€å‰‡æœƒéĄå¤–æ¸…é™¤į›Žå‰įš„æ‰€æœ‰äēēč‡‰čŗ‡æ–™ã€‚ã€ŒæŽ’å…ĨæœĒč™•į†ã€æœƒå°‡å°šæœĒč™•į†éŽįš„åĒ’éĢ”æĒ”æĄˆåŠ å…ĨäŊ‡åˆ—ã€‚åœ¨åŽŒæˆã€Œč‡‰å­”åĩæ¸Ŧ』垌īŧŒåĩæ¸Ŧåˆ°įš„č‡‰å­”å°‡æœƒčĸĢ加å…Ĩã€Œč‡‰å­”čž¨č­˜ã€įš„äŊ‡åˆ—īŧŒä¸Ļäžį…§čž¨č­˜įĩæžœæ­¸éĄžåˆ°įžæœ‰æˆ–æ–°įš„äēēį‰Šįž¤įĩ„中。", - "facial_recognition_job_description": "將åĩæ¸Ŧåˆ°įš„č‡‰å­”äžį…§äēēį‰Šåˆ†éĄžã€‚æ­¤æ­ĨéŠŸæœƒåœ¨č‡‰å­”åĩæ¸ŦåŽŒæˆåžŒåŸˇčĄŒã€‚é¸æ“‡ã€Œé‡č¨­ã€æœƒé‡æ–°åˆ†įĩ„æ‰€æœ‰č‡‰å­”ã€‚é¸æ“‡ã€ŒæŽ’å…ĨæœĒč™•į†ã€æœƒå°‡å°šæœĒ指洞äēēį‰Šįš„č‡‰å­”åŠ å…ĨäŊ‡åˆ—。", + "face_detection_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’åĩæ¸Ŧé …į›Žä¸­įš„č‡‰å­”ã€‚å°æ–ŧåŊąį‰‡īŧŒåƒ…æœƒåˆ†æžį¸Žåœ–ã€‚ã€Œé‡æ–°æ•´į†ã€æœƒīŧˆé‡æ–°īŧ‰č™•į†æ‰€æœ‰é …į›Žīŧ›ã€Œé‡č¨­ã€å‰‡æœƒéĄå¤–æ¸…é™¤į›Žå‰įš„č‡‰å­”čŗ‡æ–™īŧ›ã€ŒæŽ’å…ĨæœĒč™•į†ã€æœƒå°‡å°šæœĒč™•į†įš„é …į›ŽåŠ å…ĨäŊ‡åˆ—ã€‚åŽŒæˆã€Œč‡‰å­”åĩæ¸Ŧ」垌īŧŒåĩæ¸Ŧåˆ°įš„č‡‰å­”å°‡åŠ å…Ĩã€Œč‡‰å­”čž¨č­˜ã€äŊ‡åˆ—īŧŒä¸Ļæ­¸éĄžč‡ŗįžæœ‰æˆ–æ–°įš„äēēį‰Šįž¤įĩ„。", + "facial_recognition_job_description": "將åĩæ¸Ŧåˆ°įš„č‡‰å­”æ­¸éĄžį‚ēäēēį‰Šã€‚æ­¤æ­ĨéŠŸæœƒåœ¨č‡‰å­”åĩæ¸ŦåŽŒæˆåžŒåŸˇčĄŒã€‚ã€Œé‡č¨­ã€æœƒé‡æ–°å°æ‰€æœ‰č‡‰å­”é€˛čĄŒåˆ†įž¤īŧ›ã€ŒæŽ’å…ĨæœĒč™•į†ã€å‰‡æœƒå°‡å°šæœĒ指洞äēēį‰Šįš„č‡‰å­”åŠ å…ĨäŊ‡åˆ—。", "failed_job_command": "{job} äģģå‹™įš„ {command} 指äģ¤åŸˇčĄŒå¤ąæ•—", - "force_delete_user_warning": "č­Ļ告īŧšé€™å°‡įĢ‹åŗåˆĒ除äŊŋį”¨č€…åŠå…￉€æœ‰é …į›Žã€‚æ­¤æ“äŊœį„Ąæŗ•æ’¤éŠˇä¸Ļä¸”į„Ąæŗ•é‚„åŽŸåˆĒé™¤įš„æĒ”æĄˆã€‚", + "force_delete_user_warning": "č­Ļ告īŧšé€™å°‡įĢ‹åŗåˆĒ除äŊŋį”¨č€…åŠå…￉€æœ‰é …į›Žã€‚æ­¤å‹•äŊœį„Ąæŗ•垊原īŧŒä¸”į„Ąæŗ•æ‰žå›žåˇ˛åˆĒé™¤įš„æĒ”æĄˆã€‚", "image_format": "æ ŧåŧ", "image_format_description": "WebP čƒŊį”ĸį”Ÿį›¸å°æ–ŧ JPEG æ›´å°įš„æĒ”æĄˆīŧŒäŊ†įˇ¨įĸŧ速åēĻčŧƒæ…ĸ。", "image_fullsize_description": "į§ģ除中įšŧčŗ‡æ–™įš„å¤§å°ē寸åŊąåƒīŧŒåœ¨æ”žå¤§åœ–į‰‡æ™‚äŊŋᔍ", "image_fullsize_enabled": "å•Ÿį”¨å¤§å°ē寸åŊąåƒį”ĸį”Ÿ", - "image_fullsize_enabled_description": "į”ĸį”Ÿéžįļ˛é å‹å–„æ ŧåŧįš„大å°ē寸åŊąåƒã€‚å•Ÿį”¨ã€ŒååĨŊåĩŒå…Ĩįš„é čĻŊ」時īŧŒæœƒį›´æŽĨäŊŋᔍ內åĩŒé čĻŊ而不進行čŊ‰æ›ã€‚不會åŊąéŸŋ JPEG į­‰įļ˛é å‹å–„æ ŧåŧã€‚", + "image_fullsize_enabled_description": "į‚ē非įļ˛é å‹å–„æ ŧåŧį”ĸį”Ÿå¤§å°ēå¯¸į›¸į‰‡ã€‚å•Ÿį”¨ã€ŒååĨŊ內åĩŒé čĻŊ」時īŧŒįŗģįĩąå°‡į›´æŽĨäŊŋᔍ內åĩŒé čĻŊ而不進行čŊ‰įĸŧīŧŒä¸åŊąéŸŋ JPEG į­‰įļ˛é å‹å–„æ ŧåŧã€‚", "image_fullsize_quality_description": "大å°ē寸åŊąåƒå“čŗĒīŧŒį¯„圍į‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒčļŠå¤§ã€‚", "image_fullsize_title": "大å°ē寸åŊąåƒč¨­åޚ", - "image_prefer_embedded_preview": "偏åĨŊåĩŒå…Ĩįš„é čĻŊ", - "image_prefer_embedded_preview_setting_description": "åœ¨å¯čĄŒįš„æƒ…æŗä¸‹īŧŒå°‡ RAW į…§į‰‡ä¸­įš„å…§åĩŒé čĻŊᔍäŊœåŊąåƒč™•į†įš„čŧ¸å…Ĩ來æēã€‚這對某äē›åŊąåƒčƒŊį”ĸį”Ÿæ›´æē–įĸēįš„č‰˛åŊŠīŧŒäŊ†é čĻŊįš„å“čŗĒ取æąēæ–ŧį›¸æŠŸīŧŒåŊąåƒå¯čƒŊ會å‡ēįžæ›´å¤šåŖ“į¸Žį‘•į–ĩ。", + "image_prefer_embedded_preview": "偏åĨŊ內åĩŒé čĻŊ", + "image_prefer_embedded_preview_setting_description": "åœ¨å¯į”¨æ™‚īŧŒå°‡ RAW į›¸į‰‡ä¸­įš„å…§åĩŒé čĻŊäŊœį‚ēåŊąåƒč™•į†įš„čŧ¸å…Ĩ來æēã€‚é›–į„ļ這čƒŊčŽ“éƒ¨åˆ†į›¸į‰‡č‰˛åŊŠæ›´æē–įĸēīŧŒäŊ†é čĻŊ品čŗĒ取æąēæ–ŧį›¸æŠŸīŧŒä¸”åŊąåƒå¯čƒŊ會å‡ēįžčŧƒå¤šåŖ“į¸Žį‘•į–ĩ。", "image_prefer_wide_gamut": "偏åĨŊåģŖč‰˛åŸŸ", - "image_prefer_wide_gamut_setting_description": "äŊŋᔍ Display P3 來čŖŊäŊœį¸Žåœ–。這čƒŊ更åĨŊ地äŋį•™å¯ŦåģŖč‰˛åŸŸåŊąåƒįš„鎎蹔åēĻīŧŒäŊ†åœ¨čˆŠčŖįŊŽčˆ‡čˆŠį‰ˆæœŦį€čĻŊ器上īŧŒåŊąåƒå¯čƒŊæœƒå‘ˆįžä¸åŒįš„æ•ˆæžœã€‚sRGB åŊąåƒæœƒäŋæŒį‚ē sRGBīŧŒäģĨéŋå…č‰˛åŊŠåį§ģ。", - "image_preview_description": "į§ģ除中įšŧčŗ‡æ–™įš„ä¸­å°ē寸åŊąåƒīŧŒį”¨æ–ŧæĒĸčĻ–å–Žä¸€åĒ’éĢ”æĒ”æĄˆäģĨ及抟器學įŋ’時äŊŋᔍ", + "image_prefer_wide_gamut_setting_description": "äŊŋᔍ Display P3 čŖŊäŊœį¸Žåœ–。這čƒŊ更åĨŊ地äŋį•™åģŖč‰˛åŸŸåŊąåƒįš„鎎蹔åēĻīŧŒäŊ†åœ¨čˆŠčŖįŊŽčˆ‡čˆŠį‰ˆį€čĻŊ器上īŧŒåŊąåƒå‘ˆįžįš„æ•ˆæžœå¯čƒŊ會有所不同。sRGB åŊąåƒå°‡äŋæŒį‚ē sRGBīŧŒäģĨéŋå…č‰˛åŊŠåį§ģ。", + "image_preview_description": "䏭ᭉå°ē寸åŊąåƒīŧˆä¸åĢ中įšŧčŗ‡æ–™īŧ‰īŧŒį”¨æ–ŧæĒĸčĻ–å–Žä¸€é …į›Žčˆ‡æŠŸå™¨å­¸įŋ’", "image_preview_quality_description": "預čĻŊ品čŗĒį¯„åœį‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻã€‚č¨­åŽšéŽäŊŽįš„æ•¸å€ŧ可čƒŊ會åŊąéŸŋ抟器學įŋ’įš„å“čŗĒ。", "image_preview_title": "預čĻŊč¨­åŽš", + "image_progressive": "逐æ­Ĩ", + "image_progressive_description": "對 JPEG åŊąåƒé€˛čĄŒæŧ¸é€˛åŧįˇ¨įĸŧīŧŒäģĨå¯Ļįžæŧ¸é€˛åŧčŧ‰å…ĨéĄ¯į¤ē。這不會åŊąéŸŋ WebP åŊąåƒã€‚", "image_quality": "品čŗĒ", "image_resolution": "č§ŖæžåēĻ", "image_resolution_description": "čŧƒéĢ˜įš„č§ŖæžåēĻčƒŊäŋį•™æ›´å¤šį´°į¯€īŧŒäŊ†įˇ¨įĸŧæ™‚é–“æœƒæ›´é•ˇã€æĒ”æĄˆå¤§å°æœƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻ。", "image_settings": "åœ–į‰‡č¨­åŽš", - "image_settings_description": "įŽĄį†į”ĸį”Ÿåœ–į‰‡įš„å“čŗĒå’Œč§ŖæžåēĻ", - "image_thumbnail_description": "į§ģ除中įšŧčŗ‡æ–™įš„å°åž‹į¸Žåœ–īŧŒäģĨᔍæ–ŧæĒĸčĻ–å¤§é‡į…§į‰‡æ™‚äŊŋᔍīŧŒäž‹åĻ‚ä¸ģ時間čģ¸", + "image_settings_description": "įŽĄį†į”ĸį”Ÿįš„åŊąåƒå“čŗĒčˆ‡č§ŖæžåēĻ", + "image_thumbnail_description": "į§ģ除中įšŧčŗ‡æ–™įš„å°åž‹į¸Žåœ–īŧŒäģĨᔍæ–ŧæĒĸčĻ–å¤§é‡į›¸į‰‡æ™‚äŊŋᔍīŧŒäž‹åĻ‚ä¸ģ時間čģ¸", "image_thumbnail_quality_description": "į¸Žåœ–å“čŗĒį¯„åœį‚ē 1 到 100。數å€ŧčļŠéĢ˜å“čŗĒčļŠåĨŊīŧŒäŊ†æĒ”æĄˆä🿜ƒæ›´å¤§īŧŒä¸Ļ可čƒŊ降äŊŽæ‡‰į”¨į¨‹åŧįš„回應速åēĻ。", "image_thumbnail_title": "į¸Žåœ–č¨­åŽš", - "import_config_from_json_description": "é€šéŽä¸Šå‚ŗJSONč¨­åŽšæĒ”å°Žå…Ĩįŗģįĩąé…å¯˜", - "job_concurrency": "{job}äŊĩį™ŧ", + "import_config_from_json_description": "é€éŽä¸Šå‚ŗ JSON č¨­åŽšæĒ”匯å…Ĩįŗģįĩąč¨­åޚ", + "job_concurrency": "{job} ä¸ĻčĄŒæ•¸", "job_created": "厞åģēįĢ‹äģģ務", - "job_not_concurrency_safe": "這個äģģ務äŊĩį™ŧä¸Ļ不厉全。", + "job_not_concurrency_safe": "æ­¤äģģ務不支援ä¸ĻčĄŒåŸˇčĄŒã€‚", "job_settings": "äģģå‹™č¨­åŽš", - "job_settings_description": "äŊĩį™ŧäģģå‹™įŽĄį†", + "job_settings_description": "įŽĄį†äģģ務ä¸ĻčĄŒæ•¸", "jobs_delayed": "{jobCount, plural, other {# 項äģģ務厞åģļ垌}}", "jobs_failed": "{jobCount, plural, other {# 項äģģå‹™åˇ˛å¤ąæ•—}}", - "jobs_over_time": "įĩ„į𔿙‚é–“äģģ務數", + "jobs_over_time": "äģģ務數量čļ¨å‹ĸ", "library_created": "厞åģēįĢ‹åĒ’éĢ”åēĢīŧš{library}", "library_deleted": "åĒ’éĢ”åēĢ厞åˆĒ除", - "library_details": "åĒ’éĢ”åēĢčŠŗæƒ…", - "library_folder_description": "指厚čĻå°Žå…Ĩįš„čŗ‡æ–™å¤žã€‚ å°‡æŽƒææ­¤čŗ‡æ–™å¤žīŧˆåŒ…æ‹Ŧå­čŗ‡æ–™å¤žīŧ‰ä¸­įš„åŊąåƒå’ŒčĻ–é ģ。", - "library_remove_exclusion_pattern_prompt": "您įĸē厚čĻåˆ é™¤æ­¤æŽ’é™¤æ¨Ąåŧå—ŽīŧŸ", - "library_remove_folder_prompt": "您įĸē厚čĻåˆ é™¤æ­¤å°Žå…Ĩčŗ‡æ–™å¤žå—ŽīŧŸ", + "library_details": "åĒ’éĢ”åēĢčŠŗį´°čŗ‡č¨Š", + "library_folder_description": "指厚čρ匝å…Ĩįš„čŗ‡æ–™å¤žã€‚įŗģįĩąå°‡æŽƒææ­¤čŗ‡æ–™å¤žīŧˆåŒ…åĢå­čŗ‡æ–™å¤žīŧ‰ä¸­įš„åŊąåƒčˆ‡åŊąį‰‡ã€‚", + "library_remove_exclusion_pattern_prompt": "įĸē厚čρį§ģé™¤æ­¤æŽ’é™¤æ¨Ąåŧå—ŽīŧŸ", + "library_remove_folder_prompt": "įĸē厚čρį§ģ除此匯å…Ĩčŗ‡æ–™å¤žå—ŽīŧŸ", "library_scanning": "厚期掃描", - "library_scanning_description": "厚期åĒ’éĢ”åēĢæŽƒæč¨­åޚ", + "library_scanning_description": "č¨­åŽšåŽšæœŸåĒ’éĢ”åēĢæŽƒæ", "library_scanning_enable_description": "å•Ÿį”¨åĒ’éĢ”åēĢ厚期掃描", "library_settings": "外部åĒ’éĢ”åēĢ", "library_settings_description": "įŽĄį†å¤–éƒ¨åĒ’éĢ”åēĢč¨­åŽš", @@ -130,9 +139,9 @@ "library_watching_enable_description": "į›ŖæŽ§å¤–éƒ¨åĒ’éĢ”åēĢįš„æĒ”æĄˆčŽŠåŒ–", "library_watching_settings": "åĒ’éĢ”åēĢį›ŖæŽ§[å¯Ļ銗性]", "library_watching_settings_description": "č‡Ēå‹•į›ŖæŽ§æĒ”æĄˆįš„čŽŠåŒ–", - "logging_enable_description": "å•Ÿį”¨æ—ĨčĒŒč¨˜éŒ„", - "logging_level_description": "å•Ÿį”¨æ™‚įš„æ—ĨčĒŒåą¤į´šã€‚", - "logging_settings": "æ—Ĩčnj", + "logging_enable_description": "å•Ÿį”¨į´€éŒ„åŠŸčƒŊ", + "logging_level_description": "å•Ÿį”¨æ™‚įš„į´€éŒ„åą¤į´šã€‚", + "logging_settings": "į´€éŒ„", "machine_learning_availability_checks": "å¯į”¨æ€§æĒĸæŸĨ", "machine_learning_availability_checks_description": "č‡Ē動åĩæ¸Ŧä¸Ļå„Ēå…ˆé¸æ“‡å¯į”¨įš„æŠŸå™¨å­¸įŋ’äŧ翜å™¨", "machine_learning_availability_checks_enabled": "å•Ÿį”¨å¯į”¨æ€§æĒĸæŸĨ", @@ -141,53 +150,64 @@ "machine_learning_availability_checks_timeout": "čĢ‹æą‚čļ…æ™‚", "machine_learning_availability_checks_timeout_description": "å¯į”¨æ€§æĒĸæŸĨčļ…æ™‚īŧˆæ¯Ģį§’īŧ‰", "machine_learning_clip_model": "CLIP æ¨Ąåž‹", - "machine_learning_clip_model_description": "é€™čŖĄæœ‰äģŊ CLIP æ¨Ąåž‹åå–Žã€‚æŗ¨æ„īŧšæ›´æ›æ¨Ąåž‹åžŒé ˆå°æ‰€æœ‰åœ–į‰‡é‡æ–°åŸˇčĄŒã€Œæ™ē慧搜尋」äģģ務。", + "machine_learning_clip_model_description": "é€™čŖĄæœ‰äģŊ CLIP æ¨Ąåž‹æ¸…å–Žã€‚æŗ¨æ„īŧšæ›´æ›æ¨Ąåž‹åžŒåŋ…é ˆå°æ‰€æœ‰į›¸į‰‡é‡æ–°åŸˇčĄŒã€Œæ™ē慧搜尋」äģģ務。", "machine_learning_duplicate_detection": "é‡č¤‡é …į›Žåĩæ¸Ŧ", "machine_learning_duplicate_detection_enabled": "å•Ÿį”¨é‡č¤‡é …į›Žåĩæ¸Ŧ", - "machine_learning_duplicate_detection_enabled_description": "č‹Ĩåœį”¨īŧŒåŽŒå…¨į›¸åŒįš„åĒ’éĢ”æĒ”æĄˆäģæœƒé€˛čĄŒé‡č¤‡čŗ‡æ–™åˆĒ除。", + "machine_learning_duplicate_detection_enabled_description": "č‹Ĩåœį”¨īŧŒåŽŒå…¨į›¸åŒįš„é …į›Žäģæœƒé€˛čĄŒé‡č¤‡é …į›ŽåˆĒ除。", "machine_learning_duplicate_detection_setting_description": "äŊŋᔍ CLIP 向量比對尋扞可čƒŊįš„é‡č¤‡é …į›Ž", "machine_learning_enabled": "å•Ÿį”¨æŠŸå™¨å­¸įŋ’", - "machine_learning_enabled_description": "č‹Ĩåœį”¨īŧŒå‰‡į„ĄčĻ–ä¸‹æ–šįš„č¨­åŽšīŧŒæ‰€æœ‰æŠŸå™¨å­¸įŋ’įš„åŠŸčƒŊéƒŊå°‡åœį”¨ã€‚", + "machine_learning_enabled_description": "č‹Ĩåœį”¨īŧŒä¸čĢ–ä¸‹æ–šįš„č¨­åŽšį‚ēäŊ•īŧŒæ‰€æœ‰æŠŸå™¨å­¸įŋ’功čƒŊéƒŊå°‡åœį”¨ã€‚", "machine_learning_facial_recognition": "äēē臉辨識", "machine_learning_facial_recognition_description": "åĩæ¸Ŧã€čž¨č­˜ä¸Ļå°åœ–į‰‡ä¸­įš„č‡‰å­”åˆ†éĄž", "machine_learning_facial_recognition_model": "äēēč‡‰čž¨č­˜æ¨Ąåž‹", - "machine_learning_facial_recognition_model_description": "æ¨Ąåž‹é †åēį”ąå¤§č‡ŗå°æŽ’åˆ—ã€‚å¤§įš„æ¨Ąåž‹čŧƒæ…ĸ且äŊŋᔍčŧƒå¤šč¨˜æ†ļéĢ”īŧŒäŊ†æˆæ•ˆčŧƒäŊŗã€‚æ›´æ›æ¨Ąåž‹åžŒéœ€å°æ‰€æœ‰åŊąåƒé‡æ–°åŸˇčĄŒã€Œäēēč‡‰čž¨č­˜ã€ã€‚", + "machine_learning_facial_recognition_model_description": "æ¨Ąåž‹é †åēį”ąå¤§č‡ŗå°æŽ’列。čŧƒå¤§įš„æ¨Ąåž‹é€ŸåēĻčŧƒæ…ĸ且äŊ”ᔍčŧƒå¤šč¨˜æ†ļéĢ”īŧŒäŊ†æ•ˆæžœčŧƒäŊŗã€‚čĢ‹æŗ¨æ„īŧŒæ›´æ›æ¨Ąåž‹åžŒåŋ…須對所有åŊąåƒé‡æ–°åŸˇčĄŒã€Œč‡‰å­”åĩæ¸Ŧ」äģģ務。", "machine_learning_facial_recognition_setting": "å•Ÿį”¨äēē臉辨識", - "machine_learning_facial_recognition_setting_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒį”ĸį”Ÿäēēč‡‰čž¨č­˜įˇ¨įĸŧīŧŒä¸Ļ且在「æŽĸį´ĸ」頁éĸ不會有「äēēį‰Šã€åŠŸčƒŊ。", + "machine_learning_facial_recognition_setting_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒäēēč‡‰čž¨č­˜įˇ¨įĸŧīŧŒä¸”「æŽĸį´ĸ」頁éĸįš„ã€Œäēēį‰Šã€å€åĄŠå°‡ä¸æœƒéĄ¯į¤ēäģģäŊ•內厚。", "machine_learning_max_detection_distance": "åĩæ¸Ŧ距é›ĸ上限", "machine_learning_max_detection_distance_description": "č‹Ĩå…ŠåŧĩåŊąåƒé–“įš„čˇé›ĸ小æ–ŧ此將čĸĢåˆ¤æ–ˇį‚ēį›¸åŒīŧŒį¯„圍į‚ē 0.001-0.1。數å€ŧčļŠé̘čƒŊåĩæ¸Ŧ到čļŠå¤šé‡č¤‡īŧŒäŊ†äšŸæ›´æœ‰å¯čƒŊčĒ¤åˆ¤ã€‚", "machine_learning_max_recognition_distance": "辨識距é›ĸ上限", - "machine_learning_max_recognition_distance_description": "å…Šåŧĩ臉孔čĸĢčĻ–į‚ē同一äēēį‰Šįš„æœ€å¤§čˇé›ĸīŧŒį¯„圍į‚ē 0 臺 2。降äŊŽæ­¤æ•¸å€ŧ可éŋ免將不同äēēį‰Šæ¨™č¨˜į‚ē同一äēēīŧ›æéĢ˜æ­¤æ•¸å€ŧ則可éŋ免將同一äēēį‰Šæ¨™č¨˜į‚ēå…Šå€‹ä¸åŒįš„äēē。čĢ‹æŗ¨æ„īŧŒåˆäŊĩ兊個äēēį‰Šæ¯”å°‡ä¸€å€‹äēēį‰Šæ‹†åˆ†æˆå…Šå€‹æ›´åŽšæ˜“īŧŒå› æ­¤åœ¨å¯čƒŊįš„æƒ…æŗä¸‹īŧŒåģēč­°å°‡æ­¤é–žå€ŧč¨­åŽšåž—čŧƒäŊŽã€‚", + "machine_learning_max_recognition_distance_description": "å…Šåŧĩ臉孔čĸĢčĻ–į‚ē同一äēēį‰Šįš„æœ€å¤§čˇé›ĸīŧŒį¯„圍į‚ē 0 臺 2。降äŊŽæ­¤æ•¸å€ŧ可éŋ免將不同äēēį‰Šæ¨™č¨˜į‚ē同一äēēīŧ›æéĢ˜æ­¤æ•¸å€ŧ則可éŋ免將同一äēēį‰Šæ¨™č¨˜į‚ēå…Šå€‹ä¸åŒįš„äēē。čĢ‹æŗ¨æ„īŧŒåˆäŊĩäēēį‰Šæ¯”æ‹†åˆ†äēēį‰Šæ›´åŽšæ˜“īŧŒå› æ­¤åģēč­°åœ¨å¯čƒŊįš„æƒ…æŗä¸‹å°‡æ­¤é–€æĒģå€ŧč¨­åŽšåž—čŧƒäŊŽã€‚", "machine_learning_min_detection_score": "最äŊŽåĩæ¸Ŧ分數", - "machine_learning_min_detection_score_description": "臉孔åĩæ¸Ŧįš„æœ€äŊŽäŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 臺 1。數å€ŧčŧƒäŊŽæ™‚會åĩæ¸Ŧåˆ°æ›´å¤šč‡‰å­”īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", + "machine_learning_min_detection_score_description": "臉孔åĩæ¸Ŧįš„æœ€äŊŽäŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 - 1。čŧƒäŊŽįš„æ•¸å€ŧ會åĩæ¸Ŧåˆ°æ›´å¤šč‡‰å­”īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", "machine_learning_min_recognized_faces": "最äŊŽč‡‰éƒ¨čž¨č­˜æ•¸é‡", "machine_learning_min_recognized_faces_description": "åģēįĢ‹æ–°äēēį‰Šæ‰€éœ€įš„æœ€äŊŽåˇ˛čž¨č­˜č‡‰å­”數量。提éĢ˜æ­¤æ•¸å€ŧå¯čŽ“č‡‰å­”čž¨č­˜æ›´į˛žįĸēīŧŒäŊ†åŒæ™‚會åĸžåР臉孔æœĒčĸĢæŒ‡æ´žįĩĻäģģäŊ•äēēį‰Šįš„å¯čƒŊ性。", "machine_learning_ocr": "æ–‡å­—čž¨č­˜(OCR)", - "machine_learning_ocr_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’äž†č­˜åˆĨåœ–į‰‡ä¸­įš„æ–‡å­—", + "machine_learning_ocr_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’čž¨č­˜åŊąåƒä¸­įš„æ–‡å­—", "machine_learning_ocr_enabled": "å•Ÿį”¨OCR", - "machine_learning_ocr_enabled_description": "åĻ‚æžœįρᔍīŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒæ–‡å­—č­˜åˆĨ。", - "machine_learning_ocr_max_resolution": "æœ€å¤§åˆ†čž¯įŽ‡", - "machine_learning_ocr_max_resolution_description": "é̘æ–ŧæ­¤åˆ†čž¯įŽ‡įš„é čĻŊ將čĒŋ整大小īŧŒåŒæ™‚äŋæŒį¸ąæŠĢ比。 更éĢ˜įš„å€ŧ更æē–įĸēīŧŒäŊ†č™•į†æ™‚é–“æ›´é•ˇīŧŒäŊ”į”¨æ›´å¤šč¨˜æ†ļéĢ”ã€‚", + "machine_learning_ocr_enabled_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒæ–‡å­—čž¨č­˜ã€‚", + "machine_learning_ocr_max_resolution": "æœ€å¤§č§ŖæžåēĻ", + "machine_learning_ocr_max_resolution_description": "č§ŖæžåēĻé̘æ–ŧæ­¤å€ŧįš„é čĻŊåŊąåƒå°‡åœ¨äŋæŒé•ˇå¯Ŧæ¯”įš„æƒ…æŗä¸‹čĒŋ整大小。數å€ŧčļŠé̘čē–įĸēīŧŒäŊ†č™•į†æ™‚é–“æ›´é•ˇä¸”æœƒäŊ”į”¨æ›´å¤šč¨˜æ†ļéĢ”ã€‚", "machine_learning_ocr_min_detection_score": "最äŊŽæĒĸæ¸Ŧ分數", - "machine_learning_ocr_min_detection_score_description": "čρæĒĸæ¸Ŧįš„æ–‡å­—įš„æœ€å°įŊŽäŋĄåēĻ分數į‚ē0-1。 čŧƒäŊŽįš„å€ŧ將æĒĸæ¸Ŧåˆ°æ›´å¤šįš„æ–‡å­—īŧŒäŊ†å¯čƒŊæœƒå°Žč‡´čĒ¤å ąã€‚", - "machine_learning_ocr_min_recognition_score": "最äŊŽč­˜åˆĨ分數", - "machine_learning_ocr_min_score_recognition_description": "æĒĸæ¸Ŧåˆ°įš„æ–‡å­—įš„æœ€å°įŊŽäŋĄåēĻ垗分į‚ē0-1。 čŧƒäŊŽįš„å€ŧå°‡č­˜åˆĨæ›´å¤šįš„æ–‡å­—īŧŒäŊ†å¯čƒŊæœƒå°Žč‡´čĒ¤å ąã€‚", + "machine_learning_ocr_min_detection_score_description": "文字åĩæ¸Ŧįš„æœ€äŊŽäŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 - 1。čŧƒäŊŽįš„æ•¸å€ŧ會åĩæ¸Ŧ到更多文字īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", + "machine_learning_ocr_min_recognition_score": "最äŊŽčž¨č­˜åˆ†æ•¸", + "machine_learning_ocr_min_score_recognition_description": "厞åĩæ¸Ŧæ–‡å­—įš„æœ€äŊŽčž¨č­˜äŋĄåŋƒåˆ†æ•¸īŧŒį¯„圍į‚ē 0 - 1。čŧƒäŊŽįš„æ•¸å€ŧæœƒčž¨č­˜å‡ē更多文字īŧŒäŊ†å¯čƒŊå°Žč‡´čĒ¤åˆ¤ã€‚", "machine_learning_ocr_model": "OCRæ¨Ąåž‹", - "machine_learning_ocr_model_description": "æœå‹™å™¨æ¨Ąåž‹æ¯”į§ģå‹•æ¨Ąåž‹æ›´æē–įĸēīŧŒäŊ†éœ€čĻæ›´é•ˇįš„æ™‚é–“äž†č™•į†å’ŒäŊŋį”¨æ›´å¤šįš„č¨˜æ†ļéĢ”ã€‚", + "machine_learning_ocr_model_description": "äŧ翜å™¨æ¨Ąåž‹æ¯”čĄŒå‹•čŖįŊŽæ¨Ąåž‹æ›´æē–įĸēīŧŒäŊ†č™•į†æ™‚é–“čŧƒé•ˇä¸”會äŊ”į”¨æ›´å¤šč¨˜æ†ļéĢ”ã€‚", "machine_learning_settings": "抟器學įŋ’設åޚ", "machine_learning_settings_description": "įŽĄį†æŠŸå™¨å­¸įŋ’įš„åŠŸčƒŊå’Œč¨­åŽš", "machine_learning_smart_search": "æ™ē慧搜尋", "machine_learning_smart_search_description": "äŊŋᔍ CLIP åĩŒå…Ĩ向量äģĨčĒžæ„æ–šåŧæœå°‹åŊąåƒ", "machine_learning_smart_search_enabled": "å•Ÿį”¨æ™ē慧搜尋", - "machine_learning_smart_search_enabled_description": "åĻ‚æžœåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒčĸĢᎍįĸŧäģĨ進行æ™ē慧搜尋。", + "machine_learning_smart_search_enabled_description": "č‹Ĩåœį”¨īŧŒåŊąåƒå°‡ä¸æœƒé€˛čĄŒæ™ēæ…§æœå°‹įˇ¨įĸŧ。", "machine_learning_url_description": "抟器學įŋ’äŧ翜å™¨įš„ URL。č‹Ĩ提䞛多個 URLīŧŒįŗģįĩ࿜ƒäžåēé€ä¸€å˜—čŠĻīŧŒį›´åˆ°å…ļ中一č‡ē成功回應į‚ēæ­ĸīŧˆį”ąå‰åˆ°åžŒīŧ‰ã€‚æœĒå›žæ‡‰įš„äŧ翜å™¨å°‡čĸĢæšĢ時åŋŊį•ĨīŧŒį›´åˆ°å…ļé‡æ–°ä¸Šįˇšã€‚", + "maintenance_delete_backup": "åˆĒ除備äģŊ", + "maintenance_delete_backup_description": "æ­¤æĒ”æĄˆå°‡čĸĢæ°¸äš…åˆĒé™¤ä¸”į„Ąæŗ•åžŠåŽŸã€‚", + "maintenance_delete_error": "åˆĒ除備äģŊå¤ąæ•—ã€‚", + "maintenance_restore_backup": "還原備äģŊ", + "maintenance_restore_backup_description": "Immich įš„čŗ‡æ–™å°‡čĸĢæ¸…除īŧŒä¸Ļåžžé¸å–įš„å‚™äģŊ還原。在įšŧį猿“äŊœå‰īŧŒįŗģįĩ࿜ƒå…ˆåģēįĢ‹į›Žå‰įš„čŗ‡æ–™å‚™äģŊ。", + "maintenance_restore_backup_different_version": "此備äģŊæ˜¯į”ąä¸åŒį‰ˆæœŦįš„ Immich 所åģēįĢ‹īŧ", + "maintenance_restore_backup_unknown_version": "į„Ąæŗ•įĸē厚備äģŊį‰ˆæœŦ。", + "maintenance_restore_database_backup": "é‚„åŽŸčŗ‡æ–™åēĢ備äģŊ", + "maintenance_restore_database_backup_description": "äŊŋᔍ備äģŊæĒ”æĄˆå°‡čŗ‡æ–™åēĢé‚„åŽŸč‡ŗčŧƒæ—Šįš„į‹€æ…‹", "maintenance_settings": "įļ­č­ˇ", - "maintenance_settings_description": "將ImmichįŊŽæ–ŧįļ­č­ˇæ¨Ąåŧã€‚", + "maintenance_settings_description": "將 Immich åˆ‡æ›č‡ŗįļ­č­ˇæ¨Ąåŧã€‚", "maintenance_start": "啟動įļ­č­ˇæ¨Ąåŧ", "maintenance_start_error": "啟動įļ­č­ˇæ¨Ąåŧå¤ąæ•—。", - "manage_concurrency": "įŽĄį†äŊĩį™ŧ", - "manage_concurrency_description": "導čˆĒ到äģģ務頁éĸäģĨįŽĄį†äģģ務äŊĩį™ŧ性", - "manage_log_settings": "įŽĄį†æ—ĨčĒŒč¨­åŽš", + "maintenance_upload_backup": "ä¸Šå‚ŗčŗ‡æ–™åēĢ備äģŊæĒ”æĄˆ", + "maintenance_upload_backup_error": "į„Ąæŗ•ä¸Šå‚ŗå‚™äģŊīŧŒåŽƒæ˜¯ .sql 或 .sql.gz æ ŧåŧįš„æĒ”æĄˆå—ŽīŧŸ", + "manage_concurrency": "įŽĄį†ä¸ĻčĄŒč¨­åŽš", + "manage_concurrency_description": "前垀äģģ務頁éĸäģĨįŽĄį†äģģ務ä¸ĻčĄŒč¨­åŽš", + "manage_log_settings": "įŽĄį†į´€éŒ„č¨­åŽš", "map_dark_style": "æˇąč‰˛æ¨Ŗåŧ", "map_enable_description": "å•Ÿį”¨åœ°åœ–åŠŸčƒŊ", "map_gps_settings": "åœ°åœ–čˆ‡ GPS č¨­åŽš", @@ -204,21 +224,21 @@ "memory_cleanup_job": "回æ†ļæ¸…į†", "memory_generate_job": "į”ĸį”Ÿå›žæ†ļ", "metadata_extraction_job": "æ“ˇå–ä¸­įšŧčŗ‡æ–™", - "metadata_extraction_job_description": "垞每個åĒ’éĢ”æĒ”æĄˆä¸­æ“ˇå–ä¸­įšŧčŗ‡æ–™čŗ‡č¨ŠīŧŒäž‹åĻ‚ GPSã€č‡‰å­”čˆ‡č§ŖæžåēĻ", + "metadata_extraction_job_description": "åžžæ¯å€‹é …į›Žä¸­æ“ˇå–ä¸­įšŧčŗ‡æ–™čŗ‡č¨ŠīŧŒäž‹åĻ‚ GPSã€č‡‰å­”čˆ‡č§ŖæžåēĻ", "metadata_faces_import_setting": "å•Ÿį”¨č‡‰å­”åŒ¯å…Ĩ", - "metadata_faces_import_setting_description": "åžžåŊąåƒ EXIF čŗ‡æ–™čˆ‡å´æŽĨæĒ”æĄˆåŒ¯å…Ĩ臉孔", + "metadata_faces_import_setting_description": "åžžåŊąåƒ EXIF čŗ‡æ–™čˆ‡ Sidecar æĒ”æĄˆåŒ¯å…Ĩ臉孔", "metadata_settings": "中įšŧčŗ‡æ–™č¨­åŽš", "metadata_settings_description": "įŽĄį†ä¸­įšŧčŗ‡æ–™č¨­åŽš", "migration_job": "遡į§ģ", - "migration_job_description": "將åĒ’éĢ”æĒ”æĄˆčˆ‡č‡‰å­”įš„į¸Žåœ–éˇį§ģč‡ŗæœ€æ–°įš„čŗ‡æ–™å¤žįĩæ§‹", + "migration_job_description": "å°‡é …į›Žčˆ‡č‡‰å­”į¸Žåœ–éˇį§ģč‡ŗæœ€æ–°įš„čŗ‡æ–™å¤žįĩæ§‹", "nightly_tasks_cluster_faces_setting_description": "對新åĩæ¸Ŧåˆ°įš„č‡‰å­”åŸˇčĄŒč‡‰å­”čž¨č­˜", "nightly_tasks_cluster_new_faces_setting": "į‚ēæ–°č‡‰å­”é€˛čĄŒåˆ†įž¤", "nightly_tasks_database_cleanup_setting": "čŗ‡æ–™åēĢæ¸…ᐆäŊœæĨ­", "nightly_tasks_database_cleanup_setting_description": "æ¸…é™¤čŗ‡æ–™åēĢä¸­čˆŠįš„čˆ‡åˇ˛éŽæœŸįš„čŗ‡æ–™", "nightly_tasks_generate_memories_setting": "į”ĸį”Ÿå›žæ†ļ", - "nightly_tasks_generate_memories_setting_description": "åžžåĒ’éĢ”æĒ”æĄˆåģēįĢ‹æ–°å›žæ†ļ", + "nightly_tasks_generate_memories_setting_description": "åžžé …į›ŽåģēįĢ‹æ–°å›žæ†ļ", "nightly_tasks_missing_thumbnails_setting": "į”ĸį”Ÿįŧēå°‘įš„į¸Žåœ–", - "nightly_tasks_missing_thumbnails_setting_description": "å°‡æ˛’æœ‰į¸Žåœ–įš„åĒ’éĢ”æĒ”æĄˆæŽ’å…ĨäŊ‡åˆ—äģĨį”ĸį”Ÿį¸Žåœ–", + "nightly_tasks_missing_thumbnails_setting_description": "將įŧēå°‘į¸Žåœ–įš„é …į›ŽæŽ’å…ĨäŊ‡åˆ—äģĨį”ĸį”Ÿį¸Žåœ–", "nightly_tasks_settings": "夜間äģģå‹™č¨­åŽš", "nightly_tasks_settings_description": "įŽĄį†å¤œé–“äģģ務", "nightly_tasks_start_time_setting": "開始時間", @@ -227,7 +247,7 @@ "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 \"。čĢ‹įĸēäŋäŊŋį”¨įš„æ˜¯æ‚¨æœ‰æŦŠé™å¯„送éƒĩäģļįš„åœ°å€ã€‚", @@ -235,7 +255,7 @@ "notification_email_ignore_certificate_errors": "åŋŊį•Ĩæ†‘č­‰éŒ¯čǤ", "notification_email_ignore_certificate_errors_description": "åŋŊį•Ĩ TLS æ†‘č­‰éŠ—č­‰éŒ¯čǤīŧˆä¸åģēč­°īŧ‰", "notification_email_password_description": "ᔍæ–ŧ與é›ģ子éƒĩäģļäŧ翜å™¨éŠ—č­‰įš„å¯†įĸŧ", - "notification_email_port_description": "é›ģ子éƒĩäģļäŧ翜å™¨åŸ åŖīŧˆäž‹åĻ‚ 25、465 或 587īŧ‰", + "notification_email_port_description": "é›ģ子éƒĩäģļäŧ翜å™¨įš„逪æŽĨ埠īŧˆäž‹åĻ‚ 25、465 或 587īŧ‰", "notification_email_secure": "SMTPS", "notification_email_secure_description": "äŊŋᔍSMTPSīŧˆåŸēæ–ŧTLSįš„SMTPīŧ‰", "notification_email_sent_test_email_button": "傺送æ¸ŦčŠĻé›ģ子éƒĩäģļä¸Ļå„˛å­˜", @@ -252,7 +272,7 @@ "oauth_auto_register": "č‡Ē動č¨ģ冊", "oauth_auto_register_description": "äŊŋᔍ OAuth į™ģå…Ĩ垌č‡Ē動č¨ģ冊新äŊŋᔍ者", "oauth_button_text": "按鈕文字", - "oauth_client_secret_description": "åĻ‚æžœ OAuth æäž›č€…ä¸æ”¯æ´ PKCEīŧˆæŽˆæŦŠįĸŧ驗證įĸŧä礿›æŠŸåˆļīŧ‰īŧŒå‰‡æ­¤į‚ēåŋ…åĄĢé …į›Ž", + "oauth_client_secret_description": "æŠŸå¯†į”¨æˆļįĢ¯įš„åŋ…åĄĢé …į›Žīŧ›č‹Ĩå…Ŧ開ᔍæˆļįĢ¯ä¸æ”¯æ´ PKCE (äģŖįĸŧä礿›įš„éЗ證金鑰)īŧŒäēĻ須åĄĢå¯Ģ。", "oauth_enable_description": "äŊŋᔍ OAuth į™ģå…Ĩ", "oauth_mobile_redirect_uri": "čĄŒå‹•įĢ¯é‡æ–°å°Žå‘ URI", "oauth_mobile_redirect_uri_override": "čĻ†č“‹čĄŒå‹•įĢ¯é‡æ–°å°Žå‘ URI", @@ -270,7 +290,7 @@ "oauth_storage_quota_default_description": "æœĒæäž›åŽŖå‘Šæ™‚æ‰€äŊŋį”¨įš„é…éĄīŧˆGiBīŧ‰ã€‚", "oauth_timeout": "čĢ‹æą‚é€žæ™‚", "oauth_timeout_description": "čĢ‹æą‚įš„é€žæ™‚æ™‚é–“īŧˆæ¯Ģį§’īŧ‰", - "ocr_job_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’äž†č­˜åˆĨåœ–į‰‡ä¸­įš„æ–‡å­—", + "ocr_job_description": "äŊŋį”¨æŠŸå™¨å­¸įŋ’čž¨č­˜åŊąåƒä¸­įš„æ–‡å­—", "password_enable_description": "äŊŋᔍé›ģ子éƒĩäģļ和密įĸŧį™ģå…Ĩ", "password_settings": "密įĸŧį™ģå…Ĩ", "password_settings_description": "įŽĄį†å¯†įĸŧį™ģå…Ĩč¨­åŽš", @@ -291,38 +311,38 @@ "search_jobs": "搜尋äģģ務â€Ļ", "send_welcome_email": "å‚ŗé€æ­ĄčŋŽé›ģ子éƒĩäģļ", "server_external_domain_settings": "外部įļ˛åŸŸ", - "server_external_domain_settings_description": "å…Ŧ開分äēĢ逪įĩįš„įļ˛åŸŸīŧŒåŒ…åĢ http(s)://", + "server_external_domain_settings_description": "å…Ŧ開分äēĢ逪įĩįš„įļ˛åŸŸ", "server_public_users": "å…Ŧ開äŊŋᔍ者", - "server_public_users_description": "在將äŊŋį”¨č€…æ–°åĸžåˆ°å…ąäēĢᛏį°ŋ時īŧŒæœƒåˆ—å‡ē所有äŊŋį”¨č€…įš„å§“åčˆ‡é›ģ子éƒĩäģļã€‚åœį”¨æ­¤åŠŸčƒŊ垌īŧŒäŊŋį”¨č€…æ¸…å–Žå°‡åƒ…äž›įŗģįĩąįŽĄį†å“ĄæĒĸčĻ–ã€‚", + "server_public_users_description": "將äŊŋį”¨č€…æ–°åĸžč‡ŗå…ąäēĢᛏį°ŋ時īŧŒæœƒåˆ—å‡ē所有äŊŋᔍ者īŧˆå§“åčˆ‡é›ģ子éƒĩäģļīŧ‰ã€‚č‹Ĩåœį”¨īŧŒäŊŋį”¨č€…æ¸…å–Žå°‡åƒ…äž›įŽĄį†å“ĄæŸĨįœ‹ã€‚", "server_settings": "äŧ翜å™¨č¨­åޚ", "server_settings_description": "įŽĄį†äŧ翜å™¨č¨­åޚ", - "server_stats_page_description": "įŽĄį†æœå‹™å™¨įĩąč¨ˆé éĸ", + "server_stats_page_description": "įŽĄį†äŧ翜å™¨įĩąč¨ˆé éĸ", "server_welcome_message": "æ­ĄčŋŽč¨Šæ¯", "server_welcome_message_description": "在į™ģå…Ĩ頁éĸéĄ¯į¤ēįš„č¨Šæ¯ã€‚", "settings_page_description": "įŽĄį†č¨­åŽšé éĸ", "sidecar_job": "側æŽĨæĒ”æĄˆä¸­įšŧčŗ‡æ–™", "sidecar_job_description": "åžžæĒ”æĄˆįŗģįĩąåĩæ¸Ŧ或同æ­Ĩ側æŽĨæĒ”æĄˆä¸­įšŧčŗ‡æ–™", "slideshow_duration_description": "每åŧĩåœ–į‰‡æ”žæ˜ įš„į§’æ•¸", - "smart_search_job_description": "åŸˇčĄŒæŠŸå™¨å­¸įŋ’有劊æ–ŧæ™ē慧搜尋", + "smart_search_job_description": "å°é …į›ŽåŸˇčĄŒæŠŸå™¨å­¸įŋ’äģĨ支援æ™ē慧搜尋", "storage_template_date_time_description": "æĒ”æĄˆįš„åģēįĢ‹æ™‚é–“æˆŗæœƒį”¨æ–ŧæ—ĨæœŸčˆ‡æ™‚é–“čŗ‡č¨Š", "storage_template_date_time_sample": "å–æ¨Ŗæ™‚é–“ {date}", "storage_template_enable_description": "å•Ÿį”¨å„˛å­˜į¯„æœŦåŧ•擎", "storage_template_hash_verification_enabled": "雜暊å‡ŊåŧéŠ—č­‰åˇ˛å•Ÿį”¨", "storage_template_hash_verification_enabled_description": "å•Ÿį”¨é›œæšŠå‡ŊåŧéŠ—č­‰īŧŒé™¤éžæ‚¨åžˆæ¸…æĨšåœ°įŸĨé“é€™å€‹é¸é …įš„äŊœį”¨īŧŒåĻ則čĢ‹å‹ŋåœį”¨æ­¤åŠŸčƒŊ", "storage_template_migration": "å„˛å­˜į¯„æœŦ遡į§ģ", - "storage_template_migration_description": "å°‡į›Žå‰įš„ {template} åĨ—į”¨åˆ°å…ˆå‰ä¸Šå‚ŗįš„é …į›Ž", - "storage_template_migration_info": "å„˛å­˜į¯„æœŦ會將所有副æĒ”名čŊ‰æ›į‚ē小å¯Ģã€‚į¯„æœŦčŽŠæ›´åĒ會åĨ—į”¨åˆ°æ–°įš„é …į›Žã€‚č‹Ĩčρ將ᝄæœŦčŋŊæē¯åĨ—į”¨åˆ°å…ˆå‰ä¸Šå‚ŗįš„é …į›ŽīŧŒčĢ‹åŸˇčĄŒ {job}。", - "storage_template_migration_job": "å„˛å­˜į¯„æœŦ遡į§ģäģģ務", + "storage_template_migration_description": "åĨ—į”¨į›Žå‰įš„ {template} č‡ŗå…ˆå‰ä¸Šå‚ŗįš„é …į›Ž", + "storage_template_migration_info": "å„˛å­˜į¯„æœŦ會將所有副æĒ”名čŊ‰æ›į‚ē小å¯Ģã€‚į¯„æœŦčŽŠæ›´åƒ…æœƒåĨ—į”¨č‡ŗæ–°é …į›Žã€‚č‹ĨčρčŋŊæē¯åĨ—ᔍᝄæœŦč‡ŗå…ˆå‰ä¸Šå‚ŗįš„é …į›ŽīŧŒčĢ‹åŸˇčĄŒ {job}。", + "storage_template_migration_job": "å„˛å­˜į¯„æœŦ遡į§ģäŊœæĨ­", "storage_template_more_details": "關æ–ŧ此功čƒŊįš„æ›´å¤ščŠŗį´°čŗ‡č¨ŠīŧŒčĢ‹åƒé–ąå„˛å­˜į¯„æœŦ及å…ļåŊąéŸŋ", - "storage_template_onboarding_description_v2": "å•Ÿį”¨åžŒīŧŒæ­¤åŠŸčƒŊ會䞝äŊŋᔍ者č‡Ēč¨‚įš„į¯„æœŦč‡Ēå‹•æ•´į†æĒ”æĄˆã€‚æ›´å¤ščŗ‡č¨ŠčĢ‹åƒé–ąčĒĒæ˜Žæ–‡äģļ。", + "storage_template_onboarding_description_v2": "å•Ÿį”¨åžŒīŧŒæ­¤åŠŸčƒŊ將䞝據äŊŋᔍ者č‡Ē荂ᝄæœŦč‡Ēå‹•æ•´į†æĒ”æĄˆã€‚æ›´å¤ščŗ‡č¨ŠčĢ‹åƒé–ąčĒĒæ˜Žæ–‡äģļ。", "storage_template_path_length": "預äŧ°čˇ¯åž‘镡åēĻ上限īŧš{length, number}/{limit, number}", "storage_template_settings": "å„˛å­˜į¯„æœŦ", - "storage_template_settings_description": "įŽĄį†ä¸Šå‚ŗæĒ”æĄˆįš„čŗ‡æ–™å¤žįĩæ§‹å’ŒæĒ”名", + "storage_template_settings_description": "įŽĄį†ä¸Šå‚ŗé …į›Žįš„čŗ‡æ–™å¤žįĩæ§‹čˆ‡æĒ”名", "storage_template_user_label": "{label} 是äŊŋį”¨č€…įš„å„˛å­˜æ¨™įą¤", "system_settings": "įŗģįĩąč¨­åޚ", "tag_cleanup_job": "æ¸…į†æ¨™įą¤", - "template_email_available_tags": "您可äģĨåœ¨æ‚¨įš„į¯„æœŦ中äŊŋᔍäģĨä¸‹čŽŠæ•¸īŧš{tags}", - "template_email_if_empty": "åĻ‚æžœį¯„æœŦį‚ēįŠēīŧŒå°‡äŊŋᔍ預荭é›ģ子éƒĩäģļᝄæœŦ。", + "template_email_available_tags": "您可äģĨåœ¨į¯„æœŦ中äŊŋį”¨ä¸‹åˆ—čŽŠæ•¸īŧš{tags}", + "template_email_if_empty": "č‹ĨᝄæœŦ內厚į‚ēįŠēīŧŒå‰‡æœƒäŊŋᔍ預荭éƒĩäģļᝄæœŦ。", "template_email_invite_album": "ᛏį°ŋ邀č̋ᝄæœŦ", "template_email_preview": "預čĻŊ", "template_email_settings": "é›ģ子éƒĩäģļᝄæœŦ", @@ -331,13 +351,13 @@ "template_settings": "通įŸĨᝄæœŦ", "template_settings_description": "įŽĄį†é€šįŸĨįš„č‡Ē荂ᝄæœŦ", "theme_custom_css_settings": "č‡Ē訂 CSS", - "theme_custom_css_settings_description": "可äģĨį”¨åą¤į–Šæ¨ŖåŧčĄ¨īŧˆCSSīŧ‰äž†č‡Ē訂 Immich įš„č¨­č¨ˆã€‚", + "theme_custom_css_settings_description": "é€éŽéšŽåą¤åŧæ¨ŖåŧčĄ¨ (CSS) åŗå¯č‡Ē訂 Immich įš„å¤–č§€č¨­č¨ˆã€‚", "theme_settings": "ä¸ģéĄŒč¨­åŽš", "theme_settings_description": "č‡Ē訂 Immich įš„įļ˛é äģ‹éĸ", "thumbnail_generation_job": "į”ĸį”Ÿį¸Žåœ–", - "thumbnail_generation_job_description": "į‚ē每個æĒ”æĄˆį”ĸį”Ÿå¤§ã€å°åŠæ¨ĄįŗŠį¸Žåœ–īŧŒäšŸį‚ē每äŊäēēį‰Šį”ĸį”Ÿį¸Žåœ–", + "thumbnail_generation_job_description": "į‚翝å€‹é …į›Žį”ĸį”Ÿå¤§ã€å°åŠæ¨ĄįŗŠį¸Žåœ–īŧŒäšŸį‚ē每äŊäēēį‰Šį”ĸį”Ÿį¸Žåœ–", "transcoding_acceleration_api": "加速 API", - "transcoding_acceleration_api_description": "æ­¤ API 會äŊŋį”¨æ‚¨įš„įĄŦéĢ”äģĨ加速čŊ‰įĸŧæĩį¨‹ã€‚æ­¤č¨­åŽšæŽĄã€Œį›ĄåŠ›č€Œį‚ēã€æ¨Ąåŧâ€”—č‹ĨčŊ‰įĸŧå¤ąæ•—īŧŒå°‡æœƒå›žé€€č‡ŗčģŸéĢ”čŊ‰įĸŧ。VP9 是åĻčƒŊ運äŊœīŧŒå–æąēæ–ŧæ‚¨įš„įĄŦéĢ”č¨­åŽšã€‚", + "transcoding_acceleration_api_description": "æ­¤ API å°‡čˆ‡æ‚¨įš„čŖįŊŽäē’å‹•äģĨ加速čŊ‰įĸŧã€‚æ­¤č¨­åŽšæŽĄã€Œį›ĄåŠ›č€Œį‚ēã€æ¨Ąåŧīŧšč‹Ĩå¤ąæ•—å°‡å›žé€€č‡ŗčģŸéĢ”čŊ‰įĸŧ。VP9 是åĻå¯į”¨å–æąēæ–ŧįĄŦéĢ”ã€‚", "transcoding_acceleration_nvenc": "NVENCīŧˆéœ€čρ NVIDIA GPUīŧ‰", "transcoding_acceleration_qsv": "Quick Syncīŧˆéœ€čρįŦŦ 7 äģŖæˆ–æ›´æ–°įš„ Intel 處ᐆ噍īŧ‰", "transcoding_acceleration_rkmpp": "RKMPPīŧˆåƒ…éŠį”¨æ–ŧ Rockchip SOCsīŧ‰", @@ -347,17 +367,17 @@ "transcoding_accepted_containers": "可æŽĨå—įš„å°čŖæ ŧåŧ", "transcoding_accepted_containers_description": "選擇å“Ēäē›å°čŖæ ŧåŧä¸éœ€čĻé‡æ–°å°čŖīŧˆremuxīŧ‰į‚ē MP4ã€‚æ­¤č¨­åŽšåƒ…éŠį”¨æ–ŧį‰šåŽšįš„čŊ‰įĸŧį­–į•Ĩ。", "transcoding_accepted_video_codecs": "æŽĨå—įš„åŊąį‰‡įˇ¨č§Ŗįĸŧ器", - "transcoding_accepted_video_codecs_description": "選擇å“Ēäē›čĻ–č¨Šįˇ¨č§Ŗįĸŧ器不需čρčŊ‰įĸŧã€‚æ­¤č¨­åŽšåƒ…éŠį”¨æ–ŧį‰šåŽšįš„čŊ‰įĸŧį­–į•Ĩ。", + "transcoding_accepted_video_codecs_description": "選擇å“Ēäē›åŊąį‰‡įˇ¨č§Ŗįĸŧ器不需čρčŊ‰įĸŧã€‚æ­¤č¨­åŽšåƒ…éŠį”¨æ–ŧį‰šåŽšįš„čŊ‰įĸŧį­–į•Ĩ。", "transcoding_advanced_options_description": "大多數äŊŋį”¨č€…ä¸éœ€æ›´å‹•įš„é¸é …", "transcoding_audio_codec": "韺荊ᎍ觪įĸŧ器", - "transcoding_audio_codec_description": "æ˜¯éŸŗčŗĒ最äŊŗįš„選項īŧŒäŊ†čˆ‡čˆŠčŖįŊŽæˆ–čˆŠį‰ˆčģŸéĢ”įš„į›¸åŽšæ€§čŧƒäŊŽã€‚", + "transcoding_audio_codec_description": "Opus æ˜¯éŸŗčŗĒ最äŊŗįš„選項īŧŒäŊ†čˆ‡čˆŠčŖįŊŽæˆ–čˆŠį‰ˆčģŸéĢ”įš„į›¸åŽšæ€§čŧƒäŊŽã€‚", "transcoding_bitrate_description": "äŊå…ƒįއé̘æ–ŧ最大å€ŧ或æ ŧåŧä¸åœ¨å¯æŽĨå—į¯„åœįš„åŊąį‰‡", - "transcoding_codecs_learn_more": "åĻ‚éœ€é€˛ä¸€æ­Ĩäē†č§Ŗæ­¤č™•äŊŋį”¨įš„čĄ“čĒžīŧŒčĢ‹åƒé–ą FFmpeg 文äģļ中關æ–ŧ H.264 ᎍ觪įĸŧ器、HEVC ᎍ觪įĸŧ器 及 VP9 ᎍ觪įĸŧ器 įš„čĒĒæ˜Žã€‚", + "transcoding_codecs_learn_more": "åĻ‚éœ€é€˛ä¸€æ­Ĩäē†č§Ŗæ­¤č™•äŊŋį”¨įš„čĄ“čĒžīŧŒčĢ‹åƒé–ą FFmpeg čĒĒæ˜Žæ–‡äģļ中關æ–ŧ H.264 ᎍ觪įĸŧ器、HEVC ᎍ觪įĸŧ器 及 VP9 ᎍ觪įĸŧ器 įš„čĒĒæ˜Žã€‚", "transcoding_constant_quality_mode": "恆厚品čŗĒæ¨Ąåŧ", "transcoding_constant_quality_mode_description": "ICQ įš„æ•ˆæžœå„Ēæ–ŧ CQPīŧŒäŊ†éƒ¨åˆ†įĄŦéĢ”åŠ é€ŸčŖįŊŽä¸æ”¯æ´æ­¤æ¨Ąåŧã€‚č¨­åŽšæ­¤é¸é …æ™‚īŧŒåœ¨äŊŋᔍäģĨ品čŗĒį‚ēåŸēæē–įš„įˇ¨įĸŧ時會å„Ēå…ˆæŽĄį”¨æ‰€æŒ‡åŽšįš„æ¨Ąåŧã€‚NVENC 不支援 ICQīŧŒå› æ­¤æ­¤č¨­åޚ圍 NVENC 下會čĸĢåŋŊį•Ĩ。", "transcoding_constant_rate_factor": "æ†åŽšé€ŸįŽ‡å› å­īŧˆ-crfīŧ‰", "transcoding_constant_rate_factor_description": "čĻ–č¨Šå“čŗĒį­‰į´šã€‚å…¸åž‹å€ŧį‚ē H.264 įš„ 23、HEVC įš„ 28、VP9 įš„ 31 和 AV1 įš„ 35。數å€ŧčļŠäŊŽīŧŒå“čŗĒčļŠåĨŊīŧŒäŊ†æœƒį”ĸį”Ÿčŧƒå¤§įš„æĒ”æĄˆã€‚", - "transcoding_disabled_description": "不對äģģäŊ•åŊąį‰‡é€˛čĄŒčŊ‰įĸŧīŧŒå¯čƒŊæœƒå°Žč‡´éƒ¨åˆ†į”¨æˆļįĢ¯į„Ąæŗ•æ­Ŗå¸¸æ’­æ”ž", + "transcoding_disabled_description": "不對äģģäŊ•åŊąį‰‡é€˛čĄŒčŊ‰įĸŧīŧŒé€™å¯čƒŊæœƒå°Žč‡´éƒ¨åˆ†į”¨æˆļįĢ¯į„Ąæŗ•æ­Ŗå¸¸æ’­æ”ž", "transcoding_encoding_options": "ᎍįĸŧ選項", "transcoding_encoding_options_description": "č¨­åŽšįˇ¨įĸŧåŊąį‰‡įš„ᎍ觪įĸŧå™¨ã€č§ŖæžåēĻ、品čŗĒ和å…ļäģ–選項", "transcoding_hardware_acceleration": "įĄŦéĢ”åŠ é€Ÿ", @@ -367,16 +387,16 @@ "transcoding_max_b_frames": "最大 B 嚀數", "transcoding_max_b_frames_description": "čŧƒéĢ˜įš„æ•¸å€ŧå¯æå‡åŖ“į¸Žæ•ˆįŽ‡īŧŒäŊ†æœƒé™äŊŽįˇ¨įĸŧ速åēĻ。在čŧƒčˆŠįš„čŖįŊŽä¸ŠīŧŒå¯čƒŊ與įĄŦéĢ”åŠ é€Ÿä¸į›¸åŽšã€‚0 äģŖčĄ¨åœį”¨ B 嚀īŧŒč€Œ -1 則會č‡Ēå‹•č¨­åŽšæ­¤æ•¸å€ŧ。", "transcoding_max_bitrate": "最大äŊå…ƒé€Ÿįއ", - "transcoding_max_bitrate_description": "č¨­åŽšæœ€å¤§äŊå…ƒįŽ‡å¯äģĨ在čŧ•åžŽįŠ§į‰˛å“čŗĒįš„æƒ…æŗä¸‹īŧŒčŽ“æĒ”æĄˆå¤§å°æ›´åŽšæ˜“é æ¸Ŧ。在 720p č§ŖæžåēĻ下īŧŒVP9 或 HEVC įš„å…¸åž‹å€ŧį‚ē 2600 kbit/sīŧŒH.264 則į‚ē 4500 kbit/sã€‚č¨­į‚ē 0 å‰‡åœį”¨æ­¤åŠŸčƒŊ。į•￞’有指厚įĩ„į𔿙‚īŧŒå‡č¨­kīŧˆäģŖčĄ¨kbit/sīŧ‰īŧ› 囙此īŧŒ5000、5000k和5MīŧˆMbit/sīŧ‰æ˜¯į­‰æ•ˆįš„。", + "transcoding_max_bitrate_description": "č¨­åŽšæœ€å¤§äŊå…ƒįއčƒŊ讓æĒ”æĄˆå¤§å°æ›´įŠŠåŽšīŧŒäŊ†æœƒį¨åžŽįЧ቞品čŗĒ。720p ä¸‹įš„å…¸åž‹å€ŧį‚ēīŧšVP9 或 HEVC į‚ē 2600 kbit/sīŧŒH.264 į‚ē 4500 kbit/sã€‚č¨­į‚ē 0 å‰‡åœį”¨ã€‚č‹ĨæœĒ指厚喎äŊīŧŒįŗģįĩąå°‡é č¨­į‚ē k (åŗ kbit/s)īŧ›å› æ­¤ 5000、5000k 與 5M (åŗ Mbit/s) æ˜¯į­‰æ•ˆįš„ã€‚", "transcoding_max_keyframe_interval": "最大關éĩ嚀間隔", - "transcoding_max_keyframe_interval_description": "č¨­åŽšé—œéĩåš€äš‹é–“įš„æœ€å¤§åš€čˇã€‚čŧƒäŊŽįš„å€ŧ會降äŊŽåŖ“į¸Žæ•ˆįŽ‡īŧŒäŊ†å¯äģĨ攚善搜尋時間īŧŒä¸Ļ有可čƒŊ會攚善åŋĢé€ŸčŽŠå‹•å ´æ™¯įš„å“čŗĒ。0 會č‡Ēå‹•č¨­åŽšæ­¤å€ŧ。", + "transcoding_max_keyframe_interval_description": "č¨­åŽšé—œéĩåš€äš‹é–“įš„æœ€å¤§åš€čˇã€‚čŧƒäŊŽįš„æ•¸å€ŧ會降äŊŽåŖ“į¸Žæ•ˆįŽ‡īŧŒäŊ†å¯æ”šå–„莺čŊ‰æœå°‹æ™‚é–“ä¸Ļ提升éĢ˜å‹•æ…‹å ´æ™¯å“čŗĒ。0 į‚ēč‡Ēå‹•č¨­åŽšã€‚", "transcoding_optimal_description": "é̘æ–ŧį›Žæ¨™č§ŖæžåēĻæˆ–æ ŧåŧä¸åœ¨å¯æŽĨå—į¯„åœįš„åŊąį‰‡", "transcoding_policy": "čŊ‰įĸŧį­–į•Ĩ", "transcoding_policy_description": "č¨­åŽšåŊąį‰‡é€˛čĄŒčŊ‰įĸŧįš„æĸäģļ", "transcoding_preferred_hardware_device": "éϖ遏įĄŦéĢ”čŖįŊŽ", "transcoding_preferred_hardware_device_description": "åƒ…éŠį”¨æ–ŧ VAAPI 和 QSVã€‚č¨­åŽšį”¨æ–ŧįĄŦéĢ”čŊ‰įĸŧįš„ dri ᝀéģžã€‚", "transcoding_preset_preset": "預設å€ŧīŧˆ-presetīŧ‰", - "transcoding_preset_preset_description": "åŖ“į¸Žé€ŸåēĻ。čŧƒæ…ĸįš„é č¨­å€ŧ會į”ĸį”Ÿčŧƒå°įš„æĒ”æĄˆīŧŒä¸Ļ在鎖厚äŊå…ƒįŽ‡æ™‚æå‡å“čŗĒ。VP9 在速åēĻé̘æ–ŧ「faster」時將åŋŊį•Ĩč¨­åŽšã€‚", + "transcoding_preset_preset_description": "åŖ“į¸Žé€ŸåēĻ。čŧƒæ…ĸįš„é č¨­å€ŧ可į”ĸį”ŸéĢ”įŠčŧƒå°įš„æĒ”æĄˆīŧŒä¸Ļ在指厚äŊå…ƒįŽ‡æ™‚æå‡å“čŗĒ。VP9 會åŋŊį•Ĩé̘æ–ŧ「fasterã€įš„č¨­åŽšã€‚", "transcoding_reference_frames": "åƒč€ƒåš€", "transcoding_reference_frames_description": "åœ¨åŖ“į¸Žį‰šåŽšåš€æ™‚æ‰€åƒč€ƒįš„åš€æ•¸é‡ã€‚æ•¸å€ŧčļŠéĢ˜å¯æå‡åŖ“į¸Žæ•ˆįŽ‡īŧŒäŊ†æœƒé™äŊŽįˇ¨įĸŧ速åēĻã€‚č¨­į‚ē 0 則č‡Ē動æąē厚此數å€ŧ。", "transcoding_required_description": "僅限æ ŧåŧä¸čĸĢæŽĨå—įš„åŊąį‰‡", @@ -387,29 +407,29 @@ "transcoding_temporal_aq": "時間č‡Ē遊應量化īŧˆTemporal AQīŧ‰", "transcoding_temporal_aq_description": "åƒ…éŠį”¨æ–ŧ NVENCīŧŒæ™‚域č‡Ē我čĒŋ整量化可提升éĢ˜į´°į¯€ã€äŊŽå‹•æ…‹å ´æ™¯įš„į•ĢčŗĒ。可čƒŊ與čŧƒčˆŠįš„čŖįŊŽä¸į›¸åŽšã€‚", "transcoding_threads": "åŸˇčĄŒįˇ’æ•¸é‡", - "transcoding_threads_description": "čŧƒéĢ˜įš„å€ŧ會加åŋĢᎍįĸŧ速åēĻīŧŒäŊ†æœƒæ¸›å°‘äŧ翜å™¨åœ¨åŸˇčĄŒéŽį¨‹ä¸­č™•ᐆå…ļäģ–äģģå‹™įš„įŠē間。此å€ŧ不應čļ…過 CPU æ ¸åŋƒæ•¸ã€‚設åޚį‚ē 0 可äģĨæœ€å¤§åŒ–åˆŠį”¨įŽ‡ã€‚", + "transcoding_threads_description": "čŧƒéĢ˜įš„æ•¸å€ŧ會加åŋĢᎍįĸŧ速åēĻīŧŒäŊ†åŸˇčĄŒæ™‚會äŊ”į”¨æ›´å¤šäŧ翜å™¨č™•ᐆå…ļäģ–äģģå‹™įš„æ•ˆčƒŊ。此數å€ŧ不應čļ…過 CPU æ ¸åŋƒæ•¸ã€‚設į‚ē 0 å¯æœ€å¤§åŒ–åˆŠį”¨įŽ‡ã€‚", "transcoding_tone_mapping": "色čĒŋ對映", "transcoding_tone_mapping_description": "在將 HDR åŊąį‰‡čŊ‰æ›į‚ē SDR 時īŧŒį›Ąé‡įļ­æŒåŽŸå§‹č§€æ„Ÿã€‚æ¯į¨Žæŧ”įŽ—æŗ•åœ¨č‰˛åŊŠã€į´°į¯€å’ŒäēŽåēĻæ–šéĸéƒŊæœ‰ä¸åŒįš„æŦŠčĄĄã€‚Hable äŋį•™į´°į¯€īŧŒMobius äŋį•™č‰˛åŊŠīŧŒReinhard äŋį•™äēŽåēĻ。", "transcoding_transcode_policy": "čŊ‰įĸŧį­–į•Ĩ", - "transcoding_transcode_policy_description": "åŊąį‰‡äŊ•æ™‚æ‡‰é€˛čĄŒčŊ‰įĸŧįš„į­–į•Ĩ。HDR åŊąį‰‡ä¸€åŽšæœƒčŊ‰įĸŧīŧˆé™¤éžåœį”¨čŊ‰įĸŧīŧ‰ã€‚", + "transcoding_transcode_policy_description": "åŊąį‰‡čŊ‰įĸŧį­–į•Ĩ。HDR åŊąį‰‡ä¸€åž‹æœƒé€˛čĄŒčŊ‰įĸŧīŧˆé™¤éžåœį”¨čŊ‰įĸŧ功čƒŊīŧ‰ã€‚", "transcoding_two_pass_encoding": "兊階æŽĩᎍįĸŧ", - "transcoding_two_pass_encoding_setting_description": "äŊŋį”¨å…ŠéšŽæŽĩᎍįĸŧ來į”ĸį”Ÿå“čŗĒ更äŊŗįš„ᎍįĸŧåŊąį‰‡ã€‚į•ļå•Ÿį”¨æœ€å¤§äŊå…ƒé€ŸįŽ‡æ™‚īŧˆH.264 和 HEVC åŋ…é ˆå•Ÿį”¨æ­¤é¸é …æ‰čƒŊ運äŊœīŧ‰īŧŒæ­¤æ¨ĄåŧæœƒäģĨ最大äŊå…ƒé€ŸįŽ‡äž†čĒŋ整äŊå…ƒé€ŸįŽ‡į¯„åœīŧŒä¸ĻåŋŊį•Ĩ CRF。對æ–ŧ VP9īŧŒåĻ‚æžœåœį”¨æœ€å¤§äŊå…ƒé€ŸįއīŧŒå¯äģĨäŊŋᔍ CRF。", + "transcoding_two_pass_encoding_setting_description": "åŸˇčĄŒå…ŠæŦĄįˇ¨įĸŧäģĨį”ĸį”Ÿå“čŗĒ更äŊŗįš„åŊąį‰‡ã€‚å•Ÿį”¨æœ€å¤§äŊå…ƒé€ŸįŽ‡æ™‚īŧˆH.264 與 HEVC åŋ…é ˆå•Ÿį”¨īŧ‰īŧŒæ­¤æ¨Ąåŧæœƒäžæœ€å¤§äŊå…ƒé€ŸįއčĒŋæ•´į¯„åœä¸ĻåŋŊį•Ĩ CRF。č‹Ĩį‚ē VP9īŧŒå‰‡å¯åœ¨åœį”¨æœ€å¤§äŊå…ƒé€ŸįŽ‡æ™‚äŊŋᔍ CRF。", "transcoding_video_codec": "åŊąį‰‡įˇ¨č§Ŗįĸŧ器", "transcoding_video_codec_description": "VP9 å…ˇæœ‰éĢ˜åŖ“į¸Žæ•ˆįŽ‡čˆ‡č‰¯åĨŊįš„įļ˛é į›¸åŽšæ€§īŧŒäŊ†čŊ‰įĸŧ速åēĻčŧƒæ…ĸ。HEVC įš„æ•ˆčƒŊ類äŧŧīŧŒäŊ†įļ˛é į›¸åŽšæ€§čŧƒåˇŽã€‚H.264 兎備åģŖæŗ›įš„į›¸åŽšæ€§ä¸”čŊ‰įĸŧ速åēĻåŋĢīŧŒäŊ†į”ĸį”Ÿįš„æĒ”æĄˆéĢ”įŠčŧƒå¤§ã€‚AV1 æ˜¯æ•ˆįŽ‡æœ€éĢ˜įš„įˇ¨č§Ŗįĸŧ器īŧŒäŊ†åœ¨čˆŠčŖįŊŽä¸Šįŧē䚏支援。", "trash_enabled_description": "å•Ÿį”¨åžƒåœžæĄļ功čƒŊ", "trash_number_of_days": "夊數", - "trash_number_of_days_description": "åĒ’éĢ”åœ¨åžƒåœžæĄļ中äŋį•™įš„夊數īŧŒé€žæœŸåžŒå°‡æ°¸äš…åˆĒ除", + "trash_number_of_days_description": "é …į›Žåœ¨åžƒåœžæĄļ中äŋį•™įš„夊數īŧŒé€žæœŸåžŒå°‡æ°¸äš…åˆĒ除", "trash_settings": "垃圞æĄļč¨­åŽš", "trash_settings_description": "įŽĄį†åžƒåœžæĄļč¨­åŽš", "unlink_all_oauth_accounts": "č§Ŗé™¤æ‰€æœ‰ OAuth å¸ŗč™Ÿįš„é€Ŗįĩ", "unlink_all_oauth_accounts_description": "圍過į§ģč‡ŗæ–°įš„æœå‹™æäž›č€…å‰īŧŒčĢ‹ä¸čρåŋ˜č¨˜čĻå…ˆč§Ŗé™¤æ‰€æœ‰čˆ‡ OAuth 叺æˆļįš„é€Ŗįĩã€‚", - "unlink_all_oauth_accounts_prompt": "您是åĻįĸēčĒčĻč§Ŗé™¤æ‰€æœ‰čˆ‡ OAuth 叺æˆļįš„é€ŖįĩīŧŸæ‰€æœ‰į›¸é—œįš„äŊŋᔍ者čēĢäģŊ會čĸĢ重設īŧŒä¸Ļ且不čƒŊčĸĢ還原。", + "unlink_all_oauth_accounts_prompt": "您įĸē厚čĻč§Ŗé™¤æ‰€æœ‰čˆ‡ OAuth å¸ŗč™Ÿįš„é€Ŗįĩå—ŽīŧŸé€™æœƒé‡č¨­æ¯äŊäŊŋį”¨č€…įš„ OAuth ID ä¸”į„Ąæŗ•åžŠåŽŸã€‚", "user_cleanup_job": "æ¸…į†äŊŋᔍ者", "user_delete_delay": "{user} įš„å¸ŗč™Ÿå’Œé …į›Žæœƒåœ¨ {delay, plural, one {# 夊} other {# 夊}} 垌永䚅åˆĒ除。", "user_delete_delay_settings": "åģļ垌åˆĒ除", - "user_delete_delay_settings_description": "č‡Ēį§ģ除垌čĩˇįŽ—įš„å¤Šæ•¸īŧŒé€žæœŸåžŒå°‡æ°¸äš…åˆĒ除äŊŋį”¨č€…å¸ŗč™Ÿčˆ‡åĒ’éĢ”ã€‚äŊŋᔍ者åˆĒ除äŊœæĨ­æœƒåœ¨æ¯æ—Ĩåˆå¤œåŸˇčĄŒīŧŒäģĨæĒĸæŸĨįŦĻ合åˆĒ除æĸäģļįš„å¸ŗč™Ÿã€‚æ­¤č¨­åŽšįš„čŽŠæ›´æœƒåœ¨ä¸‹ä¸€æŦĄåŸˇčĄŒæ™‚į”Ÿæ•ˆã€‚", - "user_delete_immediately": "{user} įš„å¸ŗč™Ÿčˆ‡åĒ’é̔將įĢ‹åŗæŽ’å…Ĩ永䚅åˆĒé™¤įš„äŊ‡åˆ—。", - "user_delete_immediately_checkbox": "įĢ‹åŗå°‡äŊŋį”¨č€…čˆ‡čŗ‡į”ĸ排å…Ĩ永䚅åˆĒ除äŊ‡åˆ—", + "user_delete_delay_settings_description": "č‡Ēį§ģ除垌čĩˇįŽ—įš„å¤Šæ•¸īŧŒé€žæœŸåžŒå°‡æ°¸äš…åˆĒ除äŊŋį”¨č€…å¸ŗč™Ÿčˆ‡é …į›Žã€‚äŊŋᔍ者åˆĒ除äŊœæĨ­æœƒåœ¨æ¯æ—Ĩåˆå¤œåŸˇčĄŒīŧŒäģĨæĒĸæŸĨįŦĻ合åˆĒ除æĸäģļįš„å¸ŗč™Ÿã€‚æ­¤č¨­åŽšįš„čŽŠæ›´å°‡åœ¨ä¸‹ä¸€æŦĄåŸˇčĄŒæ™‚į”Ÿæ•ˆã€‚", + "user_delete_immediately": "{user} įš„å¸ŗč™Ÿčˆ‡é …į›Žå°‡įĢ‹åŗæŽ’å…Ĩ永䚅åˆĒ除äŊ‡åˆ—。", + "user_delete_immediately_checkbox": "įĢ‹åŗå°‡äŊŋį”¨č€…čˆ‡é …į›ŽæŽ’å…Ĩ永䚅åˆĒ除äŊ‡åˆ—", "user_details": "äŊŋį”¨č€…čŠŗį´°čŗ‡č¨Š", "user_management": "äŊŋį”¨č€…įŽĄį†", "user_password_has_been_reset": "äŊŋᔍ者坆įĸŧåˇ˛é‡č¨­īŧš", @@ -418,10 +438,10 @@ "user_restore_scheduled_removal": "還原äŊŋᔍ者 - 預厚æ–ŧ {date, date, long} į§ģ除", "user_settings": "äŊŋį”¨č€…č¨­åŽš", "user_settings_description": "įŽĄį†äŊŋį”¨č€…č¨­åŽš", - "user_successfully_removed": "ᔍæˆļ{email}åˇ˛æˆåŠŸåˆ é™¤ã€‚", - "users_page_description": "įŽĄį†į”¨æˆļ頁éĸ", + "user_successfully_removed": "åˇ˛æˆåŠŸåˆĒ除äŊŋᔍ者 {email}。", + "users_page_description": "įŽĄį†äŊŋᔍ者頁éĸ", "version_check_enabled_description": "å•Ÿį”¨į‰ˆæœŦæĒĸæŸĨ", - "version_check_implications": "į‰ˆæœŦæĒĸæŸĨ功čƒŊæœƒåŽšæœŸčˆ‡ github.com 通訊", + "version_check_implications": "į‰ˆæœŦæĒĸæŸĨ功čƒŊäģ°čŗ´čˆ‡ github.com įš„åŽšæœŸé€šč¨Š", "version_check_settings": "į‰ˆæœŦæĒĸæŸĨ", "version_check_settings_description": "å•Ÿį”¨ / åœį”¨æ–°į‰ˆæœŦ通įŸĨ", "video_conversion_job": "åŊąį‰‡čŊ‰įĸŧ", @@ -429,20 +449,23 @@ }, "admin_email": "įŽĄį†å“Ąé›ģ子éƒĩäģļ", "admin_password": "įŽĄį†å“Ąå¯†įĸŧ", - "administration": "įŽĄį†", + "administration": "įŗģįĩąįŽĄį†", "advanced": "進階", + "advanced_settings_clear_image_cache": "æ¸…é™¤åœ–į‰‡åŋĢ取", + "advanced_settings_clear_image_cache_error": "æ¸…é™¤åœ–į‰‡åŋĢå–å¤ąæ•—", + "advanced_settings_clear_image_cache_success": "成功清除{size}", "advanced_settings_enable_alternate_media_filter_subtitle": "äŊŋį”¨æ­¤é¸é …å¯åœ¨åŒæ­Ĩ時䞝å…ļäģ–æĸäģļį¯Šé¸åĒ’éĢ”ã€‚åƒ…åœ¨æ‡‰į”¨į¨‹åŧį„Ąæŗ•åĩæ¸Ŧåˆ°æ‰€æœ‰į›¸į°ŋ時再嘗čŠĻäŊŋį”¨ã€‚", "advanced_settings_enable_alternate_media_filter_title": "[å¯Ļ銗性] äŊŋᔍæ›ŋäģŖįš„čŖįŊŽį›¸į°ŋ同æ­Ĩį¯Šé¸å™¨", - "advanced_settings_log_level_title": "æ—ĨčĒŒį­‰į´šīŧš{level}", - "advanced_settings_prefer_remote_subtitle": "éƒ¨åˆ†čŖįŊŽåžžæœŦ抟åĒ’éĢ”åēĢčŧ‰å…Ĩį¸Žåœ–įš„é€ŸåēĻ非常æ…ĸã€‚å•Ÿį”¨æ­¤č¨­åŽšå¯æ”šį‚ēčŧ‰å…Ĩ遠įĢ¯åœ–į‰‡ã€‚", + "advanced_settings_log_level_title": "į´€éŒ„į­‰į´šīŧš{level}", + "advanced_settings_prefer_remote_subtitle": "éƒ¨åˆ†čŖįŊŽåžžæœŦæŠŸé …į›Žčŧ‰å…Ĩį¸Žåœ–įš„é€ŸåēĻ非常æ…ĸã€‚å•Ÿį”¨æ­¤č¨­åŽšå¯æ”šį‚ēčŧ‰å…Ĩ遠įĢ¯åœ–į‰‡ã€‚", "advanced_settings_prefer_remote_title": "偏åĨŊ遠į̝åŊąåƒ", "advanced_settings_proxy_headers_subtitle": "åŽšįžŠ Immich 在每æŦĄįļ˛čˇ¯čĢ‹æą‚æ™‚æ‡‰čŠ˛å‚ŗé€įš„äģŖį†æ¨™é ­", "advanced_settings_proxy_headers_title": "č‡ĒåŽšįžŠäģŖį†æ¨™é ­[å¯Ļ銗性]", - "advanced_settings_readonly_mode_subtitle": "é–‹å•Ÿå”¯čŽ€æ¨ĄåŧåžŒīŧŒį…§į‰‡åĒčƒŊį€čĻŊīŧŒåƒæ˜¯å¤šé¸åŊąåƒã€åˆ†äēĢ、投攞、åˆĒé™¤į­‰åŠŸčƒŊéƒŊ會關閉。可在ä¸ģį•Ģéĸ透過äŊŋį”¨č€…é ­åƒäž†é–‹å•Ÿ/é—œé–‰å”¯čŽ€æ¨Ąåŧ", + "advanced_settings_readonly_mode_subtitle": "å•Ÿį”¨å”¯čŽ€æ¨ĄåŧåžŒåƒ…čƒŊį€čĻŊᛏቇīŧŒå°‡åœį”¨å¤šé¸ã€åˆ†äēĢ、投攞及åˆĒé™¤į­‰åŠŸčƒŊ。可透過ä¸ģį•Ģéĸä¸Šįš„äŊŋᔍ者個äēē圖į¤ēå•Ÿį”¨æˆ–åœį”¨å”¯čŽ€æ¨Ąåŧ", "advanced_settings_readonly_mode_title": "å”¯čŽ€æ¨Ąåŧ", "advanced_settings_self_signed_ssl_subtitle": "į•Ĩ過äŧ翜å™¨į̝éģžįš„ SSL æ†‘č­‰éŠ—č­‰ã€‚č‡Ēį°Ŋæ†‘č­‰æ™‚åŋ…é ˆå•Ÿį”¨æ­¤č¨­åŽšã€‚", "advanced_settings_self_signed_ssl_title": "å…č¨ąč‡Ēį°Ŋįš„ SSL æ†‘č­‰[å¯Ļ銗性]", - "advanced_settings_sync_remote_deletions_subtitle": "į•ļ在įļ˛é įĢ¯åŸˇčĄŒåˆĒ除或還原操äŊœæ™‚īŧŒč‡Ēå‹•åœ¨æ­¤čŖįŊŽä¸ŠåˆĒé™¤æˆ–é‚„åŽŸčŠ˛åĒ’éĢ”", + "advanced_settings_sync_remote_deletions_subtitle": "į•ļ在įļ˛é įĢ¯åŸˇčĄŒåˆĒ除或還原操äŊœæ™‚īŧŒč‡Ēå‹•åœ¨æ­¤čŖįŊŽä¸ŠåˆĒé™¤æˆ–é‚„åŽŸčŠ˛é …į›Ž", "advanced_settings_sync_remote_deletions_title": "同æ­Ĩ遠į̝åˆĒ除 [å¯Ļ銗性]", "advanced_settings_tile_subtitle": "進階äŊŋį”¨č€…č¨­åŽš", "advanced_settings_troubleshooting_subtitle": "å•Ÿį”¨éĄå¤–åŠŸčƒŊäģĨé€˛čĄŒį–‘é›ŖæŽ’č§Ŗ", @@ -451,14 +474,14 @@ "age_year_months": "1 æ­˛īŧŒ{months, plural, one {# 個月} other {# 個月}}", "age_years": "{years, plural, other {# æ­˛}}", "album": "ᛏį°ŋ", - "album_added": "čĸĢ加å…Ĩåˆ°į›¸į°ŋ", - "album_added_notification_setting_description": "į•ļ我čĸĢ加å…Ĩå…ąäēĢᛏį°ŋ時īŧŒį”¨é›ģ子éƒĩäģļ通įŸĨ我", + "album_added": "厞加å…Ĩᛏį°ŋ", + "album_added_notification_setting_description": "į•ļ我čĸĢ加å…Ĩå…ąäēĢᛏį°ŋ時īŧŒé€éŽé›ģ子éƒĩäģļ通įŸĨ我", "album_cover_updated": "åˇ˛æ›´æ–°į›¸į°ŋ封éĸ", "album_delete_confirmation": "äŊ įĸē厚čρåˆĒ除ᛏį°ŋ {album} 嗎īŧŸ", - "album_delete_confirmation_description": "åĻ‚æžœæ­¤į›¸į°ŋ厞čĸĢ分äēĢīŧŒå…ļäģ–äŊŋį”¨č€…å°‡į„Ąæŗ•å†å­˜å–ã€‚", + "album_delete_confirmation_description": "åĻ‚æžœæ­¤į›¸į°ŋ厞čĸĢå…ąäēĢīŧŒå…ļäģ–äŊŋį”¨č€…å°‡į„Ąæŗ•å†å­˜å–ã€‚", "album_deleted": "ᛏį°ŋ厞åˆĒ除", "album_info_card_backup_album_excluded": "åˇ˛æŽ’é™¤", - "album_info_card_backup_album_included": "厞遏䏭", + "album_info_card_backup_album_included": "åˇ˛åŒ…åĢ", "album_info_updated": "åˇ˛æ›´æ–°į›¸į°ŋčŗ‡č¨Š", "album_leave": "é›ĸ開ᛏį°ŋīŧŸ", "album_leave_confirmation": "您įĸē厚čρé›ĸ開 {album} 嗎īŧŸ", @@ -467,10 +490,12 @@ "album_remove_user": "į§ģ除äŊŋᔍ者īŧŸ", "album_remove_user_confirmation": "įĸē厚čρį§ģ除 {user} 嗎īŧŸ", "album_search_not_found": "扞不到įŦĻ合搜尋æĸäģļįš„į›¸į°ŋ", + "album_selected": "åˇ˛é¸å–į›¸į°ŋ", "album_share_no_users": "įœ‹äž†æ‚¨čˆ‡æ‰€æœ‰äŊŋį”¨č€…å…ąäēĢäē†é€™æœŦᛏį°ŋīŧŒæˆ–æ˛’æœ‰å…ļäģ–äŊŋį”¨č€…å¯äž›åˆ†äēĢ。", "album_summary": "ᛏį°ŋ摘čρ", "album_updated": "æ›´æ–°į›¸į°ŋ時", "album_updated_setting_description": "į•ļå…ąäēĢᛏį°ŋæœ‰æ–°é …į›Žæ™‚į”¨é›ģ子éƒĩäģļ通įŸĨ我", + "album_upload_assets": "åžžæ‚¨įš„é›ģč…Ļä¸Šå‚ŗæĒ”æĄˆä¸Ļ加å…Ĩᛏį°ŋ", "album_user_left": "é›ĸ開 {album}", "album_user_removed": "į§ģ除 {user}", "album_viewer_appbar_delete_confirm": "您įĸē厚čĻåžžå¸ŗč™Ÿä¸­åˆĒé™¤æ­¤į›¸į°ŋ嗎īŧŸ", @@ -481,16 +506,18 @@ "album_viewer_appbar_share_leave": "é›ĸ開ᛏį°ŋ", "album_viewer_appbar_share_to": "分äēĢįĩĻ", "album_viewer_page_share_add_users": "邀čĢ‹å…ļäģ–äēē", - "album_with_link_access": "äģģäŊ•æ“æœ‰é€Ŗįĩįš„äēēéƒŊčƒŊæĒĸčĻ–æ­¤į›¸į°ŋä¸­įš„į…§į‰‡čˆ‡äēēį‰Šã€‚", + "album_with_link_access": "äģģäŊ•æ“æœ‰é€Ŗįĩįš„äēēįš†å¯æĒĸčĻ–æ­¤į›¸į°ŋä¸­įš„į›¸į‰‡čˆ‡äēēį‰Šã€‚", "albums": "ᛏį°ŋ", "albums_count": "{count, plural, one {{count, number} 個ᛏį°ŋ} other {{count, number} 個ᛏį°ŋ}}", "albums_default_sort_order": "預荭ᛏį°ŋ排åē", "albums_default_sort_order_description": "åģēįĢ‹æ–°į›¸į°ŋ時čĻåˆå§‹åŒ–é …į›ŽæŽ’åēæ–šåŧã€‚", - "albums_feature_description": "一įŗģ列可äģĨ分äēĢįĩĻå…ļäģ–äŊŋį”¨č€…įš„é …į›Žã€‚", + "albums_feature_description": "å¯å…ąäēĢįĩĻå…ļäģ–äŊŋį”¨č€…įš„é …į›Žé›†åˆã€‚", "albums_on_device_count": "æ­¤čŖįŊŽæœ‰ ({count}) 個ᛏį°ŋ", + "albums_selected": "{count, plural, one {åˇ˛é¸å– # æœŦᛏį°ŋ} other {åˇ˛é¸å– # æœŦᛏį°ŋ}}", "all": "全部", "all_albums": "æ‰€æœ‰į›¸į°ŋ", "all_people": "所有äēēį‰Š", + "all_photos": "æ‰€æœ‰į›¸į‰‡", "all_videos": "所有åŊąį‰‡", "allow_dark_mode": "å…č¨ąæˇąč‰˛æ¨Ąåŧ", "allow_edits": "å…č¨ąįˇ¨čŧ¯", @@ -498,25 +525,28 @@ "allow_public_user_to_upload": "å…č¨ąå…Ŧ開äŊŋį”¨č€…ä¸Šå‚ŗ", "allowed": "å…č¨ą", "alt_text_qr_code": "QR code åœ–į‰‡", + "always_keep": "一型äŋį•™", + "always_keep_photos_hint": "「釋攞įŠē間」功čƒŊæœƒå°‡æ‰€æœ‰į›¸į‰‡äŋį•™åœ¨æ­¤čŖįŊŽä¸Šã€‚", + "always_keep_videos_hint": "「釋攞įŠē間」功čƒŊ會將所有åŊąį‰‡äŋį•™åœ¨æ­¤čŖįŊŽä¸Šã€‚", "anti_clockwise": "逆時針", "api_key": "API 金鑰", - "api_key_description": "æ­¤é‡‘é‘°åƒ…éĄ¯į¤ē一æŦĄã€‚čĢ‹åœ¨é—œé–‰å‰č¤‡čŖŊ厃。", - "api_key_empty": "æ‚¨įš„ API é‡‘é‘°åį¨ąä¸čƒŊį‚ēįŠēå€ŧ", + "api_key_description": "æ­¤é‡‘é‘°åƒ…æœƒéĄ¯į¤ē一æŦĄã€‚關閉čĻ–įĒ—å‰č̋務åŋ…å…ˆč¤‡čŖŊ金鑰。", + "api_key_empty": "API é‡‘é‘°åį¨ąä¸åž—į‚ēįŠēį™Ŋ", "api_keys": "API 金鑰", - "app_architecture_variant": "變éĢ”īŧˆæžļ構īŧ‰", + "app_architecture_variant": "čŽŠåŒ–į‰ˆæœŦīŧˆæžļ構īŧ‰", "app_bar_signout_dialog_content": "您įĸē厚čρį™ģå‡ē嗎īŧŸ", "app_bar_signout_dialog_ok": "是", "app_bar_signout_dialog_title": "į™ģå‡ē", - "app_download_links": "æ‡‰į”¨ä¸‹čŧ‰é€Ŗįĩ", + "app_download_links": "App 下čŧ‰é€Ŗįĩ", "app_settings": "æ‡‰į”¨į¨‹åŧč¨­åޚ", - "app_stores": "æ‡‰į”¨å•†åē—", - "app_update_available": "æ‡‰į”¨į¨‹åēæ›´æ–°å¯į”¨", + "app_stores": "æ‡‰į”¨į¨‹åŧå•†åē—", + "app_update_available": "åˇ˛æœ‰æ‡‰į”¨į¨‹åŧæ›´æ–°", "appears_in": "å‡ēįžæ–ŧ", - "apply_count": "æ‡‰į”¨ ({count, number})", + "apply_count": "åĨ—ᔍ ({count, number})", "archive": "封存", - "archive_action_prompt": "厞將 ({count}) 個加å…Ĩé€˛å°å­˜", - "archive_or_unarchive_photo": "封存或取æļˆå°å­˜į…§į‰‡", - "archive_page_no_archived_assets": "æœĒ扞到封存åĒ’éĢ”", + "archive_action_prompt": "厞將 {count} å€‹é …į›ŽåŠ å…Ĩ封存", + "archive_or_unarchive_photo": "封存或取æļˆå°å­˜į›¸į‰‡", + "archive_page_no_archived_assets": "æ‰žä¸åˆ°å°å­˜é …į›Ž", "archive_page_title": "封存 ({count})", "archive_size": "封存大小", "archive_size_description": "č¨­åŽščρ䏋čŧ‰įš„封存æĒ”æĄˆå¤§å° (å–ŽäŊīŧšGiB)", @@ -524,55 +554,60 @@ "archived_count": "{count, plural, other {厞封存 # å€‹é …į›Ž}}", "are_these_the_same_person": "同一äŊäēēį‰ŠīŧŸ", "are_you_sure_to_do_this": "您įĸē厚嗎īŧŸ", - "asset_action_delete_err_read_only": "į•ĨéŽį„Ąæŗ•åˆĒé™¤å”¯čŽ€é …į›Ž", - "asset_action_share_err_offline": "į•ĨéŽį„Ąæŗ•å–åž—įš„é›ĸįˇšé …į›Ž", - "asset_added_to_album": "厞åģēį̋ᛏį°ŋ", + "array_field_not_fully_supported": "é™Ŗåˆ—æŦ„äŊéœ€čĻæ‰‹å‹•įˇ¨čŧ¯ JSON", + "asset_action_delete_err_read_only": "å”¯čŽ€é …į›Žį„Ąæŗ•åˆĒ除īŧŒåˇ˛į•Ĩ過", + "asset_action_share_err_offline": "į„Ąæŗ•å–åž—é›ĸįˇšé …į›ŽīŧŒåˇ˛į•Ĩ過", + "asset_added_to_album": "åˇ˛æ–°åĸžč‡ŗį›¸į°ŋ", "asset_adding_to_album": "新åĸžåˆ°į›¸į°ŋâ€Ļ", - "asset_description_updated": "åĒ’éĢ”æčŋ°åˇ˛æ›´æ–°", - "asset_filename_is_offline": "åĒ’éĢ” {filename} 厞é›ĸ᎚", - "asset_has_unassigned_faces": "åĒ’éĢ”æœ‰æœĒåˆ†é…įš„č‡‰å­”", + "asset_created": "é …į›Žåˇ˛åģēįĢ‹", + "asset_description_updated": "é …į›ŽčĒĒæ˜Žåˇ˛æ›´æ–°", + "asset_filename_is_offline": "é …į›Ž {filename} 厞é›ĸ᎚", + "asset_has_unassigned_faces": "é …į›Žæœ‰æœĒæŒ‡æ´žįš„č‡‰å­”", "asset_hashing": "æ­Ŗåœ¨č¨ˆįŽ—é›œæšŠâ€Ļ", "asset_list_group_by_sub_title": "åˆ†éĄžæ–šåŧ", "asset_list_layout_settings_dynamic_layout_title": "å‹•æ…‹į‰ˆéĸ", "asset_list_layout_settings_group_automatically": "č‡Ē動", - "asset_list_layout_settings_group_by": "åĒ’éĢ”åˆ†éĄžæ–šåŧ", + "asset_list_layout_settings_group_by": "é …į›Žåˆ†éĄžæ–šåŧ", "asset_list_layout_settings_group_by_month_day": "月äģŊ和æ—Ĩ期", "asset_list_layout_sub_title": "į‰ˆéĸ", "asset_list_settings_subtitle": "ᛏቇæ ŧį‹€į‰ˆéĸč¨­åŽš", "asset_list_settings_title": "ᛏቇæ ŧį‹€æĒĸčĻ–", - "asset_offline": "åĒ’éĢ”é›ĸ᎚", - "asset_offline_description": "此外部åĒ’éĢ”åˇ˛į„Ąæŗ•åœ¨įŖįĸŸä¸­æ‰žåˆ°ã€‚č̋聝įĩĄæ‚¨įš„ Immich įŽĄį†å“ĄäģĨ取垗協劊。", - "asset_restored_successfully": "åĒ’éĢ”åžŠåŽŸæˆåŠŸ", + "asset_not_found_on_device_android": "į„Ąæŗ•åœ¨čŖįŊŽä¸Šæ‰žåˆ°é …į›Ž", + "asset_not_found_on_device_ios": "į„Ąæŗ•åœ¨čŖįŊŽä¸Šæ‰žåˆ°é …į›Žã€‚iCloud ä¸Šįš„é …į›Žå¯čƒŊ因æĒ”æĄˆææ¯€č€Œį„Ąæŗ•æŸĨ閱", + "asset_not_found_on_icloud": "iCloud ä¸Šæ‰žä¸åˆ°é …į›Žã€‚čŠ˛é …į›Žå¯čƒŊ因æĒ”æĄˆæ¯€æč€Œį„Ąæŗ•å­˜å–", + "asset_offline": "é …į›Žé›ĸ᎚", + "asset_offline_description": "æ­¤å¤–éƒ¨é …į›Žåˇ˛į„Ąæŗ•åœ¨įŖįĸŸä¸Šæ‰žåˆ°ã€‚č̋聝įĩĄæ‚¨įš„ Immich įŽĄį†å“ĄäģĨ取垗協劊。", + "asset_restored_successfully": "é …į›Žé‚„åŽŸæˆåŠŸ", "asset_skipped": "åˇ˛čˇŗéŽ", "asset_skipped_in_trash": "åˇ˛åœ¨åžƒåœžæĄļ", - "asset_trashed": "čŗ‡į”ĸčĸĢä¸ŸæŖ„", - "asset_troubleshoot": "čŗ‡į”ĸ故障排除", + "asset_trashed": "é …į›Žåˇ˛į§ģč‡ŗåžƒåœžæĄļ", + "asset_troubleshoot": "é …į›Žæ•…éšœæŽ’é™¤", "asset_uploaded": "åˇ˛ä¸Šå‚ŗ", "asset_uploading": "ä¸Šå‚ŗä¸­â€Ļ", "asset_viewer_settings_subtitle": "įŽĄį†æ‚¨įš„åĒ’éĢ”åēĢæĒĸčĻ–å™¨č¨­åŽš", - "asset_viewer_settings_title": "åĒ’éĢ”æĒĸčϖ噍", - "assets": "åĒ’éĢ”", - "assets_added_count": "åˇ˛æ–°åĸž {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}", - "assets_added_to_album_count": "厞將 {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}加å…Ĩᛏį°ŋ", - "assets_added_to_albums_count": "åˇ˛æ–°åĸž {assetTotal, plural, one {# 個} other {# 個}}é …į›Žåˆ° {albumTotal, plural, one {# 個} other {# 個}}ᛏį°ŋ中", - "assets_cannot_be_added_to_album_count": "į„Ąæŗ•å°‡ {count, plural, one {åĒ’éĢ”} other {åĒ’éĢ”}} 加å…Ĩ臺ᛏį°ŋ", - "assets_cannot_be_added_to_albums": "{count, plural, one {個} other {個}}é …į›Žį„Ąæŗ•čĸĢ加å…Ĩᛏį°ŋ", - "assets_count": "{count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}", - "assets_deleted_permanently": "{count} 個åĒ’é̔厞čĸĢæ°¸äš…åˆĒ除", - "assets_deleted_permanently_from_server": "åˇ˛åžž Immich äŧ翜å™¨ä¸­æ°¸äš…į§ģ除 {count} 個åĒ’éĢ”", + "asset_viewer_settings_title": "é …į›ŽæĒĸčϖ噍", + "assets": "é …į›Ž", + "assets_added_count": "åˇ˛æ–°åĸž {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", + "assets_added_to_album_count": "厞將 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}加å…Ĩ臺ᛏį°ŋ", + "assets_added_to_albums_count": "厞將 {assetTotal, plural, other {# å€‹é …į›Ž}} 新åĸžč‡ŗ {albumTotal, plural, other {# æœŦᛏį°ŋ}}", + "assets_cannot_be_added_to_album_count": "į„Ąæŗ•å°‡ {count, plural, one {é …į›Ž} other {é …į›Ž}} 加å…Ĩ臺ᛏį°ŋ", + "assets_cannot_be_added_to_albums": "į„Ąæŗ•å°‡ {count, plural, other {# å€‹é …į›Ž}} 加å…ĨäģģäŊ•ᛏį°ŋ", + "assets_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", + "assets_deleted_permanently": "åˇ˛æ°¸äš…åˆĒ除 {count} å€‹é …į›Ž", + "assets_deleted_permanently_from_server": "åˇ˛åžž Immich äŧ翜å™¨ä¸­æ°¸äš…į§ģ除 {count} å€‹é …į›Ž", "assets_downloaded_failed": "{count, plural, one {厞䏋čŧ‰ # 個æĒ”æĄˆ - {error} 個æĒ”æĄˆå¤ąæ•—} other {厞䏋čŧ‰ # 個æĒ”æĄˆ - {error} 個æĒ”æĄˆå¤ąæ•—}}", "assets_downloaded_successfully": "{count, plural, one {åˇ˛æˆåŠŸä¸‹čŧ‰ # 個æĒ”æĄˆ} other {åˇ˛æˆåŠŸä¸‹čŧ‰ # 個æĒ”æĄˆ}}", - "assets_moved_to_trash_count": "厞將 {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}į§ģå‹•é€˛åžƒåœžæĄļ", - "assets_permanently_deleted_count": "åˇ˛æ°¸äš…åˆĒ除 {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}", - "assets_removed_count": "厞į§ģ除 {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}", - "assets_removed_permanently_from_device": "åˇ˛åžžæ‚¨įš„čŖįŊŽæ°¸äš…į§ģ除 {count} 個åĒ’éĢ”", - "assets_restore_confirmation": "您įĸē厚čĻé‚„åŽŸæ‰€æœ‰åžƒåœžæĄļä¸­įš„åĒ’éĢ”å—ŽīŧŸæ­¤æ“äŊœį„Ąæŗ•垊原īŧčĢ‹æŗ¨æ„īŧŒäģģäŊ•é›ĸ᎚åĒ’éĢ”éƒŊį„Ąæŗ•é€éŽæ­¤æ–šåŧé‚„原。", - "assets_restored_count": "åˇ˛é‚„åŽŸ {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}}", - "assets_restored_successfully": "åˇ˛æˆåŠŸé‚„åŽŸ {count} 個åĒ’éĢ”", - "assets_trashed": "厞將 {count} 個åĒ’éĢ”į§ģč‡ŗåžƒåœžæĄļ", - "assets_trashed_count": "厞將 {count, plural, one {# 個åĒ’éĢ”} other {# 個åĒ’éĢ”}} į§ģč‡ŗåžƒåœžæĄļ", - "assets_trashed_from_server": "åˇ˛åžž Immich äŧ翜å™¨å°‡ {count} 個åĒ’éĢ”į§ģč‡ŗåžƒåœžæĄļ", - "assets_were_part_of_album_count": "{count, plural, one {芲åĒ’é̔厞} other {這äē›åĒ’é̔厞}}åœ¨į›¸į°ŋ中", + "assets_moved_to_trash_count": "厞將 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}į§ģč‡ŗåžƒåœžæĄļ", + "assets_permanently_deleted_count": "åˇ˛æ°¸äš…åˆĒ除 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", + "assets_removed_count": "厞į§ģ除 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", + "assets_removed_permanently_from_device": "åˇ˛åžžæ‚¨įš„čŖįŊŽæ°¸äš…į§ģ除 {count} å€‹é …į›Ž", + "assets_restore_confirmation": "您įĸē厚čĻé‚„åŽŸæ‰€æœ‰åžƒåœžæĄļä¸­įš„é …į›Žå—ŽīŧŸæ­¤æ“äŊœį„Ąæŗ•垊原īŧčĢ‹æŗ¨æ„īŧŒäģģäŊ•é›ĸįˇšé …į›ŽéƒŊį„Ąæŗ•é€éŽæ­¤æ–šåŧé‚„原。", + "assets_restored_count": "åˇ˛é‚„åŽŸ {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", + "assets_restored_successfully": "åˇ˛æˆåŠŸé‚„åŽŸ {count} å€‹é …į›Ž", + "assets_trashed": "厞將 {count} å€‹é …į›Žį§ģč‡ŗåžƒåœžæĄļ", + "assets_trashed_count": "厞將 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}į§ģč‡ŗåžƒåœžæĄļ", + "assets_trashed_from_server": "åˇ˛åžž Immich äŧ翜å™¨å°‡ {count} å€‹é …į›Žį§ģč‡ŗåžƒåœžæĄļ", + "assets_were_part_of_album_count": "{count, plural, one {čŠ˛é …į›Žåˇ˛} other {這äē›é …į›Žåˇ˛}}åœ¨į›¸į°ŋ中", "assets_were_part_of_albums_count": "{count, plural, one {個} other {個}}é …į›Žåˇ˛čĸĢå„˛å­˜åœ¨į›¸į°ŋ中", "authorized_devices": "åˇ˛æŽˆæŦŠčŖįŊŽ", "automatic_endpoint_switching_subtitle": "į•ļå¯į”¨æ™‚īŧŒé€éŽæŒ‡åŽšįš„ Wi-Fi 在æœŦæŠŸé€ŖįˇšīŧŒå…ļäģ–æƒ…æŗå‰‡äŊŋᔍæ›ŋäģŖé€Ŗįˇš", @@ -587,19 +622,19 @@ "backup": "備äģŊ", "backup_album_selection_page_albums_device": "čŖįŊŽä¸Šįš„ᛏį°ŋīŧˆ{count}īŧ‰", "backup_album_selection_page_albums_tap": "éģžä¸€ä¸‹äģĨ選取īŧŒéģžå…Šä¸‹äģĨ排除", - "backup_album_selection_page_assets_scatter": "åĒ’éĢ”å¯äģĨåˆ†æ•Ŗåœ¨å¤šå€‹į›¸į°ŋ中īŧŒå› æ­¤åœ¨å‚™äģŊéŽį¨‹ä¸­å¯äģĨé¸æ“‡į´å…Ĩæˆ–æŽ’é™¤į›¸į°ŋ。", + "backup_album_selection_page_assets_scatter": "é …į›Žå¯äģĨåˆ†æ•Ŗåœ¨å¤šå€‹į›¸į°ŋ中īŧŒå› æ­¤åœ¨å‚™äģŊéŽį¨‹ä¸­å¯äģĨé¸æ“‡į´å…Ĩæˆ–æŽ’é™¤į›¸į°ŋ。", "backup_album_selection_page_select_albums": "é¸å–į›¸į°ŋ", "backup_album_selection_page_selection_info": "é¸å–čŗ‡č¨Š", - "backup_album_selection_page_total_assets": "į¸Ŋ不重複åĒ’éĢ”æ•¸", + "backup_album_selection_page_total_assets": "į¸Ŋä¸é‡č¤‡é …į›Žæ•¸", "backup_albums_sync": "備äģŊᛏį°ŋ同æ­Ĩ", "backup_all": "全部", - "backup_background_service_backup_failed_message": "備äģŊåĒ’éĢ”å¤ąæ•—ã€‚æ­Ŗåœ¨é‡čŠĻâ€Ļ", - "backup_background_service_complete_notification": "čŗ‡į”ĸ備äģŊ厌成", + "backup_background_service_backup_failed_message": "備äģŊé …į›Žå¤ąæ•—ã€‚æ­Ŗåœ¨é‡čŠĻâ€Ļ", + "backup_background_service_complete_notification": "é …į›Žå‚™äģŊ厌成", "backup_background_service_connection_failed_message": "é€Ŗįˇšč‡ŗäŧ翜å™¨å¤ąæ•—ã€‚æ­Ŗåœ¨é‡čŠĻâ€Ļ", "backup_background_service_current_upload_notification": "æ­Ŗåœ¨ä¸Šå‚ŗ {filename}", - "backup_background_service_default_notification": "æ­Ŗåœ¨æĒĸæŸĨ新åĒ’éĢ”â€Ļ", + "backup_background_service_default_notification": "æ­Ŗåœ¨æĒĸæŸĨæ–°é …į›Žâ€Ļ", "backup_background_service_error_title": "備äģŊ錯čǤ", - "backup_background_service_in_progress_notification": "æ­Ŗåœ¨å‚™äģŊæ‚¨įš„åĒ’éĢ”â€Ļ", + "backup_background_service_in_progress_notification": "æ­Ŗåœ¨å‚™äģŊæ‚¨įš„é …į›Žâ€Ļ", "backup_background_service_upload_failure_notification": "{filename} ä¸Šå‚ŗå¤ąæ•—", "backup_controller_page_albums": "備äģŊᛏį°ŋ", "backup_controller_page_background_app_refresh_disabled_content": "čĢ‹åœ¨ã€Œč¨­åŽšã€>「一čˆŦ」>ã€ŒčƒŒæ™¯ App é‡æ–°æ•´į†ã€ä¸­å•Ÿį”¨īŧŒäģĨäŊŋį”¨čƒŒæ™¯å‚™äģŊ功čƒŊ。", @@ -611,18 +646,18 @@ "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": "é–‹å•ŸčƒŒæ™¯æœå‹™īŧŒåŗå¯åœ¨ä¸éœ€é–‹å•Ÿ App įš„æƒ…æŗä¸‹īŧŒč‡Ē動備äģŊ所有新åĒ’éĢ”", + "backup_controller_page_background_delay": "æ–°é …į›Žå‚™äģŊåģļ遲īŧš{duration}", + "backup_controller_page_background_description": "é–‹å•ŸčƒŒæ™¯æœå‹™īŧŒåŗå¯åœ¨ä¸éœ€é–‹å•Ÿ App įš„æƒ…æŗä¸‹īŧŒč‡Ē動備äģŊæ‰€æœ‰æ–°é …į›Ž", "backup_controller_page_background_is_off": "čƒŒæ™¯č‡Ē動備äģŊåˇ˛é—œé–‰", "backup_controller_page_background_is_on": "čƒŒæ™¯č‡Ē動備äģŊåˇ˛é–‹å•Ÿ", "backup_controller_page_background_turn_off": "é—œé–‰čƒŒæ™¯æœå‹™", "backup_controller_page_background_turn_on": "é–‹å•ŸčƒŒæ™¯æœå‹™", "backup_controller_page_background_wifi": "僅äŊŋᔍ Wi-Fi", "backup_controller_page_backup": "備äģŊ", - "backup_controller_page_backup_selected": "厞遏䏭īŧš ", - "backup_controller_page_backup_sub": "厞備äģŊįš„į…§į‰‡å’ŒåŊąį‰‡", + "backup_controller_page_backup_selected": "åˇ˛é¸å–īŧš ", + "backup_controller_page_backup_sub": "厞備äģŊįš„į›¸į‰‡čˆ‡åŊąį‰‡", "backup_controller_page_created": "åģēįĢ‹æ™‚é–“īŧš{date}", - "backup_controller_page_desc_backup": "開啟前č‡ē備äģŊīŧŒåœ¨é–‹å•Ÿ App 時č‡Ē動將新åĒ’éĢ”ä¸Šå‚ŗč‡ŗäŧ翜å™¨ã€‚", + "backup_controller_page_desc_backup": "開啟前景備äģŊīŧŒåœ¨é–‹å•Ÿ App 時č‡Ēå‹•å°‡æ–°é …į›Žä¸Šå‚ŗč‡ŗäŧ翜å™¨ã€‚", "backup_controller_page_excluded": "åˇ˛æŽ’é™¤īŧš ", "backup_controller_page_failed": "å¤ąæ•—īŧˆ{count}īŧ‰", "backup_controller_page_filename": "æĒ”æĄˆåį¨ąīŧš{filename} [{size}]", @@ -630,20 +665,20 @@ "backup_controller_page_info": "備äģŊčŗ‡č¨Š", "backup_controller_page_none_selected": "æœĒ選取äģģäŊ•é …į›Ž", "backup_controller_page_remainder": "削餘", - "backup_controller_page_remainder_sub": "é¸å–é …į›Žä¸­å°šæœĒ備äģŊįš„į…§į‰‡čˆ‡åŊąį‰‡", + "backup_controller_page_remainder_sub": "é¸å–é …į›Žä¸­å°šæœĒ備äģŊįš„į›¸į‰‡čˆ‡åŊąį‰‡", "backup_controller_page_server_storage": "äŧ翜å™¨å„˛å­˜įŠē間", "backup_controller_page_start_backup": "開始備äģŊ", "backup_controller_page_status_off": "前č‡ēč‡Ē動備äģŊåˇ˛é—œé–‰", "backup_controller_page_status_on": "前č‡ēč‡Ē動備äģŊåˇ˛é–‹å•Ÿ", "backup_controller_page_storage_format": "{used} / {total} 厞äŊŋᔍ", "backup_controller_page_to_backup": "čρ備äģŊįš„į›¸į°ŋ", - "backup_controller_page_total_sub": "åˇ˛é¸å–į›¸į°ŋä¸­įš„æ‰€æœ‰ä¸é‡č¤‡įš„į…§į‰‡čˆ‡åŊąį‰‡", + "backup_controller_page_total_sub": "åˇ˛é¸å–į›¸į°ŋä¸­įš„æ‰€æœ‰ä¸é‡č¤‡įš„į›¸į‰‡čˆ‡åŊąį‰‡", "backup_controller_page_turn_off": "關閉前č‡ē備äģŊ", "backup_controller_page_turn_on": "開啟前č‡ē備äģŊ", "backup_controller_page_uploading_file_info": "ä¸Šå‚ŗä¸­įš„æĒ”æĄˆčŗ‡č¨Š", "backup_err_only_album": "不čƒŊį§ģé™¤å”¯ä¸€įš„į›¸į°ŋ", "backup_error_sync_failed": "同æ­Ĩå¤ąæ•—īŧŒį„Ąæŗ•處ᐆ備äģŊ。", - "backup_info_card_assets": "個åĒ’éĢ”", + "backup_info_card_assets": "å€‹é …į›Ž", "backup_manual_cancelled": "åˇ˛å–æļˆ", "backup_manual_in_progress": "ä¸Šå‚ŗæ­Ŗåœ¨é€˛čĄŒä¸­īŧŒčĢ‹į¨åžŒå†čŠĻ", "backup_manual_success": "成功", @@ -655,23 +690,23 @@ "backup_upload_details_page_more_details": "éģžæ“ŠæŸĨįœ‹æ›´å¤ščŠŗį´°čŗ‡č¨Š", "backward": "į”ąčˆŠč‡ŗæ–°", "biometric_auth_enabled": "į”Ÿį‰Ščž¨č­˜éŠ—č­‰åˇ˛å•Ÿį”¨", - "biometric_locked_out": "æ‚¨åˇ˛čĸĢéŽ–åŽšį„Ąæŗ•äŊŋį”¨į”Ÿį‰Ščž¨č­˜éŠ—č­‰", + "biometric_locked_out": "į”Ÿį‰Ščž¨č­˜éŠ—č­‰åˇ˛čĸĢ鎖厚", "biometric_no_options": "æ˛’æœ‰į”Ÿį‰Ščž¨č­˜é¸é …å¯į”¨", - "biometric_not_available": "æ­¤čŖįŊŽä¸Šį„Ąæŗ•äŊŋį”¨į”Ÿį‰Ščž¨č­˜éŠ—č­‰", + "biometric_not_available": "æ­¤čŖįŊŽä¸æ”¯æ´į”Ÿį‰Ščž¨č­˜éŠ—č­‰", "birthdate_saved": "å‡ēį”Ÿæ—ĨæœŸå„˛å­˜æˆåŠŸ", - "birthdate_set_description": "å‡ēį”Ÿæ—ĨæœŸį”¨æ–ŧč¨ˆįŽ—æ­¤äēēåœ¨į…§į‰‡æ‹æ”æ™‚įš„åš´éŊĄã€‚", + "birthdate_set_description": "å‡ēį”Ÿæ—ĨæœŸį”¨æ–ŧč¨ˆįŽ—æ­¤äēēåœ¨į›¸į‰‡æ‹æ”æ™‚įš„åš´éŊĄã€‚", "blurred_background": "čƒŒæ™¯æ¨ĄįŗŠ", "bugs_and_feature_requests": "錯čĒ¤åŠåŠŸčƒŊčĢ‹æą‚", "build": "åģēįŊŽįˇ¨č™Ÿ", "build_image": "åģēįŊŽæ˜ åƒ", - "bulk_delete_duplicates_confirmation": "您įĸē厚čĻæ‰šæŦĄåˆĒ除 {count, plural, one {# å€‹é‡č¤‡åĒ’éĢ”} other {# å€‹é‡č¤‡åĒ’éĢ”}} 嗎īŧŸįŗģįĩąå°‡äŋį•™æ¯įĩ„ä¸­å¤§å°æœ€å¤§įš„åĒ’éĢ”īŧŒä¸Ļ永䚅åˆĒ除所有å…ļäģ–é‡č¤‡é …į›Žã€‚æ­¤æ“äŊœį„Ąæŗ•垊原īŧ", - "bulk_keep_duplicates_confirmation": "您įĸē厚čρäŋį•™ {count, plural, one {# å€‹é‡č¤‡åĒ’éĢ”} other {# å€‹é‡č¤‡åĒ’éĢ”}} 嗎īŧŸé€™å°‡åœ¨ä¸åˆĒ除äģģäŊ•é …į›Žįš„æƒ…æŗä¸‹č§Ŗæąēæ‰€æœ‰é‡č¤‡įž¤įĩ„。", - "bulk_trash_duplicates_confirmation": "您įĸē厚čĻæ‰šæŦĄå°‡ {count, plural, one {# å€‹é‡č¤‡åĒ’éĢ”} other {# å€‹é‡č¤‡åĒ’éĢ”}}į§ģč‡ŗåžƒåœžæĄļ嗎īŧŸįŗģįĩąå°‡äŋį•™æ¯įĩ„ä¸­å¤§å°æœ€å¤§įš„åĒ’éĢ”īŧŒä¸Ļ將所有å…ļäģ–é‡č¤‡é …į›Žį§ģč‡ŗåžƒåœžæĄļ。", + "bulk_delete_duplicates_confirmation": "您įĸē厚čĻæ‰šæŦĄåˆĒ除 {count, plural, one {# å€‹é‡č¤‡é …į›Ž} other {# å€‹é‡č¤‡é …į›Ž}} 嗎īŧŸįŗģįĩąå°‡äŋį•™æ¯įĩ„ä¸­åŽšé‡æœ€å¤§įš„é …į›ŽīŧŒä¸Ļ永䚅åˆĒ除所有å…ļäģ–é‡č¤‡é …į›Žã€‚æ­¤å‹•äŊœį„Ąæŗ•垊原īŧ", + "bulk_keep_duplicates_confirmation": "您įĸē厚čρäŋį•™ {count, plural, one {# å€‹é‡č¤‡é …į›Ž} other {# å€‹é‡č¤‡é …į›Ž}} 嗎īŧŸé€™å°‡åœ¨ä¸åˆĒ除äģģäŊ•å…§åŽšįš„æƒ…æŗä¸‹č§Ŗæąēæ‰€æœ‰é‡č¤‡įž¤įĩ„。", + "bulk_trash_duplicates_confirmation": "您įĸē厚čĻæ‰šæŦĄå°‡ {count, plural, one {# å€‹é‡č¤‡é …į›Ž} other {# å€‹é‡č¤‡é …į›Ž}}į§ģč‡ŗåžƒåœžæĄļ嗎īŧŸįŗģįĩąå°‡äŋį•™æ¯įĩ„ä¸­åŽšé‡æœ€å¤§įš„é …į›ŽīŧŒä¸Ļ將所有å…ļäģ–é‡č¤‡é …į›Žį§ģč‡ŗåžƒåœžæĄļ。", "buy": "čŗŧ財 Immich", "cache_settings_clear_cache_button": "清除åŋĢ取", "cache_settings_clear_cache_button_title": "清除 App įš„åŋĢ取。此動äŊœæœƒåœ¨åŋĢ取重新åģēįĢ‹å‰īŧŒéĄ¯č‘—åŊąéŸŋ App įš„æ•ˆčƒŊ。", "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": "åŽŒæ•´åœ–į‰‡", @@ -700,28 +735,42 @@ "change_expiration_time": "čŽŠæ›´åˆ°æœŸæ™‚é–“", "change_location": "čŽŠæ›´äŊįŊŽ", "change_name": "čŽŠæ›´åį¨ą", - "change_name_successfully": "čŽŠæ›´åį¨ąæˆåŠŸ", + "change_name_successfully": "åį¨ąčŽŠæ›´æˆåŠŸ", "change_password": "čŽŠæ›´å¯†įĸŧ", "change_password_description": "這是您éĻ–æŦĄį™ģå…ĨįŗģįĩąīŧŒæˆ–æ˜¯åˇ˛æ”ļåˆ°čŽŠæ›´å¯†įĸŧįš„čĢ‹æą‚ã€‚čĢ‹åœ¨ä¸‹æ–ščŧ¸å…Ĩ新密įĸŧ。", "change_password_form_confirm_password": "įĸēčĒå¯†įĸŧ", "change_password_form_description": "您åĨŊ {name}īŧŒ\n\n這是您éĻ–æŦĄį™ģå…ĨįŗģįĩąīŧŒæˆ–æ˜¯åˇ˛æ”ļåˆ°čŽŠæ›´å¯†įĸŧįš„čĢ‹æą‚ã€‚čĢ‹åœ¨ä¸‹æ–ščŧ¸å…Ĩ新密įĸŧ。", - "change_password_form_log_out": "č¨ģéŠˇæ‰€æœ‰å…ļäģ–荭備", - "change_password_form_log_out_description": "åģē議退å‡ē所有å…ļäģ–荭備", + "change_password_form_log_out": "į™ģå‡ē所有å…ļäģ–čŖįŊŽ", + "change_password_form_log_out_description": "åģēč­°åžžæ‰€æœ‰å…ļäģ–čŖįŊŽį™ģå‡ē", "change_password_form_new_password": "新密įĸŧ", "change_password_form_password_mismatch": "密įĸŧ不一致", "change_password_form_reenter_new_password": "再æŦĄčŧ¸å…Ĩ新密įĸŧ", "change_pin_code": "čŽŠæ›´ PIN įĸŧ", + "change_trigger": "æ›´æ”šč§¸į™ŧ器", + "change_trigger_prompt": "įĸē厚čĻčŽŠæ›´č§¸į™ŧæĸäģļ嗎īŧŸé€™å°‡į§ģé™¤æ‰€æœ‰įžæœ‰įš„å‹•äŊœčˆ‡į¯Šé¸å™¨ã€‚", "change_your_password": "čŽŠæ›´æ‚¨įš„å¯†įĸŧ", "changed_visibility_successfully": "åˇ˛æˆåŠŸčŽŠæ›´å¯čĻ‹æ€§", "charging": "充é›ģ", "charging_requirement_mobile_backup": "垌č‡ē備äģŊčĻæą‚čŖįŊŽæ­Ŗåœ¨å……é›ģ", "check_corrupt_asset_backup": "æĒĸæŸĨææ¯€įš„å‚™äģŊé …į›Ž", "check_corrupt_asset_backup_button": "åŸˇčĄŒæĒĸæŸĨ", - "check_corrupt_asset_backup_description": "åƒ…åœ¨åˇ˛é€Ŗįˇšč‡ŗ Wi-Fi 且所有åĒ’éĢ”åˇ˛åŽŒæˆå‚™äģŊåžŒåŸˇčĄŒæ­¤æĒĸæŸĨã€‚æ­¤į¨‹åŧå¯čƒŊ需čĻæ•¸åˆ†é˜ã€‚", - "check_logs": "æĒĸæŸĨæ—Ĩčnj", - "checksum": "æ ĄéŠ—å’Œ", + "check_corrupt_asset_backup_description": "åƒ…åœ¨åˇ˛é€Ŗįˇšč‡ŗ Wi-Fi ä¸”æ‰€æœ‰é …į›Žåˇ˛åŽŒæˆå‚™äģŊåžŒåŸˇčĄŒæ­¤æĒĸæŸĨã€‚æ­¤į¨‹åŧå¯čƒŊ需čĻæ•¸åˆ†é˜ã€‚", + "check_logs": "æĒĸæŸĨį´€éŒ„", + "checksum": "æ ĄéŠ—įĸŧ", "choose_matching_people_to_merge": "選擇čρ合äŊĩįš„į›¸įŦĻäēēį‰Š", "city": "城市", + "cleanup_confirm_description": "Immich į™ŧįžæœ‰ {count} å€‹é …į›ŽīŧˆåģēįĢ‹æ–ŧ {date} 䚋前īŧ‰åˇ˛åމ免備äģŊ臺äŧ翜å™¨ã€‚是åĻčĻåžžæ­¤čŖįŊŽä¸­åˆĒ除æœŦ抟副æœŦīŧŸ", + "cleanup_confirm_prompt_title": "åžžæ­¤čŖįŊŽåˆĒ除īŧŸ", + "cleanup_deleted_assets": "厞將{count}é …į›Žį§ģåˆ°čŖįŊŽįš„垃圞æĄļčŖĄ", + "cleanup_deleting": "æ­Ŗåœ¨į§ģ動到垃圞æĄļ...", + "cleanup_found_assets": "扞到{count}äģļåˇ˛ä¸Šå‚ŗįš„é …į›Ž", + "cleanup_found_assets_with_size": "扞到{count}äģļīŧŒį¸Ŋå…ą({size})åˇ˛ä¸Šå‚ŗįš„é …į›Ž", + "cleanup_icloud_shared_albums_excluded": "iCloudå…ąäēĢᛏį°ŋčĸ̿ޒ除æ–ŧ搜尋䚋外", + "cleanup_no_assets_found": "扞不到įŦĻ合上čŋ°æĸäģļįš„é …į›Žã€‚é‡‹æ”žįŠē間功čƒŊ僅čƒŊį§ģ除厞備äģŊ臺äŧ翜å™¨įš„é …į›Ž", + "cleanup_preview_title": "{count} 項需čρį§ģé™¤įš„é …į›Ž", + "cleanup_step3_description": "掃描įŦĻ合æ—ĨæœŸčˆ‡å„˛å­˜č¨­åŽšįš„åˇ˛å‚™äģŊé …į›Žã€‚", + "cleanup_step4_summary": "å°‡åžžæ­¤čŖįŊŽį§ģ除 {count} 個åģēįĢ‹æ–ŧ {date} äš‹å‰įš„é …į›Žã€‚æ‚¨äģå¯é€éŽ Immich æ‡‰į”¨į¨‹åŧå­˜å–這äē›į›¸į‰‡ã€‚", + "cleanup_trash_hint": "č‹ĨčĻåžšåē•é‡‹æ”žå„˛å­˜įŠē間īŧŒčĢ‹é–‹å•Ÿįŗģįĩąį›¸į°ŋ App ä¸Ļ清įŠē垃圞æĄļ", "clear": "清įŠē", "clear_all": "全部清除", "clear_all_recent_searches": "清除所有最čŋ‘įš„æœå°‹", @@ -733,9 +782,11 @@ "client_cert_import": "匯å…Ĩ", "client_cert_import_success_msg": "厞匝å…ĨᔍæˆļįĢ¯æ†‘č­‰", "client_cert_invalid_msg": "į„Ąæ•ˆįš„æ†‘č­‰æĒ”æĄˆæˆ–å¯†įĸŧ錯čǤ", + "client_cert_password_message": "čĢ‹čŧ¸å…Ĩæ­¤č­‰æ›¸įš„å¯†įĸŧ", + "client_cert_password_title": "č­‰æ›¸å¯†įĸŧ", "client_cert_remove_msg": "ᔍæˆļįĢ¯æ†‘č­‰åˇ˛į§ģ除", - "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) æ ŧåŧã€‚僅可在į™ģå…Ĩå‰é€˛čĄŒæ†‘č­‰įš„åŒ¯å…Ĩ和į§ģ除", - "client_cert_title": "SSL ᔍæˆļįĢ¯æ†‘č­‰[å¯Ļ銗性]", + "client_cert_subtitle": "僅支援 PKCS12 (.p12, .pfx) æ ŧåŧã€‚æ†‘č­‰åŒ¯å…Ĩ與į§ģ除僅可在į™ģå…Ĩå‰é€˛čĄŒ", + "client_cert_title": "SSL ᔍæˆļįĢ¯æ†‘č­‰ [å¯Ļ銗性]", "clockwise": "順時針", "close": "關閉", "collapse": "æŠ˜į–Š", @@ -743,6 +794,11 @@ "color": "顏色", "color_theme": "色åŊŠä¸ģ題", "command": "å‘Ŋäģ¤", + "command_palette_prompt": "åŋĢ速尋扞頁éĸīŧŒå‹•äŊœæˆ–č€…æŒ‡äģ¤", + "command_palette_to_close": "關閉", + "command_palette_to_navigate": "čŧ¸å…Ĩ", + "command_palette_to_select": "選擇", + "command_palette_to_show_all": "éĄ¯į¤ē全部", "comment_deleted": "ᕙ荀厞åˆĒ除", "comment_options": "ᕙ荀遏項", "comments_and_likes": "į•™č¨€čˆ‡å–œæ­Ą", @@ -751,9 +807,9 @@ "completed": "åˇ˛åŽŒæˆ", "confirm": "įĸēčĒ", "confirm_admin_password": "įĸēčĒįŽĄį†å“Ąå¯†įĸŧ", - "confirm_delete_face": "您įĸē厚čĻåžžčŠ˛åĒ’é̔䏭åˆĒ除{name}įš„č‡‰å­”å—ŽīŧŸ", - "confirm_delete_shared_link": "您įĸē厚čρåˆĒé™¤é€™å€‹å…ąäēĢ逪įĩå—ŽīŧŸ", - "confirm_keep_this_delete_others": "除此åĒ’é̔外īŧŒå †į–Šä¸­įš„å…ļäģ–åĒ’éĢ”éƒŊ將čĸĢåˆĒ除。您įĸē厚čρįšŧįēŒå—ŽīŧŸ", + "confirm_delete_face": "您įĸē厚čĻåžžčŠ˛é …į›Žä¸­åˆĒ除 {name} įš„č‡‰å­”å—ŽīŧŸ", + "confirm_delete_shared_link": "您įĸē厚čρåˆĒ除此分äēĢ逪įĩå—ŽīŧŸ", + "confirm_keep_this_delete_others": "é™¤æ­¤é …į›Žå¤–īŧŒå †į–Šä¸­įš„å…ļäģ–é …į›ŽéƒŊ將čĸĢåˆĒ除。您įĸē厚čρįšŧįēŒå—ŽīŧŸ", "confirm_new_pin_code": "įĸēčĒæ–° PIN įĸŧ", "confirm_password": "įĸēčĒå¯†įĸŧ", "confirm_tag_face": "æ‚¨æƒŗčĻå°‡æ­¤č‡‰å­”æ¨™įą¤į‚ē {name} 嗎īŧŸ", @@ -786,32 +842,41 @@ "create": "åģēįĢ‹", "create_album": "åģēį̋ᛏį°ŋ", "create_album_page_untitled": "æœĒå‘Ŋ名", - "create_api_key": "å‰ĩåģēAPI金鑰", + "create_api_key": "åģēįĢ‹ API 金鑰", + "create_first_workflow": "åģēįĢ‹įŦŦ一個åˇĨäŊœæĩį¨‹", "create_library": "åģēįĢ‹åĒ’éĢ”åēĢ", "create_link": "åģēį̋逪įĩ", - "create_link_to_share": "åģēįĢ‹å…ąäēĢ逪įĩ", - "create_link_to_share_description": "äģģäŊ•æŒæœ‰é€Ŗįĩįš„äēēéƒŊå…č¨ąæĒĸčĻ–æ‰€é¸į›¸į‰‡", + "create_link_to_share": "åģēįĢ‹åˆ†äēĢ逪įĩ", + "create_link_to_share_description": "æŒæœ‰é€Ŗįĩįš„äēēįš†å¯æĒĸčĻ–æ‰€é¸é …į›Ž", "create_new": "新åĸž", "create_new_person": "åģēįĢ‹æ–°äēēį‰Š", - "create_new_person_hint": "å°‡é¸åŽšįš„åĒ’éĢ”åˆ†é…įĩĻæ–°äēēį‰Š", + "create_new_person_hint": "å°‡é¸å–įš„é …į›ŽæŒ‡æ´žįĩĻæ–°įš„äēēį‰Š", "create_new_user": "åģēįĢ‹æ–°äŊŋᔍ者", - "create_shared_album_page_share_add_assets": "新åĸžč†œéĢ”", - "create_shared_album_page_share_select_photos": "é¸æ“‡į…§į‰‡", - "create_shared_link": "åģēįĢ‹å…ąäēĢ逪įĩ", + "create_shared_album_page_share_add_assets": "新åĸžé …į›Ž", + "create_shared_album_page_share_select_photos": "é¸å–į›¸į‰‡", + "create_shared_link": "åģēįĢ‹åˆ†äēĢ逪įĩ", "create_tag": "åģēįĢ‹æ¨™įą¤", "create_tag_description": "åģēįĢ‹æ–°æ¨™įą¤ã€‚č‹ĨčρåģēįĢ‹åˇĸį‹€æ¨™įą¤īŧŒčĢ‹čŧ¸å…Ĩ包åĢæ­Ŗæ–œįˇšįš„åŽŒæ•´æ¨™įą¤čˇ¯åž‘ã€‚", "create_user": "åģēįĢ‹äŊŋᔍ者", + "create_workflow": "åģēįĢ‹åˇĨäŊœæĩį¨‹", "created": "åģēįĢ‹æ–ŧ", "created_at": "åģēįĢ‹æ–ŧ", "creating_linked_albums": "åģēį̋逪įĩį›¸į°ŋ ...", "crop": "誁å‰Ē", + "crop_aspect_ratio_fixed": "厞äŋŽåžŠ", + "crop_aspect_ratio_free": "į„Ąé™åˆļ", + "crop_aspect_ratio_original": "原æĒ”", "curated_object_page_title": "äē‹į‰Š", "current_device": "į›Žå‰čŖįŊŽ", "current_pin_code": "į›Žå‰ PIN įĸŧ", "current_server_address": "į›Žå‰įš„äŧ翜å™¨äŊå€", + "custom_date": "åĻ選æ—Ĩ期", "custom_locale": "č‡Ēč¨‚åœ°å€č¨­åŽš", "custom_locale_description": "栚據čĒžč¨€čˆ‡åœ°å€æ ŧåŧåŒ–æ—ĨæœŸčˆ‡æ•¸å­—", "custom_url": "č‡Ē訂 URL", + "cutoff_date_description": "äŋį•™æœ€čŋ‘å¤šå°‘å¤Šįš„į›¸į‰‡â€Ļ", + "cutoff_day": "{count, plural, one {夊} other {夊}}", + "cutoff_year": "{count, plural, one {åš´} other {åš´}}", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "YYYY åš´ M 月 D æ—Ĩ (E)", "dark": "æˇąč‰˛", @@ -829,11 +894,11 @@ "deduplication_criteria_1": "åŊąåƒå¤§å°īŧˆäģĨäŊå…ƒįĩ„į‚ēå–ŽäŊīŧ‰", "deduplication_criteria_2": "EXIF čŗ‡æ–™æ•¸é‡", "deduplication_info": "é‡č¤‡čŗ‡æ–™åˆĒé™¤čŗ‡č¨Š", - "deduplication_info_description": "čρč‡Ē動預先選取åĒ’éĢ”ä¸Ļ扚æŦĄį§ģé™¤é‡č¤‡é …į›ŽīŧŒæˆ‘們會æĒĸæŸĨīŧš", - "default_locale": "é č¨­åœ°å€", + "deduplication_info_description": "č‹Ĩčρč‡Ēå‹•é å…ˆé¸å–é …į›Žä¸Ļ扚æŦĄį§ģé™¤é‡č¤‡é …į›ŽīŧŒæˆ‘們會æĒĸæŸĨīŧš", + "default_locale": "é č¨­åœ°å€č¨­åŽš", "default_locale_description": "äžį…§æ‚¨įš„į€čĻŊå™¨åœ°å€č¨­åŽšæ ŧåŧåŒ–æ—ĨæœŸčˆ‡æ•¸å­—", "delete": "åˆĒ除", - "delete_action_confirmation_message": "您įĸē厚čρåˆĒ除此åĒ’éĢ”å—ŽīŧŸæ­¤æ“äŊœæœƒå°‡čОåĒ’éĢ”į§ģ臺äŧ翜å™¨įš„垃圞æĄļīŧŒä¸Ļ會提į¤ē您是åĻčρ圍æœŦ抟同時åˆĒ除", + "delete_action_confirmation_message": "您įĸē厚čρåˆĒé™¤æ­¤é …į›Žå—ŽīŧŸæ­¤å‹•äŊœæœƒå°‡čŠ˛é …į›Žį§ģ臺äŧ翜å™¨įš„垃圞æĄļīŧŒä¸ĻčŠĸ問您是åĻčρ圍æœŦ抟同æ­ĨåˆĒ除", "delete_action_prompt": "{count} 個厞åˆĒ除", "delete_album": "åˆĒ除ᛏį°ŋ", "delete_api_key_prompt": "您įĸē厚čρåˆĒ除這個 API 金鑰嗎īŧŸ", @@ -854,35 +919,36 @@ "delete_others": "åˆĒ除å…ļäģ–", "delete_permanently": "永䚅åˆĒ除", "delete_permanently_action_prompt": "åˇ˛æ°¸äš…åˆĒ除 {count} å€‹é …į›Ž", - "delete_shared_link": "åˆĒé™¤å…ąäēĢ逪įĩ", - "delete_shared_link_dialog_title": "åˆĒé™¤å…ąäēĢ逪įĩ", + "delete_shared_link": "åˆĒ除分äēĢ逪įĩ", + "delete_shared_link_dialog_title": "åˆĒ除分äēĢ逪įĩ", "delete_tag": "åˆĒé™¤æ¨™įą¤", "delete_tag_confirmation_prompt": "您įĸē厚čρåˆĒ除「{tagName}ã€æ¨™įą¤å—ŽīŧŸ", "delete_user": "åˆĒ除äŊŋᔍ者", "deleted_shared_link": "å…ąäēĢ逪įĩåˇ˛åˆĒ除", - "deletes_missing_assets": "åˆĒ除᪁įĸŸä¸­éēå¤ąįš„åĒ’éĢ”", + "deletes_missing_assets": "åˆĒ除᪁įĸŸä¸­éēå¤ąįš„é …į›Ž", "description": "描čŋ°", "description_input_hint_text": "新åĸžæčŋ°...", - "description_input_submit_error": "更新描čŋ°æ™‚į™ŧį”ŸéŒ¯čǤīŧŒčĢ‹æĒĸæŸĨæ—ĨčnjäģĨå–åž—æ›´å¤ščŠŗį´°čŗ‡č¨Š", + "description_input_submit_error": "更新čĒĒæ˜Žæ™‚į™ŧį”ŸéŒ¯čǤīŧŒčĢ‹æĒĸæŸĨį´€éŒ„äģĨå–åž—æ›´å¤ščŠŗį´°čŗ‡č¨Š", "deselect_all": "取æļˆå…¨é¸", "details": "čŠŗį´°čŗ‡č¨Š", "direction": "斚向", + "disable": "åœį”¨", "disabled": "åˇ˛åœį”¨", "disallow_edits": "ä¸å…č¨ąįˇ¨čŧ¯", "discord": "Discord", "discover": "æŽĸį´ĸ", - "discovered_devices": "厞æŽĸį´ĸįš„čŖįŊŽ", + "discovered_devices": "厞į™ŧįžįš„čŖįŊŽ", "dismiss_all_errors": "åŋŊį•Ĩ所有錯čǤ", "dismiss_error": "åŋŊį•Ĩ錯čǤ", "display_options": "éĄ¯į¤ē選項", "display_order": "éĄ¯į¤ē順åē", - "display_original_photos": "éĄ¯į¤ēåŽŸå§‹į…§į‰‡", - "display_original_photos_setting_description": "在æĒĸčĻ–åĒ’éĢ”æ™‚īŧŒč‹Ĩ原始åĒ’éĢ”čˆ‡įļ˛é į›¸åŽšīŧŒå‰‡å„Ēå…ˆéĄ¯į¤ēåŽŸå§‹į›¸į‰‡č€Œéžį¸Žåœ–ã€‚é€™å¯čƒŊæœƒå°Žč‡´į…§į‰‡éĄ¯į¤ē速åēĻ變æ…ĸ。", + "display_original_photos": "éĄ¯į¤ēåŽŸå§‹į›¸į‰‡", + "display_original_photos_setting_description": "在æĒĸčĻ–é …į›Žæ™‚īŧŒč‹ĨåŽŸå§‹é …į›Žčˆ‡įļ˛é į›¸åŽšīŧŒå‰‡å„Ēå…ˆéĄ¯į¤ēåŽŸå§‹į›¸į‰‡č€Œéžį¸Žåœ–ã€‚é€™å¯čƒŊæœƒå°Žč‡´į›¸į‰‡čŧ‰å…Ĩ速åēĻ變æ…ĸ。", "do_not_show_again": "ä¸å†éĄ¯į¤ēæ­¤č¨Šæ¯", "documentation": "čĒĒæ˜Žæ–‡äģļ", "done": "厌成", "download": "下čŧ‰", - "download_action_prompt": "æ­Ŗåœ¨ä¸‹čŧ‰ {count} 個åĒ’éĢ”", + "download_action_prompt": "æ­Ŗåœ¨ä¸‹čŧ‰ {count} å€‹é …į›Ž", "download_canceled": "下čŧ‰åˇ˛å–æļˆ", "download_complete": "下čŧ‰åŽŒæˆ", "download_enqueue": "厞加å…Ĩ下čŧ‰äŊ‡åˆ—", @@ -890,21 +956,23 @@ "download_failed": "下čŧ‰å¤ąæ•—", "download_finished": "下čŧ‰åŽŒæˆ", "download_include_embedded_motion_videos": "åĩŒå…ĨåŊąį‰‡", - "download_include_embedded_motion_videos_description": "å°‡å‹•æ…‹į›¸į‰‡ä¸­å…§åĩŒįš„åŊąį‰‡åĻ存į‚ēį¨įĢ‹æĒ”æĄˆ", + "download_include_embedded_motion_videos_description": "å°‡å‹•æ…‹į›¸į‰‡ä¸­å…§åĩŒįš„åŊąį‰‡å„˛å­˜į‚ēį¨įĢ‹æĒ”æĄˆ", "download_notfound": "į„Ąæŗ•æ‰žåˆ°ä¸‹čŧ‰", + "download_original": "下čŧ‰åŽŸå§‹æĒ”æĄˆ", "download_paused": "下čŧ‰åˇ˛æšĢ停", "download_settings": "下čŧ‰", - "download_settings_description": "įŽĄį†čˆ‡åĒ’é̔䏋čŧ‰į›¸é—œįš„設åޚ", + "download_settings_description": "įŽĄį†čˆ‡é …į›Žä¸‹čŧ‰į›¸é—œįš„設åޚ", "download_started": "厞開始䏋čŧ‰", "download_sucess": "下čŧ‰æˆåŠŸ", "download_sucess_android": "åĒ’é̔厞䏋čŧ‰č‡ŗ DCIM/Immich", "download_waiting_to_retry": "į­‰åž…é‡čŠĻ", "downloading": "下čŧ‰ä¸­", - "downloading_asset_filename": "æ­Ŗåœ¨ä¸‹čŧ‰åĒ’éĢ” {filename}", + "downloading_asset_filename": "æ­Ŗåœ¨ä¸‹čŧ‰é …į›Ž {filename}", + "downloading_from_icloud": "æ­ŖåžžiCloud下čŧ‰", "downloading_media": "æ­Ŗåœ¨ä¸‹čŧ‰åĒ’éĢ”", "drop_files_to_upload": "將æĒ”æĄˆæ‹–æ”žåˆ°äģģäŊ•äŊįŊŽäģĨä¸Šå‚ŗ", "duplicates": "é‡č¤‡é …į›Ž", - "duplicates_description": "逐一æĒĸæŸĨæ¯å€‹įž¤įĩ„īŧŒä¸Ļ標į¤ēå…ļ中是åĻæœ‰é‡č¤‡åĒ’éĢ”", + "duplicates_description": "逐一æĒĸæŸĨæ¯å€‹įž¤įĩ„īŧŒä¸Ļ標į¤ēå…ļ中是åĻæœ‰é‡č¤‡é …į›Ž", "duration": "éĄ¯į¤ēæ™‚é•ˇ", "edit": "ᎍčŧ¯", "edit_album": "ᎍčŧ¯į›¸į°ŋ", @@ -929,16 +997,27 @@ "edit_tag": "ᎍčŧ¯æ¨™įą¤", "edit_title": "ᎍčŧ¯æ¨™éĄŒ", "edit_user": "ᎍčŧ¯äŊŋᔍ者", + "edit_workflow": "ᎍčŧ¯åˇĨäŊœæĩį¨‹", "editor": "ᎍčŧ¯å™¨", - "editor_close_without_save_prompt": "æ­¤čŽŠæ›´å°‡ä¸æœƒčĸĢå„˛å­˜", + "editor_close_without_save_prompt": "čŽŠæ›´å°‡ä¸æœƒčĸĢå„˛å­˜", "editor_close_without_save_title": "čĻé—œé–‰įˇ¨čŧ¯å™¨å—ŽīŧŸ", - "editor_crop_tool_h2_aspect_ratios": "長å¯Ŧ比", - "editor_crop_tool_h2_rotation": "旋čŊ‰", + "editor_confirm_reset_all_changes": "äŊ įĸē厚čĻé‡č¨­æ‰€æœ‰čŽŠæ›´å—ŽīŧŸ", + "editor_discard_edits_confirm": "æ”žæŖ„įˇ¨čŧ¯", + "editor_discard_edits_prompt": "您有尚æœĒå„˛å­˜įš„įˇ¨čŧ¯å…§åŽšã€‚įĸē厚čĻæ¨æŖ„å—ŽīŧŸ", + "editor_discard_edits_title": "įĸēčĒæ”žæŖ„įˇ¨čŧ¯å—ŽīŧŸ", + "editor_edits_applied_error": "į„Ąæŗ•åĨ—ᔍᎍčŧ¯", + "editor_edits_applied_success": "åˇ˛æˆåŠŸåĨ—ᔍᎍčŧ¯", + "editor_flip_horizontal": "æ°´åšŗįŋģčŊ‰", + "editor_flip_vertical": "åž‚į›´įŋģčŊ‰", + "editor_orientation": "斚向", + "editor_reset_all_changes": "é‡č¨­čŽŠæ›´", + "editor_rotate_left": "逆時針旋čŊ‰90åēĻ", + "editor_rotate_right": "順時針旋čŊ‰90åēĻ", "email": "é›ģ子éƒĩäģļ", "email_notifications": "Email 通įŸĨ", "empty_folder": "é€™å€‹čŗ‡æ–™å¤žæ˜¯įŠēįš„", "empty_trash": "清įŠē垃圞æĄļ", - "empty_trash_confirmation": "您įĸē厚čĻæ¸…įŠē垃圞æĄļ嗎īŧŸé€™æœƒæ°¸äš…åˆĒ除 Immich 垃圞æĄļä¸­æ‰€æœ‰įš„åĒ’éĢ”ã€‚\næ‚¨į„Ąæŗ•æ’¤éŠˇæ­¤čŽŠæ›´īŧ", + "empty_trash_confirmation": "您įĸē厚čĻæ¸…įŠē垃圞æĄļ嗎īŧŸé€™æœƒåžž Immich 永䚅į§ģ除垃圞æĄļä¸­æ‰€æœ‰įš„é …į›Žã€‚\næ‚¨į„Ąæŗ•åžŠåŽŸæ­¤å‹•äŊœīŧ", "enable": "å•Ÿį”¨", "enable_backup": "å•Ÿį”¨å‚™äģŊ", "enable_biometric_auth_description": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧäģĨå•Ÿį”¨į”Ÿį‰Ščž¨č­˜éŠ—č­‰", @@ -947,90 +1026,95 @@ "enqueued": "åˇ˛æŽ’å…ĨäŊ‡åˆ—", "enter_wifi_name": "čŧ¸å…Ĩ Wi-Fi åį¨ą", "enter_your_pin_code": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧ", - "enter_your_pin_code_subtitle": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧäģĨå­˜å–éŽ–åŽšįš„čŗ‡æ–™å¤ž", + "enter_your_pin_code_subtitle": "čŧ¸å…Ĩæ‚¨įš„ PIN įĸŧäģĨå­˜å–ã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤ž", "error": "錯čǤ", "error_change_sort_album": "čŽŠæ›´į›¸į°ŋ排åēå¤ąæ•—", - "error_delete_face": "åžžåĒ’éĢ”åˆĒé™¤č‡‰å­”æ™‚å¤ąæ•—", + "error_delete_face": "åžžé …į›ŽåˆĒé™¤č‡‰å­”æ™‚į™ŧį”ŸéŒ¯čǤ", "error_getting_places": "取垗äŊįŊŽæ™‚å‡ē錯", + "error_loading_albums": "čŧ‰å…Ĩᛏį°ŋ時į™ŧį”ŸéŒ¯čǤ", "error_loading_image": "åœ–į‰‡čŧ‰å…Ĩ錯čǤ", - "error_loading_partners": "čŧ‰å…Ĩ合äŊœå¤Ĩäŧ´æ™‚å‡ē錯īŧš{error}", + "error_loading_partners": "čŧ‰å…ĨčĻĒ友時į™ŧį”ŸéŒ¯čǤīŧš{error}", + "error_retrieving_asset_information": "į„Ąæŗ•å–åž—é …į›Žčŗ‡č¨Š", "error_saving_image": "錯čǤīŧš{error}", "error_tag_face_bounding_box": "æ¨™č¨˜č‡‰éƒ¨éŒ¯čǤ - į„Ąæŗ•å–åž—é‚Šį•ŒæĄ†åæ¨™", "error_title": "錯čǤ - į™ŧį”ŸéŒ¯čǤ", + "error_while_navigating": "į„Ąæŗ•åŧ•å°Žč‡ŗé …į›Ž", "errors": { - "cannot_navigate_next_asset": "į„Ąæŗ•å°ŽčĻŊ臺䏋䏀個åĒ’éĢ”", - "cannot_navigate_previous_asset": "į„Ąæŗ•å°ŽčĻŊč‡ŗä¸Šä¸€å€‹åĒ’éĢ”", + "cannot_navigate_next_asset": "į„Ąæŗ•å°ŽčĻŊč‡ŗä¸‹ä¸€å€‹é …į›Ž", + "cannot_navigate_previous_asset": "į„Ąæŗ•å°ŽčĻŊč‡ŗä¸Šä¸€å€‹é …į›Ž", "cant_apply_changes": "į„Ąæŗ•åĨ—į”¨čŽŠæ›´", "cant_change_activity": "į„Ąæŗ•{enabled, select, true {åœį”¨} other {å•Ÿį”¨}}æ´ģ動", - "cant_change_asset_favorite": "į„Ąæŗ•čŽŠæ›´æĒ”æĄˆįš„æ”ļč—į‹€æ…‹", - "cant_change_metadata_assets_count": "į„Ąæŗ•čŽŠæ›´ {count, plural, other {# 個æĒ”æĄˆ}}įš„ä¸­įšŧčŗ‡æ–™", + "cant_change_asset_favorite": "į„Ąæŗ•čŽŠæ›´é …į›Žįš„æ”ļč—į‹€æ…‹", + "cant_change_metadata_assets_count": "į„Ąæŗ•čŽŠæ›´ {count, plural, other {# å€‹é …į›Ž}} įš„ä¸­įšŧčŗ‡æ–™", "cant_get_faces": "į„Ąæŗ•å–åž—č‡‰å­”", "cant_get_number_of_comments": "į„Ąæŗ•å–åž—į•™č¨€æ•¸é‡", "cant_search_people": "į„Ąæŗ•æœå°‹äēēį‰Š", "cant_search_places": "į„Ąæŗ•æœå°‹åœ°éģž", - "error_adding_assets_to_album": "將åĒ’éĢ”åŠ å…Ĩᛏį°ŋ時į™ŧį”ŸéŒ¯čǤ", + "error_adding_assets_to_album": "å°‡é …į›ŽåŠ å…Ĩᛏį°ŋ時į™ŧį”ŸéŒ¯čǤ", "error_adding_users_to_album": "將äŊŋį”¨č€…åŠ å…Ĩᛏį°ŋ時į™ŧį”ŸéŒ¯čǤ", "error_deleting_shared_user": "åˆĒé™¤å…ąäēĢäŊŋį”¨č€…æ™‚į™ŧį”ŸéŒ¯čǤ", "error_downloading": "下čŧ‰ {filename} 時į™ŧį”ŸéŒ¯čǤ", "error_hiding_buy_button": "隱藏čŗŧč˛ˇæŒ‰éˆ•æ™‚į™ŧį”ŸéŒ¯čǤ", - "error_removing_assets_from_album": "åžžį›¸į°ŋį§ģ除åĒ’éĢ”æ™‚į™ŧį”ŸéŒ¯čǤīŧŒčĢ‹æĒĸæŸĨä¸ģ控č‡ēäģĨå–åž—æ›´å¤ščŠŗį´°čŗ‡č¨Š", + "error_removing_assets_from_album": "åžžį›¸į°ŋį§ģé™¤é …į›Žæ™‚į™ŧį”ŸéŒ¯čǤīŧŒčĢ‹æĒĸæŸĨä¸ģ控台äģĨå–åž—æ›´å¤ščŠŗį´°čŗ‡č¨Š", "error_selecting_all_assets": "選取所有æĒ”æĄˆæ™‚į™ŧį”ŸéŒ¯čǤ", "exclusion_pattern_already_exists": "æ­¤æŽ’é™¤æ¨Ąåŧåˇ˛å­˜åœ¨ã€‚", "failed_to_create_album": "ᛏį°ŋåģēįĢ‹å¤ąæ•—", - "failed_to_create_shared_link": "åģēįĢ‹å…ąäēĢ逪įĩå¤ąæ•—", - "failed_to_edit_shared_link": "ᎍčŧ¯å…ąäēĢ逪įĩå¤ąæ•—", + "failed_to_create_shared_link": "分äēĢ逪įĩåģēįĢ‹å¤ąæ•—", + "failed_to_edit_shared_link": "分äēĢ逪įĩįˇ¨čŧ¯å¤ąæ•—", "failed_to_get_people": "į„Ąæŗ•å–åž—äēēį‰Š", - "failed_to_keep_this_delete_others": "į„Ąæŗ•äŋį•™æ­¤åĒ’éĢ”ä¸ĻåˆĒ除å…ļäģ–åĒ’éĢ”", - "failed_to_load_asset": "åĒ’éĢ”čŧ‰å…Ĩå¤ąæ•—", - "failed_to_load_assets": "åĒ’éĢ”čŧ‰å…Ĩå¤ąæ•—", + "failed_to_keep_this_delete_others": "į„Ąæŗ•äŋį•™æ­¤é …į›Žä¸ĻåˆĒ除å…ļäģ–é …į›Ž", + "failed_to_load_asset": "é …į›Žčŧ‰å…Ĩå¤ąæ•—", + "failed_to_load_assets": "é …į›Žčŧ‰å…Ĩå¤ąæ•—", "failed_to_load_notifications": "čŧ‰å…Ĩ通įŸĨå¤ąæ•—", "failed_to_load_people": "čŧ‰å…Ĩäēēį‰Šå¤ąæ•—", "failed_to_remove_product_key": "į§ģ除į”ĸå“é‡‘é‘°å¤ąæ•—", "failed_to_reset_pin_code": "重設 PIN įĸŧå¤ąæ•—", - "failed_to_stack_assets": "į„Ąæŗ•åĒ’éĢ”å †į–Š", - "failed_to_unstack_assets": "觪除åĒ’éĢ”å †į–Šå¤ąæ•—", + "failed_to_stack_assets": "é …į›Žå †į–Šå¤ąæ•—", + "failed_to_unstack_assets": "č§Ŗé™¤é …į›Žå †į–Šå¤ąæ•—", "failed_to_update_notification_status": "į„Ąæŗ•æ›´æ–°é€šįŸĨį‹€æ…‹", "incorrect_email_or_password": "é›ģ子éƒĩäģ￈–密įĸŧ錯čǤ", - "library_folder_already_exists": "此導å…Ĩčˇ¯åž‘åˇ˛å­˜åœ¨ã€‚", + "library_folder_already_exists": "此匯å…Ĩčˇ¯åž‘åˇ˛å­˜åœ¨ã€‚", "paths_validation_failed": "{paths, plural, one {# å€‹čˇ¯åž‘} other {# å€‹čˇ¯åž‘}} éŠ—č­‰å¤ąæ•—", "profile_picture_transparent_pixels": "個äēēčŗ‡æ–™åœ–į‰‡ä¸čƒŊ有透明į•Ģį´ ã€‚čĢ‹æ”žå¤§ä¸Ļ/或į§ģ動åŊąåƒã€‚", - "quota_higher_than_disk_size": "æ‚¨æ‰€č¨­åŽšįš„é…éĄå¤§æ–ŧ᪁įĸŸå¤§å°", + "quota_higher_than_disk_size": "æ‚¨č¨­åŽšįš„é…éĄå¤§æ–ŧ᪁įĸŸåŽšé‡", "something_went_wrong": "į™ŧį”ŸéŒ¯čǤ", "unable_to_add_album_users": "į„Ąæŗ•å°‡äŊŋį”¨č€…åŠ å…Ĩᛏį°ŋ", - "unable_to_add_assets_to_shared_link": "į„Ąæŗ•åŠ å…ĨåĒ’éĢ”åˆ°å…ąäēĢ逪įĩ", + "unable_to_add_assets_to_shared_link": "į„Ąæŗ•å°‡é …į›ŽåŠ å…Ĩč‡ŗåˆ†äēĢ逪įĩ", "unable_to_add_comment": "į„Ąæŗ•æ–°åĸžį•™č¨€", - "unable_to_add_exclusion_pattern": "į„Ąæŗ•æ–°åĸžį¯Šé¸æĸäģļ", - "unable_to_add_partners": "į„Ąæŗ•æ–°åĸžčĻĒæœ‹åĨŊ友", - "unable_to_add_remove_archive": "į„Ąæŗ•{archived, select, true {垞封存中į§ģ除åĒ’éĢ”} other {將æĒ”æĄˆåŠ å…ĨåĒ’éĢ”}}", - "unable_to_add_remove_favorites": "į„Ąæŗ•å°‡åĒ’éĢ”{favorite, select, true {加å…Ĩæ”ļ藏} other {åžžæ”ļ藏中į§ģ除}}", + "unable_to_add_exclusion_pattern": "į„Ąæŗ•æ–°åĸžæŽ’é™¤æ¨Ąåŧ", + "unable_to_add_partners": "į„Ąæŗ•æ–°åĸžčĻĒ友", + "unable_to_add_remove_archive": "į„Ąæŗ•å°‡é …į›Ž{archived, select, true {垞封存中į§ģ除} other {加å…Ĩč‡ŗå°å­˜}}", + "unable_to_add_remove_favorites": "į„Ąæŗ•å°‡é …į›Ž{favorite, select, true {加å…Ĩæ”ļ藏} other {åžžæ”ļ藏中į§ģ除}}", "unable_to_archive_unarchive": "į„Ąæŗ•{archived, select, true {封存} other {取æļˆå°å­˜}}", "unable_to_change_album_user_role": "į„Ąæŗ•čŽŠæ›´į›¸į°ŋäŊŋį”¨č€…įš„č§’č‰˛", "unable_to_change_date": "į„Ąæŗ•čŽŠæ›´æ—Ĩ期", "unable_to_change_description": "į„Ąæŗ•čŽŠæ›´æčŋ°", - "unable_to_change_favorite": "į„Ąæŗ•čŽŠæ›´åĒ’éĢ”įš„æ”ļč—į‹€æ…‹", + "unable_to_change_favorite": "į„Ąæŗ•čŽŠæ›´é …į›Žįš„æ”ļč—į‹€æ…‹", "unable_to_change_location": "į„Ąæŗ•čŽŠæ›´äŊįŊŽ", "unable_to_change_password": "į„Ąæŗ•čŽŠæ›´å¯†įĸŧ", "unable_to_change_visibility": "į„Ąæŗ•čŽŠæ›´ {count, plural, one {# äŊäēēį‰Š} other {# äŊäēēį‰Š}} įš„å¯čĻ‹æ€§", "unable_to_complete_oauth_login": "į„Ąæŗ•åŽŒæˆ OAuth į™ģå…Ĩ", "unable_to_connect": "į„Ąæŗ•é€Ŗįˇš", - "unable_to_copy_to_clipboard": "į„Ąæŗ•č¤‡čŖŊ到å‰Ēč˛ŧį°ŋīŧŒčĢ‹įĸēäŋæ‚¨æ˜¯äģĨ https 存取æœŦ頁éĸ", + "unable_to_copy_to_clipboard": "į„Ąæŗ•č¤‡čŖŊ到å‰Ēč˛ŧį°ŋīŧŒčĢ‹įĸēäŋæ‚¨æ­Ŗé€éŽ https 存取此頁éĸ", + "unable_to_create": "į„Ąæŗ•åģēįĢ‹åˇĨäŊœæĩį¨‹", "unable_to_create_admin_account": "į„Ąæŗ•åģēįĢ‹įŽĄį†å“Ąå¸ŗč™Ÿ", "unable_to_create_api_key": "į„Ąæŗ•åģēįĢ‹æ–°įš„ API 金鑰", "unable_to_create_library": "į„Ąæŗ•åģēįĢ‹åĒ’éĢ”åēĢ", "unable_to_create_user": "į„Ąæŗ•åģēįĢ‹äŊŋᔍ者", "unable_to_delete_album": "į„Ąæŗ•åˆĒ除ᛏį°ŋ", - "unable_to_delete_asset": "į„Ąæŗ•åˆĒ除åĒ’éĢ”", - "unable_to_delete_assets": "åˆĒ除åĒ’éĢ”æ™‚į™ŧį”ŸéŒ¯čǤ", + "unable_to_delete_asset": "į„Ąæŗ•åˆĒé™¤é …į›Ž", + "unable_to_delete_assets": "åˆĒé™¤é …į›Žæ™‚į™ŧį”ŸéŒ¯čǤ", "unable_to_delete_exclusion_pattern": "į„Ąæŗ•åˆĒé™¤į¯Šé¸æĸäģļ", - "unable_to_delete_shared_link": "åˆĒé™¤å…ąäēĢ逪įĩå¤ąæ•—", + "unable_to_delete_shared_link": "į„Ąæŗ•åˆĒ除分äēĢ逪įĩ", "unable_to_delete_user": "į„Ąæŗ•åˆĒ除äŊŋᔍ者", + "unable_to_delete_workflow": "į„Ąæŗ•åˆĒ除åˇĨäŊœæĩį¨‹", "unable_to_download_files": "į„Ąæŗ•ä¸‹čŧ‰æĒ”æĄˆ", "unable_to_edit_exclusion_pattern": "į„Ąæŗ•įˇ¨čŧ¯į¯Šé¸æĸäģļ", "unable_to_empty_trash": "į„Ąæŗ•æ¸…įŠē垃圞æĄļ", "unable_to_enter_fullscreen": "į„Ąæŗ•é€˛å…Ĩ全čžĸåš•", "unable_to_exit_fullscreen": "į„Ąæŗ•įĩæŸå…¨čžĸåš•", "unable_to_get_comments_number": "į„Ąæŗ•å–åž—į•™č¨€æ•¸é‡", - "unable_to_get_shared_link": "å–åž—å…ąäēĢ逪įĩå¤ąæ•—", + "unable_to_get_shared_link": "取垗分äēĢ逪įĩå¤ąæ•—", "unable_to_hide_person": "į„Ąæŗ•éšąč—äēēį‰Š", "unable_to_link_motion_video": "į„Ąæŗ•é€Ŗįĩå‹•æ…‹åŊąį‰‡", "unable_to_link_oauth_account": "į„Ąæŗ•é€Ŗįĩ OAuth å¸ŗč™Ÿ", @@ -1038,19 +1122,19 @@ "unable_to_log_out_device": "į„Ąæŗ•į™ģå‡ēčŖįŊŽ", "unable_to_login_with_oauth": "į„Ąæŗ•äŊŋᔍ OAuth į™ģå…Ĩ", "unable_to_play_video": "į„Ąæŗ•æ’­æ”žåŊąį‰‡", - "unable_to_reassign_assets_existing_person": "į„Ąæŗ•å°‡æĒ”æĄˆé‡æ–°æŒ‡æ´žįĩĻ {name, select, null {įžæœ‰įš„äēēå“Ą} other {{name}}}", - "unable_to_reassign_assets_new_person": "į„Ąæŗ•å°‡åĒ’éĢ”é‡æ–°æŒ‡æ´žįĩĻæ–°įš„äēēį‰Š", + "unable_to_reassign_assets_existing_person": "į„Ąæŗ•å°‡é …į›Žé‡æ–°æŒ‡æ´žįĩĻ {name, select, null {įžæœ‰äēēį‰Š} other {{name}}}", + "unable_to_reassign_assets_new_person": "į„Ąæŗ•å°‡é …į›Žé‡æ–°æŒ‡æ´žįĩĻæ–°įš„äēēį‰Š", "unable_to_refresh_user": "į„Ąæŗ•é‡æ–°æ•´į†äŊŋᔍ者", "unable_to_remove_album_users": "į„Ąæŗ•åžžį›¸į°ŋ中į§ģ除äŊŋᔍ者", "unable_to_remove_api_key": "į„Ąæŗ•į§ģ除 API 金鑰", - "unable_to_remove_assets_from_shared_link": "åˆĒé™¤å…ąäēĢ逪įĩä¸­åĒ’éĢ”å¤ąæ•—", + "unable_to_remove_assets_from_shared_link": "į„Ąæŗ•åžžåˆ†äēĢ逪įĩä¸­į§ģé™¤é …į›Ž", "unable_to_remove_library": "į„Ąæŗ•į§ģ除åĒ’éĢ”åēĢ", - "unable_to_remove_partner": "į„Ąæŗ•į§ģ除čĻĒæœ‹åĨŊ友", + "unable_to_remove_partner": "į„Ąæŗ•į§ģ除čĻĒ友", "unable_to_remove_reaction": "į„Ąæŗ•į§ģ除反應", "unable_to_reset_password": "į„Ąæŗ•é‡č¨­å¯†įĸŧ", "unable_to_reset_pin_code": "į„Ąæŗ•é‡č¨­ PIN įĸŧ", "unable_to_resolve_duplicate": "į„Ąæŗ•č§Ŗæąēé‡č¤‡é …į›Ž", - "unable_to_restore_assets": "į„Ąæŗ•é‚„åŽŸåĒ’éĢ”", + "unable_to_restore_assets": "į„Ąæŗ•é‚„åŽŸé …į›Ž", "unable_to_restore_trash": "į„Ąæŗ•é‚„åŽŸåžƒåœžæĄļ", "unable_to_restore_user": "į„Ąæŗ•é‚„åŽŸäŊŋᔍ者", "unable_to_save_album": "į„Ąæŗ•å„˛å­˜į›¸į°ŋ", @@ -1061,10 +1145,11 @@ "unable_to_save_settings": "į„Ąæŗ•å„˛å­˜č¨­åŽš", "unable_to_scan_libraries": "į„Ąæŗ•æŽƒæåĒ’éĢ”åēĢ", "unable_to_scan_library": "į„Ąæŗ•æŽƒæåĒ’éĢ”åēĢ", - "unable_to_set_feature_photo": "į„Ąæŗ•č¨­åŽšå°éĸåœ–į‰‡", + "unable_to_set_feature_photo": "į„Ąæŗ•č¨­åŽšį˛žé¸į›¸į‰‡", "unable_to_set_profile_picture": "į„Ąæŗ•č¨­åŽšå€‹äēēčŗ‡æ–™åœ–į‰‡", + "unable_to_set_rating": "į„Ąæŗ•č¨­åŽščŠ•æ˜Ÿ", "unable_to_submit_job": "į„Ąæŗ•æäē¤äģģ務", - "unable_to_trash_asset": "į„Ąæŗ•å°‡åĒ’éĢ”ä¸Ÿé€˛åžƒåœžæĄļ", + "unable_to_trash_asset": "į„Ąæŗ•å°‡é …į›Žį§ģč‡ŗåžƒåœžæĄļ", "unable_to_unlink_account": "į„Ąæŗ•č§Ŗé™¤å¸ŗč™Ÿé€Ŗįĩ", "unable_to_unlink_motion_video": "į„Ąæŗ•č§Ŗé™¤é€Ŗįĩå‹•æ…‹åŊąį‰‡", "unable_to_update_album_cover": "į„Ąæŗ•æ›´æ–°į›¸į°ŋ封éĸ", @@ -1074,10 +1159,12 @@ "unable_to_update_settings": "į„Ąæŗ•æ›´æ–°č¨­åŽš", "unable_to_update_timeline_display_status": "į„Ąæŗ•æ›´æ–°æ™‚é–“čģ¸éĄ¯į¤ēį‹€æ…‹", "unable_to_update_user": "į„Ąæŗ•æ›´æ–°äŊŋᔍ者", + "unable_to_update_workflow": "į„Ąæŗ•æ›´æ–°åˇĨäŊœæĩį¨‹", "unable_to_upload_file": "į„Ąæŗ•ä¸Šå‚ŗæĒ”æĄˆ" }, + "errors_text": "錯čǤ", "exclusion_pattern": "æŽ’é™¤æ¨Ąåŧ", - "exif": "EXIF 可ä礿›åŊąåƒæĒ”æ ŧåŧ", + "exif": "EXIF", "exif_bottom_sheet_description": "新åĸžæčŋ°...", "exif_bottom_sheet_description_error": "更新描čŋ°æ™‚į™ŧį”ŸéŒ¯čǤ", "exif_bottom_sheet_details": "čŠŗį´°čŗ‡æ–™", @@ -1086,6 +1173,7 @@ "exif_bottom_sheet_people": "äēēį‰Š", "exif_bottom_sheet_person_add_person": "新åĸžå§“名", "exit_slideshow": "įĩæŸåšģį‡ˆį‰‡", + "expand": "åą•é–‹", "expand_all": "åą•é–‹å…¨éƒ¨", "experimental_settings_new_asset_list_subtitle": "æ­Ŗåœ¨č™•į†", "experimental_settings_new_asset_list_title": "å•Ÿį”¨å¯ĻéŠ—æ€§į›¸į‰‡æ ŧį‹€į‰ˆéĸ", @@ -1104,31 +1192,34 @@ "external": "外部", "external_libraries": "外部åĒ’éĢ”åēĢ", "external_network": "外部įļ˛čˇ¯", - "external_network_sheet_info": "č‹ĨæœĒé€Ŗįˇšč‡ŗååĨŊįš„ Wi-FiīŧŒå°‡äžåˆ—čĄ¨åžžä¸Šåˆ°ä¸‹é¸æ“‡å¯é€Ŗįˇšįš„äŧ翜å™¨įļ˛å€", - "face_unassigned": "æœĒ指厚", + "external_network_sheet_info": "č‹ĨæœĒ逪įļ˛č‡ŗååĨŊįš„ Wi-FiīŧŒå°‡äžæ¸…å–Žåžžä¸Šåˆ°ä¸‹é¸æ“‡å¯é€Ŗįˇšįš„äŧ翜å™¨įļ˛å€", + "face_unassigned": "æœĒ指洞", "failed": "å¤ąæ•—", "failed_count": "å¤ąæ•—īŧš{count}", "failed_to_authenticate": "čēĢäģŊéŠ—č­‰å¤ąæ•—", - "failed_to_load_assets": "į„Ąæŗ•čŧ‰å…ĨåĒ’éĢ”", + "failed_to_load_assets": "é …į›Žčŧ‰å…Ĩå¤ąæ•—", "failed_to_load_folder": "į„Ąæŗ•čŧ‰å…Ĩčŗ‡æ–™å¤ž", "favorite": "æ”ļ藏", - "favorite_action_prompt": "åˇ˛æ–°åĸž {count} 個到æ”ļ藏", - "favorite_or_unfavorite_photo": "æ”ļč—æˆ–å–æļˆæ”ļč—į…§į‰‡", + "favorite_action_prompt": "厞將 {count} å€‹é …į›ŽåŠ å…Ĩæ”ļ藏", + "favorite_or_unfavorite_photo": "æ”ļč—æˆ–å–æļˆæ”ļč—į›¸į‰‡", "favorites": "æ”ļ藏", "favorites_page_no_favorites": "æœĒ扞到æ”ļč—é …į›Ž", - "feature_photo_updated": "į‰šč‰˛į…§į‰‡åˇ˛æ›´æ–°", + "feature_photo_updated": "į˛žé¸į›¸į‰‡åˇ˛æ›´æ–°", "features": "功čƒŊ", "features_in_development": "į™ŧåą•ä¸­įš„į‰šéģž", "features_setting_description": "įŽĄį†æ‡‰į”¨į¨‹åŧåŠŸčƒŊ", - "file_name": "æĒ”æĄˆåį¨ą", "file_name_or_extension": "æĒ”æĄˆåį¨ąæˆ–å‰¯æĒ”名", - "file_size": "文äģļ大小", + "file_name_text": "æĒ”æĄˆåį¨ą", + "file_name_with_value": "æĒ”æĄˆåį¨ą: {file_name}", + "file_size": "æĒ”æĄˆå¤§å°", "filename": "æĒ”æĄˆåį¨ą", "filetype": "æĒ”æĄˆéĄžåž‹", "filter": "æŋžéĄ", + "filter_description": "į¯Šé¸į›Žæ¨™é …į›Žįš„æĸäģļ", "filter_people": "į¯Šé¸äēēį‰Š", "filter_places": "į¯Šé¸åœ°éģž", - "find_them_fast": "é€éŽæœå°‹åį¨ąåŋĢ速扞到äģ–們", + "filters": "į¯Šé¸å™¨", + "find_them_fast": "透過搜尋姓名åŋĢ速扞到äģ–們", "first": "įŦŦ一個", "fix_incorrect_match": "äŋŽåžŠä¸į›¸įŦĻįš„", "folder": "čŗ‡æ–™å¤ž", @@ -1137,12 +1228,16 @@ "folders_feature_description": "é€éŽčŗ‡æ–™å¤žæĒĸčĻ–į€čĻŊæĒ”æĄˆįŗģįĩąä¸­įš„į›¸į‰‡čˆ‡åŊąį‰‡", "forgot_pin_code_question": "åŋ˜č¨˜æ‚¨įš„ PIN įĸŧīŧŸ", "forward": "į”ąæ–°č‡ŗčˆŠ", + "free_up_space": "釋攞įŠē間", + "free_up_space_description": "將厞備äģŊįš„į›¸į‰‡čˆ‡åŊąį‰‡į§ģč‡ŗčŖįŊŽåžƒåœžæĄļäģĨ釋攞įŠē間。äŧ翜å™¨ä¸Šįš„å‚™äģŊ將äŋæŒåŽ‰å…¨ã€‚", + "free_up_space_settings_subtitle": "é‡‹æ”žčŖįŊŽå„˛å­˜įŠē間", "full_path": "åŽŒæ•´čˇ¯åž‘īŧš{path}", "gcast_enabled": "Google Cast", "gcast_enabled_description": "此功čƒŊ需čĻåžž Google čŧ‰å…Ĩå¤–éƒ¨čŗ‡æēæ‰čƒŊæ­Ŗå¸¸é‹äŊœã€‚", "general": "一čˆŦ", "geolocation_instruction_location": "éģžé¸å…ˇæœ‰ GPS åē§æ¨™įš„é …į›ŽäģĨäŊŋᔍå…ļäŊįŊŽīŧŒæˆ–į›´æŽĨ垞地圖中選擇地éģž", "get_help": "取垗協劊", + "get_people_error": "取垗äēēį‰Šæ™‚į™ŧį”ŸéŒ¯čǤ", "get_wifiname_error": "į„Ąæŗ•å–åž— Wi-Fi åį¨ąã€‚čĢ‹įĸēčĒæ‚¨åˇ˛æŽˆäēˆåŋ…čĻįš„æŦŠé™īŧŒä¸Ļåˇ˛é€Ŗįˇšč‡ŗ Wi-Fi įļ˛čˇ¯", "getting_started": "開始äŊŋᔍ", "go_back": "上一頁", @@ -1153,15 +1248,15 @@ "grant_permission": "授ä爿ŦŠé™", "group_albums_by": "åˆ†éĄžįž¤įĩ„įš„æ–šåŧ...", "group_country": "æŒ‰į…§åœ‹åŽļåˆ†éĄž", - "group_no": "æ˛’æœ‰åˆ†éĄž", + "group_no": "不分įĩ„", "group_owner": "æŒ‰æ“æœ‰č€…åˆ†éĄž", "group_places_by": "åˆ†éĄžåœ°éģžįš„æ–šåŧ...", "group_year": "按嚴äģŊåˆ†éĄž", "haptic_feedback_switch": "å•Ÿį”¨éœ‡å‹•å›žéĨ‹", "haptic_feedback_title": "震動回éĨ‹", "has_quota": "åˇ˛č¨­åŽšé…éĄ", - "hash_asset": "雜暊åĒ’éĢ”", - "hashed_assets": "åˇ˛é›œæšŠįš„åĒ’éĢ”", + "hash_asset": "é›œæšŠé …į›Ž", + "hashed_assets": "åˇ˛é›œæšŠįš„é …į›Ž", "hashing": "æ­Ŗåœ¨č¨ˆįŽ—é›œæšŠå€ŧ", "header_settings_add_header_tip": "新åĸžæ¨™é ­", "header_settings_field_validator_msg": "å€ŧ不可į‚ēįŠē", @@ -1175,42 +1270,43 @@ "hide_named_person": "隱藏 {name}", "hide_password": "éšąč—å¯†įĸŧ", "hide_person": "隱藏äēēį‰Š", - "hide_text_recognition": "éšąč—æ–‡å­—č­˜åˆĨ", + "hide_schema": "隱藏æžļ構", + "hide_text_recognition": "éšąč—æ–‡å­—čž¨č­˜", "hide_unnamed_people": "隱藏æœĒå‘Ŋåįš„äēēį‰Š", - "home_page_add_to_album_conflicts": "厞將 {added} 個åĒ’éĢ”æ–°åĸžåˆ°į›¸į°ŋ {album}。{failed} 個åĒ’éĢ”åˇ˛åœ¨čŠ˛į›¸į°ŋ中。", - "home_page_add_to_album_err_local": "æšĢ時不čƒŊ將æœŦ抟åĒ’éĢ”æ–°åĸžåˆ°į›¸į°ŋīŧŒåˇ˛į•Ĩ過", - "home_page_add_to_album_success": "厞圍 {album} ᛏį°ŋ中新åĸž {added} 個åĒ’éĢ”ã€‚", - "home_page_album_err_partner": "æšĢ時不čƒŊį„Ąæŗ•å°‡čĻĒæœ‹åĨŊå‹įš„åĒ’éĢ”æ–°åĸžåˆ°į›¸į°ŋīŧŒåˇ˛į•Ĩ過", - "home_page_archive_err_local": "æšĢ時不čƒŊ封存æœŦ抟åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", - "home_page_archive_err_partner": "į„Ąæŗ•å°å­˜čĻĒæœ‹åĨŊå‹įš„åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", + "home_page_add_to_album_conflicts": "厞將 {added} å€‹é …į›Žæ–°åĸžč‡ŗį›¸į°ŋ {album}。{failed} å€‹é …į›Žåˇ˛åœ¨čŠ˛į›¸į°ŋ中。", + "home_page_add_to_album_err_local": "į›Žå‰į„Ąæŗ•å°‡æœŦæŠŸé …į›Žæ–°åĸžč‡ŗį›¸į°ŋīŧŒåˇ˛į•Ĩ過", + "home_page_add_to_album_success": "厞將 {added} å€‹é …į›Žæ–°åĸžč‡ŗį›¸į°ŋ {album}。", + "home_page_album_err_partner": "į›Žå‰į„Ąæŗ•å°‡čĻĒå‹å…ąäēĢé …į›Žæ–°åĸžč‡ŗį›¸į°ŋīŧŒåˇ˛į•Ĩ過", + "home_page_archive_err_local": "į›Žå‰į„Ąæŗ•å°å­˜æœŦæŠŸé …į›ŽīŧŒåˇ˛į•Ĩ過", + "home_page_archive_err_partner": "į„Ąæŗ•å°å­˜čĻĒå‹å…ąäēĢé …į›ŽīŧŒåˇ˛į•Ĩ過", "home_page_building_timeline": "æ­Ŗåœ¨åģēįĢ‹æ™‚é–“čģ¸", - "home_page_delete_err_partner": "į„Ąæŗ•åˆĒ除čĻĒæœ‹åĨŊå‹įš„åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", - "home_page_delete_remote_err_local": "åˆĒ除遠į̝åĒ’éĢ”įš„é¸å–ä¸­åŒ…åĢæœŦ抟åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", - "home_page_favorite_err_local": "æšĢ不čƒŊæ”ļ藏æœŦæŠŸé …į›ŽīŧŒį•Ĩ過", - "home_page_favorite_err_partner": "æšĢį„Ąæŗ•æ”ļ藏čĻĒæœ‹åĨŊå‹įš„é …į›ŽīŧŒį•Ĩ過", - "home_page_first_time_notice": "åĻ‚æžœé€™æ˜¯æ‚¨įŦŦ一æŦĄäŊŋᔍæœŦፋåŧīŧŒčĢ‹įĸēäŋé¸æ“‡ä¸€å€‹čρ備äģŊįš„į›¸į°ŋīŧŒäģĨå°‡į…§į‰‡čˆ‡åŊąį‰‡åŠ å…Ĩ時間čģ¸", - "home_page_locked_error_local": "į„Ąæŗ•į§ģ動æœŦ抟æĒ”æĄˆč‡ŗéŽ–åŽšįš„čŗ‡æ–™å¤žīŧŒåˇ˛į•Ĩ過", - "home_page_locked_error_partner": "į„Ąæŗ•į§ģ動čĻĒæœ‹åĨŊ友分äēĢįš„åĒ’éĢ”č‡ŗéŽ–åŽšįš„čŗ‡æ–™å¤žīŧŒåˇ˛į•Ĩ過", - "home_page_share_err_local": "į„Ąæŗ•é€éŽé€Ŗįĩå…ąäēĢæœŦ抟åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", - "home_page_upload_err_limit": "一æŦĄæœ€å¤šéšģčƒŊä¸Šå‚ŗ 30 個åĒ’éĢ”īŧŒåˇ˛į•Ĩ過", + "home_page_delete_err_partner": "į„Ąæŗ•åˆĒ除čĻĒå‹å…ąäēĢé …į›ŽīŧŒåˇ˛į•Ĩ過", + "home_page_delete_remote_err_local": "é¸å–įš„é į̝åˆĒ除清喎包åĢæœŦæŠŸé …į›ŽīŧŒåˇ˛į•Ĩ過", + "home_page_favorite_err_local": "æšĢæ™‚į„Ąæŗ•å°‡æœŦæŠŸé …į›Žč¨­į‚ēæ”ļ藏īŧŒåˇ˛į•Ĩ過", + "home_page_favorite_err_partner": "æšĢæ™‚į„Ąæŗ•å°‡čĻĒå‹å…ąäēĢé …į›Žč¨­į‚ēæ”ļ藏īŧŒåˇ˛į•Ĩ過", + "home_page_first_time_notice": "åĻ‚æžœé€™æ˜¯æ‚¨įŦŦ一æŦĄäŊŋᔍæœŦፋåŧīŧŒčĢ‹įĸēäŋé¸æ“‡ä¸€å€‹čρ備äģŊįš„į›¸į°ŋīŧŒäģĨå°‡į›¸į‰‡čˆ‡åŊąį‰‡åŠ å…Ĩ時間čģ¸", + "home_page_locked_error_local": "į„Ąæŗ•å°‡æœŦæŠŸé …į›Žį§ģå‹•č‡ŗã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žīŧŒåˇ˛į•Ĩ過", + "home_page_locked_error_partner": "į„Ąæŗ•å°‡čĻĒå‹å…ąäēĢé …į›Žį§ģå‹•č‡ŗã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žīŧŒåˇ˛į•Ĩ過", + "home_page_share_err_local": "į„Ąæŗ•é€éŽé€Ŗįĩåˆ†äēĢæœŦæŠŸé …į›ŽīŧŒåˇ˛į•Ĩ過", + "home_page_upload_err_limit": "一æŦĄæœ€å¤šåĒčƒŊä¸Šå‚ŗ 30 å€‹é …į›ŽīŧŒåˇ˛į•Ĩ過", "host": "ä¸ģ抟", "hour": "小時", "hours": "小時", "id": "ID", "idle": "閒įŊŽ", - "ignore_icloud_photos": "åŋŊį•Ĩ iCloud ᅧቇ", - "ignore_icloud_photos_description": "å„˛å­˜åœ¨ iCloud ä¸­įš„į…§į‰‡ä¸æœƒä¸Šå‚ŗč‡ŗ Immich äŧ翜å™¨", + "ignore_icloud_photos": "åŋŊį•Ĩ iCloud ᛏቇ", + "ignore_icloud_photos_description": "å„˛å­˜åœ¨ iCloud ä¸­įš„į›¸į‰‡ä¸æœƒä¸Šå‚ŗč‡ŗ Immich äŧ翜å™¨", "image": "åœ–į‰‡", "image_alt_text_date": "{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}拍攝æ–ŧ {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}} 與 {person1} 一同æ–ŧ {date} 拍攝", - "image_alt_text_date_2_people": "{person1} 和 {person2} 一同æ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_3_people": "{person1}、{person2} 和 {person3} 一同æ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_4_or_more_people": "{person1}、{person2} 和å…ļäģ– {additionalCount, number} äēēæ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", + "image_alt_text_date_1_person": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 與 {person1} 一同拍攝", + "image_alt_text_date_2_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 與 {person1} 及 {person2} 一同拍攝", + "image_alt_text_date_3_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 與 {person1}、{person2} 及 {person3} 一同拍攝", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 與 {person1}、{person2} 及å…ļäģ– {additionalCount, number} äēē一同拍攝", "image_alt_text_date_place": "æ–ŧ {date} 在 {country} - {city} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_place_1_person": "在 {country} - {city}īŧŒčˆ‡ {person1} 一同æ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_place_2_people": "在 {country} - {city} 與 {person1} 和 {person2} 一同æ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_place_3_people": "在 {country} - {city} 與 {person1}、{person2} 和 {person3} 一同æ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", - "image_alt_text_date_place_4_or_more_people": "在 {country} - {city} 與 {person1}、{person2} 和å…ļäģ– {additionalCount, number} äēēæ–ŧ {date} æ‹æ”įš„{isVideo, select, true {åŊąį‰‡} other {åœ–į‰‡}}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 在 {country}{city} 與 {person1} 一同拍攝", + "image_alt_text_date_place_2_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 在 {country}{city} 與 {person1} 及 {person2} 一同拍攝", + "image_alt_text_date_place_3_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 在 {country}{city} 與 {person1}、{person2} 及 {person3} 一同拍攝", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {åŊąį‰‡} other {ᛏቇ}}īŧšæ–ŧ {date} 在 {country}{city} 與 {person1}、{person2} 及å…ļäģ– {additionalCount, number} äēē一同拍攝", "image_saved_successfully": "åˇ˛å„˛å­˜åœ–į‰‡", "image_viewer_page_state_provider_download_started": "下čŧ‰åˇ˛å•Ÿå‹•", "image_viewer_page_state_provider_download_success": "下čŧ‰æˆåŠŸ", @@ -1225,7 +1321,7 @@ "in_year_selector": "在", "include_archived": "包åĢ厞封存", "include_shared_albums": "包åĢå…ąäēĢᛏį°ŋ", - "include_shared_partner_assets": "包æ‹Ŧå…ąäēĢčĻĒæœ‹åĨŊå‹įš„åĒ’éĢ”", + "include_shared_partner_assets": "包åĢčĻĒå‹å…ąäēĢé …į›Ž", "individual_share": "個åˆĨ分äēĢ", "individual_shares": "個åˆĨ分äēĢ", "info": "čŗ‡č¨Š", @@ -1237,7 +1333,7 @@ }, "invalid_date": "į„Ąæ•ˆįš„æ—Ĩ期", "invalid_date_format": "į„Ąæ•ˆįš„æ—Ĩ期æ ŧåŧ", - "invite_people": "邀čĢ‹äēēå“Ą", + "invite_people": "邀čĢ‹æˆå“Ą", "invite_to_album": "邀č̋臺ᛏį°ŋ", "ios_debug_info_fetch_ran_at": "æŠ“å–åˇ˛æ–ŧ {dateTime} åŸˇčĄŒ", "ios_debug_info_last_sync_at": "上æŦĄåŒæ­Ĩæ–ŧ {dateTime}", @@ -1247,9 +1343,18 @@ "ios_debug_info_processing_ran_at": "æ–ŧ {dateTime} åŸˇčĄŒč™•į†", "items_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "jobs": "äģģ務", + "json_editor": "JSONᎍčŧ¯å™¨", + "json_error": "JSON錯čǤ", "keep": "äŋį•™", + "keep_albums": "äŋį•™į›¸į°ŋ", + "keep_albums_count": "äŋį•™{count} {count, plural, one {個ᛏį°ŋ} other {個ᛏį°ŋ}}", "keep_all": "全部äŋį•™", + "keep_description": "é¸æ“‡åŸˇčĄŒé‡‹æ”žįŠē間時čρäŋį•™åœ¨čŖįŊŽä¸Šįš„é …į›Žã€‚", + "keep_favorites": "äŋį•™æœ€æ„›įš„ᛏቇ", + "keep_on_device": "äŋį•™åœ¨čŖįŊŽä¸Š", + "keep_on_device_hint": "選擇äŋį•™åœ¨čŖįŊŽä¸Šįš„ᛏቇ", "keep_this_delete_others": "äŋį•™é€™å€‹īŧŒåˆĒ除å…ļäģ–", + "keeping": "äŋį•™:{items}", "kept_this_deleted_others": "äŋį•™é€™å€‹é …į›Žä¸ĻåˆĒ除{count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "éĩᛤåŋĢæˇéĩ", "language": "čĒžč¨€", @@ -1268,14 +1373,14 @@ "lens_model": "éĄé ­åž‹č™Ÿ", "let_others_respond": "å…č¨ąäģ–äēē回čφ", "level": "į­‰į´š", - "library": "ᛏį°ŋ", - "library_add_folder": "æˇģåŠ čŗ‡æ–™å¤ž", + "library": "åĒ’éĢ”åēĢ", + "library_add_folder": "新åĸžčŗ‡æ–™å¤ž", "library_edit_folder": "ᎍčŧ¯čŗ‡æ–™å¤ž", - "library_options": "čŗ‡æ–™åēĢ選項", + "library_options": "åĒ’éĢ”åēĢ選項", "library_page_device_albums": "čŖįŊŽä¸Šįš„ᛏį°ŋ", "library_page_new_album": "新åĸžį›¸į°ŋ", "library_page_sort_asset_count": "é …į›Žæ•¸é‡", - "library_page_sort_created": "新åĸžæ—Ĩ期", + "library_page_sort_created": "åģēįĢ‹æ—Ĩ期", "library_page_sort_last_modified": "上æŦĄäŋŽæ”š", "library_page_sort_title": "ᛏį°ŋæ¨™éĄŒ", "licenses": "授æŦŠ", @@ -1285,7 +1390,7 @@ "link_motion_video": "逪įĩå‹•æ…‹åŊąį‰‡", "link_to_oauth": "逪įĩ OAuth", "linked_oauth_account": "厞逪įĩ OAuth å¸ŗč™Ÿ", - "list": "åˆ—čĄ¨", + "list": "清喎", "loading": "čŧ‰å…Ĩ中", "loading_search_results_failed": "čŧ‰å…Ĩ搜尋įĩæžœå¤ąæ•—", "local": "æœŦ抟", @@ -1304,8 +1409,8 @@ "location_picker_longitude_error": "čŧ¸å…Ĩæœ‰æ•ˆįš„įļ“åēĻå€ŧ", "location_picker_longitude_hint": "čĢ‹åœ¨æ­¤č™•čŧ¸å…Ĩæ‚¨įš„įļ“åēĻå€ŧ", "lock": "鎖厚", - "locked_folder": "éŽ–åŽšįš„čŗ‡æ–™å¤ž", - "log_detail_title": "æ—ĨčĒŒčŠŗį´°čŗ‡č¨Š", + "locked_folder": "åˇ˛éŽ–åŽščŗ‡æ–™å¤ž", + "log_detail_title": "į´€éŒ„čŠŗį´°čŗ‡č¨Š", "log_out": "į™ģå‡ē", "log_out_all_devices": "į™ģå‡ēæ‰€æœ‰čŖįŊŽ", "logged_in_as": "äģĨ{user}čēĢ分į™ģå…Ĩ", @@ -1321,12 +1426,12 @@ "login_form_err_http": "čĢ‹č¨ģ明 http:// 或 https://", "login_form_err_invalid_email": "é›ģ子éƒĩäģļåœ°å€į„Ąæ•ˆ", "login_form_err_invalid_url": "į„Ąæ•ˆįš„ URL", - "login_form_err_leading_whitespace": "å¸ļ有前導įŠēæ ŧ", - "login_form_err_trailing_whitespace": "å¸ļ有尞隨įŠēæ ŧ", - "login_form_failed_get_oauth_server_config": "äŊŋᔍ OAuth į™ģå…Ĩ時錯čǤīŧŒčĢ‹æĒĸæŸĨäŧ翜å™¨äŊå€", + "login_form_err_leading_whitespace": "開頭包åĢįŠēį™Ŋ字元", + "login_form_err_trailing_whitespace": "įĩå°žåŒ…åĢįŠēį™Ŋ字元", + "login_form_failed_get_oauth_server_config": "äŊŋᔍ OAuth į™ģå…Ĩ時į™ŧį”ŸéŒ¯čǤīŧŒčĢ‹æĒĸæŸĨäŧ翜å™¨įļ˛å€", "login_form_failed_get_oauth_server_disable": "OAuth 功čƒŊ在此äŧ翜å™¨ä¸Šį„Ąæŗ•äŊŋᔍ", "login_form_failed_login": "į™ģå…Ĩå¤ąæ•—īŧŒčĢ‹æĒĸæŸĨäŧ翜å™¨äŊå€ã€é›ģ子éƒĩäģļåœ°å€čˆ‡å¯†įĸŧ", - "login_form_handshake_exception": "與äŧ翜å™¨é€šč¨Šæ™‚å‡ēįžæĄæ‰‹į•°å¸¸ã€‚č‹ĨäŊŋᔍč‡Ēį°Ŋåæ†‘č­‰īŧŒčĢ‹åœ¨č¨­åŽšä¸­å•Ÿį”¨č‡Ēį°Ŋåæ†‘č­‰æ”¯æ´ã€‚", + "login_form_handshake_exception": "與äŧ翜å™¨é€šč¨Šæ™‚å‡ēįžä礿Ąį•°å¸¸ã€‚č‹ĨäŊŋᔍč‡Ēį°Ŋæ†‘č­‰īŧŒčĢ‹åœ¨č¨­åŽšä¸­å•Ÿį”¨č‡Ēį°Ŋæ†‘č­‰æ”¯æ´ã€‚", "login_form_password_hint": "密įĸŧ", "login_form_save_login": "äŋæŒį™ģå…Ĩ", "login_form_server_empty": "čĢ‹čŧ¸å…Ĩäŧ翜å™¨įļ˛å€ã€‚", @@ -1336,41 +1441,59 @@ "login_password_changed_success": "密įĸŧ更新成功", "logout_all_device_confirmation": "您įĸē厚čρį™ģå‡ēæ‰€æœ‰čŖįŊŽå—ŽīŧŸ", "logout_this_device_confirmation": "čρį™ģå‡ē這č‡ēčŖįŊŽå—ŽīŧŸ", - "logs": "æ—Ĩčnj", + "logs": "į´€éŒ„", "longitude": "įļ“åēĻ", "look": "æ¨Ŗč˛Œ", "loop_videos": "重播åŊąį‰‡", "loop_videos_description": "å•Ÿį”¨åžŒīŧŒåŊąį‰‡įĩæŸæœƒč‡Ē動重播。", - "main_branch_warning": "æ‚¨įžåœ¨äŊŋį”¨įš„æ˜¯é–‹į™ŧį‰ˆæœŦīŧ›æˆ‘們åŧˇįƒˆæ‚¨åģēč­°äŊŋį”¨æ­Ŗåŧį™ŧčĄŒį‰ˆīŧ", + "main_branch_warning": "æ‚¨æ­ŖäŊŋᔍ開į™ŧį‰ˆæœŦīŧ›åŧˇįƒˆåģēč­°äŊŋį”¨æ­Ŗåŧį‰ˆæœŦīŧ", "main_menu": "ä¸ģ選喎", - "maintenance_description": "Immich厞逞å…Ĩįļ­č­ˇæ¨Ąåŧã€‚", + "maintenance_action_restore": "é‚„åŽŸčŗ‡æ–™åēĢ", + "maintenance_description": "Immich 厞逞å…Ĩ įļ­č­ˇæ¨Ąåŧã€‚", "maintenance_end": "įĩæŸįļ­č­ˇæ¨Ąåŧ", "maintenance_end_error": "æœĒčƒŊįĩæŸįļ­č­ˇæ¨Ąåŧã€‚", - "maintenance_logged_in_as": "į•ļ前äģĨ{user}čēĢäģŊį™ģå…Ĩ", + "maintenance_logged_in_as": "į›Žå‰äģĨ {user} čēĢ分į™ģå…Ĩ", + "maintenance_restore_from_backup": "åžžå‚™äģŊ還原", + "maintenance_restore_library": "é‚„åŽŸæ‚¨įš„åĒ’éĢ”åēĢ", + "maintenance_restore_library_confirm": "įĸēčĒæ˜¯åĻæ­ŖįĸēīŧŒå°‡įšŧįēŒé‚„原備äģŊīŧ", + "maintenance_restore_library_description": "æ­Ŗåœ¨é‚„åŽŸčŗ‡æ–™åēĢ", + "maintenance_restore_library_folder_has_files": "{folder} åĢ有 {count} å€‹čŗ‡æ–™å¤ž", + "maintenance_restore_library_folder_no_files": "{folder} įŧē少æĒ”æĄˆīŧ", + "maintenance_restore_library_folder_pass": "å¯čŽ€å–čˆ‡å¯Ģå…Ĩ", + "maintenance_restore_library_folder_read_fail": "į„Ąæŗ•čŽ€å–", + "maintenance_restore_library_folder_write_fail": "į„Ąæŗ•å¯Ģå…Ĩ", + "maintenance_restore_library_hint_missing_files": "您可čƒŊéēå¤ąäē†é‡čρæĒ”æĄˆ", + "maintenance_restore_library_hint_regenerate_later": "䚋垌可äģĨåœ¨č¨­åŽšé‡æ–°į”ĸį”Ÿ", + "maintenance_restore_library_hint_storage_template_missing_files": "æ­Ŗåœ¨äŊŋį”¨å„˛å­˜į¯„æœŦīŧŸæ‚¨å¯čƒŊéēå¤ąäē†éƒ¨åˆ†æĒ”æĄˆ", + "maintenance_restore_library_loading": "æ­Ŗåœ¨čŧ‰å…Ĩ厌整性æĒĸæŸĨčˆ‡å•Ÿį™ŧåŧåˆ†æžâ€Ļ", + "maintenance_task_backup": "æ­Ŗåœ¨åģēįĢ‹įžæœ‰čŗ‡æ–™åēĢįš„å‚™äģŊâ€Ļ", + "maintenance_task_migrations": "æ­Ŗåœ¨åŸˇčĄŒčŗ‡æ–™åēĢ遡į§ģâ€Ļ", + "maintenance_task_restore": "æ­Ŗåœ¨åžžé¸å–įš„å‚™äģŊé€˛čĄŒé‚„åŽŸâ€Ļ", + "maintenance_task_rollback": "é‚„åŽŸå¤ąæ•—īŧŒæ­Ŗåœ¨å›žæē¯č‡ŗé‚„原éģžâ€Ļ", "maintenance_title": "æšĢæ™‚ä¸å¯į”¨", "make": "čŖŊ造商", "manage_geolocation": "įŽĄį†äŊįŊŽ", - "manage_media_access_rationale": "æ­Ŗįĸē處ᐆ將躇į”ĸį§ģč‡ŗåžƒåœžæĄļä¸Ļ將å…ļ垞垃圞æĄļ中æĸ垊需čĻæ­¤č¨ąå¯ã€‚", + "manage_media_access_rationale": "需čĻæ­¤æŦŠé™æ‰čƒŊč™•į†é …į›Žį§ģč‡ŗåžƒåœžæĄļčˆ‡é‚„åŽŸįš„æ“äŊœã€‚", "manage_media_access_settings": "æ‰“é–‹č¨­åŽš", - "manage_media_access_subtitle": "å…č¨ąImmichæ‡‰į”¨į¨‹åēįŽĄį†å’Œį§ģ動åĒ’éĢ”æĒ”æĄˆã€‚", - "manage_media_access_title": "åĒ’éĢ”įŽĄį†č¨Ē問", - "manage_shared_links": "įŽĄį†å…ąäēĢ逪įĩ", - "manage_sharing_with_partners": "įŽĄį†čˆ‡čĻĒæœ‹åĨŊå‹įš„åˆ†äēĢ", + "manage_media_access_subtitle": "å…č¨ą Immich App įŽĄį†čˆ‡į§ģ動åĒ’éĢ”æĒ”æĄˆã€‚", + "manage_media_access_title": "åĒ’éĢ”įŽĄį†å­˜å–æŦŠé™", + "manage_shared_links": "įŽĄį†åˆ†äēĢ逪įĩ", + "manage_sharing_with_partners": "įŽĄį†čĻĒå‹å…ąäēĢč¨­åŽš", "manage_the_app_settings": "įŽĄį†æ‡‰į”¨į¨‹åŧč¨­åޚ", "manage_your_account": "įŽĄį†æ‚¨įš„å¸ŗč™Ÿ", "manage_your_api_keys": "įŽĄį†æ‚¨įš„ API 金鑰", "manage_your_devices": "įŽĄį†åˇ˛į™ģå…Ĩįš„čŖįŊŽ", "manage_your_oauth_connection": "įŽĄį†æ‚¨įš„ OAuth 逪įĩ", "map": "地圖", - "map_assets_in_bounds": "{count, plural, one {# åŧĩᅧቇ} other {# åŧĩᅧቇ}}", + "map_assets_in_bounds": "{count, plural, one {# åŧĩᛏቇ} other {# åŧĩᛏቇ}}", "map_cannot_get_user_location": "į„Ąæŗ•å–åž—äŊŋᔍ者äŊįŊŽ", "map_location_dialog_yes": "įĸē厚", "map_location_picker_page_use_location": "äŊŋį”¨æ­¤äŊįŊŽ", - "map_location_service_disabled_content": "需čĻå•Ÿį”¨åŽšäŊæœå‹™æ‰čƒŊéĄ¯į¤ēį›Žå‰äŊįŊŽį›¸é—œįš„é …į›Žã€‚čĻįžåœ¨å•Ÿį”¨å—ŽīŧŸ", + "map_location_service_disabled_content": "需čĻå•Ÿį”¨åŽšäŊæœå‹™æ‰čƒŊéĄ¯į¤ēæ‚¨į›Žå‰äŊįŊŽį›¸é—œįš„é …į›Žã€‚čĻįžåœ¨å•Ÿį”¨å—ŽīŧŸ", "map_location_service_disabled_title": "厚äŊæœå‹™åˇ˛åœį”¨", "map_marker_for_images": "在 {city}、{country} 拍攝åŊąåƒįš„地圖į¤ē記", "map_marker_with_image": "å¸ļ有åŊąåƒįš„地圖į¤ē記", - "map_no_location_permission_content": "需čρäŊįŊŽæŦŠé™æ‰čƒŊéĄ¯į¤ēčˆ‡į›Žå‰äŊįŊŽã€‚čĻįžåœ¨å°ąæŽˆäēˆäŊįŊŽæŦŠé™å—ŽīŧŸ", + "map_no_location_permission_content": "需čρäŊįŊŽæŦŠé™æ‰čƒŊéĄ¯į¤ēčˆ‡æ‚¨į›Žå‰äŊįŊŽį›¸é—œįš„é …į›Žã€‚čĻįžåœ¨å°ąæŽˆäēˆäŊįŊŽæŦŠé™å—ŽīŧŸ", "map_no_location_permission_title": "æ˛’æœ‰äŊįŊŽæŦŠé™", "map_settings": "åœ°åœ–č¨­åŽš", "map_settings_dark_mode": "æˇąč‰˛æ¨Ąåŧ", @@ -1380,7 +1503,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": "į¸Žå°äģĨæĒĸčĻ–é …į›Ž", @@ -1388,7 +1511,7 @@ "mark_as_read": "æ¨™č¨˜į‚ēåˇ˛čŽ€", "marked_all_as_read": "åˇ˛å…¨éƒ¨æ¨™č¨˜į‚ēåˇ˛čŽ€", "matches": "ᛏįŦĻ", - "matching_assets": "åŒšé…čŗ‡į”ĸ", + "matching_assets": "įŦĻåˆįš„é …į›Ž", "media_type": "åĒ’éĢ”éĄžåž‹", "memories": "回æ†ļ", "memories_all_caught_up": "åˇ˛å…¨éƒ¨įœ‹åŽŒ", @@ -1404,38 +1527,44 @@ "merge_people_limit": "一æŦĄæœ€å¤šéšģčƒŊ合äŊĩ 5 åŧĩ臉孔", "merge_people_prompt": "您čρ合äŊĩ這äē›äēēį‰Šå—ŽīŧŸæ­¤æ“äŊœį„Ąæŗ•æ’¤éŠˇã€‚", "merge_people_successfully": "成功合äŊĩäēēį‰Š", - "merged_people_count": "合äŊĩäē† {count, plural, one {# äŊäēēåŖĢ} other {# äŊäēēåŖĢ}}", + "merged_people_count": "厞合äŊĩ {count, plural, other {# äŊäēēį‰Š}}", "minimize": "最小化", "minute": "分", "minutes": "分鐘", + "mirror_horizontal": "æ°´åšŗ", + "mirror_vertical": "åž‚į›´", "missing": "排å…ĨæœĒ處ᐆ", - "mobile_app": "į§ģå‹•æ‡‰į”¨į¨‹åē", - "mobile_app_download_onboarding_note": "äŊŋᔍäģĨ下選項下čŧ‰é…åĨ—į§ģå‹•æ‡‰į”¨į¨‹åē", + "mobile_app": "čĄŒå‹•æ‡‰į”¨į¨‹åŧ", + "mobile_app_download_onboarding_note": "čĢ‹äŊŋᔍäģĨ下選項下čŧ‰éš¨é™„įš„čĄŒå‹•æ‡‰į”¨į¨‹åŧ", "model": "åž‹č™Ÿ", "month": "月", "monthly_title_text_date_format": "y MMMM", "more": "更多", "move": "į§ģ動", + "move_down": "向下į§ģ動", "move_off_locked_folder": "į§ģå‡ēéŽ–åŽšįš„čŗ‡æ–™å¤ž", "move_to": "į§ģ動到", - "move_to_lock_folder_action_prompt": "{count} åˇ˛æ–°åĸžč‡ŗéŽ–åŽšįš„čŗ‡æ–™å¤žä¸­", - "move_to_locked_folder": "į§ģč‡ŗéŽ–åŽšįš„čŗ‡æ–™å¤ž", - "move_to_locked_folder_confirmation": "這äē›į…§į‰‡å’ŒåŊąį‰‡å°‡åžžæ‰€æœ‰į›¸į°ŋ中į§ģ除īŧŒä¸Ļåƒ…å¯åžžéŽ–åŽšįš„čŗ‡æ–™å¤žæĒĸčĻ–", + "move_to_device_trash": "į§ģå‹•åˆ°čŖįŊŽįš„垃圞æĄļ", + "move_to_lock_folder_action_prompt": "厞將 {count} å€‹é …į›Žæ–°åĸžč‡ŗã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤ž", + "move_to_locked_folder": "į§ģč‡ŗã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤ž", + "move_to_locked_folder_confirmation": "這äē›į›¸į‰‡čˆ‡åŊąį‰‡å°‡åžžæ‰€æœ‰į›¸į°ŋ中į§ģ除īŧŒä¸”僅čƒŊåžžã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žä¸­æĒĸčĻ–", + "move_up": "向上į§ģ動", "moved_to_archive": "厞封存 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}", "moved_to_library": "厞į§ģ動 {count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}} 臺ᛏį°ŋ", "moved_to_trash": "åˇ˛ä¸Ÿé€˛åžƒåœžæĄļ", - "multiselect_grid_edit_date_time_err_read_only": "į„Ąæŗ•įˇ¨čŧ¯å”¯čŽ€é …į›Žįš„æ—Ĩ期īŧŒį•Ĩ過", - "multiselect_grid_edit_gps_err_read_only": "į„Ąæŗ•įˇ¨čŧ¯å”¯čŽ€é …į›Žįš„äŊįŊŽčŗ‡č¨ŠīŧŒį•Ĩ過", + "multiselect_grid_edit_date_time_err_read_only": "å”¯čŽ€é …į›Žįš„æ—ĨæœŸį„Ąæŗ•įˇ¨čŧ¯īŧŒåˇ˛į•Ĩ過", + "multiselect_grid_edit_gps_err_read_only": "å”¯čŽ€é …į›Žįš„äŊįŊŽčŗ‡č¨Šį„Ąæŗ•ᎍčŧ¯īŧŒåˇ˛į•Ĩ過", "mute_memories": "éœéŸŗå›žæ†ļ", "my_albums": "æˆ‘įš„į›¸į°ŋ", "name": "åį¨ą", "name_or_nickname": "åį¨ąæˆ–æšąį¨ą", + "name_required": "åį¨ąæ˜¯åŋ…åĄĢ項", "navigate": "導čˆĒ", - "navigate_to_time": "導čˆĒ到時間", - "network_requirement_photos_upload": "äŊŋį”¨čĄŒå‹•įļ˛čˇ¯æĩé‡å‚™äģŊᅧቇ", + "navigate_to_time": "莺čŊ‰č‡ŗæŒ‡åŽšæ™‚é–“", + "network_requirement_photos_upload": "äŊŋį”¨čĄŒå‹•įļ˛čˇ¯æĩé‡å‚™äģŊᛏቇ", "network_requirement_videos_upload": "äŊŋį”¨čĄŒå‹•įļ˛čˇ¯æĩé‡å‚™äģŊåŊąį‰‡", "network_requirements": "įļ˛čˇ¯čĻæą‚", - "network_requirements_updated": "įļ˛čˇ¯éœ€æą‚åˇ˛čŽŠæ›´īŧŒįžé‡č¨­å‚™äģŊäŊ‡åˆ—", + "network_requirements_updated": "įļ˛čˇ¯éœ€æą‚åˇ˛čŽŠæ›´īŧŒæ­Ŗåœ¨é‡č¨­å‚™äģŊäŊ‡åˆ—", "networking_settings": "įļ˛čˇ¯", "networking_subtitle": "įŽĄį†äŧ翜å™¨į̝éģžč¨­åޚ", "never": "æ°¸ä¸å¤ąæ•ˆ", @@ -1445,7 +1574,7 @@ "new_password": "新密įĸŧ", "new_person": "æ–°įš„äēēį‰Š", "new_pin_code": "新 PIN įĸŧ", - "new_pin_code_subtitle": "這是您įŦŦ一æŦĄå­˜å–éŽ–åŽšįš„čŗ‡æ–™å¤žã€‚åģēįĢ‹ PIN įĸŧäģĨ厉全存取此頁éĸ", + "new_pin_code_subtitle": "這是您įŦŦ一æŦĄå­˜å–ã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žã€‚čĢ‹åģēįĢ‹ PIN įĸŧäģĨ厉全存取此頁éĸ", "new_timeline": "新時間čģ¸", "new_update": "新更新", "new_user_created": "厞åģēįĢ‹æ–°äŊŋᔍ者", @@ -1454,38 +1583,42 @@ "next": "下一æ­Ĩ", "next_memory": "下一åŧĩ回æ†ļ", "no": "åĻ", - "no_albums_message": "åģēį̋ᛏį°ŋäž†æ•´į†į…§į‰‡å’ŒåŊąį‰‡", + "no_actions_added": "尚æœĒ新åĸžäģģäŊ•å‹•äŊœ", + "no_albums_found": "į„Ąį›¸į°ŋ", + "no_albums_message": "åģēį̋ᛏį°ŋäž†æ•´į†į›¸į‰‡å’ŒåŊąį‰‡", "no_albums_with_name_yet": "įœ‹äž†é‚„æ˛’æœ‰é€™å€‹åå­—įš„į›¸į°ŋ。", "no_albums_yet": "įœ‹äž†æ‚¨é‚„æ˛’æœ‰äģģäŊ•ᛏį°ŋ。", - "no_archived_assets_message": "å°‡į…§į‰‡å’ŒåŊąį‰‡å°å­˜īŧŒå°ąä¸æœƒéĄ¯į¤ēåœ¨ã€Œį…§į‰‡ã€ä¸­", - "no_assets_message": "æŒ‰é€™čŖĄä¸Šå‚ŗæ‚¨įš„įŦŦ一åŧĩᅧቇ", + "no_archived_assets_message": "å°‡į›¸į‰‡čˆ‡åŊąį‰‡å°å­˜åžŒīŧŒå°ąä¸æœƒéĄ¯į¤ēåœ¨ã€Œį›¸į‰‡ã€čĻ–åœ–ä¸­", + "no_assets_message": "æŒ‰é€™čŖĄä¸Šå‚ŗæ‚¨įš„įŦŦ一åŧĩᛏቇ", "no_assets_to_show": "į„Ąé …į›Žåą•į¤ē", "no_cast_devices_found": "扞不到 Google Cast čŖįŊŽ", - "no_checksum_local": "æ˛’æœ‰å¯į”¨įš„æ ĄéŠ—å’Œ - į„Ąæŗ•å–åž—æœŦæŠŸčŗ‡į”ĸ", - "no_checksum_remote": "æ˛’æœ‰å¯į”¨įš„æ ĄéŠ—å’Œ - į„Ąæŗ•å–åž—é į̝躇į”ĸ", - "no_devices": "į„ĄæŽˆæŦŠč¨­å‚™", - "no_duplicates_found": "æ˛’į™ŧįžé‡č¤‡é …į›Žã€‚", + "no_checksum_local": "į„Ąå¯į”¨æ ĄéŠ—įĸŧ - į„Ąæŗ•å–åž—æœŦæŠŸé …į›Ž", + "no_checksum_remote": "į„Ąå¯į”¨æ ĄéŠ—įĸŧ - į„Ąæŗ•å–åž—é›˛įĢ¯é …į›Ž", + "no_configuration_needed": "į„Ąéœ€č¨­åŽš", + "no_devices": "į„ĄæŽˆæŦŠčŖįŊŽ", + "no_duplicates_found": "æœĒį™ŧįžäģģäŊ•é‡č¤‡é …į›Žã€‚", "no_exif_info_available": "æ˛’æœ‰å¯į”¨įš„ Exif čŗ‡č¨Š", - "no_explore_results_message": "ä¸Šå‚ŗæ›´å¤šį…§į‰‡äģĨ刊æŽĸį´ĸ。", + "no_explore_results_message": "ä¸Šå‚ŗæ›´å¤šį›¸į‰‡äž†æŽĸį´ĸæ‚¨įš„įč—ã€‚", "no_favorites_message": "加å…Ĩæ”ļ藏īŧŒåŠ é€Ÿå°‹æ‰žåŊąåƒ", - "no_libraries_message": "åģēįĢ‹å¤–éƒ¨åĒ’éĢ”åēĢäģĨæĒĸčĻ–æ‚¨įš„į…§į‰‡å’ŒåŊąį‰‡", - "no_local_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéŠ—å’Œįš„æœŦæŠŸčŗ‡į”ĸ", + "no_filters_added": "尚æœĒ新åĸžäģģäŊ•į¯Šé¸å™¨", + "no_libraries_message": "åģēįĢ‹å¤–éƒ¨åĒ’éĢ”åēĢäģĨæĒĸčĻ–æ‚¨įš„į›¸į‰‡å’ŒåŊąį‰‡", + "no_local_assets_found": "æ‰žä¸åˆ°å…ˇæœ‰æ­¤æ ĄéŠ—įĸŧįš„æœŦæŠŸé …į›Ž", "no_location_set": "æœĒč¨­åŽšäŊįŊŽ", - "no_locked_photos_message": "éŽ–åŽšįš„čŗ‡æ–™å¤žä¸­įš„į…§į‰‡å’ŒåŊąį‰‡æœƒčĸĢ隱藏īŧŒį•ļæ‚¨į€čĻŊæˆ–æœå°‹į›¸į°ŋæ™‚ä¸æœƒéĄ¯į¤ē。", + "no_locked_photos_message": "ã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žä¸­įš„į›¸į‰‡čˆ‡åŊąį‰‡æœƒčĸĢ隱藏īŧŒä¸”不會å‡ēįžåœ¨į€čĻŊ或搜尋įĩæžœä¸­ã€‚", "no_name": "į„Ąå", "no_notifications": "æ˛’æœ‰é€šįŸĨ", "no_people_found": "扞不到įŦĻåˆįš„äēēį‰Š", "no_places": "æ˛’æœ‰åœ°éģž", - "no_remote_assets_found": "æœĒæ‰žåˆ°å…ˇæœ‰æ­¤æ ĄéŠ—å’Œįš„é į̝躇į”ĸ", + "no_remote_assets_found": "æ‰žä¸åˆ°å…ˇæœ‰æ­¤æ ĄéŠ—įĸŧįš„é›˛įĢ¯é …į›Ž", "no_results": "æ˛’æœ‰įĩæžœ", "no_results_description": "čŠĻčŠĻåŒįžŠčŠžæˆ–æ›´é€šį”¨įš„é—œéĩ字吧", - "no_shared_albums_message": "åģēį̋ᛏį°ŋ分äēĢį…§į‰‡å’ŒåŊąį‰‡", + "no_shared_albums_message": "åģēįĢ‹å…ąäēĢᛏį°ŋäģĨ分äēĢį›¸į‰‡čˆ‡åŊąį‰‡", "no_uploads_in_progress": "æ˛’æœ‰æ­Ŗåœ¨ä¸Šå‚ŗįš„é …į›Ž", + "none": "į„Ą", "not_allowed": "ä¸å…č¨ą", "not_available": "ä¸éŠį”¨", "not_in_any_album": "不在äģģäŊ•ᛏį°ŋ中", - "not_selected": "æœĒ選擇", - "note_apply_storage_label_to_previously_uploaded assets": "*č¨ģīŧšåŸˇčĄŒåĨ—į”¨å„˛å­˜æ¨™įą¤å‰å…ˆä¸Šå‚ŗé …į›Ž", + "not_selected": "æœĒ選取", "notes": "提į¤ē", "nothing_here_yet": "æšĢį„Ąč¨Šæ¯", "notification_permission_dialog_content": "開啟通įŸĨīŧŒčĢ‹å‰åž€ã€Œč¨­åŽšã€īŧŒä¸Ļé¸æ“‡ã€Œå…č¨ąã€ã€‚", @@ -1496,8 +1629,8 @@ "notifications": "通įŸĨ", "notifications_setting_description": "įŽĄį†é€šįŸĨ", "oauth": "OAuth", - "obtainium_configurator": "Obtainium配寘器", - "obtainium_configurator_instructions": "äŊŋᔍObtainiumį›´æŽĨåžžImmich GitHubįš„į‰ˆæœŦåŽ‰čŖå’Œæ›´æ–°Androidæ‡‰į”¨į¨‹åēã€‚ å‰ĩåģē一個API金鑰ä¸Ļé¸æ“‡ä¸€å€‹čŽŠéĢ”äž†å‰ĩåģēæ‚¨įš„Obtainiumé…å¯˜é€Ŗįĩ", + "obtainium_configurator": "Obtainium č¨­åŽšå™¨", + "obtainium_configurator_instructions": "äŊŋᔍ Obtainium į›´æŽĨåžž Immich GitHub įš„į™ŧčĄŒį‰ˆæœŦåŽ‰čŖä¸Ļ更新 Android App。åģēįĢ‹ API 金鑰ä¸Ļé¸å–į‰ˆæœŦéĄžåž‹äģĨį”ĸį”Ÿæ‚¨įš„ Obtainium č¨­åŽšé€Ŗįĩ", "ocr": "OCR", "official_immich_resources": "厘斚 Immich čŗ‡æē", "offline": "é›ĸ᎚", @@ -1507,21 +1640,22 @@ "on_this_device": "åœ¨æ­¤čŖįŊŽ", "onboarding": "å…Ĩ門指南", "onboarding_locale_description": "é¸æ“‡æ‚¨æƒŗčĻéĄ¯į¤ēįš„čĒžč¨€ã€‚č¨­åŽšåŽŒæˆäš‹åžŒį”Ÿæ•ˆã€‚", - "onboarding_privacy_description": "äģĨ下īŧˆå¯é¸īŧ‰åŠŸčƒŊäģ°čŗ´å¤–部服務īŧŒå¯éš¨æ™‚åœ¨č¨­åŽšä¸­åœį”¨ã€‚", + "onboarding_privacy_description": "äģĨä¸‹é¸į”¨åŠŸčƒŊäģ°čŗ´å¤–部服務īŧŒæ‚¨å¯äģĨéš¨æ™‚åœ¨č¨­åŽšä¸­å°‡å…ļåœį”¨ã€‚", "onboarding_server_welcome_description": "čŽ“æˆ‘å€‘į‚ēæ‚¨įš„įŗģįĩąé€˛čĄŒä¸€äē›åŸēæœŦč¨­åŽšã€‚", - "onboarding_theme_description": "åšĢåŸˇčĄŒå€‹é̔遏艞åŊŠä¸ģéĄŒã€‚äš‹åžŒäšŸå¯äģĨåœ¨č¨­åŽšä¸­čŽŠæ›´ã€‚", + "onboarding_theme_description": "čĢ‹į‚ēæ‚¨įš„åŸˇčĄŒå€‹éĢ”é¸æ“‡č‰˛åŊŠä¸ģéĄŒã€‚æ‚¨į¨åžŒäģå¯åœ¨č¨­åŽšä¸­čŽŠæ›´ã€‚", "onboarding_user_welcome_description": "čŽ“æˆ‘å€‘é–‹å§‹å§īŧ", "onboarding_welcome_user": "æ­ĄčŋŽīŧŒ{user}", "online": "᎚䏊", "only_favorites": "åƒ…éĄ¯į¤ēåˇąæ”ļ藏", "open": "開啟", + "open_calendar": "打開æ—Ĩ曆", "open_in_map_view": "開啟地圖æĒĸčĻ–", "open_in_openstreetmap": "ᔍ OpenStreetMap 開啟", "open_the_search_filters": "é–‹å•Ÿæœå°‹į¯Šé¸å™¨", "options": "選項", "or": "或", "organize_into_albums": "æ•´į†æˆį›¸į°ŋ", - "organize_into_albums_description": "äŊŋį”¨į›Žå‰åŒæ­Ĩč¨­åŽšå°‡įžæœ‰į…§į‰‡æ”žå…Ĩᛏį°ŋ", + "organize_into_albums_description": "äŊŋį”¨į›Žå‰åŒæ­Ĩč¨­åŽšå°‡įžæœ‰į›¸į‰‡æ”žå…Ĩᛏį°ŋ", "organize_your_library": "æ•´į†æ‚¨įš„į›¸į°ŋ", "original": "原圖", "other": "å…ļäģ–", @@ -1529,22 +1663,22 @@ "other_entities": "å…ļäģ–é …į›Ž", "other_variables": "å…ļäģ–čŽŠæ•¸", "owned": "æˆ‘įš„", - "owner": "æ‰€æœ‰č€…", + "owner": "æ“æœ‰č€…", "page": "頁", - "partner": "čĻĒæœ‹åĨŊ友", - "partner_can_access": "{partner} 可äģĨ存取", - "partner_can_access_assets": "除äē†åˇ˛å°å­˜å’Œåˇ˛åˆĒ除䚋外īŧŒæ‚¨æ‰€æœ‰įš„į…§į‰‡å’ŒåŊąį‰‡", - "partner_can_access_location": "æ‚¨į…§į‰‡æ‹æ”įš„äŊįŊŽ", - "partner_list_user_photos": "{user} įš„į…§į‰‡", - "partner_list_view_all": "åą•į¤ē全部", - "partner_page_empty_message": "æ‚¨įš„į…§į‰‡å°šæœĒ與äģģäŊ•čĻĒæœ‹åĨŊå‹å…ąäēĢ。", - "partner_page_no_more_users": "į„Ąéœ€æ–°åĸžæ›´å¤šäŊŋᔍ者", - "partner_page_partner_add_failed": "新åĸžčĻĒæœ‹åĨŊå‹å¤ąæ•—", - "partner_page_select_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_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": "čĻĒæœ‹åĨŊ友", + "partner_page_stop_sharing_content": "{partner} å°‡į„Ąæŗ•å†å­˜å–æ‚¨įš„į›¸į‰‡ã€‚", + "partner_sharing": "čĻĒå‹å…ąäēĢ", + "partners": "čĻĒ友", "password": "密įĸŧ", "password_does_not_match": "密įĸŧä¸į›¸įŦĻ", "password_required": "需čρ坆įĸŧ", @@ -1561,8 +1695,9 @@ "paused": "厞æšĢ停", "pending": "åž…č™•į†", "people": "äēēį‰Š", - "people_edits_count": "ᎍčŧ¯äē† {count, plural, one {# äŊäēēåŖĢ} other {# äŊäēēåŖĢ}}", - "people_feature_description": "äģĨäēēį‰Šåˆ†éĄžį€čĻŊį…§į‰‡å’ŒåŊąį‰‡", + "people_edits_count": "厞ᎍčŧ¯ {count, plural, one {# äŊäēēį‰Š} other {# äŊäēēį‰Š}}", + "people_feature_description": "äģĨäēēį‰Šåˆ†éĄžį€čĻŊį›¸į‰‡å’ŒåŊąį‰‡", + "people_selected": "{count, plural, one {åˇ˛é¸å– # äŊäēēį‰Š} other {åˇ˛é¸å– # äŊäēēį‰Š}}", "people_sidebar_description": "在側邊æŦ„éĄ¯į¤ē「äēēį‰Šã€įš„é€Ŗįĩ", "permanent_deletion_warning": "永䚅åˆĒ除č­Ļ告", "permanent_deletion_warning_setting_description": "在永䚅åˆĒ除æĒ”æĄˆæ™‚éĄ¯į¤ēč­Ļ告", @@ -1580,37 +1715,40 @@ "permission_onboarding_permission_denied": "åĻ‚čρįšŧįēŒīŧŒčĢ‹å…č¨ą Immich å­˜å–į›¸į‰‡å’ŒåŊąį‰‡æŦŠé™ã€‚", "permission_onboarding_permission_granted": "åˇ˛å…č¨ąīŧä¸€åˆ‡å°ąįˇ’。", "permission_onboarding_permission_limited": "åĻ‚čρįšŧįēŒīŧŒčĢ‹å…č¨ą Immich 備äģŊå’ŒįŽĄį†æ‚¨įš„į›¸į°ŋæ”ļ藏īŧŒåœ¨č¨­åŽšä¸­æŽˆäēˆį›¸į‰‡å’ŒåŊąį‰‡æŦŠé™ã€‚", - "permission_onboarding_request": "Immich 需čρæŦŠé™æ‰čƒŊæĒĸčĻ–æ‚¨įš„į›¸į‰‡å’ŒįŸ­į‰‡ã€‚", + "permission_onboarding_request": "Immich 需čρæŦŠé™æ‰čƒŊæĒĸčĻ–æ‚¨įš„į›¸į‰‡čˆ‡åŊąį‰‡ã€‚", "person": "äēēį‰Š", - "person_age_months": "{months, plural, one {# 個月} other {# 個月}}前", - "person_age_year_months": "1 åš´ {months, plural, one {# 個月} other {# 個月}}前", + "person_age_months": "{months, plural, one {# 個月} other {# 個月}}", + "person_age_year_months": "1 åš´ {months, plural, one {# 個月} other {# 個月}}", "person_age_years": "{years, plural, other {# æ­˛}}", - "person_birthdate": "į”Ÿæ–ŧ {date}", + "person_birthdate": "å‡ēį”Ÿæ–ŧ {date}", "person_hidden": "{name}{hidden, select, true {īŧˆéšąč—īŧ‰} other {}}", - "photo_shared_all_users": "įœ‹äž†æ‚¨čˆ‡æ‰€æœ‰äŊŋį”¨č€…åˆ†äēĢäē†į…§į‰‡īŧŒæˆ–æ˛’æœ‰å…ļäģ–äŊŋį”¨č€…å¯äž›åˆ†äēĢ。", - "photos": "ᅧቇ", - "photos_and_videos": "į…§į‰‡åŠåŊąį‰‡", - "photos_count": "{count, plural, other {{count, number} åŧĩᅧቇ}}", - "photos_from_previous_years": "åž€åš´įš„į…§į‰‡", + "person_recognized": "åˇ˛čž¨č­˜äēēį‰Š", + "person_selected": "åˇ˛é¸å–äēēį‰Š", + "photo_shared_all_users": "įœ‹äž†æ‚¨čˆ‡æ‰€æœ‰äŊŋį”¨č€…åˆ†äēĢäē†į›¸į‰‡īŧŒæˆ–æ˛’æœ‰å…ļäģ–äŊŋį”¨č€…å¯äž›åˆ†äēĢ。", + "photos": "ᛏቇ", + "photos_and_videos": "į›¸į‰‡åŠåŊąį‰‡", + "photos_count": "{count, plural, other {{count, number} åŧĩᛏቇ}}", + "photos_from_previous_years": "åž€åš´įš„į›¸į‰‡", + "photos_only": "åƒ…é™į›¸į‰‡", "pick_a_location": "選擇äŊįŊŽ", "pick_custom_range": "č‡ĒåŽšįžŠį¯„åœ", "pick_date_range": "選擇æ—ĨæœŸį¯„åœ", - "pin_code_changed_successfully": "čŽŠæ›´ PIN įĸŧ成功", - "pin_code_reset_successfully": "重設 PIN įĸŧ成功", - "pin_code_setup_successfully": "č¨­åŽš PIN įĸŧ成功", + "pin_code_changed_successfully": "PIN įĸŧčŽŠæ›´æˆåŠŸ", + "pin_code_reset_successfully": "PIN įĸŧé‡č¨­æˆåŠŸ", + "pin_code_setup_successfully": "PIN įĸŧč¨­åŽšæˆåŠŸ", "pin_verification": "PIN įĸŧ驗證", "place": "地éģž", "places": "地éģž", "places_count": "{count, plural, one {{count, number} 個地éģž} other {{count, number} 個地éģž}}", "play": "播攞", "play_memories": "播攞回æ†ļ", - "play_motion_photo": "æ’­æ”žå‹•æ…‹į…§į‰‡", + "play_motion_photo": "æ’­æ”žå‹•æ…‹į›¸į‰‡", "play_or_pause_video": "播攞或æšĢ停åŊąį‰‡", - "play_original_video": "播攞原始čĻ–é ģ", - "play_original_video_setting_description": "æ›´å–œæ­Ąæ’­æ”žåŽŸå§‹čĻ–é ģīŧŒč€Œä¸æ˜¯čŊ‰įĸŧčĻ–é ģ。 åĻ‚æžœåŽŸå§‹čŗ‡æēä¸į›¸åŽšīŧŒå‰‡å¯čƒŊį„Ąæŗ•æ­Ŗįĸēæ’­æ”žã€‚", - "play_transcoded_video": "播攞čŊ‰įĸŧčĻ–é ģ", + "play_original_video": "播攞原始åŊąį‰‡", + "play_original_video_setting_description": "å„Ē先播攞原始åŊąį‰‡č€ŒéžčŊ‰įĸŧåžŒįš„åŊąį‰‡ã€‚č‹ĨåŽŸå§‹é …į›Žä¸į›¸åŽšīŧŒå¯čƒŊį„Ąæŗ•æ­Ŗå¸¸æ’­æ”žã€‚", + "play_transcoded_video": "播攞čŊ‰įĸŧåŊąį‰‡", "please_auth_to_access": "čĢ‹é€˛čĄŒčēĢäģŊéŠ—č­‰æ‰čƒŊ存取", - "port": "åŸ åŖ", + "port": "逪æŽĨ埠", "preferences_settings_subtitle": "įŽĄį†æ‡‰į”¨į¨‹åŧååĨŊč¨­åŽš", "preferences_settings_title": "偏åĨŊč¨­åŽš", "preparing": "æē–å‚™", @@ -1620,70 +1758,72 @@ "previous_memory": "上一åŧĩ回æ†ļ", "previous_or_next_day": "前一夊/垌一夊", "previous_or_next_month": "上一個月/下一個月", - "previous_or_next_photo": "上一åŧĩᅧቇ/下一åŧĩᅧቇ", + "previous_or_next_photo": "上一åŧĩᛏቇ/下一åŧĩᛏቇ", "previous_or_next_year": "上一嚴/下一嚴", "primary": "éĻ–čρ", "privacy": "éšąį§", "profile": "叺æˆļč¨­åŽš", - "profile_drawer_app_logs": "æ—Ĩčnj", - "profile_drawer_client_server_up_to_date": "ᔍæˆļįĢ¯čˆ‡äŧ翜å™¨į̝éƒŊæ˜¯æœ€æ–°įš„", + "profile_drawer_app_logs": "į´€éŒ„", + "profile_drawer_client_server_up_to_date": "ᔍæˆļįĢ¯čˆ‡äŧ翜å™¨į‰ˆæœŦįš†į‚ē最新", "profile_drawer_github": "GitHub", - "profile_drawer_readonly_mode": "å”¯čŽ€æ¨Ąåŧåˇ˛é–‹å•Ÿã€‚čĢ‹é•ˇæŒ‰äŊŋį”¨č€…é ­åƒåœ–į¤ēäģĨįĩæŸã€‚", + "profile_drawer_readonly_mode": "å”¯čŽ€æ¨Ąåŧåˇ˛å•Ÿį”¨ã€‚é•ˇæŒ‰äŊŋᔍ者個äēē圖į¤ēåŗå¯é€€å‡ē。", "profile_image_of_user": "{user} įš„å€‹äēēčŗ‡æ–™åœ–į‰‡", "profile_picture_set": "åˇ˛č¨­åŽšå€‹äēēčŗ‡æ–™åœ–į‰‡ã€‚", "public_album": "å…Ŧ開ᛏį°ŋ", "public_share": "å…Ŧ開分äēĢ", - "purchase_account_info": "æ“č­ˇč€…", - "purchase_activated_subtitle": "感čŦæ‚¨å° Immich 及開æēčģŸéĢ”įš„æ”¯æ´", + "purchase_account_info": "æ”¯æŒč€…", + "purchase_activated_subtitle": "感čŦæ‚¨å° Immich 及開æēčģŸéĢ”įš„æ”¯æŒ", "purchase_activated_time": "æ–ŧ {date} å•Ÿį”¨", - "purchase_activated_title": "é‡‘é‘°æˆåŠŸå•Ÿį”¨äē†", + "purchase_activated_title": "é‡‘é‘°åˇ˛æˆåŠŸå•Ÿį”¨", "purchase_button_activate": "å•Ÿį”¨", - "purchase_button_buy": "čŗŧįŊŽ", - "purchase_button_buy_immich": "čŗŧįŊŽ Immich", + "purchase_button_buy": "čŗŧ財", + "purchase_button_buy_immich": "čŗŧ財 Immich", "purchase_button_never_show_again": "ä¸å†éĄ¯į¤ē", - "purchase_button_reminder": "過 30 夊再提醒我", + "purchase_button_reminder": "30 夊垌提醒我", "purchase_button_remove_key": "į§ģ除金鑰", - "purchase_button_select": "選這個", + "purchase_button_select": "選擇", "purchase_failed_activation": "å•Ÿį”¨å¤ąæ•—īŧčĢ‹æĒĸæŸĨæ‚¨įš„é›ģ子éƒĩäģļīŧŒå–åž—æ­Ŗįĸēįš„į”ĸ品金鑰īŧ", "purchase_individual_description_1": "針對個äēē", - "purchase_individual_description_2": "æ“č­ˇč€…į‹€æ…‹", + "purchase_individual_description_2": "æ”¯æŒč€…į‹€æ…‹", "purchase_individual_title": "個äēē", - "purchase_input_suggestion": "有į”ĸ品金鑰嗎īŧŸčĢ‹åœ¨ä¸‹éĸčŧ¸å…Ĩ金鑰", - "purchase_license_subtitle": "čŗŧįŊŽ Immich 䞆支援čģŸé̔開į™ŧ", - "purchase_lifetime_description": "įĩ‚čēĢčŗŧįŊŽ", - "purchase_option_title": "čŗŧįŊŽé¸é …", - "purchase_panel_info_1": "開į™ŧ Immich 可不是äģļåŽšæ˜“įš„äē‹īŧŒčŠąä熿ˆ‘們不少功å¤Ģ。åĨŊåœ¨æœ‰ä¸€įž¤å…¨čˇåˇĨፋå¸Ģåœ¨čƒŒåžŒéģ˜éģ˜åŠĒ力īŧŒį‚ēįš„å°ąæ˜¯æŠŠåŽƒåšåˆ°æœ€åĨŊã€‚æˆ‘å€‘įš„į›Žæ¨™åžˆį°Ąå–ŽīŧščŽ“é–‹æ”žåŽŸå§‹įĸŧčģŸéĢ”å’Œæ­Ŗį•ļįš„å•†æĨ­æ¨ĄåŧčƒŊ成į‚ē開į™ŧč€…įš„é•ˇæœŸéŖ¯įĸ—īŧŒåŒæ™‚打造å‡ē重čĻ–éšąį§įš„į”Ÿæ…‹įŗģįĩąīŧŒčޓ大åŽļ有個不čĸĢ限åˆļįš„é›˛įĢ¯æœå‹™æ–°é¸æ“‡ã€‚", - "purchase_panel_info_2": "我們æ‰ŋčĢžä¸č¨­äģ˜č˛ģቆīŧŒæ‰€äģĨčŗŧįŊŽ Immich ä¸Ļä¸æœƒčŽ“æ‚¨į˛åž—éĄå¤–įš„åŠŸčƒŊ。我們äģ°čŗ´äŊŋį”¨č€…å€‘įš„æ”¯æ´äž†é–‹į™ŧ Immich。", - "purchase_panel_title": "æ”¯æ´é€™é …å°ˆæĄˆ", + "purchase_input_suggestion": "åˇ˛æœ‰į”ĸ品金鑰īŧŸčĢ‹åœ¨ä¸‹æ–ščŧ¸å…Ĩ", + "purchase_license_subtitle": "čŗŧ財 Immich äģĨ支持čģŸéĢ”įš„æŒįēŒé–‹į™ŧ", + "purchase_lifetime_description": "įĩ‚čēĢæŽˆæŦŠ", + "purchase_option_title": "čŗŧ買選項", + "purchase_panel_info_1": "開į™ŧ Immich 需čĻæŠ•å…Ĩå¤§é‡æ™‚é–“čˆ‡į˛žåŠ›īŧŒæˆ‘å€‘æœ‰å…¨čˇåˇĨፋå¸Ģč‡´åŠ›æ–ŧ將å…￉“é€ åž—į›Ąå–„į›ĄįžŽã€‚æˆ‘å€‘įš„äŊŋå‘Ŋæ˜¯čŽ“é–‹æēčģŸéĢ”čˆ‡åˆäšŽé“åžˇįš„å•†æĨ­æ¨Ąåŧæˆį‚ē開į™ŧč€…æ°¸įēŒįš„æ”ļå…Ĩ來æēīŧŒä¸ĻåģēįĢ‹ä¸€å€‹é‡čĻ–éšąį§įš„į”Ÿæ…‹įŗģįĩąīŧŒæäž›å–äģŖå‰å‰Šæ€§é›˛įĢ¯æœå‹™įš„įœŸå¯Ļ選擇。", + "purchase_panel_info_2": "我們æ‰ŋčĢžä¸č¨­äģ˜č˛ģቆīŧŒæ‰€äģĨčŗŧ財 Immich ä¸Ļä¸æœƒčŽ“æ‚¨į˛åž—éĄå¤–įš„åŠŸčƒŊ。我們äģ°čŗ´åƒæ‚¨é€™æ¨Ŗįš„äŊŋį”¨č€…äž†æ”¯æŒ Immich įš„æŒįēŒé–‹į™ŧ。", + "purchase_panel_title": "æ”¯æŒæ­¤å°ˆæĄˆ", "purchase_per_server": "每č‡ēäŧ翜å™¨", "purchase_per_user": "每äŊäŊŋᔍ者", "purchase_remove_product_key": "į§ģ除į”ĸ品金鑰", "purchase_remove_product_key_prompt": "įĸē厚čρį§ģ除į”ĸ品金鑰嗎īŧŸ", "purchase_remove_server_product_key": "į§ģ除äŧ翜å™¨į”ĸ品金鑰", "purchase_remove_server_product_key_prompt": "įĸē厚čρį§ģ除äŧ翜å™¨į”ĸ品金鑰嗎īŧŸ", - "purchase_server_description_1": "įĩĻæ•´č‡ēäŧ翜å™¨", - "purchase_server_description_2": "æ“č­ˇč€…į‹€æ…‹", + "purchase_server_description_1": "éŠį”¨æ–ŧ全äŧ翜å™¨", + "purchase_server_description_2": "æ”¯æŒč€…į‹€æ…‹", "purchase_server_title": "äŧ翜å™¨", - "purchase_settings_server_activated": "äŧ翜å™¨į”ĸå“é‡‘é‘°æ˜¯į”ąįŽĄį†č€…įŽĄį†įš„", - "query_asset_id": "æŸģčŠĸčŗ‡į”ĸ ID", + "purchase_settings_server_activated": "äŧ翜å™¨į”ĸå“é‡‘é‘°į”ąįŽĄį†å“ĄįŽĄį†", + "query_asset_id": "æŸĨčŠĸé …į›Ž ID", "queue_status": "處ᐆ䏭 {count}/{total}", + "rate_asset": "é …į›ŽčŠ•åˆ†", "rating": "čŠ•æ˜Ÿ", "rating_clear": "æ¸…é™¤čŠ•į­‰", "rating_count": "{count, plural, other {# 星}}", "rating_description": "åœ¨čŗ‡č¨Šéĸæŋä¸­éĄ¯į¤ē EXIF čŠ•į­‰", + "rating_set": "åˇ˛č¨­åŽšį‚ē{rating, plural, one {# 星} other {# 星}}", "reaction_options": "反應選項", - "read_changelog": "閱čĻŊčŽŠæ›´æ—Ĩčnj", - "readonly_mode_disabled": "å”¯čŽ€æ¨Ąåŧåˇ˛é—œé–‰", + "read_changelog": "閱čĻŊæ›´æ–°į´€éŒ„", + "readonly_mode_disabled": "å”¯čŽ€æ¨Ąåŧåˇ˛åœį”¨", "readonly_mode_enabled": "å”¯čŽ€æ¨Ąåŧåˇ˛é–‹å•Ÿ", "ready_for_upload": "厞æē–å‚™åĨŊä¸Šå‚ŗ", - "reassign": "重新指厚", - "reassigned_assets_to_existing_person": "厞將 {count, plural, other {# 個æĒ”æĄˆ}}重新指厚įĩĻ{name, select, null {įžæœ‰įš„äēē} other {{name}}}", - "reassigned_assets_to_new_person": "厞將 {count, plural, other {# 個æĒ”æĄˆ}}重新指厚įĩĻ一äŊæ–°äēēį‰Š", - "reassing_hint": "å°‡é¸åŽšįš„æĒ”æĄˆåˆ†é…įĩĻåˇąå­˜åœ¨įš„äēēį‰Š", + "reassign": "重新指洞", + "reassigned_assets_to_existing_person": "厞將 {count, plural, other {# å€‹é …į›Ž}} 重新指洞įĩĻ {name, select, null {įžæœ‰äēēį‰Š} other {{name}}}", + "reassigned_assets_to_new_person": "厞將 {count, plural, other {# å€‹é …į›Ž}} 重新指洞įĩĻæ–°įš„äēēį‰Š", + "reassing_hint": "å°‡é¸å–įš„é …į›ŽæŒ‡æ´žįĩĻįžæœ‰äēēį‰Š", "recent": "最čŋ‘", - "recent-albums": "最čŋ‘ᛏį°ŋ", + "recent_albums": "最čŋ‘ᛏį°ŋ", "recent_searches": "最čŋ‘æœå°‹é …į›Ž", - "recently_added": "čŋ‘期新åĸž", + "recently_added": "最čŋ‘æ–°åĸž", "recently_added_page_title": "最čŋ‘æ–°åĸž", "recently_taken": "最čŋ‘拍攝", "recently_taken_page_title": "最čŋ‘拍攝", @@ -1693,7 +1833,7 @@ "refresh_metadata": "é‡æ–°æ•´į†ä¸­įšŧčŗ‡æ–™", "refresh_thumbnails": "é‡æ–°æ•´į†į¸Žåœ–", "refreshed": "é‡æ–°æ•´į†åŽŒį•ĸ", - "refreshes_every_file": "é‡æ–°čŽ€å–įžæœ‰įš„æ‰€æœ‰æĒ”æĄˆå’Œæ–°æĒ”æĄˆ", + "refreshes_every_file": "é‡æ–°čŽ€å–æ‰€æœ‰įžæœ‰čˆ‡æ–°åĸžæĒ”æĄˆ", "refreshing_encoded_video": "æ­Ŗåœ¨é‡æ–°æ•´į†åˇ˛įˇ¨įĸŧįš„åŊąį‰‡", "refreshing_faces": "重整éĸéƒ¨čŗ‡æ–™ä¸­", "refreshing_metadata": "æ­Ŗåœ¨é‡æ–°æ•´į†ä¸­įšŧčŗ‡æ–™", @@ -1703,16 +1843,16 @@ "remote_media_summary": "遠į̝åĒ’éĢ”æ‘˜čρ", "remove": "į§ģ除", "remove_assets_album_confirmation": "įĸē厚čĻåžžį›¸į°ŋ中į§ģ除 {count, plural, other {# 個æĒ”æĄˆ}}嗎īŧŸ", - "remove_assets_shared_link_confirmation": "įĸē厚åˆĒé™¤å…ąäēĢ逪įĩä¸­{count, plural, other {# å€‹é …į›Ž}}嗎īŧŸ", + "remove_assets_shared_link_confirmation": "įĸē厚čĻåžžæ­¤åˆ†äēĢ逪įĩä¸­į§ģ除 {count, plural, other {# å€‹é …į›Ž}} 嗎īŧŸ", "remove_assets_title": "į§ģ除æĒ”æĄˆīŧŸ", "remove_custom_date_range": "į§ģ除č‡Ē訂æ—ĨæœŸį¯„åœ", "remove_deleted_assets": "į§ģ除é›ĸ᎚æĒ”æĄˆ", "remove_from_album": "åžžį›¸į°ŋ中į§ģ除", "remove_from_album_action_prompt": "åˇ˛åžžį›¸į°ŋ中į§ģ除äē† {count} å€‹é …į›Ž", "remove_from_favorites": "åžžæ”ļ藏中į§ģ除", - "remove_from_lock_folder_action_prompt": "åˇ˛åžžéŽ–åŽšįš„čŗ‡æ–™å¤žä¸­į§ģ除äē† {count} å€‹é …į›Ž", - "remove_from_locked_folder": "åžžéŽ–åŽšįš„čŗ‡æ–™å¤žä¸­į§ģ除", - "remove_from_locked_folder_confirmation": "您įĸē厚čρ將這äē›į…§į‰‡å’ŒåŊąį‰‡į§ģå‡ēéŽ–åŽšįš„čŗ‡æ–™å¤žå—ŽīŧŸé€™äē›å…§åŽšå°‡æœƒéĄ¯į¤ēåœ¨æ‚¨įš„į›¸į°ŋ中。", + "remove_from_lock_folder_action_prompt": "åˇ˛åžžã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žä¸­į§ģ除 {count} å€‹é …į›Ž", + "remove_from_locked_folder": "åžžã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žä¸­į§ģ除", + "remove_from_locked_folder_confirmation": "您įĸē厚čρ將這äē›į›¸į‰‡čˆ‡åŊąį‰‡į§ģå‡ēã€Œåˇ˛éŽ–åŽšã€čŗ‡æ–™å¤žå—ŽīŧŸį§ģå‡ē垌將會å‡ēįžåœ¨æ‚¨įš„åĒ’éĢ”åēĢ中。", "remove_from_shared_link": "åžžå…ąäēĢ逪įĩä¸­į§ģ除", "remove_memory": "į§ģ除記æ†ļ", "remove_photo_from_memory": "å°‡åœ–į‰‡åžžæ­¤č¨˜æ†ļ中į§ģ除", @@ -1724,11 +1864,11 @@ "removed_from_favorites": "åˇ˛åžžæ”ļ藏中į§ģ除", "removed_from_favorites_count": "厞į§ģ除æ”ļč—įš„ {count, plural, other {# å€‹é …į›Ž}}", "removed_memory": "厞į§ģ除記æ†ļ", - "removed_photo_from_memory": "åˇ˛åžžč¨˜æ†ļ中į§ģ除ᅧቇ", + "removed_photo_from_memory": "åˇ˛åžžč¨˜æ†ļ中į§ģ除ᛏቇ", "removed_tagged_assets": "厞į§ģ除 {count, plural, one {# 個æĒ”æĄˆ} other {# 個æĒ”æĄˆ}}įš„æ¨™įą¤", "rename": "攚名", "repair": "įŗžæ­Ŗ", - "repair_no_results_message": "æœĒčĸĢčŋŊčš¤åŠæœĒč™•į†įš„æĒ”æĄˆæœƒéĄ¯į¤ēåœ¨é€™čŖĄ", + "repair_no_results_message": "æœĒčĸĢčŋŊčš¤åŠéēå¤ąįš„æĒ”æĄˆæœƒéĄ¯į¤ēåœ¨é€™čŖĄ", "replace_with_upload": "į”¨ä¸Šå‚ŗįš„æĒ”æĄˆå–äģŖ", "repository": "å„˛å­˜åēĢ", "require_password": "需čρ坆įĸŧ", @@ -1738,19 +1878,19 @@ "reset_password": "é‡č¨­å¯†įĸŧ", "reset_people_visibility": "重設äēēį‰Šå¯čĻ‹æ€§", "reset_pin_code": "重設 PIN įĸŧ", - "reset_pin_code_description": "č‹Ĩåŋ˜č¨˜äē† PIN įĸŧīŧŒé–Ŗä¸‹å¯čĻæą‚įŗģįĩąäŧ翜å™¨įŽĄį†å“Ąį‚ēæ‚¨é‡č¨­", - "reset_pin_code_success": "é–Ŗä¸‹åˇ˛æˆåŠŸé‡č¨­ PIN įĸŧ", + "reset_pin_code_description": "č‹Ĩåŋ˜č¨˜ PIN įĸŧīŧŒæ‚¨å¯äģĨ聝įĩĄäŧ翜å™¨įŽĄį†å“Ąé€˛čĄŒé‡č¨­", + "reset_pin_code_success": "PIN įĸŧåˇ˛æˆåŠŸé‡č¨­", "reset_pin_code_with_password": "您可隨時äŊŋį”¨æ‚¨įš„å¯†įĸŧ來重設 PIN įĸŧ", "reset_sqlite": "重設 SQLite čŗ‡æ–™åēĢ", - "reset_sqlite_confirmation": "įĸē厚čĻé‡č¨­ SQLite čŗ‡æ–™åēĢ嗎īŧŸé–Ŗä¸‹éœ€į™ģå‡ēä¸Ļ重新į™ģå…Ĩ才čƒŊ重新同æ­Ĩčŗ‡æ–™", + "reset_sqlite_confirmation": "įĸē厚čĻé‡č¨­ SQLite čŗ‡æ–™åēĢ嗎īŧŸæ‚¨éœ€čρį™ģå‡ēä¸Ļ重新į™ģå…Ĩ才čƒŊ重新同æ­Ĩčŗ‡æ–™", "reset_sqlite_success": "åˇ˛æˆåŠŸé‡č¨­ SQLite čŗ‡æ–™åēĢ", - "reset_to_default": "é‡č¨­å›žé č¨­", - "resolution": "åˆ†čž¯įŽ‡", + "reset_to_default": "重設į‚ē預設å€ŧ", + "resolution": "č§ŖæžåēĻ", "resolve_duplicates": "č§Ŗæąē重複項", "resolved_all_duplicates": "厞觪æąēæ‰€æœ‰é‡č¤‡é …į›Ž", "restore": "還原", "restore_all": "全部還原", - "restore_trash_action_prompt": "åˇ˛åžžåžƒåœžæĄļ垊原äē† {count} å€‹é …į›Ž", + "restore_trash_action_prompt": "åˇ˛åžžåžƒåœžæĄļ還原 {count} å€‹é …į›Ž", "restore_user": "還原äŊŋᔍ者", "restored_asset": "åˇ˛é‚„åŽŸæĒ”æĄˆ", "resume": "įšŧįēŒ", @@ -1770,18 +1910,20 @@ "saved_settings": "åˇ˛å„˛å­˜č¨­åŽš", "say_something": "čĒĒčĒĒæ‚¨įš„æƒŗæŗ•吧", "scaffold_body_error_occurred": "į™ŧį”ŸéŒ¯čǤ", + "scan": "掃描", "scan_all_libraries": "æŽƒææ‰€æœ‰į›¸į°ŋ", "scan_library": "掃描", "scan_settings": "æŽƒæč¨­åŽš", + "scanning": "æ­Ŗåœ¨æŽƒæ", "scanning_for_album": "æŽƒæį›¸į°ŋ中â€Ļâ€Ļ", "search": "搜尋", "search_albums": "æœå°‹į›¸į°ŋ", "search_by_context": "äģĨ情åĸƒæœå°‹", "search_by_description": "äģĨ描čŋ°æœå°‹", "search_by_description_example": "åœ¨æ˛™åŖŠįš„åĨ行之æ—Ĩ", - "search_by_filename": "äģĨæĒ”名或副æĒ”名搜尋", + "search_by_filename": "䞝æĒ”名或副æĒ”名搜尋", "search_by_filename_example": "åĻ‚ IMG_1234.JPG 或 PNG", - "search_by_ocr": "通過OCR蒐į´ĸ", + "search_by_ocr": "透過OCR搜尋", "search_by_ocr_example": "æ‹ŋéĩ", "search_camera_lens_model": "蒐į´ĸéĄé ­åž‹č™Ÿ...", "search_camera_make": "æœå°‹į›¸æŠŸčŖŊ造商â€Ļ", @@ -1800,21 +1942,22 @@ "search_filter_location_title": "選擇äŊįŊŽ", "search_filter_media_type": "åĒ’éĢ”éĄžåž‹", "search_filter_media_type_title": "選擇åĒ’éĢ”éĄžåž‹", - "search_filter_ocr": "通過OCR蒐į´ĸ", + "search_filter_ocr": "透過OCR搜尋", "search_filter_people_title": "選擇äēēį‰Š", + "search_filter_star_rating": "čŠ•åˆ†", "search_for": "搜尋", - "search_for_existing_person": "æœå°‹įžæœ‰įš„äēēį‰Š", + "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_motion_photos": "å‹•æ…‹į›¸į‰‡", "search_page_no_objects": "æ‰žä¸åˆ°į‰Šäģļčŗ‡č¨Š", "search_page_no_places": "扞不到地éģžčŗ‡č¨Š", "search_page_screenshots": "čžĸåš•æˆĒ圖", - "search_page_search_photos_videos": "æœå°‹æ‚¨įš„į…§į‰‡čˆ‡åŊąį‰‡", + "search_page_search_photos_videos": "æœå°‹æ‚¨įš„į›¸į‰‡čˆ‡åŊąį‰‡", "search_page_selfies": "č‡Ē拍", "search_page_things": "äē‹į‰Š", "search_page_view_all_button": "æĒĸčĻ–å…¨éƒ¨", @@ -1831,29 +1974,35 @@ "search_tags": "æœå°‹æ¨™įą¤...", "search_timezone": "搜尋時區â€Ļ", "search_type": "æœå°‹éĄžåž‹", - "search_your_photos": "æœå°‹į…§į‰‡", + "search_your_photos": "æœå°‹į›¸į‰‡", "searching_locales": "搜尋區域â€Ļ", "second": "į§’", "see_all_people": "æĒĸčĻ–æ‰€æœ‰äēēį‰Š", "select": "選擇", + "select_album": "é¸æ“‡į›¸į°ŋ", "select_album_cover": "é¸æ“‡į›¸į°ŋ封éĸ", + "select_albums": "é¸æ“‡į›¸į°ŋ", "select_all": "選擇全部", "select_all_duplicates": "äŋį•™æ‰€æœ‰é‡č¤‡é …", "select_all_in": "選擇在 {group} ä¸­įš„æ‰€æœ‰é …į›Ž", - "select_avatar_color": "選擇個äēēčŗ‡æ–™åœ–į‰‡éĄč‰˛", + "select_avatar_color": "選擇個äēē圖į¤ē顏色", + "select_count": "{count, plural, one {選擇 #} other {選擇 #}}", + "select_cutoff_date": "選擇æˆĒæ­ĸæ—Ĩ期", "select_face": "é¸æ“‡č‡‰å­”", - "select_featured_photo": "é¸æ“‡į‰šč‰˛į…§į‰‡", + "select_featured_photo": "é¸å–į˛žé¸į›¸į‰‡", "select_from_computer": "åžžé›ģč…Ļ中選取", "select_keep_all": "全部äŋį•™", "select_library_owner": "é¸æ“‡į›¸į°ŋæ“æœ‰č€…", "select_new_face": "é¸æ“‡æ–°č‡‰å­”", + "select_people": "選擇äēēį‰Š", + "select_person": "選取äēēį‰Š", "select_person_to_tag": "選擇čĻæ¨™č¨˜įš„äēēį‰Š", - "select_photos": "遏ᅧቇ", + "select_photos": "遏ᛏቇ", "select_trash_all": "全部åˆĒ除", - "select_user_for_sharing_page_err_album": "新åĸžį›¸į°ŋå¤ąæ•—", - "selected": "åˇ˛é¸æ“‡", - "selected_count": "{count, plural, other {選äē† # 項}}", - "selected_gps_coordinates": "é¸åŽšįš„ GPS åē§æ¨™", + "select_user_for_sharing_page_err_album": "åģēį̋ᛏį°ŋå¤ąæ•—", + "selected": "åˇ˛é¸å–", + "selected_count": "{count, plural, other {åˇ˛é¸å– # 項}}", + "selected_gps_coordinates": "é¸å–įš„ GPS åē§æ¨™", "send_message": "å‚ŗč¨Šæ¯", "send_welcome_email": "å‚ŗé€æ­ĄčŋŽé›ģ子éƒĩäģļ", "server_endpoint": "äŧ翜å™¨į̝éģž", @@ -1862,23 +2011,23 @@ "server_offline": "äŧ翜å™¨åˇ˛é›ĸ᎚", "server_online": "äŧ翜å™¨åˇ˛ä¸Šįˇš", "server_privacy": "äŧ翜å™¨éšąį§", - "server_restarting_description": "此頁éĸ將įĢ‹åŗé‡įšĒ。", - "server_restarting_title": "æœå‹™å™¨æ­Ŗåœ¨é‡æ–°å•Ÿå‹•", + "server_restarting_description": "此頁éĸå°‡åœ¨į¨åžŒč‡Ēå‹•é‡æ–°æ•´į†ã€‚", + "server_restarting_title": "äŧ翜å™¨æ­Ŗåœ¨é‡æ–°å•Ÿå‹•", "server_stats": "äŧ翜å™¨įĩąč¨ˆ", - "server_update_available": "æœå‹™å™¨æ›´æ–°å¯į”¨", + "server_update_available": "åˇ˛æœ‰å¯į”¨įš„äŧ翜å™¨æ›´æ–°", "server_version": "į›Žå‰į‰ˆæœŦ", "set": "č¨­åŽš", "set_as_album_cover": "設į‚ēᛏį°ŋ封éĸ", - "set_as_featured_photo": "設į‚ēį‰šč‰˛į…§į‰‡", + "set_as_featured_photo": "設į‚ēį˛žé¸į›¸į‰‡", "set_as_profile_picture": "設į‚ē個äēēčŗ‡æ–™åœ–į‰‡", "set_date_of_birth": "č¨­åŽšå‡ēį”Ÿæ—Ĩ期", "set_profile_picture": "č¨­åŽšå€‹äēēčŗ‡æ–™åœ–į‰‡", "set_slideshow_to_fullscreen": "äģĨ全čžĸ嚕攞映åšģį‡ˆį‰‡", "set_stack_primary_asset": "č¨­åŽšå †į–Šįš„éĻ–čĻé …į›Ž", - "setting_image_viewer_help": "čŠŗį´°čŗ‡č¨ŠæĒĸčϖ噍éĻ–å…ˆčŧ‰å…Ĩå°į¸Žåœ–īŧŒį„ļ垌čŧ‰å…Ĩä¸­į­‰å¤§å°įš„é čĻŊ圖īŧˆč‹Ĩå•Ÿį”¨īŧ‰īŧŒæœ€åžŒčŧ‰å…ĨåŽŸå§‹åœ–į‰‡ã€‚", - "setting_image_viewer_original_subtitle": "å•Ÿį”¨äģĨčŧ‰å…Ĩ原圖īŧŒåœį”¨äģĨæ¸›å°‘čŗ‡æ–™äŊŋį”¨é‡īŧˆåŒ…æ‹Ŧįļ˛čˇ¯å’ŒčŖįŊŽåŋĢ取īŧ‰ã€‚", + "setting_image_viewer_help": "čŠŗį´°čŗ‡č¨ŠæĒĸčĻ–å™¨æœƒäžåēčŧ‰å…Ĩå°åž‹į¸Žåœ–ã€ä¸­į­‰å°ē寸預čĻŊ圖īŧˆč‹Ĩå•Ÿį”¨īŧ‰īŧŒæœ€åžŒčŧ‰å…ĨåŽŸå§‹į›¸į‰‡ã€‚", + "setting_image_viewer_original_subtitle": "å•Ÿį”¨äģĨčŧ‰å…ĨåŽŸå§‹å…¨č§ŖæžåēĻåœ–į‰‡īŧˆæĒ”æĄˆčŧƒå¤§īŧīŧ‰ã€‚åœį”¨äģĨ減少æĩé‡äŊŋᔍīŧˆåŒ…æ‹Ŧįļ˛čˇ¯å‚ŗčŧ¸čˆ‡čŖįŊŽåŋĢ取īŧ‰ã€‚", "setting_image_viewer_original_title": "čŧ‰å…Ĩ原圖", - "setting_image_viewer_preview_subtitle": "å•Ÿį”¨äģĨčŧ‰å…Ĩ䏭ᭉ品čŗĒįš„åœ–į‰‡īŧŒåœį”¨äģĨčŧ‰å…ĨåŽŸåœ–æˆ–į¸Žåœ–ã€‚", + "setting_image_viewer_preview_subtitle": "å•Ÿį”¨äģĨčŧ‰å…Ĩä¸­į­‰č§ŖæžåēĻåœ–į‰‡ã€‚åœį”¨å‰‡į›´æŽĨčŧ‰å…Ĩ原圖或僅äŊŋį”¨į¸Žåœ–ã€‚", "setting_image_viewer_preview_title": "čŧ‰å…Ĩ預čĻŊ圖", "setting_image_viewer_title": "åœ–į‰‡", "setting_languages_apply": "åĨ—ᔍ", @@ -1894,10 +2043,10 @@ "setting_notifications_subtitle": "čĒŋ整通įŸĨ選項", "setting_notifications_total_progress_subtitle": "į¸ŊéĢ”ä¸Šå‚ŗé€˛åēĻ (åˇ˛åŽŒæˆ/į¸Ŋ計)", "setting_notifications_total_progress_title": "éĄ¯į¤ēčƒŒæ™¯å‚™äģŊį¸Ŋ進åēĻ", - "setting_video_viewer_auto_play_subtitle": "打開čĻ–é ģ時č‡Ē動開始播攞", - "setting_video_viewer_auto_play_title": "č‡Ē動播攞čĻ–é ģ", - "setting_video_viewer_looping_title": "čŋ´åœˆæ’­æ”ž", - "setting_video_viewer_original_video_subtitle": "åžžäŧ翜å™¨ä¸˛æĩåŊąį‰‡æ™‚īŧŒå„Ē先播攞原始į•ĢčŗĒīŧˆåŗäŊŋ有čŊ‰æĒ”įš„į‰ˆæœŦå¯į”¨īŧ‰ã€‚這可čƒŊæœƒå°Žč‡´æ’­æ”žæ™‚å‡ēįžįˇŠčĄæƒ…æŗã€‚č‹ĨåŊąį‰‡åˇ˛å„˛å­˜åœ¨æœŦ抟īŧŒå‰‡ä¸€åž‹äģĨ原始į•ĢčŗĒ播攞īŧŒčˆ‡æ­¤č¨­åŽšį„Ąé—œã€‚", + "setting_video_viewer_auto_play_subtitle": "開啟åŊąį‰‡æ™‚č‡Ē動開始播攞", + "setting_video_viewer_auto_play_title": "č‡Ē動播攞åŊąį‰‡", + "setting_video_viewer_looping_title": "åžĒį’°æ’­æ”ž", + "setting_video_viewer_original_video_subtitle": "åžžäŧ翜å™¨ä¸˛æĩåŊąį‰‡æ™‚īŧŒåŗäŊŋåˇ˛æœ‰čŊ‰įĸŧį‰ˆæœŦīŧŒäģå„Ē先播攞原始į•ĢčŗĒ。這可čƒŊæœƒå°Žč‡´įˇŠčĄã€‚å„˛å­˜æ–ŧæœŦæŠŸįš„åŊąį‰‡å‰‡ä¸€åž‹äģĨ原始į•ĢčŗĒ播攞īŧŒä¸å—æ­¤č¨­åޚåŊąéŸŋ。", "setting_video_viewer_original_video_title": "一型播攞原始åŊąį‰‡", "settings": "č¨­åŽš", "settings_require_restart": "čĢ‹é‡å•Ÿ Immich äģĨäŊŋč¨­åŽšį”Ÿæ•ˆ", @@ -1906,11 +2055,11 @@ "share": "分äēĢ", "share_action_prompt": "åˇ˛åˆ†äēĢäē† {count} å€‹é …į›Ž", "share_add_photos": "新åĸžé …į›Ž", - "share_assets_selected": "{count} åˇ˛é¸æ“‡", + "share_assets_selected": "åˇ˛é¸å– {count} 項", "share_dialog_preparing": "æ­Ŗåœ¨æē–å‚™...", "share_link": "分äēĢ逪įĩ", "shared": "å…ąäēĢ", - "shared_album_activities_input_disable": "åˇ˛åœį”¨čŠ•čĢ–", + "shared_album_activities_input_disable": "į•™č¨€åŠŸčƒŊåˇ˛åœį”¨", "shared_album_activity_remove_content": "您įĸē厚čρåˆĒ除此æ´ģ動嗎īŧŸ", "shared_album_activity_remove_title": "åˆĒ除æ´ģ動", "shared_album_section_people_action_error": "įĩæŸ/åˆĒ除ᛏį°ŋå¤ąæ•—", @@ -1918,14 +2067,14 @@ "shared_album_section_people_action_remove_user": "åžžį›¸į°ŋ中åˆĒ除äŊŋᔍ者", "shared_album_section_people_title": "äēēį‰Š", "shared_by": "å…ąäēĢč‡Ē", - "shared_by_user": "į”ą {user} 分äēĢ", - "shared_by_you": "į”ąæ‚¨åˆ†äēĢ", - "shared_from_partner": "來č‡Ē {partner} įš„į…§į‰‡", + "shared_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_app_bar_title": "分äēĢ逪įĩ", + "shared_link_clipboard_copied_massage": "厞複čŖŊ到å‰Ēč˛ŧį°ŋ", "shared_link_clipboard_text": "逪įĩīŧš {link}\n密įĸŧīŧš {password}", - "shared_link_create_error": "新åĸžå…ąäēĢ逪įĩæ™‚į™ŧį”ŸéŒ¯čǤ", + "shared_link_create_error": "åģēįĢ‹åˆ†äēĢ逪įĩæ™‚į™ŧį”ŸéŒ¯čǤ", "shared_link_custom_url_description": "äŊŋᔍč‡Ē訂 URL", "shared_link_edit_description_hint": "ᎍčŧ¯å…ąäēĢæčŋ°", "shared_link_edit_expire_after_option_day": "1 夊", @@ -1950,22 +2099,22 @@ "shared_link_expires_seconds": "將在 {count} į§’åžŒéŽæœŸ", "shared_link_individual_shared": "個äēēå…ąäēĢ", "shared_link_info_chip_metadata": "EXIF", - "shared_link_manage_links": "įŽĄį†å…ąäēĢ逪įĩ", - "shared_link_options": "å…ąäēĢ逪įĩé¸é …", - "shared_link_password_description": "čĻæą‚åœ¨å­˜å–æ­¤é€Ŗįĩæ™‚提䞛密įĸŧ", - "shared_links": "å…ąäēĢ逪įĩ", - "shared_links_description": "äģĨ逪įĩåˆ†äēĢį…§į‰‡å’ŒåŊąį‰‡", - "shared_photos_and_videos_count": "{assetCount, plural, other {åˇ˛åˆ†äēĢ # åŧĩį…§į‰‡åŠåŊąį‰‡ã€‚}}", + "shared_link_manage_links": "įŽĄį†åˆ†äēĢ逪įĩ", + "shared_link_options": "分äēĢ逪įĩé¸é …", + "shared_link_password_description": "存取此分äēĢ逪įĩæ™‚čĻæą‚å¯†įĸŧ", + "shared_links": "分äēĢ逪įĩ", + "shared_links_description": "äģĨ逪įĩåˆ†äēĢį›¸į‰‡å’ŒåŊąį‰‡", + "shared_photos_and_videos_count": "{assetCount, plural, other {åˇ˛åˆ†äēĢ # åŧĩį›¸į‰‡åŠåŊąį‰‡ã€‚}}", "shared_with_me": "čˆ‡æˆ‘å…ąäēĢ", "shared_with_partner": "與 {partner} å…ąäēĢ", "sharing": "å…ąäēĢ", "sharing_enter_password": "čρæĒĸčĻ–æ­¤é éĸčĢ‹čŧ¸å…Ĩ密įĸŧ。", "sharing_page_album": "å…ąäēĢᛏį°ŋ", - "sharing_page_description": "新åĸžå…ąäēĢᛏį°ŋäģĨ與įļ˛čˇ¯ä¸­įš„äēēå…ąäēĢį…§į‰‡å’ŒįŸ­į‰‡ã€‚", + "sharing_page_description": "åģēįĢ‹å…ąäēĢᛏį°ŋīŧŒčˆ‡æ‚¨įļ˛čˇ¯ä¸­įš„æˆå“Ąåˆ†äēĢį›¸į‰‡čˆ‡åŊąį‰‡ã€‚", "sharing_page_empty_list": "įŠēį™Ŋ清喎", "sharing_sidebar_description": "在側邊æŦ„éĄ¯į¤ēå…ąäēĢ逪įĩ", - "sharing_silver_appbar_create_shared_album": "新åĸžå…ąäēĢᛏį°ŋ", - "sharing_silver_appbar_share_partner": "å…ąäēĢįĩĻčĻĒæœ‹åĨŊ友", + "sharing_silver_appbar_create_shared_album": "åģēįĢ‹å…ąäēĢᛏį°ŋ", + "sharing_silver_appbar_share_partner": "與čĻĒå‹å…ąäēĢ", "shift_to_permanent_delete": "按 ⇧ 永䚅åˆĒ除æĒ”æĄˆ", "show_album_options": "éĄ¯į¤ēᛏį°ŋ選項", "show_albums": "éĄ¯į¤ēᛏį°ŋ", @@ -1975,19 +2124,20 @@ "show_gallery": "éĄ¯į¤ēį•ĢåģŠ", "show_hidden_people": "éĄ¯į¤ēéšąč—įš„äēēį‰Š", "show_in_timeline": "在時間čģ¸ä¸­éĄ¯į¤ē", - "show_in_timeline_setting_description": "åœ¨æ‚¨įš„æ™‚é–“čģ¸ä¸­éĄ¯į¤ē這äŊäŊŋį”¨č€…įš„į…§į‰‡å’ŒåŊąį‰‡", + "show_in_timeline_setting_description": "åœ¨æ‚¨įš„æ™‚é–“čģ¸ä¸­éĄ¯į¤ē這äŊäŊŋį”¨č€…įš„į›¸į‰‡å’ŒåŊąį‰‡", "show_keyboard_shortcuts": "éĄ¯į¤ēéĩᛤåŋĢæˇéĩ", "show_metadata": "éĄ¯į¤ē中įšŧčŗ‡æ–™", "show_or_hide_info": "éĄ¯į¤ēæˆ–éšąč—čŗ‡č¨Š", "show_password": "éĄ¯į¤ē密įĸŧ", "show_person_options": "éĄ¯į¤ēäēēį‰Šé¸é …", "show_progress_bar": "éĄ¯į¤ē進åēĻæĸ", + "show_schema": "éĄ¯į¤ēæžļ構", "show_search_options": "éĄ¯į¤ē搜尋選項", - "show_shared_links": "éĄ¯į¤ēå…ąäēĢ逪įĩ", + "show_shared_links": "éĄ¯į¤ē分äēĢ逪įĩ", "show_slideshow_transition": "éĄ¯į¤ēåšģį‡ˆį‰‡čŊ‰å ´", - "show_supporter_badge": "æ“č­ˇč€…åžŊįĢ ", - "show_supporter_badge_description": "éĄ¯į¤ēæ“č­ˇč€…åžŊįĢ ", - "show_text_recognition": "éĄ¯į¤ēæ–‡å­—č­˜åˆĨ", + "show_supporter_badge": "æ”¯æŒč€…åžŊįĢ ", + "show_supporter_badge_description": "éĄ¯į¤ēæ”¯æŒč€…åžŊįĢ ", + "show_text_recognition": "éĄ¯į¤ēæ–‡å­—čž¨č­˜", "show_text_search_menu": "éĄ¯į¤ēæ–‡å­—č’į´ĸ選喎", "shuffle": "隨抟排åē", "sidebar": "側邊æŦ„", @@ -1999,22 +2149,24 @@ "skip_to_folders": "čˇŗåˆ°čŗ‡æ–™å¤ž", "skip_to_tags": "莺čŊ‰åˆ°æ¨™įą¤", "slideshow": "åšģį‡ˆį‰‡", + "slideshow_repeat": "é‡č¤‡æŠ•åŊąį‰‡", + "slideshow_repeat_description": "åžĒį’°æ’­æ”ž", "slideshow_settings": "åšģį‡ˆį‰‡č¨­åŽš", "sort_albums_by": "ᛏį°ŋ排åēäžæ“š...", "sort_created": "åģēįĢ‹æ—Ĩ期", "sort_items": "é …į›Žæ•¸é‡", "sort_modified": "æ—ĨæœŸåˇ˛äŋŽæ”š", "sort_newest": "æœ€æ–°įš„į›¸į‰‡", - "sort_oldest": "æœ€čˆŠįš„į…§į‰‡", - "sort_people_by_similarity": "æŒ‰į›¸äŧŧåēϿޒåēäēēå“Ą", - "sort_recent": "æœ€æ–°įš„į…§į‰‡", + "sort_oldest": "æœ€čˆŠįš„į›¸į‰‡", + "sort_people_by_similarity": "äžį›¸äŧŧåēϿޒåēäēēį‰Š", + "sort_recent": "æœ€æ–°įš„į›¸į‰‡", "sort_title": "æ¨™éĄŒ", "source": "來æē", "stack": "å †į–Š", "stack_action_prompt": "åˇ˛å †į–Šäē†{count} å€‹é …į›Ž", "stack_duplicates": "å †į–Šé‡č¤‡é …į›Ž", - "stack_select_one_photo": "į‚ēå †į–Šé¸ä¸€åŧĩä¸ģčρᅧቇ", - "stack_selected_photos": "å †į–Šæ‰€é¸įš„į…§į‰‡", + "stack_select_one_photo": "į‚ēå †į–Šé¸ä¸€åŧĩä¸ģčρᛏቇ", + "stack_selected_photos": "å †į–Šé¸å–įš„į›¸į‰‡", "stacked_assets_count": "åˇ˛å †į–Š {count, plural, one {# 個æĒ”æĄˆ} other {# 個æĒ”æĄˆ}}", "stacktrace": "å †į–ŠčŋŊ蚤", "start": "開始", @@ -2023,10 +2175,10 @@ "state": "地區", "status": "į‹€æ…‹", "stop_casting": "停æ­ĸ投攞", - "stop_motion_photo": "停æ­ĸå‹•æ…‹į…§į‰‡", - "stop_photo_sharing": "čρ停æ­ĸ分äēĢæ‚¨įš„į…§į‰‡å—ŽīŧŸ", - "stop_photo_sharing_description": "{partner} å°‡į„Ąæŗ•å†å­˜å–æ‚¨įš„į…§į‰‡ã€‚", - "stop_sharing_photos_with_user": "停æ­ĸčˆ‡æ­¤äŊŋį”¨č€…å…ąäēĢæ‚¨įš„ᅧቇ", + "stop_motion_photo": "停æ­ĸå‹•æ…‹į›¸į‰‡", + "stop_photo_sharing": "čρ停æ­ĸ分äēĢæ‚¨įš„į›¸į‰‡å—ŽīŧŸ", + "stop_photo_sharing_description": "{partner} å°‡į„Ąæŗ•å†å­˜å–æ‚¨įš„į›¸į‰‡ã€‚", + "stop_sharing_photos_with_user": "停æ­ĸčˆ‡æ­¤äŊŋį”¨č€…å…ąäēĢæ‚¨įš„ᛏቇ", "storage": "å„˛å­˜įŠē間", "storage_label": "å„˛å­˜æ¨™įą¤", "storage_quota": "å„˛å­˜įŠē間", @@ -2038,19 +2190,20 @@ "support": "支援", "support_and_feedback": "æ”¯æ´čˆ‡å›žéĨ‹", "support_third_party_description": "æ‚¨åŽ‰čŖįš„ Immich æ˜¯į”ąįŦŦä¸‰æ–šæ‰“åŒ…įš„ã€‚æ‚¨é‡åˆ°įš„å•éĄŒå¯čƒŊæ˜¯čŠ˛åĨ—äģļé€ æˆįš„īŧŒæ‰€äģĨčĢ‹å…ˆäŊŋᔍ䏋éĸįš„é€Ŗįĩå‘äģ–們提å‡ēå•éĄŒã€‚", + "supporter": "æ”¯æŒč€…", "swap_merge_direction": "ä礿›åˆäŊĩ斚向", "sync": "同æ­Ĩ", "sync_albums": "同æ­Ĩᛏį°ŋ", - "sync_albums_manual_subtitle": "å°‡æ‰€æœ‰ä¸Šå‚ŗįš„įŸ­į‰‡å’Œį…§į‰‡åŒæ­Ĩåˆ°é¸åŽšįš„å‚™äģŊᛏį°ŋ", + "sync_albums_manual_subtitle": "å°‡æ‰€æœ‰ä¸Šå‚ŗįš„åŊąį‰‡čˆ‡į›¸į‰‡åŒæ­Ĩč‡ŗé¸å–įš„å‚™äģŊᛏį°ŋ", "sync_local": "同æ­ĨæœŦ抟", "sync_remote": "同æ­Ĩ遠į̝", "sync_status": "同æ­Ĩį‹€æ…‹", "sync_status_subtitle": "æĒĸčĻ–å’ŒįŽĄį†åŒæ­Ĩįŗģįĩą", - "sync_upload_album_setting_subtitle": "新åĸžį…§į‰‡å’ŒįŸ­į‰‡ä¸Ļä¸Šå‚ŗåˆ° Immich ä¸Šįš„é¸åŽšį›¸į°ŋ中", + "sync_upload_album_setting_subtitle": "åģēįĢ‹ä¸Ļä¸Šå‚ŗį›¸į‰‡čˆ‡åŊąį‰‡č‡ŗ Immich ä¸Šé¸å–įš„į›¸į°ŋ", "tag": "æ¨™įą¤", "tag_assets": "æ¨™č¨˜æĒ”æĄˆ", "tag_created": "厞åģēįĢ‹æ¨™įą¤īŧš{tag}", - "tag_feature_description": "äģĨ邏čŧ¯æ¨™č¨˜čĻæ—¨åˆ†éĄžį€čĻŊį…§į‰‡å’ŒåŊąį‰‡", + "tag_feature_description": "äģĨ邏čŧ¯æ¨™č¨˜čĻæ—¨åˆ†éĄžį€čĻŊį›¸į‰‡å’ŒåŊąį‰‡", "tag_not_found_question": "æ‰žä¸åˆ°æ¨™įą¤īŧŸåģēįĢ‹æ–°æ¨™įą¤ã€‚", "tag_people": "æ¨™įą¤äēēį‰Š", "tag_updated": "åˇ˛æ›´æ–°æ¨™įą¤īŧš{tag}", @@ -2058,7 +2211,7 @@ "tags": "æ¨™įą¤", "tap_to_run_job": "éģžé¸äģĨ進行äŊœæĨ­", "template": "æ¨Ąæŋ", - "text_recognition": "æ–‡å­—č­˜åˆĨ", + "text_recognition": "æ–‡å­—čž¨č­˜", "theme": "ä¸ģ題", "theme_selection": "ä¸ģ題選項", "theme_selection_description": "äžį€čĻŊ器įŗģįĩąååĨŊč‡Ēå‹•č¨­åŽšæˇąã€æˇē色ä¸ģ題", @@ -2075,6 +2228,7 @@ "theme_setting_theme_subtitle": "選擇åĨ—ᔍä¸ģ題", "theme_setting_three_stage_loading_subtitle": "三æŽĩåŧčŧ‰å…Ĩ可čƒŊ提升čŧ‰å…Ĩ效čƒŊīŧŒäŊ†æœƒå¤§åš…åĸžåŠ įļ˛čˇ¯č˛ čŧ‰", "theme_setting_three_stage_loading_title": "å•Ÿį”¨ä¸‰æŽĩåŧčŧ‰å…Ĩ", + "then": "į„ļ垌", "they_will_be_merged_together": "厃們將會čĸĢ合äŊĩ在一čĩˇ", "third_party_resources": "įŦŦä¸‰æ–ščŗ‡æē", "time": "時間", @@ -2100,15 +2254,22 @@ "trash_count": "丟掉 {count, number} 個æĒ”æĄˆ", "trash_delete_asset": "將æĒ”æĄˆä¸Ÿé€˛åžƒåœžæĄļ / åˆĒ除", "trash_emptied": "åˇ˛æ¸…įŠē回æ”ļæĄļ", - "trash_no_results_message": "垃圞æĄļä¸­įš„į…§į‰‡å’ŒåŊąį‰‡å°‡éĄ¯į¤ēåœ¨é€™čŖĄã€‚", + "trash_no_results_message": "垃圞æĄļä¸­įš„į›¸į‰‡å’ŒåŊąį‰‡å°‡éĄ¯į¤ēåœ¨é€™čŖĄã€‚", "trash_page_delete_all": "åˆĒ除全部", "trash_page_empty_trash_dialog_content": "是åĻ清įŠē回æ”ļæĄļīŧŸé€™äē›é …į›Žå°‡čĸĢåžž Immich 中永䚅åˆĒ除", "trash_page_info": "回æ”ļæĄļä¸­é …į›Žå°‡åœ¨ {days} 夊垌永䚅åˆĒ除", "trash_page_no_assets": "æšĢį„Ąåˇ˛åˆĒé™¤é …į›Ž", - "trash_page_restore_all": "æĸ垊全部", + "trash_page_restore_all": "全部還原", "trash_page_select_assets_btn": "é¸æ“‡é …į›Ž", "trash_page_title": "垃圞æĄļ ({count})", "trashed_items_will_be_permanently_deleted_after": "垃圞æĄļä¸­įš„é …į›Žæœƒåœ¨ {days, plural, other {# 夊}}垌永䚅åˆĒ除。", + "trigger": "觸į™ŧ", + "trigger_asset_uploaded": "é …į›Žåˇ˛ä¸Šå‚ŗ", + "trigger_asset_uploaded_description": "åœ¨ä¸Šå‚ŗæ–°é …į›Žæ™‚č§¸į™ŧ", + "trigger_description": "觸į™ŧåˇĨäŊœæĩį¨‹įš„äē‹äģļ", + "trigger_person_recognized": "åˇ˛čž¨č­˜äēēį‰Š", + "trigger_person_recognized_description": "åĩæ¸Ŧ到äēēį‰Šæ™‚č§¸į™ŧ", + "trigger_type": "觸į™ŧéĄžåž‹", "troubleshoot": "ᖑ雪觪᭔", "type": "éĄžåž‹", "unable_to_change_pin_code": "į„Ąæŗ•čŽŠæ›´ PIN įĸŧ", @@ -2123,6 +2284,7 @@ "unhide_person": "取æļˆéšąč—äēēį‰Š", "unknown": "æœĒįŸĨ", "unknown_country": "æœĒįŸĨ國åŽļ", + "unknown_date": "æœĒįŸĨįš„æ—Ĩ期", "unknown_year": "æœĒįŸĨåš´äģŊ", "unlimited": "不限åˆļ", "unlink_motion_video": "觪除逪įĩå‹•æ…‹åŊąį‰‡", @@ -2139,18 +2301,20 @@ "unstack": "取æļˆå †į–Š", "unstack_action_prompt": "{count} 個取æļˆå †į–Š", "unstacked_assets_count": "åˇ˛č§Ŗé™¤å †į–Š {count, plural, other {# 個æĒ”æĄˆ}}", + "unsupported_field_type": "ä¸æ”¯æ´įš„æŦ„äŊéĄžåž‹", "untagged": "į„Ąæ¨™įą¤", + "untitled_workflow": "æœĒå‘Ŋ名åˇĨäŊœæĩį¨‹", "up_next": "下一個", - "update_location_action_prompt": "äŊŋᔍäģĨ下å‘Ŋä줿›´æ–°{count}å€‹æ‰€é¸čŗ‡į”ĸįš„äŊįŊŽīŧš", + "update_location_action_prompt": "更新 {count} å€‹æ‰€é¸é …į›Žįš„äŊįŊŽīŧš", "updated_at": "更新æ–ŧ", "updated_password": "åˇ˛æ›´æ–°å¯†įĸŧ", "upload": "ä¸Šå‚ŗ", - "upload_action_prompt": "{count} å€‹åˇ˛åŠ å…Ĩä¸Šå‚ŗäŊ‡åˆ—", - "upload_concurrency": "ä¸Šå‚ŗä¸Ļ行", + "upload_concurrency": "ä¸Šå‚ŗä¸ĻčĄŒæ•¸", "upload_details": "ä¸Šå‚ŗčŠŗį´°čŗ‡č¨Š", "upload_dialog_info": "是åĻčĻå°‡æ‰€é¸é …į›Žå‚™äģŊ到äŧ翜å™¨īŧŸ", "upload_dialog_title": "ä¸Šå‚ŗé …į›Ž", - "upload_errors": "ä¸Šå‚ŗåŽŒæˆīŧŒäŊ†æœ‰ {count, plural, other {# č™•æ™‚į™ŧį”ŸéŒ¯čǤ}}īŧŒčρæĒĸčĻ–æ–°ä¸Šå‚ŗįš„æĒ”æĄˆčĢ‹é‡æ–°æ•´į†é éĸ。", + "upload_error_with_count": "{count, plural, one {# å€‹é …į›Ž} other {# å€‹é …į›Ž}}ä¸Šå‚ŗéŒ¯čǤ", + "upload_errors": "ä¸Šå‚ŗåŽŒæˆīŧŒäŊ†æœ‰ {count, plural, other {# 個錯čǤ}}。čĢ‹é‡æ–°æ•´į†é éĸäģĨæŸĨįœ‹æ–°ä¸Šå‚ŗįš„é …į›Žã€‚", "upload_finished": "ä¸Šå‚ŗåŽŒæˆ", "upload_progress": "削餘 {remaining, number} - 厞處ᐆ {processed, number}/{total, number}", "upload_skipped_duplicates": "厞į•Ĩ過 {count, plural, other {# å€‹é‡č¤‡įš„æĒ”æĄˆ}}", @@ -2160,7 +2324,7 @@ "upload_success": "ä¸Šå‚ŗæˆåŠŸīŧŒčρæĒĸčĻ–æ–°ä¸Šå‚ŗįš„æĒ”æĄˆčĢ‹é‡æ–°æ•´į†é éĸ。", "upload_to_immich": "ä¸Šå‚ŗč‡ŗ Immich ({count})", "uploading": "ä¸Šå‚ŗä¸­", - "uploading_media": "åĒ’éĢ”ä¸Šå‚ŗä¸­", + "uploading_media": "é …į›Žä¸Šå‚ŗä¸­", "url": "įļ˛å€", "usage": "į”¨é‡", "use_biometric": "äŊŋį”¨į”Ÿį‰Ščž¨č­˜", @@ -2169,11 +2333,11 @@ "user": "äŊŋᔍ者", "user_has_been_deleted": "æ­¤äŊŋᔍ者厞čĸĢåˆĒ除。", "user_id": "äŊŋᔍ者 ID", - "user_liked": "{user} å–œæ­Ąäē† {type, select, photo {這åŧĩᅧቇ} video {這æŽĩåŊąį‰‡} asset {這個æĒ”æĄˆ} other {厃}}", + "user_liked": "{user} å–œæ­Ąäē† {type, select, photo {這åŧĩᛏቇ} video {這æŽĩåŊąį‰‡} asset {這個æĒ”æĄˆ} other {厃}}", "user_pin_code_settings": "PIN įĸŧ", "user_pin_code_settings_description": "įŽĄį†æ‚¨įš„ PIN įĸŧ", "user_privacy": "äŊŋį”¨č€…éšąį§", - "user_purchase_settings": "čŗŧįŊŽ", + "user_purchase_settings": "čŗŧ財", "user_purchase_settings_description": "įŽĄį†æ‚¨įš„čŗŧ財", "user_role_set": "設 {user} į‚ē{role}", "user_usage_detail": "äŊŋį”¨č€…į”¨é‡čŠŗį´°čŗ‡č¨Š", @@ -2185,22 +2349,24 @@ "utilities": "åˇĨå…ˇ", "validate": "驗證", "validate_endpoint_error": "čĢ‹čŧ¸å…Ĩæœ‰æ•ˆįš„ URL", + "validation_error": "éŠ—č­‰éŒ¯čǤ", "variables": "čŽŠæ•¸", "version": "į‰ˆæœŦ", "version_announcement_closing": "æ•ŦįĨé †åŋƒīŧŒAlex", - "version_announcement_message": "嗨īŊžæ–°į‰ˆæœŦįš„ Immich 推å‡ēäē†ã€‚į‚ē防æ­ĸč¨­åŽšį™ŧį”ŸéŒ¯čǤīŧŒčĢ‹čŠąéģžæ™‚間閹莀į™ŧ行čĒĒæ˜ŽīŧŒä¸Ļįĸēäŋč¨­åŽšæ˜¯æœ€æ–°įš„īŧŒį‰šåˆĨ是äŊŋᔍ WatchTower į­‰č‡Ē動更新åˇĨå…ˇæ™‚ã€‚", + "version_announcement_message": "嗨īŧæ–°į‰ˆæœŦįš„ Immich 厞į™ŧ布。čĢ‹čŠąéģžæ™‚間閹莀 į™ŧ行čĒĒæ˜Ž ä¸Ļįĸēäŋæ‚¨įš„č¨­åŽšæ˜¯æœ€æ–°įš„īŧŒäģĨ防æ­ĸäģģäŊ•設åޚ錝čǤīŧŒį‰šåˆĨ是åĻ‚æžœæ‚¨äŊŋᔍ WatchTower 或äģģäŊ•č‡Ē動處ᐆ Immich åŸˇčĄŒå€‹éĢ”æ›´æ–°įš„æŠŸåˆļ。", "version_history": "į‰ˆæœŦį´€éŒ„", "version_history_item": "{date} åŽ‰čŖäē† {version}", "video": "åŊąį‰‡", "video_hover_setting": "éŠæ¨™åœį•™æ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–", - "video_hover_setting_description": "į•ļæģ‘éŧ åœåœ¨é …į›Žä¸Šæ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–ã€‚åŗäŊŋåœį”¨īŧŒå°‡æģ‘éŧ åœåœ¨æ’­æ”žåœ–į¤ē上䚟可äģĨ播攞。", + "video_hover_setting_description": "į•ļæģ‘éŧ åœåœ¨é …į›Žä¸Šæ™‚æ’­æ”žåŊąį‰‡į¸Žåœ–ã€‚åŗäŊŋåœį”¨æ­¤åŠŸčƒŊīŧŒäģå¯é€éŽå°‡æģ‘éŧ åœåœ¨æ’­æ”žåœ–į¤ē上䞆開始播攞。", "videos": "åŊąį‰‡", "videos_count": "{count, plural, other {# 部åŊąį‰‡}}", + "videos_only": "åĒå…č¨ąåŊąį‰‡", "view": "æĒĸčĻ–", "view_album": "æĒĸčϖᛏį°ŋ", "view_all": "į€čĻŊ全部", "view_all_users": "æĒĸčĻ–æ‰€æœ‰äŊŋᔍ者", - "view_asset_owners": "æŸĨįœ‹čŗ‡į”ĸæ‰€æœ‰č€…", + "view_asset_owners": "æŸĨįœ‹é …į›Žæ“æœ‰č€…", "view_details": "æĒĸčĻ–čŠŗį´°čŗ‡č¨Š", "view_in_timeline": "在時間čģ¸ä¸­æĒĸčĻ–", "view_link": "æĒĸčϖ逪įĩ", @@ -2209,13 +2375,15 @@ "view_next_asset": "æĒĸčϖ䏋䏀項", "view_previous_asset": "æĒĸčĻ–ä¸Šä¸€é …", "view_qr_code": "æĒĸčĻ– QR code", - "view_similar_photos": "æĒĸčϖᛏäŧŧᅧቇ", + "view_similar_photos": "æĒĸčϖᛏäŧŧᛏቇ", "view_stack": "æĒĸčĻ–å †į–Š", "view_user": "éĄ¯į¤ēäŊŋᔍ者", "viewer_remove_from_stack": "åžžå †į–Šä¸­į§ģ除", "viewer_stack_use_as_main_asset": "äŊœį‚ēä¸ģé …į›ŽäŊŋᔍ", "viewer_unstack": "取æļˆå †į–Š", "visibility_changed": "åˇ˛čŽŠæ›´ {count, plural, other {# äŊäēēį‰Š}}įš„å¯čĻ‹æ€§", + "visual": "čĻ–čĻēįš„", + "visual_builder": "čĻ–čĻēæ§‹åģē器", "waiting": "åž…č™•į†", "waiting_count": "åž…č™•į†īŧš{count}", "warning": "č­Ļ告", @@ -2224,13 +2392,26 @@ "welcome_to_immich": "æ­ĄčŋŽäŊŋᔍ Immich", "width": "å¯ŦåēĻ", "wifi_name": "Wi-Fi åį¨ą", - "workflow": "åˇĨäŊœæĩį¨‹", + "workflow_delete_prompt": "įĸē厚čρåˆĒ除此åˇĨäŊœæĩį¨‹å—ŽīŧŸ", + "workflow_deleted": "åˇĨäŊœæĩį¨‹åˇ˛åˆĒ除", + "workflow_description": "åˇĨäŊœæĩį¨‹čĒĒæ˜Ž", + "workflow_info": "åˇĨäŊœæĩį¨‹čŠŗį´°čŗ‡č¨Š", + "workflow_json": "åˇĨäŊœæĩį¨‹ JSON", + "workflow_json_help": "äģĨ JSON æ ŧåŧįˇ¨čŧ¯åˇĨäŊœæĩį¨‹č¨­åŽšã€‚čŽŠæ›´å°‡åŒæ­Ĩ臺čĻ–čĻēåŒ–įˇ¨čŧ¯å™¨ã€‚", + "workflow_name": "åˇĨäŊœæĩį¨‹åį¨ą", + "workflow_navigation_prompt": "您įĸē厚čĻä¸å„˛å­˜čŽŠæ›´å°ąé›ĸ開嗎īŧŸ", + "workflow_summary": "åˇĨäŊœæĩį¨‹æ‘˜čρ", + "workflow_update_success": "åˇ˛æˆåŠŸæ›´æ–°åˇĨäŊœæĩį¨‹", + "workflow_updated": "åˇĨäŊœæĩį¨‹åˇ˛æ›´æ–°", + "workflows": "åˇĨäŊœæĩį¨‹", + "workflows_help_text": "æ šæ“šč§¸į™ŧæĸäģļčˆ‡į¯Šé¸å™¨č‡Ēå‹•åŸˇčĄŒå‹•äŊœ", "wrong_pin_code": "PIN įĸŧ錯čǤ", "year": "åš´", "years_ago": "{years, plural, other {# åš´}}前", "yes": "是", - "you_dont_have_any_shared_links": "æ‚¨æ˛’æœ‰äģģäŊ•å…ąäēĢ逪įĩ", + "you_dont_have_any_shared_links": "æ‚¨æ˛’æœ‰äģģäŊ•分äēĢ逪įĩ", "your_wifi_name": "æ‚¨įš„ Wi-Fi åį¨ą", + "zero_to_clear_rating": "按 0 äģĨæ¸…é™¤é …į›ŽčŠ•åˆ†", "zoom_image": "į¸Žæ”žåœ–į‰‡", "zoom_to_bounds": "į¸Žæ”žåˆ°é‚Šį•Œ" } diff --git a/machine-learning/.python-version b/machine-learning/.python-version new file mode 100644 index 0000000000..24ee5b1be9 --- /dev/null +++ b/machine-learning/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 6c976d4612..89480a8cb8 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,8 +1,8 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:e39286476f84ffedf7c3564b0b74e32c9e1193ec9ca32ee8a11f8c09dbf6aafe AS builder-cpu +FROM python:3.11-bookworm@sha256:aa23850b91cb4c7faedac8ca9aa74ddc6eb03529a519145a589a7f35df4c5927 AS builder-cpu -FROM builder-cpu AS builder-openvino +FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS builder-openvino FROM builder-cpu AS builder-cuda @@ -22,33 +22,7 @@ FROM builder-cpu AS builder-rknn # Warning: 25GiB+ disk space required to pull this image # TODO: find a way to reduce the image size -FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS builder-rocm - -# renovate: datasource=github-releases depName=Microsoft/onnxruntime -ARG ONNXRUNTIME_VERSION="v1.22.1" -WORKDIR /code - -RUN apt-get update && apt-get install -y --no-install-recommends wget git python3.10-venv -RUN wget -nv https://github.com/Kitware/CMake/releases/download/v3.30.1/cmake-3.30.1-linux-x86_64.sh && \ - chmod +x cmake-3.30.1-linux-x86_64.sh && \ - mkdir -p /code/cmake-3.30.1-linux-x86_64 && \ - ./cmake-3.30.1-linux-x86_64.sh --skip-license --prefix=/code/cmake-3.30.1-linux-x86_64 && \ - rm cmake-3.30.1-linux-x86_64.sh - -ENV PATH=/code/cmake-3.30.1-linux-x86_64/bin:${PATH} - -RUN git clone --single-branch --branch "${ONNXRUNTIME_VERSION}" --recursive "https://github.com/Microsoft/onnxruntime" onnxruntime -WORKDIR /code/onnxruntime -# Fix for multi-threading based on comments in https://github.com/microsoft/onnxruntime/pull/19567 -# TODO: find a way to fix this without disabling algo caching -COPY ./patches/* /tmp/ -RUN git apply /tmp/*.patch - -RUN /bin/sh ./dockerfiles/scripts/install_common_deps.sh -# Note: the `parallel` setting uses a substantial amount of RAM -RUN ./build.sh --allow_running_as_root --config Release --build_wheel --update --build --parallel 17 --cmake_extra_defines\ - ONNXRUNTIME_VERSION="${ONNXRUNTIME_VERSION}" --skip_tests --use_rocm --rocm_home=/opt/rocm -RUN mv /code/onnxruntime/build/Linux/Release/dist/*.whl /opt/ +FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS builder-rocm FROM builder-${DEVICE} AS builder @@ -64,24 +38,24 @@ 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 \ uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy -RUN if [ "$DEVICE" = "rocm" ]; then \ - uv pip install /opt/onnxruntime_rocm-*.whl; \ - fi -FROM python:3.11-slim-bookworm@sha256:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:04cd27899595a99dfe77709d96f08876bf2ee99139ee2f0fe9ac948005034e5b AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ MACHINE_LEARNING_MODEL_ARENA=false -FROM python:3.11-slim-bookworm@sha256:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-openvino +FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c34ba9d7ecc23d0755e35f AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ - wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-core_1.0.17384.11_amd64.deb && \ - wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17384.11/intel-igc-opencl_1.0.17384.11_amd64.deb && \ - wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/intel-opencl-icd_24.31.30508.7_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-core-2_2.28.4+20760_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-opencl-2_2.28.4+20760_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/intel-opencl-icd_26.05.37020.3-0_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \ # TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file - wget -nv https://github.com/intel/compute-runtime/releases/download/24.31.30508.7/libigdgmm12_22.4.1_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \ dpkg -i *.deb && \ rm *.deb && \ apt-get remove wget -yqq && \ @@ -102,7 +76,11 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3 COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11 COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so -FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS prod-rocm +FROM rocm/dev-ubuntu-24.04:7.2-complete@sha256:86e11093b4a7ec2a79b1b6701d10e840a6994f21c7e05929b51eb9be361c683a AS prod-rocm + +RUN apt-get update && apt-get install --no-install-recommends -yqq migraphx miopen-hip && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* FROM prod-cpu AS prod-armnn diff --git a/machine-learning/immich_ml/config.py b/machine-learning/immich_ml/config.py index 19fd5300df..08dca04a4d 100644 --- a/machine-learning/immich_ml/config.py +++ b/machine-learning/immich_ml/config.py @@ -79,6 +79,7 @@ class Settings(BaseSettings): preload: PreloadModelData | None = None max_batch_size: MaxBatchSize | None = None openvino_precision: ModelPrecision = ModelPrecision.FP32 + rocm_precision: ModelPrecision = ModelPrecision.FP32 @property def device_id(self) -> str: diff --git a/machine-learning/immich_ml/main.py b/machine-learning/immich_ml/main.py index 3d34d9bf9d..e7e3a719bb 100644 --- a/machine-learning/immich_ml/main.py +++ b/machine-learning/immich_ml/main.py @@ -36,7 +36,7 @@ from .schemas import ( T, ) -MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger +MultiPartParser.spool_max_size = 2**26 # spools to disk if payload is 64 MiB or larger model_cache = ModelCache(revalidate=settings.model_ttl > 0) thread_pool: ThreadPoolExecutor | None = None diff --git a/machine-learning/immich_ml/models/constants.py b/machine-learning/immich_ml/models/constants.py index db9e7cfa4d..0815410495 100644 --- a/machine-learning/immich_ml/models/constants.py +++ b/machine-learning/immich_ml/models/constants.py @@ -90,7 +90,7 @@ _PADDLE_MODELS = { SUPPORTED_PROVIDERS = [ "CUDAExecutionProvider", - "ROCMExecutionProvider", + "MIGraphXExecutionProvider", "OpenVINOExecutionProvider", "CoreMLExecutionProvider", "CPUExecutionProvider", diff --git a/machine-learning/immich_ml/sessions/ort.py b/machine-learning/immich_ml/sessions/ort.py index 6c52936722..5b728fce6f 100644 --- a/machine-learning/immich_ml/sessions/ort.py +++ b/machine-learning/immich_ml/sessions/ort.py @@ -8,7 +8,7 @@ import onnxruntime as ort from numpy.typing import NDArray from immich_ml.models.constants import SUPPORTED_PROVIDERS -from immich_ml.schemas import SessionNode +from immich_ml.schemas import ModelPrecision, SessionNode from ..config import log, settings @@ -90,8 +90,17 @@ class OrtSession: match provider: case "CPUExecutionProvider": options = {"arena_extend_strategy": "kSameAsRequested"} - case "CUDAExecutionProvider" | "ROCMExecutionProvider": + case "CUDAExecutionProvider": options = {"arena_extend_strategy": "kSameAsRequested", "device_id": settings.device_id} + case "MIGraphXExecutionProvider": + migraphx_dir = self.model_path.parent / "migraphx" + # MIGraphX does not create the underlying folder and will crash if it does not exist + migraphx_dir.mkdir(parents=True, exist_ok=True) + options = { + "device_id": settings.device_id, + "migraphx_model_cache_dir": migraphx_dir.as_posix(), + "migraphx_fp16_enable": "1" if settings.rocm_precision == ModelPrecision.FP16 else "0", + } case "OpenVINOExecutionProvider": openvino_dir = self.model_path.parent / "openvino" device = f"GPU.{settings.device_id}" diff --git a/machine-learning/patches/0001-disable-rocm-conv-algo-caching.patch b/machine-learning/patches/0001-disable-rocm-conv-algo-caching.patch deleted file mode 100644 index 6627f67778..0000000000 --- a/machine-learning/patches/0001-disable-rocm-conv-algo-caching.patch +++ /dev/null @@ -1,179 +0,0 @@ -commit 16839b58d9b3c3162a67ce5d776b36d4d24e801f -Author: mertalev <101130780+mertalev@users.noreply.github.com> -Date: Wed Mar 5 11:25:38 2025 -0500 - - disable algo caching (attributed to @dmnieto in https://github.com/microsoft/onnxruntime/pull/19567) - -diff --git a/onnxruntime/core/providers/rocm/nn/conv.cc b/onnxruntime/core/providers/rocm/nn/conv.cc -index d7f47d07a8..4060a2af52 100644 ---- a/onnxruntime/core/providers/rocm/nn/conv.cc -+++ b/onnxruntime/core/providers/rocm/nn/conv.cc -@@ -127,7 +127,6 @@ Status Conv::UpdateState(OpKernelContext* context, bool bias_expected) - - if (w_dims_changed) { - s_.last_w_dims = gsl::make_span(w_dims); -- s_.cached_benchmark_fwd_results.clear(); - } - - ORT_RETURN_IF_ERROR(conv_attrs_.ValidateInputShape(X->Shape(), W->Shape(), channels_last, channels_last)); -@@ -277,35 +276,6 @@ Status Conv::UpdateState(OpKernelContext* context, bool bias_expected) - HIP_CALL_THROW(hipMalloc(&s_.b_zero, malloc_size)); - HIP_CALL_THROW(hipMemsetAsync(s_.b_zero, 0, malloc_size, Stream(context))); - } -- -- if (!s_.cached_benchmark_fwd_results.contains(x_dims_miopen)) { -- miopenConvAlgoPerf_t perf; -- int algo_count = 1; -- const ROCMExecutionProvider* rocm_ep = static_cast(this->Info().GetExecutionProvider()); -- static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT; -- size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId()) -- : AlgoSearchWorkspaceSize; -- IAllocatorUniquePtr algo_search_workspace = GetTransientScratchBuffer(max_ws_size); -- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm( -- GetMiopenHandle(context), -- s_.x_tensor, -- s_.x_data, -- s_.w_desc, -- s_.w_data, -- s_.conv_desc, -- s_.y_tensor, -- s_.y_data, -- 1, // requestedAlgoCount -- &algo_count, // returnedAlgoCount -- &perf, -- algo_search_workspace.get(), -- max_ws_size, -- false)); // Do not do exhaustive algo search. -- s_.cached_benchmark_fwd_results.insert(x_dims_miopen, {perf.fwd_algo, perf.memory}); -- } -- const auto& perf = s_.cached_benchmark_fwd_results.at(x_dims_miopen); -- s_.fwd_algo = perf.fwd_algo; -- s_.workspace_bytes = perf.memory; - } else { - // set Y - s_.Y = context->Output(0, TensorShape(s_.y_dims)); -@@ -319,6 +289,31 @@ Status Conv::UpdateState(OpKernelContext* context, bool bias_expected) - s_.y_data = reinterpret_cast(s_.Y->MutableData()); - } - } -+ -+ miopenConvAlgoPerf_t perf; -+ int algo_count = 1; -+ const ROCMExecutionProvider* rocm_ep = static_cast(this->Info().GetExecutionProvider()); -+ static constexpr int num_algos = MIOPEN_CONVOLUTION_FWD_ALGO_COUNT; -+ size_t max_ws_size = rocm_ep->GetMiopenConvUseMaxWorkspace() ? GetMaxWorkspaceSize(GetMiopenHandle(context), s_, kAllAlgos, num_algos, rocm_ep->GetDeviceId()) -+ : AlgoSearchWorkspaceSize; -+ IAllocatorUniquePtr algo_search_workspace = GetTransientScratchBuffer(max_ws_size); -+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionForwardAlgorithm( -+ GetMiopenHandle(context), -+ s_.x_tensor, -+ s_.x_data, -+ s_.w_desc, -+ s_.w_data, -+ s_.conv_desc, -+ s_.y_tensor, -+ s_.y_data, -+ 1, // requestedAlgoCount -+ &algo_count, // returnedAlgoCount -+ &perf, -+ algo_search_workspace.get(), -+ max_ws_size, -+ false)); // Do not do exhaustive algo search. -+ s_.fwd_algo = perf.fwd_algo; -+ s_.workspace_bytes = perf.memory; - return Status::OK(); - } - -diff --git a/onnxruntime/core/providers/rocm/nn/conv.h b/onnxruntime/core/providers/rocm/nn/conv.h -index bc9846203e..d54218f258 100644 ---- a/onnxruntime/core/providers/rocm/nn/conv.h -+++ b/onnxruntime/core/providers/rocm/nn/conv.h -@@ -108,9 +108,6 @@ class lru_unordered_map { - list_type lru_list_; - }; - --// cached miopen descriptors --constexpr size_t MAX_CACHED_ALGO_PERF_RESULTS = 10000; -- - template - struct MiopenConvState { - // if x/w dims changed, update algo and miopenTensors -@@ -148,9 +145,6 @@ struct MiopenConvState { - decltype(AlgoPerfType().memory) memory; - }; - -- lru_unordered_map cached_benchmark_fwd_results{MAX_CACHED_ALGO_PERF_RESULTS}; -- lru_unordered_map cached_benchmark_bwd_results{MAX_CACHED_ALGO_PERF_RESULTS}; -- - // Some properties needed to support asymmetric padded Conv nodes - bool post_slicing_required; - TensorShapeVector slice_starts; -diff --git a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc -index 7447113fdf..a662e35b2e 100644 ---- a/onnxruntime/core/providers/rocm/nn/conv_transpose.cc -+++ b/onnxruntime/core/providers/rocm/nn/conv_transpose.cc -@@ -76,7 +76,6 @@ Status ConvTranspose::DoConvTranspose(OpKernelContext* context, bool dy - - if (w_dims_changed) { - s_.last_w_dims = gsl::make_span(w_dims); -- s_.cached_benchmark_bwd_results.clear(); - } - - ConvTransposeAttributes::Prepare p; -@@ -126,35 +125,29 @@ Status ConvTranspose::DoConvTranspose(OpKernelContext* context, bool dy - } - - y_data = reinterpret_cast(p.Y->MutableData()); -- -- if (!s_.cached_benchmark_bwd_results.contains(x_dims)) { -- IAllocatorUniquePtr algo_search_workspace = GetScratchBuffer(AlgoSearchWorkspaceSize, context->GetComputeStream()); -- -- miopenConvAlgoPerf_t perf; -- int algo_count = 1; -- MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm( -- GetMiopenHandle(context), -- s_.x_tensor, -- x_data, -- s_.w_desc, -- w_data, -- s_.conv_desc, -- s_.y_tensor, -- y_data, -- 1, -- &algo_count, -- &perf, -- algo_search_workspace.get(), -- AlgoSearchWorkspaceSize, -- false)); -- s_.cached_benchmark_bwd_results.insert(x_dims, {perf.bwd_data_algo, perf.memory}); -- } -- -- const auto& perf = s_.cached_benchmark_bwd_results.at(x_dims); -- s_.bwd_data_algo = perf.bwd_data_algo; -- s_.workspace_bytes = perf.memory; - } - -+ IAllocatorUniquePtr algo_search_workspace = GetScratchBuffer(AlgoSearchWorkspaceSize, context->GetComputeStream()); -+ miopenConvAlgoPerf_t perf; -+ int algo_count = 1; -+ MIOPEN_RETURN_IF_ERROR(miopenFindConvolutionBackwardDataAlgorithm( -+ GetMiopenHandle(context), -+ s_.x_tensor, -+ x_data, -+ s_.w_desc, -+ w_data, -+ s_.conv_desc, -+ s_.y_tensor, -+ y_data, -+ 1, -+ &algo_count, -+ &perf, -+ algo_search_workspace.get(), -+ AlgoSearchWorkspaceSize, -+ false)); -+ s_.bwd_data_algo = perf.bwd_data_algo; -+ s_.workspace_bytes = perf.memory; -+ - // The following block will be executed in case there has been no change in the shapes of the - // input and the filter compared to the previous run - if (!y_data) { diff --git a/machine-learning/patches/0002-target-gfx900-gfx1102.patch b/machine-learning/patches/0002-target-gfx900-gfx1102.patch deleted file mode 100644 index 11c1ab0367..0000000000 --- a/machine-learning/patches/0002-target-gfx900-gfx1102.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt -index 2714e6f59..a69da76b4 100644 ---- a/cmake/CMakeLists.txt -+++ b/cmake/CMakeLists.txt -@@ -338,7 +338,7 @@ if (onnxruntime_USE_ROCM) - if (ROCM_VERSION_DEV VERSION_LESS "6.2") - message(FATAL_ERROR "CMAKE_HIP_ARCHITECTURES is not set when ROCm version < 6.2") - else() -- set(CMAKE_HIP_ARCHITECTURES "gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx940;gfx941;gfx942;gfx1200;gfx1201") -+ set(CMAKE_HIP_ARCHITECTURES "gfx900;gfx908;gfx90a;gfx1030;gfx1100;gfx1101;gfx1102;gfx940;gfx941;gfx942;gfx1200;gfx1201") - endif() - endif() - diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 25b936bc36..e3ce9c002f 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,21 +1,20 @@ [project] name = "immich-ml" -version = "2.4.0" +version = "2.5.6" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] -requires-python = ">=3.10,<4.0" +requires-python = ">=3.11,<4.0" readme = "README.md" dependencies = [ "aiocache>=0.12.1,<1.0", "fastapi>=0.95.2,<1.0", - "ftfy>=6.1.1", "gunicorn>=21.1.0", "huggingface-hub>=0.20.1,<1.0", "insightface>=0.7.3,<1.0", - "numpy<2", + "numpy>=2.3.4", "opencv-python-headless>=4.7.0.72,<5.0", "orjson>=3.9.5", - "pillow>=9.5.0,<11.0", + "pillow>=12.1.1,<12.2", "pydantic>=2.0.0,<3", "pydantic-settings>=2.5.2,<3", "python-multipart>=0.0.6,<1.0", @@ -41,7 +40,6 @@ types = [ "types-ujson>=5.10.0.20240515", ] lint = [ - "black>=23.3.0", "mypy>=1.3.0", "ruff>=0.0.272", { include-group = "types" }, @@ -49,24 +47,16 @@ lint = [ dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }] [project.optional-dependencies] -cpu = ["onnxruntime>=1.15.0,<2"] -cuda = ["onnxruntime-gpu>=1.17.0,<2"] -openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"] -armnn = ["onnxruntime>=1.15.0,<2"] -rknn = ["onnxruntime>=1.15.0,<2", "rknn-toolkit-lite2>=2.3.0,<3"] -rocm = [] +cpu = ["onnxruntime>=1.23.2,<2"] +cuda = ["onnxruntime-gpu>=1.23.2,<2"] +openvino = ["onnxruntime-openvino>=1.24.1,<2"] +armnn = ["onnxruntime>=1.23.2,<2"] +rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"] +rocm = ["onnxruntime-migraphx>=1.23.2,<2"] [tool.uv] compile-bytecode = true -[[tool.uv.index]] -name = "cuda12" -url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" -explicit = true - -[tool.uv.sources] -onnxruntime-gpu = { index = "cuda12" } - [tool.hatch.build.targets.sdist] include = ["immich_ml"] @@ -101,9 +91,5 @@ target-version = "py311" select = ["E", "F", "I"] per-file-ignores = { "test_main.py" = ["F403"] } -[tool.black] -line-length = 120 -target-version = ['py311'] - [tool.pytest.ini_options] markers = ["providers", "ov_device_ids"] diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py index eb8706fc19..f37880610a 100644 --- a/machine-learning/test_main.py +++ b/machine-learning/test_main.py @@ -179,7 +179,7 @@ class TestOrtSession: OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"] CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"] TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"] - ROCM_EP = ["ROCMExecutionProvider", "CPUExecutionProvider"] + ROCM_EP = ["MIGraphXExecutionProvider", "CPUExecutionProvider"] COREML_EP = ["CoreMLExecutionProvider", "CPUExecutionProvider"] @pytest.mark.providers(CPU_EP) @@ -289,12 +289,38 @@ class TestOrtSession: assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}] - def test_sets_provider_options_for_rocm(self) -> None: + def test_sets_provider_options_for_rocm(self, mocker: MockerFixture) -> None: + model_path = "/cache/ViT-B-32__openai/textual/model.onnx" os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1" + mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir") - session = OrtSession("ViT-B-32__openai", providers=["ROCMExecutionProvider"]) + session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"]) - assert session.provider_options == [{"arena_extend_strategy": "kSameAsRequested", "device_id": "1"}] + assert session.provider_options == [ + { + "device_id": "1", + "migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx", + "migraphx_fp16_enable": "0", + } + ] + mkdir.assert_called_once_with(parents=True, exist_ok=True) + + def test_sets_rocm_to_fp16_if_enabled(self, path: mock.Mock, mocker: MockerFixture) -> None: + model_path = "/cache/ViT-B-32__openai/textual/model.onnx" + os.environ["MACHINE_LEARNING_DEVICE_ID"] = "1" + mocker.patch.object(settings, "rocm_precision", ModelPrecision.FP16) + mkdir = mocker.patch("immich_ml.sessions.ort.Path.mkdir") + + session = OrtSession(model_path, providers=["MIGraphXExecutionProvider"]) + + assert session.provider_options == [ + { + "device_id": "1", + "migraphx_model_cache_dir": "/cache/ViT-B-32__openai/textual/migraphx", + "migraphx_fp16_enable": "1", + } + ] + mkdir.assert_called_once_with(parents=True, exist_ok=True) def test_sets_provider_options_kwarg(self) -> None: session = OrtSession( diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 356e954ef4..5f87a59fa6 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -1,22 +1,16 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.11, <4.0" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')", "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", ] [[package]] @@ -38,8 +32,7 @@ dependencies = [ { name = "pyyaml" }, { name = "qudida" }, { name = "scikit-image" }, - { name = "scipy", version = "1.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/d6/8dd5b690d28a332a0b2c3179a345808b5d4c7ad5ddc079b7e116098dff35/albumentations-1.3.1.tar.gz", hash = "sha256:a6a38388fe546c568071e8c82f414498e86c9ed03c08b58e7a88b31cf7a244c6", size = 176371, upload-time = "2023-06-10T07:44:32.36Z" } wheels = [ @@ -75,25 +68,14 @@ name = "anyio" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f", size = 158770, upload-time = "2023-12-16T17:06:57.709Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", size = 85481, upload-time = "2023-12-16T17:06:55.989Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "bidict" version = "0.23.1" @@ -103,45 +85,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, ] -[[package]] -name = "black" -version = "25.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, - { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, - { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, - { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, - { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, - { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, - { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, - { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, - { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, - { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, -] - [[package]] name = "blinker" version = "1.7.0" @@ -157,22 +100,6 @@ version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload-time = "2023-09-07T14:03:16.894Z" }, - { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload-time = "2023-09-07T14:03:18.917Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload-time = "2023-09-07T14:03:20.398Z" }, - { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload-time = "2023-09-07T14:03:21.914Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload-time = "2023-09-07T14:03:24Z" }, - { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload-time = "2023-09-07T14:03:26.248Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload-time = "2023-09-07T14:03:27.849Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload-time = "2023-09-07T14:03:29.92Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload-time = "2023-09-07T14:03:32.035Z" }, - { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload-time = "2023-09-07T14:03:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload-time = "2024-10-18T12:32:09.016Z" }, - { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload-time = "2024-10-18T12:32:11.134Z" }, - { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload-time = "2024-10-18T12:32:12.813Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload-time = "2024-10-18T12:32:14.733Z" }, - { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172, upload-time = "2023-09-07T14:03:35.212Z" }, - { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255, upload-time = "2023-09-07T14:03:36.447Z" }, { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" }, { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" }, { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" }, @@ -223,11 +150,11 @@ wheels = [ [[package]] name = "certifi" -version = "2023.11.17" +version = "2025.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/91/c89518dd4fe1f3a4e3f6ab7ff23cb00ef2e8c9adf99dacc618ad5e068e28/certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", size = 163637, upload-time = "2023-11-18T02:54:02.397Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/62/428ef076be88fa93716b576e4a01f919d25968913e817077a386fcbe4f42/certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474", size = 162530, upload-time = "2023-11-18T02:54:00.083Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, ] [[package]] @@ -239,18 +166,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, - { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, - { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, @@ -293,21 +208,6 @@ version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809, upload-time = "2023-11-01T04:04:59.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219, upload-time = "2023-11-01T04:02:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521, upload-time = "2023-11-01T04:02:32.452Z" }, - { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383, upload-time = "2023-11-01T04:02:34.11Z" }, - { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223, upload-time = "2023-11-01T04:02:36.213Z" }, - { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101, upload-time = "2023-11-01T04:02:38.067Z" }, - { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699, upload-time = "2023-11-01T04:02:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065, upload-time = "2023-11-01T04:02:41.357Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505, upload-time = "2023-11-01T04:02:43.108Z" }, - { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425, upload-time = "2023-11-01T04:02:45.427Z" }, - { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287, upload-time = "2023-11-01T04:02:46.705Z" }, - { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929, upload-time = "2023-11-01T04:02:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605, upload-time = "2023-11-01T04:02:49.605Z" }, - { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646, upload-time = "2023-11-01T04:02:51.35Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846, upload-time = "2023-11-01T04:02:52.679Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343, upload-time = "2023-11-01T04:02:53.915Z" }, { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647, upload-time = "2023-11-01T04:02:55.329Z" }, { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434, upload-time = "2023-11-01T04:02:57.173Z" }, { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979, upload-time = "2023-11-01T04:02:58.442Z" }, @@ -362,18 +262,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "coloredlogs" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "humanfriendly" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, -] - [[package]] name = "colorlog" version = "6.9.0" @@ -395,72 +283,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, ] -[[package]] -name = "contourpy" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/a3/48ddc7ae832b000952cf4be64452381d150a41a2299c2eb19237168528d1/contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a", size = 13455881, upload-time = "2023-11-03T17:01:03.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/ea/f6e90933d82cc5aacf52f886a1c01f47f96eba99108ca2929c7b3ef45f82/contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8", size = 256873, upload-time = "2023-11-03T16:56:34.548Z" }, - { url = "https://files.pythonhosted.org/packages/fe/26/43821d61b7ee62c1809ec852bc572aaf4c27f101ebcebbbcce29a5ee0445/contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4", size = 242211, upload-time = "2023-11-03T16:56:38.028Z" }, - { url = "https://files.pythonhosted.org/packages/9b/99/c8fb63072a7573fe7682e1786a021f29f9c5f660a3aafcdce80b9ee8348d/contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f", size = 293195, upload-time = "2023-11-03T16:56:41.598Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a7/ae0b4bb8e0c865270d02ee619981413996dc10ddf1fd2689c938173ff62f/contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e", size = 332279, upload-time = "2023-11-03T16:56:46.08Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/682228b9085ff323fb7e946fe139072e5f21b71360cf91f36ea079d4ea95/contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9", size = 305326, upload-time = "2023-11-03T16:56:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/58/56/e2c43dcfa1f9c7db4d5e3d6f5134b24ed953f4e2133a4b12f0062148db58/contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa", size = 310732, upload-time = "2023-11-03T16:56:53.773Z" }, - { url = "https://files.pythonhosted.org/packages/94/0b/8495c4582057abc8377f945f6e11a86f1c07ad7b32fd4fdc968478cd0324/contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9", size = 803420, upload-time = "2023-11-03T16:57:00.669Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1f/40399c7da649297147d404aedaa675cc60018f48ad284630c0d1406133e3/contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab", size = 829204, upload-time = "2023-11-03T16:57:07.813Z" }, - { url = "https://files.pythonhosted.org/packages/8b/01/4be433b60dce7cbce8315cbcdfc016e7d25430a8b94e272355dff79cc3a8/contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488", size = 165434, upload-time = "2023-11-03T16:57:10.601Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7c/168f8343f33d861305e18c56901ef1bb675d3c7f977f435ec72751a71a54/contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41", size = 186652, upload-time = "2023-11-03T16:57:13.57Z" }, - { url = "https://files.pythonhosted.org/packages/9b/54/1dafec3c84df1d29119037330f7289db84a679cb2d5283af4ef24d89f532/contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727", size = 258243, upload-time = "2023-11-03T16:57:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ac/26fa1057f62beaa2af4c55c6ac733b114a403b746cfe0ce3dc6e4aec921a/contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd", size = 243408, upload-time = "2023-11-03T16:57:20.021Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/cd0ecc80123f499d76d2fe2807cb4d5638ef8730735c580c8a8a03e1928e/contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a", size = 294142, upload-time = "2023-11-03T16:57:23.48Z" }, - { url = "https://files.pythonhosted.org/packages/6d/75/1b7bf20bf6394e01df2c4b4b3d44d3dc280c16ddaff72724639100bd4314/contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063", size = 333129, upload-time = "2023-11-03T16:57:27.141Z" }, - { url = "https://files.pythonhosted.org/packages/22/5b/fedd961dff1877e5d3b83c5201295cfdcdc2438884c2851aa7ecf6cec045/contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e", size = 307461, upload-time = "2023-11-03T16:57:30.537Z" }, - { url = "https://files.pythonhosted.org/packages/e2/83/29a63bbc72839cc6b24b5a0e3d004d4ed4e8439f26460ad9a34e39251904/contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686", size = 313352, upload-time = "2023-11-03T16:57:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c7/4bac0fc4f1e802ab47e75076d83d2e1448e0668ba6cc9000cf4e9d5bd94a/contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286", size = 804127, upload-time = "2023-11-03T16:57:42.201Z" }, - { url = "https://files.pythonhosted.org/packages/e3/47/b3fd5bdc2f6ec13502d57a5bc390ffe62648605ed1689c93b0015150a784/contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95", size = 829561, upload-time = "2023-11-03T16:57:49.667Z" }, - { url = "https://files.pythonhosted.org/packages/5c/04/be16038e754169caea4d02d82f8e5cd97dece593e5ac9e05735da0afd0c5/contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6", size = 166197, upload-time = "2023-11-03T16:57:52.682Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2a/d197a412ec474391ee878b1218cf2fe9c6e963903755887fc5654c06636a/contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de", size = 187556, upload-time = "2023-11-03T16:57:55.286Z" }, - { url = "https://files.pythonhosted.org/packages/4f/03/839da46999173226bead08794cbd7b4d37c9e6b02686ca74c93556b43258/contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0", size = 259253, upload-time = "2023-11-03T16:57:58.572Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9e/8fb3f53144269d3fecdd8786d3a4686eeff55b9b35a3c0772a3f62f71e36/contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4", size = 242555, upload-time = "2023-11-03T16:58:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/a6/85/9815ccb5a18ee8c9a46bd5ef20d02b292cd4a99c62553f38c87015f16d59/contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779", size = 288108, upload-time = "2023-11-03T16:58:05.546Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d9/4df5c26bd0f496c8cd7940fd53db95d07deeb98518f02f805ce570590da8/contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316", size = 330810, upload-time = "2023-11-03T16:58:09.568Z" }, - { url = "https://files.pythonhosted.org/packages/67/d4/8aae9793a0cfde72959312521ebd3aa635c260c3d580448e8db6bdcdd1aa/contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399", size = 305290, upload-time = "2023-11-03T16:58:13.017Z" }, - { url = "https://files.pythonhosted.org/packages/20/84/ffddcdcc579cbf7213fd92a3578ca08a931a3bf879a22deb5a83ffc5002c/contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0", size = 303937, upload-time = "2023-11-03T16:58:16.426Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ad/6e570cf525f909da94559ed716189f92f529bc7b5f78645733c44619a0e2/contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0", size = 801977, upload-time = "2023-11-03T16:58:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/36/b4/55f23482c596eca36d16fc668b147865c56fcf90353f4c57f073d8d5e532/contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431", size = 827442, upload-time = "2023-11-03T16:58:30.724Z" }, - { url = "https://files.pythonhosted.org/packages/e9/47/9c081b1f11d6053cb0aa4c46b7de2ea2849a4a8d40de81c7bc3f99773b02/contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f", size = 165363, upload-time = "2023-11-03T16:58:33.54Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ae/a6353db548bff1a592b85ae6bb80275f0a51dc25a0410d059e5b33183e36/contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9", size = 187731, upload-time = "2023-11-03T16:58:36.585Z" }, -] - [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -539,101 +367,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.12.0" +version = "7.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" }, - { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" }, - { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" }, - { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" }, - { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" }, - { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, - { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, - { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, - { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, - { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, - { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, - { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, - { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, - { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, - { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, - { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, - { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, - { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, - { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, - { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, - { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, - { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, - { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, - { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, - { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -656,14 +484,6 @@ version = "3.0.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/09/ffb61f29b8e3d207c444032b21328327d753e274ea081bc74e009827cc81/Cython-3.0.8.tar.gz", hash = "sha256:8333423d8fd5765e7cceea3a9985dd1e0a5dfeb2734629e1a2ed2d6233d39de6", size = 2744096, upload-time = "2024-01-10T11:01:02.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/f4/d2542e186fe33ec1cc542770fb17466421ed54f4ffe04d00fe9549d0a467/Cython-3.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a846e0a38e2b24e9a5c5dc74b0e54c6e29420d88d1dafabc99e0fc0f3e338636", size = 3100459, upload-time = "2024-01-10T11:33:49.545Z" }, - { url = "https://files.pythonhosted.org/packages/fc/27/2652f395aa708fb3081148e0df3ab700bd7288636c65332ef7febad6a380/Cython-3.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45523fdc2b78d79b32834cc1cc12dc2ca8967af87e22a3ee1bff20e77c7f5520", size = 3456626, upload-time = "2024-01-10T11:01:44.897Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bd/e8a1d26d04c08a67bcc383f2ea5493a4e77f37a8770ead00a238b08ad729/Cython-3.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa0b7f3f841fe087410cab66778e2d3fb20ae2d2078a2be3dffe66c6574be39", size = 3621379, upload-time = "2024-01-10T11:01:48.777Z" }, - { url = "https://files.pythonhosted.org/packages/03/ae/ead7ec03d0062d439879d41b7830e4f2480213f7beabf2f7052a191cc6f7/Cython-3.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87294e33e40c289c77a135f491cd721bd089f193f956f7b8ed5aa2d0b8c558f", size = 3671873, upload-time = "2024-01-10T11:01:51.858Z" }, - { url = "https://files.pythonhosted.org/packages/63/b0/81dad725604d7b529c492f873a7fa1b5800704a9f26e100ed25e9fd8d057/Cython-3.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a1df7a129344b1215c20096d33c00193437df1a8fcca25b71f17c23b1a44f782", size = 3463832, upload-time = "2024-01-10T11:01:55.364Z" }, - { url = "https://files.pythonhosted.org/packages/13/cd/72b8e0af597ac1b376421847acf6d6fa252e60059a2a00dcf05ceb16d28f/Cython-3.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:13c2a5e57a0358da467d97667297bf820b62a1a87ae47c5f87938b9bb593acbd", size = 3618325, upload-time = "2024-01-10T11:01:59.03Z" }, - { url = "https://files.pythonhosted.org/packages/ef/73/11a4355d8b8966504c751e5bcb25916c4140de27bb2ba1b54ff21994d7fe/Cython-3.0.8-cp310-cp310-win32.whl", hash = "sha256:96b028f044f5880e3cb18ecdcfc6c8d3ce9d0af28418d5ab464509f26d8adf12", size = 2571305, upload-time = "2024-01-10T11:02:02.589Z" }, - { url = "https://files.pythonhosted.org/packages/18/15/fdc0c3552d20f9337b134a36d786da24e47998fc39f62cb61c1534f26123/Cython-3.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:8140597a8b5cc4f119a1190f5a2228a84f5ca6d8d9ec386cfce24663f48b2539", size = 2776113, upload-time = "2024-01-10T11:02:05.581Z" }, { url = "https://files.pythonhosted.org/packages/db/a7/f4a0bc9a80e23b380daa2ebb4879bf434aaa0b3b91f7ad8a7f9762b4bd1b/Cython-3.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aae26f9663e50caf9657148403d9874eea41770ecdd6caf381d177c2b1bb82ba", size = 3113615, upload-time = "2024-01-10T11:34:05.899Z" }, { url = "https://files.pythonhosted.org/packages/e9/e9/e9295df74246c165b91253a473bfa179debf739c9bee961cbb3ae56c2b79/Cython-3.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:547eb3cdb2f8c6f48e6865d5a741d9dd051c25b3ce076fbca571727977b28ac3", size = 3436320, upload-time = "2024-01-10T11:02:08.689Z" }, { url = "https://files.pythonhosted.org/packages/26/2c/6a887c957aa53e44f928119dea628a5dfacc8e875424034f5fecac9daba4/Cython-3.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a567d4b9ba70b26db89d75b243529de9e649a2f56384287533cf91512705bee", size = 3591755, upload-time = "2024-01-10T11:02:11.773Z" }, @@ -689,37 +509,29 @@ version = "1.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/d2/deb3296d08097fedd622d423c0ec8b68b78c1704b3f1545326f6ce05c75c/easydict-1.11.tar.gz", hash = "sha256:dcb1d2ed28eb300c8e46cd371340373abc62f7c14d6dea74fdfc6f1069061c78", size = 6644, upload-time = "2023-10-23T23:01:37.686Z" } -[[package]] -name = "exceptiongroup" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/1c/beef724eaf5b01bb44b6338c8c3494eff7cab376fab4904cfbbc3585dc79/exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68", size = 26264, upload-time = "2023-11-21T08:42:17.407Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/9a/5028fd52db10e600f1c4674441b968cf2ea4959085bfb5b99fb1250e5f68/exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14", size = 16210, upload-time = "2023-11-21T08:42:15.525Z" }, -] - [[package]] name = "fastapi" -version = "0.122.0" +version = "0.128.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b", size = 346436, upload-time = "2025-11-24T19:17:47.95Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, ] [[package]] name = "filelock" -version = "3.13.1" +version = "3.20.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/70/41905c80dcfe71b22fb06827b8eae65781783d4a14194bce79d16a013263/filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", size = 14553, upload-time = "2023-10-30T18:29:39.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/54/84d42a0bee35edba99dee7b59a8d4970eccdd44b99fe728ed912106fc781/filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c", size = 11740, upload-time = "2023-10-30T18:29:37.267Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, ] [[package]] @@ -774,35 +586,51 @@ wheels = [ [[package]] name = "fonttools" -version = "4.47.2" +version = "4.61.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/cd/75d24afa673edf92fd04657fad7d3b5e20c4abc3cad5bc14e5e30051c1f0/fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3", size = 3410067, upload-time = "2024-01-11T11:22:45.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/30/02de0b7f3d72f2c4fce3e512b166c1bdbe5a687408474b61eb0114be921c/fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df", size = 2779949, upload-time = "2024-01-11T11:19:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/9a/52/1a5e1373afb78a040ea0c371ab8a79da121060a8e518968bb8f41457ca90/fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1", size = 2281336, upload-time = "2024-01-11T11:20:08.835Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ce/9d3b5bf51aafee024566ebb374f5b040381d92660cb04647af3c5860c611/fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c", size = 4541692, upload-time = "2024-01-11T11:20:13.378Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/af41b7cfd35c7418e17b6a43bb106be4b0f0e5feb405a88dee29b186f2a7/fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8", size = 4600529, upload-time = "2024-01-11T11:20:17.27Z" }, - { url = "https://files.pythonhosted.org/packages/ab/7e/428dbb4cfc342b7a05cbc9d349e134e7fad6588f4ce2a7128e8e3e58ad3b/fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670", size = 4524215, upload-time = "2024-01-11T11:20:21.061Z" }, - { url = "https://files.pythonhosted.org/packages/a6/61/762fad1cc1debc4626f2eb373fa999591c63c231fce53d5073574a639531/fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c", size = 4584778, upload-time = "2024-01-11T11:20:25.815Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/170ca22284c1d825470e8b5871d6b25d3a70e2f5b185ffb1647d5e11ee4d/fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0", size = 2131876, upload-time = "2024-01-11T11:20:30.261Z" }, - { url = "https://files.pythonhosted.org/packages/df/07/4a30437bed355b838b8ce31d14c5983334c31adc97e70c6ecff90c60d6d2/fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1", size = 2177937, upload-time = "2024-01-11T11:20:33.814Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1d/670372323642eada0f7743cfcdd156de6a28d37769c916421fec2f32c814/fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b", size = 2782908, upload-time = "2024-01-11T11:20:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/c1/36/5f0bb863a6575db4c4b67fa9be7f98e4c551dd87638ef327bc180b988998/fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac", size = 2283501, upload-time = "2024-01-11T11:20:42.027Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1e/95de682a86567426bcc40a56c9b118ffa97de6cbfcc293addf20994e329d/fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c", size = 4848039, upload-time = "2024-01-11T11:20:47.038Z" }, - { url = "https://files.pythonhosted.org/packages/ef/95/92a0b5fc844c1db734752f8a51431de519cd6b02e7e561efa9e9fd415544/fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70", size = 4893166, upload-time = "2024-01-11T11:20:50.855Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/ed9dd7ee1afd6cd70eb7237688118fe489dbde962e3765c91c86c095f84b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e", size = 4815529, upload-time = "2024-01-11T11:20:54.696Z" }, - { url = "https://files.pythonhosted.org/packages/6b/67/cdffa0b3cd8f863b45125c335bbd3d9dc16ec42f5a8d5b64dd1244c5ce6b/fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703", size = 4875414, upload-time = "2024-01-11T11:20:58.435Z" }, - { url = "https://files.pythonhosted.org/packages/b8/fb/41638e748c8f20f5483987afcf9be746d3ccb9e9600ca62128a27c791a82/fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c", size = 2130073, upload-time = "2024-01-11T11:21:02.056Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ef/93321cf55180a778b4d97919b28739874c0afab90e7b9f5b232db70f47c2/fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9", size = 2178744, upload-time = "2024-01-11T11:21:05.88Z" }, - { url = "https://files.pythonhosted.org/packages/c0/bd/4dd1e8a9e632f325d9203ce543402f912f26efd213c8d9efec0180fbac64/fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635", size = 2754076, upload-time = "2024-01-11T11:21:09.745Z" }, - { url = "https://files.pythonhosted.org/packages/e6/4d/c2ebaac81dadbc3fc3c3c2fa5fe7b16429dc713b1b8ace49e11e92904d78/fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d", size = 2263784, upload-time = "2024-01-11T11:21:13.367Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f6/9d484cd275845c7e503a8669a5952a7fa089c7a881babb4dce5ebe6fc5d1/fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb", size = 4769142, upload-time = "2024-01-11T11:21:17.615Z" }, - { url = "https://files.pythonhosted.org/packages/7a/bf/c6ae0768a531b38245aac0bb8d30bc05d53d499e09fccdc5d72e7c8d28b6/fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07", size = 4853241, upload-time = "2024-01-11T11:21:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f0/c06709666cb7722447efb70ea456c302bd6eb3b997d30076401fb32bca4b/fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71", size = 4730447, upload-time = "2024-01-11T11:21:24.755Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/4c758ae5f4f8047904fc1c6bbbb828248c94cc7aa6406af3a62ede766f25/fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f", size = 4809265, upload-time = "2024-01-11T11:21:28.586Z" }, - { url = "https://files.pythonhosted.org/packages/81/f6/a6912c11280607d48947341e2167502605a3917925c835afcd7dfcabc289/fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085", size = 2118363, upload-time = "2024-01-11T11:21:33.245Z" }, - { url = "https://files.pythonhosted.org/packages/81/4b/42d0488765ea5aa308b4e8197cb75366b2124240a73e86f98b6107ccf282/fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4", size = 2165866, upload-time = "2024-01-11T11:21:37.23Z" }, - { url = "https://files.pythonhosted.org/packages/af/2f/c34b0f99d46766cf49566d1ee2ee3606e4c9880b5a7d734257dc61c804e9/fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184", size = 1063011, upload-time = "2024-01-11T11:22:41.676Z" }, + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] [[package]] @@ -814,18 +642,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/25/fab23259a52ece5670dcb8452e1af34b89e6135ecc17cd4b54b4b479eac6/fsspec-2023.12.2-py3-none-any.whl", hash = "sha256:d800d87f72189a745fa3d6b033b9dc4a34ad069f60ca60b943a63599f5501960", size = 168979, upload-time = "2023-12-11T21:19:52.446Z" }, ] -[[package]] -name = "ftfy" -version = "6.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, -] - [[package]] name = "gevent" version = "24.10.3" @@ -838,14 +654,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/70/f0/be10ed5d7721ed2317d7feb59e167603217156c2a6d57f128523e24e673d/gevent-24.10.3.tar.gz", hash = "sha256:aa7ee1bd5cabb2b7ef35105f863b386c8d5e332f754b60cfc354148bd70d35d1", size = 6108837, upload-time = "2024-10-18T16:06:25.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/6f/a2100e7883c7bdfc2b45cb60b310ca748762a21596258b9dd01c5c093dbc/gevent-24.10.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d7a1ad0f2da582f5bd238bca067e1c6c482c30c15a6e4d14aaa3215cbb2232f3", size = 3014382, upload-time = "2024-10-18T15:37:34.041Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b1/460e4884ed6185d9eb9c4c2e9639d2b254197e46513301c0f63dec22dc90/gevent-24.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad", size = 4853460, upload-time = "2024-10-18T16:19:39.515Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f6/7ded98760d381229183ecce8db2edcce96f13e23807d31a90c66dae85304/gevent-24.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57a5c4e0bdac482c5f02f240d0354e61362df73501ef6ebafce8ef635cad7527", size = 4977636, upload-time = "2024-10-18T16:18:45.464Z" }, - { url = "https://files.pythonhosted.org/packages/7d/21/7b928e6029eedb93ef94fc0aee701f497af2e601f0ec00aac0e72e3f450e/gevent-24.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67daed8383326dc8b5e58d88e148d29b6b52274a489e383530b0969ae7b9cb9", size = 5058031, upload-time = "2024-10-18T16:23:10.719Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/12c03fd004fbeeca01276ffc589f5a368fd741d02582ab7006d1bdef57e7/gevent-24.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e24ffea72e27987979c009536fd0868e52239b44afe6cf7135ce8aafd0f108e", size = 6683694, upload-time = "2024-10-18T15:59:35.475Z" }, - { url = "https://files.pythonhosted.org/packages/64/4c/ea14d971452d3da09e49267e052d8312f112c7835120aed78d22ef14efee/gevent-24.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d80090485da1ea3d99205fe97908b31188c1f4857f08b333ffaf2de2e89d18", size = 5286063, upload-time = "2024-10-18T16:38:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/39/3f/397efff27e637d7306caa00d1560512c44028c25c70be1e72c46b79b1b66/gevent-24.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0c129f81d60cda614acb4b0c5731997ca05b031fb406fcb58ad53a7ade53b13", size = 6817462, upload-time = "2024-10-18T16:02:48.427Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5d/19939eaa7c5b7c0f37e0a0665a911ddfe1e35c25c512446fc356a065c16e/gevent-24.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ca7a6b42d35129617025ac801135118333cad75856ffc3217b38e707383eba", size = 1566631, upload-time = "2024-10-18T16:08:38.489Z" }, { url = "https://files.pythonhosted.org/packages/6e/01/1be5cf013826d8baae235976d6a94f3628014fd2db7c071aeec13f82b4d1/gevent-24.10.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:68c3a0d8402755eba7f69022e42e8021192a721ca8341908acc222ea597029b6", size = 2966909, upload-time = "2024-10-18T15:37:31.43Z" }, { url = "https://files.pythonhosted.org/packages/fe/3e/7fa9ab023f24d8689e2c77951981f8ea1f25089e0349a0bf8b35ee9b9277/gevent-24.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d850a453d66336272be4f1d3a8126777f3efdaea62d053b4829857f91e09755", size = 4913247, upload-time = "2024-10-18T16:19:41.792Z" }, { url = "https://files.pythonhosted.org/packages/db/63/6e40eaaa3c2abd1561faff11dc3e6781f8c25e975354b8835762834415af/gevent-24.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e58ee3723f1fbe07d66892f1caa7481c306f653a6829b6fd16cb23d618a5915", size = 5049036, upload-time = "2024-10-18T16:18:47.419Z" }, @@ -870,7 +678,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/96/cc5f6ecba032a45fc312fe0db2908a893057fd81361eea93845d6c325556/gevent-24.10.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1c3a828b033fb02b7c31da4d75014a1f82e6c072fc0523456569a57f8b025861", size = 5484356, upload-time = "2024-10-18T16:38:31.709Z" }, { url = "https://files.pythonhosted.org/packages/7c/97/e680b2b2f0c291ae4db9813ffbf02c22c2a0f14c8f1a613971385e29ef67/gevent-24.10.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f2ae3efbbd120cdf4a68b7abc27a37e61e6f443c5a06ec2c6ad94c37cd8471ec", size = 6903191, upload-time = "2024-10-18T16:02:53.888Z" }, { url = "https://files.pythonhosted.org/packages/1b/1c/b4181957da062d1c060974ec6cb798cc24aeeb28e8cd2ece84eb4b4991f7/gevent-24.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:9e1210334a9bc9f76c3d008e0785ca62214f8a54e1325f6c2ecab3b6a572a015", size = 1545117, upload-time = "2024-10-18T15:45:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/89/2b/bf4af9950b8f9abd5b4025858f6311930de550e3498bbfeb47c914701a1d/gevent-24.10.3-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:e534e6a968d74463b11de6c9c67f4b4bf61775fb00f2e6e0f7fcdd412ceade18", size = 1271541, upload-time = "2024-10-18T15:37:53.146Z" }, ] [[package]] @@ -885,19 +692,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/8c/14/d4eddae757de44985718a9e38d9e6f2a923d764ed97d0f1cbc1a8aa2b0ef/geventhttpclient-2.3.1.tar.gz", hash = "sha256:b40ddac8517c456818942c7812f555f84702105c82783238c9fcb8dc12675185", size = 69345, upload-time = "2024-04-18T21:39:50.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/a5/5e49d6a581b3f1399425e22961c6e341e90c12fa2193ed0adee9afbd864c/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da22ab7bf5af4ba3d07cffee6de448b42696e53e7ac1fe97ed289037733bf1c2", size = 71729, upload-time = "2024-04-18T21:38:06.866Z" }, - { url = "https://files.pythonhosted.org/packages/eb/23/4ff584e5f344dae64b5bc588b65c4ea81083f9d662b9f64cf5f28e5ae9cc/geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2399e3d4e2fae8bbd91756189da6e9d84adf8f3eaace5eef0667874a705a29f8", size = 52062, upload-time = "2024-04-18T21:38:08.433Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/6bd8badb97b31a49f4c2b79466abce208a97dad95d447893c7546063fc8a/geventhttpclient-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e33e87d0d5b9f5782c4e6d3cb7e3592fea41af52713137d04776df7646d71b", size = 51645, upload-time = "2024-04-18T21:38:10.139Z" }, - { url = "https://files.pythonhosted.org/packages/e1/62/47d431bf05f74aa683d63163a11432bda8f576c86dec8c3bc9d6a156ee03/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071db313866c3d0510feb6c0f40ec086ccf7e4a845701b6316c82c06e8b9b29", size = 117838, upload-time = "2024-04-18T21:38:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8b/e7c9ae813bb41883a96ad9afcf86465219c3bb682daa8b09448481edef8a/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f36f0c6ef88a27e60af8369d9c2189fe372c6f2943182a7568e0f2ad33bb69f1", size = 123272, upload-time = "2024-04-18T21:38:13.704Z" }, - { url = "https://files.pythonhosted.org/packages/4d/26/71e9b2526009faadda9f588dac04f8bf837a5b97628ab44145efc3fa796e/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4624843c03a5337282a42247d987c2531193e57255ee307b36eeb4f243a0c21", size = 114319, upload-time = "2024-04-18T21:38:15.097Z" }, - { url = "https://files.pythonhosted.org/packages/34/8c/1da2960293c42b7a6b01dbe3204b569e4cdb55b8289cb1c7154826500f19/geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d614573621ba827c417786057e1e20e9f96c4f6b3878c55b1b7b54e1026693bc", size = 112705, upload-time = "2024-04-18T21:38:17.005Z" }, - { url = "https://files.pythonhosted.org/packages/a7/a1/4d08ecf0f213fdc63f78a217f87c07c1cb9891e68cdf74c8cbca76298bdb/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d51330a40ac9762879d0e296c279c1beae8cfa6484bb196ac829242c416b709", size = 121236, upload-time = "2024-04-18T21:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f7/42ece3e1f54602c518d74364a214da3b35b6be267b335564b7e9f0d37705/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc9f2162d4e8cb86bb5322d99bfd552088a3eacd540a841298f06bb8bc1f1f03", size = 117859, upload-time = "2024-04-18T21:38:20.917Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8e/de026b3697bffe5fa1a4938a3882107e378eea826905acf8e46c69b71ffd/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e59d3397e63c65ecc7a7561a5289f0cf2e2c2252e29632741e792f57f5d124", size = 127268, upload-time = "2024-04-18T21:38:22.676Z" }, - { url = "https://files.pythonhosted.org/packages/54/bf/1ee99a322467e6825a24612d306a46ca94b51088170d1b5de0df1c82ab2a/geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4436eef515b3e0c1d4a453ae32e047290e780a623c1eddb11026ae9d5fb03d42", size = 116426, upload-time = "2024-04-18T21:38:24.228Z" }, - { url = "https://files.pythonhosted.org/packages/72/54/10c8ec745b3dcbfd52af62977fec85829749c0325e1a5429d050a4b45e75/geventhttpclient-2.3.1-cp310-cp310-win32.whl", hash = "sha256:5d1cf7d8a4f8e15cc8fd7d88ac4cdb058d6274203a42587e594cc9f0850ac862", size = 47599, upload-time = "2024-04-18T21:38:26.385Z" }, - { url = "https://files.pythonhosted.org/packages/da/0d/36a47cdeaa83c3b4efdbd18d77720fa27dc40600998f4dedd7c4a1259862/geventhttpclient-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4deaebc121036f7ea95430c2d0f80ab085b15280e6ab677a6360b70e57020e7f", size = 48302, upload-time = "2024-04-18T21:38:28.297Z" }, { url = "https://files.pythonhosted.org/packages/56/ad/1fcbbea0465f04d4425960e3737d4d8ae6407043cfc88688fb17b9064160/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0ae055b9ce1704f2ce72c0847df28f4e14dbb3eea79256cda6c909d82688ea3", size = 71733, upload-time = "2024-04-18T21:38:30.357Z" }, { url = "https://files.pythonhosted.org/packages/06/1a/10e547adb675beea407ff7117ecb4e5063534569ac14bb4360279d2888dd/geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f087af2ac439495b5388841d6f3c4de8d2573ca9870593d78f7b554aa5cfa7f5", size = 52060, upload-time = "2024-04-18T21:38:32.561Z" }, { url = "https://files.pythonhosted.org/packages/e0/c0/9960ac6e8818a00702743cd2a9637d6f26909ac7ac59ca231f446e367b20/geventhttpclient-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76c367d175810facfe56281e516c9a5a4a191eff76641faaa30aa33882ed4b2f", size = 51649, upload-time = "2024-04-18T21:38:34.265Z" }, @@ -924,11 +718,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/2f/b7fd96e9cfa9d9719b0c9feb50b4cbb341d1940e34fd3305006efa8c3e33/geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25d255383d3d6a6fbd643bb51ae1a7e4f6f7b0dbd5f3225b537d0bd0432eaf39", size = 117758, upload-time = "2024-04-18T21:39:11.287Z" }, { url = "https://files.pythonhosted.org/packages/fb/e0/1384c9a76379ab257b75df92283797861dcae592dd98e471df254f87c635/geventhttpclient-2.3.1-cp312-cp312-win32.whl", hash = "sha256:ad0b507e354d2f398186dcb12fe526d0594e7c9387b514fb843f7a14fdf1729a", size = 47595, upload-time = "2024-04-18T21:39:12.535Z" }, { url = "https://files.pythonhosted.org/packages/54/e3/6b8dbb24e3941e20abbe7736e59290c5d4182057ea1d984d46c853208bcd/geventhttpclient-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:7924e0883bc2b177cfe27aa65af6bb9dd57f3e26905c7675a2d1f3ef69df7cca", size = 48271, upload-time = "2024-04-18T21:39:14.479Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9f/251b1b7e665523137a8711f0f0029196cf18b57741135f01aea80a56f16c/geventhttpclient-2.3.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c31431e38df45b3c79bf3c9427c796adb8263d622bc6fa25e2f6ba916c2aad93", size = 49827, upload-time = "2024-04-18T21:39:36.14Z" }, - { url = "https://files.pythonhosted.org/packages/74/c7/ad4c23de669191e1c83cfa28c51d3b50fc246d72e1ee40d4d5b330532492/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855ab1e145575769b180b57accb0573a77cd6a7392f40a6ef7bc9a4926ebd77b", size = 54017, upload-time = "2024-04-18T21:39:37.577Z" }, - { url = "https://files.pythonhosted.org/packages/04/7b/59fc8c8fbd10596abfc46dc103654e3d9676de64229d8eee4b0a4ac2e890/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a374aad77c01539e786d0c7829bec2eba034ccd45733c1bf9811ad18d2a8ecd", size = 58359, upload-time = "2024-04-18T21:39:39.437Z" }, - { url = "https://files.pythonhosted.org/packages/94/b7/743552b0ecda75458c83d55d62937e29c9ee9a42598f57d4025d5de70004/geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c1e97460608304f400485ac099736fff3566d3d8db2038533d466f8cf5de5a", size = 54262, upload-time = "2024-04-18T21:39:40.866Z" }, - { url = "https://files.pythonhosted.org/packages/18/60/10f6215b6cc76b5845a7f4b9c3d1f47d7ecd84ce8769b1e27e0482d605d7/geventhttpclient-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4f843f81ee44ba4c553a1b3f73115e0ad8f00044023c24db29f5b1df3da08465", size = 48343, upload-time = "2024-04-18T21:39:42.173Z" }, ] [[package]] @@ -937,15 +726,6 @@ version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" }, - { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload-time = "2024-09-20T17:44:21.896Z" }, - { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" }, - { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111, upload-time = "2024-09-20T17:09:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392, upload-time = "2024-09-20T17:28:51.988Z" }, { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, @@ -984,23 +764,23 @@ wheels = [ [[package]] name = "gunicorn" -version = "23.0.0" +version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -1020,15 +800,15 @@ wheels = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/56/78a38490b834fa0942cbe6d39bd8a7fd76316e8940319305a98d2b320366/httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535", size = 81036, upload-time = "2023-11-10T13:37:42.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/ba/78b0a99c4da0ff8b0f59defa2f13ca4668189b134bd9840b6202a93d9a0f/httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7", size = 76943, upload-time = "2023-11-10T13:37:40.937Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] @@ -1037,13 +817,6 @@ version = "0.6.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" }, - { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" }, - { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" }, - { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" }, - { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" }, { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, @@ -1084,7 +857,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.0" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1096,30 +869,18 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, -] - -[[package]] -name = "humanfriendly" -version = "10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyreadline3", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, ] [[package]] name = "idna" -version = "3.6" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426, upload-time = "2023-11-25T15:40:54.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567, upload-time = "2023-11-25T15:40:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] @@ -1137,12 +898,11 @@ wheels = [ [[package]] name = "immich-ml" -version = "2.3.1" +version = "2.5.6" source = { editable = "." } dependencies = [ { name = "aiocache" }, { name = "fastapi" }, - { name = "ftfy" }, { name = "gunicorn" }, { name = "huggingface-hub" }, { name = "insightface" }, @@ -1176,10 +936,12 @@ rknn = [ { name = "onnxruntime" }, { name = "rknn-toolkit-lite2" }, ] +rocm = [ + { name = "onnxruntime-migraphx" }, +] [package.dev-dependencies] dev = [ - { name = "black" }, { name = "httpx" }, { name = "locust" }, { name = "mypy" }, @@ -1195,7 +957,6 @@ dev = [ { name = "types-ujson" }, ] lint = [ - { name = "black" }, { name = "mypy" }, { name = "ruff" }, { name = "types-pyyaml" }, @@ -1223,19 +984,19 @@ types = [ requires-dist = [ { name = "aiocache", specifier = ">=0.12.1,<1.0" }, { name = "fastapi", specifier = ">=0.95.2,<1.0" }, - { name = "ftfy", specifier = ">=6.1.1" }, { name = "gunicorn", specifier = ">=21.1.0" }, { name = "huggingface-hub", specifier = ">=0.20.1,<1.0" }, { name = "insightface", specifier = ">=0.7.3,<1.0" }, - { name = "numpy", specifier = "<2" }, - { 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-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" }, + { name = "numpy", specifier = ">=2.3.4" }, + { name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" }, + { name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" }, + { name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" }, + { name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" }, + { name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" }, + { name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "orjson", specifier = ">=3.9.5" }, - { name = "pillow", specifier = ">=9.5.0,<11.0" }, + { name = "pillow", specifier = ">=12.1.1,<12.2" }, { name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" }, @@ -1249,7 +1010,6 @@ provides-extras = ["cpu", "cuda", "openvino", "armnn", "rknn", "rocm"] [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=23.3.0" }, { name = "httpx", specifier = ">=0.24.1" }, { name = "locust", specifier = ">=2.15.1" }, { name = "mypy", specifier = ">=1.3.0" }, @@ -1265,7 +1025,6 @@ dev = [ { name = "types-ujson", specifier = ">=5.10.0.20240515" }, ] lint = [ - { name = "black", specifier = ">=23.3.0" }, { name = "mypy", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.0.272" }, { name = "types-pyyaml", specifier = ">=6.0.12.20241230" }, @@ -1306,18 +1065,15 @@ dependencies = [ { name = "albumentations" }, { name = "cython" }, { name = "easydict" }, - { name = "matplotlib", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "matplotlib", version = "3.10.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "matplotlib" }, { name = "numpy" }, { name = "onnx" }, { name = "pillow" }, { name = "prettytable" }, { name = "requests" }, { name = "scikit-image" }, - { name = "scikit-learn", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn" }, + { name = "scipy" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/8d/0f4af90999ca96cf8cb846eb5ae27c5ef5b390f9c090dd19e4fa76364c13/insightface-0.7.3.tar.gz", hash = "sha256:f191f719612ebb37018f41936814500544cd0f86e6fcd676c023f354c668ddf7", size = 439490, upload-time = "2023-04-02T08:01:54.541Z" } @@ -1358,21 +1114,6 @@ version = "1.4.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b9/2d/226779e405724344fc678fcc025b812587617ea1a48b9442628b688e85ea/kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec", size = 97552, upload-time = "2023-08-24T09:30:39.861Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/56/cb02dcefdaab40df636b91e703b172966b444605a0ea313549f3ffc05bd3/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af", size = 127397, upload-time = "2023-08-24T09:28:18.105Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c1/d084f8edb26533a191415d5173157080837341f9a06af9dd1a75f727abb4/kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3", size = 68125, upload-time = "2023-08-24T09:28:19.218Z" }, - { url = "https://files.pythonhosted.org/packages/23/11/6fb190bae4b279d712a834e7b1da89f6dcff6791132f7399aa28a57c3565/kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4", size = 66211, upload-time = "2023-08-24T09:28:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/b3/13/5e9e52feb33e9e063f76b2c5eb09cb977f5bba622df3210081bfb26ec9a3/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1", size = 1637145, upload-time = "2023-08-24T09:28:21.439Z" }, - { url = "https://files.pythonhosted.org/packages/6f/40/4ab1fdb57fced80ce5903f04ae1aed7c1d5939dda4fd0c0aa526c12fe28a/kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff", size = 1617849, upload-time = "2023-08-24T09:28:23.004Z" }, - { url = "https://files.pythonhosted.org/packages/49/ca/61ef43bd0832c7253b370735b0c38972c140c8774889b884372a629a8189/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a", size = 1400921, upload-time = "2023-08-24T09:28:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/68/6f/854f6a845c00b4257482468e08d8bc386f4929ee499206142378ba234419/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa", size = 1513009, upload-time = "2023-08-24T09:28:25.636Z" }, - { url = "https://files.pythonhosted.org/packages/50/65/76f303377167d12eb7a9b423d6771b39fe5c4373e4a42f075805b1f581ae/kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c", size = 1444819, upload-time = "2023-08-24T09:28:27.547Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ee/98cdf9dde129551467138b6e18cc1cc901e75ecc7ffb898c6f49609f33b1/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b", size = 1817054, upload-time = "2023-08-24T09:28:28.839Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5b/ab569016ec4abc7b496f6cb8a3ab511372c99feb6a23d948cda97e0db6da/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770", size = 1918613, upload-time = "2023-08-24T09:28:30.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/ac/39b9f99d2474b1ac7af1ddfe5756ddf9b6a8f24c5f3a32cd4c010317fc6b/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0", size = 1872650, upload-time = "2023-08-24T09:28:32.303Z" }, - { url = "https://files.pythonhosted.org/packages/40/5b/be568548266516b114d1776120281ea9236c732fb6032a1f8f3b1e5e921c/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525", size = 1827415, upload-time = "2023-08-24T09:28:34.141Z" }, - { url = "https://files.pythonhosted.org/packages/d4/80/c0c13d2a17a12937a19ef378bf35e94399fd171ed6ec05bcee0f038e1eaf/kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b", size = 1838094, upload-time = "2023-08-24T09:28:35.97Z" }, - { url = "https://files.pythonhosted.org/packages/70/d1/5ab93ee00ca5af708929cc12fbe665b6f1ed4ad58088e70dc00e87e0d107/kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238", size = 46585, upload-time = "2023-08-24T09:28:37.326Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a1/8a9c9be45c642fa12954855d8b3a02d9fd8551165a558835a19508fec2e6/kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276", size = 56095, upload-time = "2023-08-24T09:28:38.325Z" }, { url = "https://files.pythonhosted.org/packages/2a/eb/9e099ad7c47c279995d2d20474e1821100a5f10f847739bd65b1c1f02442/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5", size = 127403, upload-time = "2023-08-24T09:28:39.3Z" }, { url = "https://files.pythonhosted.org/packages/a6/94/695922e71288855fc7cace3bdb52edda9d7e50edba77abb0c9d7abb51e96/kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90", size = 68156, upload-time = "2023-08-24T09:28:40.301Z" }, { url = "https://files.pythonhosted.org/packages/4a/fe/23d7fa78f7c66086d196406beb1fb2eaf629dd7adc01c3453033303d17fa/kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797", size = 66166, upload-time = "2023-08-24T09:28:41.235Z" }, @@ -1407,16 +1148,82 @@ wheels = [ [[package]] name = "lazy-loader" -version = "0.3" +version = "0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/1630a735bfdf9eb857a3b9a53317a1e1658ea97a1b4b39dcb0f71dae81f8/lazy_loader-0.3.tar.gz", hash = "sha256:3b68898e34f5b2a29daaaac172c6555512d0f32074f147e2254e4a6d9d838f37", size = 12268, upload-time = "2023-06-30T21:12:55.362Z" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/c3/65b3814e155836acacf720e5be3b5757130346670ac454fee29d3eda1381/lazy_loader-0.3-py3-none-any.whl", hash = "sha256:1e9e76ee8631e264c62ce10006718e80b2cfc74340d17d1031e0f84af7478554", size = 9087, upload-time = "2023-06-30T21:12:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "librt" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/93/e4/b59bdf1197fdf9888452ea4d2048cdad61aef85eb83e99dc52551d7fdc04/librt-0.7.4.tar.gz", hash = "sha256:3871af56c59864d5fd21d1ac001eb2fb3b140d52ba0454720f2e4a19812404ba", size = 145862, upload-time = "2025-12-15T16:52:43.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/64/44089b12d8b4714a7f0e2f33fb19285ba87702d4be0829f20b36ebeeee07/librt-0.7.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3485b9bb7dfa66167d5500ffdafdc35415b45f0da06c75eb7df131f3357b174a", size = 54709, upload-time = "2025-12-15T16:51:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/6fa39fb5f37002f7d25e0da4f24d41b457582beea9369eeb7e9e73db5508/librt-0.7.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:188b4b1a770f7f95ea035d5bbb9d7367248fc9d12321deef78a269ebf46a5729", size = 56663, upload-time = "2025-12-15T16:51:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e4/cbaca170a13bee2469c90df9e47108610b4422c453aea1aec1779ac36c24/librt-0.7.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b668b1c840183e4e38ed5a99f62fac44c3a3eef16870f7f17cfdfb8b47550ed", size = 161703, upload-time = "2025-12-15T16:51:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/d0/32/0b2296f9cc7e693ab0d0835e355863512e5eac90450c412777bd699c76ae/librt-0.7.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e8f864b521f6cfedb314d171630f827efee08f5c3462bcbc2244ab8e1768cd6", size = 171027, upload-time = "2025-12-15T16:51:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/c70b6d40f7342716e5f1353c8da92d9e32708a18cbfa44897a93ec2bf879/librt-0.7.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df7c9def4fc619a9c2ab402d73a0c5b53899abe090e0100323b13ccb5a3dd82", size = 184700, upload-time = "2025-12-15T16:51:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c8/555c405155da210e4c4113a879d378f54f850dbc7b794e847750a8fadd43/librt-0.7.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f79bc3595b6ed159a1bf0cdc70ed6ebec393a874565cab7088a219cca14da727", size = 180719, upload-time = "2025-12-15T16:51:23.561Z" }, + { url = "https://files.pythonhosted.org/packages/6b/88/34dc1f1461c5613d1b73f0ecafc5316cc50adcc1b334435985b752ed53e5/librt-0.7.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77772a4b8b5f77d47d883846928c36d730b6e612a6388c74cba33ad9eb149c11", size = 174535, upload-time = "2025-12-15T16:51:25.031Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/f3fafe80a221626bcedfa9fe5abbf5f04070989d44782f579b2d5920d6d0/librt-0.7.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:064a286e6ab0b4c900e228ab4fa9cb3811b4b83d3e0cc5cd816b2d0f548cb61c", size = 195236, upload-time = "2025-12-15T16:51:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/d8/77/5c048d471ce17f4c3a6e08419be19add4d291e2f7067b877437d482622ac/librt-0.7.4-cp311-cp311-win32.whl", hash = "sha256:42da201c47c77b6cc91fc17e0e2b330154428d35d6024f3278aa2683e7e2daf2", size = 42930, upload-time = "2025-12-15T16:51:27.853Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3b/514a86305a12c3d9eac03e424b07cd312c7343a9f8a52719aa079590a552/librt-0.7.4-cp311-cp311-win_amd64.whl", hash = "sha256:d31acb5886c16ae1711741f22504195af46edec8315fe69b77e477682a87a83e", size = 49240, upload-time = "2025-12-15T16:51:29.037Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/3b7b1914f565926b780a734fac6e9a4d2c7aefe41f4e89357d73697a9457/librt-0.7.4-cp311-cp311-win_arm64.whl", hash = "sha256:114722f35093da080a333b3834fff04ef43147577ed99dd4db574b03a5f7d170", size = 42613, upload-time = "2025-12-15T16:51:30.194Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e7/b805d868d21f425b7e76a0ea71a2700290f2266a4f3c8357fcf73efc36aa/librt-0.7.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7dd3b5c37e0fb6666c27cf4e2c88ae43da904f2155c4cfc1e5a2fdce3b9fcf92", size = 55688, upload-time = "2025-12-15T16:51:31.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/69a2b02e62a14cfd5bfd9f1e9adea294d5bcfeea219c7555730e5d068ee4/librt-0.7.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c5de1928c486201b23ed0cc4ac92e6e07be5cd7f3abc57c88a9cf4f0f32108", size = 57141, upload-time = "2025-12-15T16:51:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/05dba608aae1272b8ea5ff8ef12c47a4a099a04d1e00e28a94687261d403/librt-0.7.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:078ae52ffb3f036396cc4aed558e5b61faedd504a3c1f62b8ae34bf95ae39d94", size = 165322, upload-time = "2025-12-15T16:51:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bc/199533d3fc04a4cda8d7776ee0d79955ab0c64c79ca079366fbc2617e680/librt-0.7.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce58420e25097b2fc201aef9b9f6d65df1eb8438e51154e1a7feb8847e4a55ab", size = 174216, upload-time = "2025-12-15T16:51:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/62/ec/09239b912a45a8ed117cb4a6616d9ff508f5d3131bd84329bf2f8d6564f1/librt-0.7.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b719c8730c02a606dc0e8413287e8e94ac2d32a51153b300baf1f62347858fba", size = 189005, upload-time = "2025-12-15T16:51:36.687Z" }, + { url = "https://files.pythonhosted.org/packages/46/2e/e188313d54c02f5b0580dd31476bb4b0177514ff8d2be9f58d4a6dc3a7ba/librt-0.7.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3749ef74c170809e6dee68addec9d2458700a8de703de081c888e92a8b015cf9", size = 183960, upload-time = "2025-12-15T16:51:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/f1d568d254518463d879161d3737b784137d236075215e56c7c9be191cee/librt-0.7.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b35c63f557653c05b5b1b6559a074dbabe0afee28ee2a05b6c9ba21ad0d16a74", size = 177609, upload-time = "2025-12-15T16:51:40.584Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/060bbc1c002f0d757c33a1afe6bf6a565f947a04841139508fc7cef6c08b/librt-0.7.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1ef704e01cb6ad39ad7af668d51677557ca7e5d377663286f0ee1b6b27c28e5f", size = 199269, upload-time = "2025-12-15T16:51:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7f/708f8f02d8012ee9f366c07ea6a92882f48bd06cc1ff16a35e13d0fbfb08/librt-0.7.4-cp312-cp312-win32.whl", hash = "sha256:c66c2b245926ec15188aead25d395091cb5c9df008d3b3207268cd65557d6286", size = 43186, upload-time = "2025-12-15T16:51:43.149Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a5/4e051b061c8b2509be31b2c7ad4682090502c0a8b6406edcf8c6b4fe1ef7/librt-0.7.4-cp312-cp312-win_amd64.whl", hash = "sha256:71a56f4671f7ff723451f26a6131754d7c1809e04e22ebfbac1db8c9e6767a20", size = 49455, upload-time = "2025-12-15T16:51:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d2/90d84e9f919224a3c1f393af1636d8638f54925fdc6cd5ee47f1548461e5/librt-0.7.4-cp312-cp312-win_arm64.whl", hash = "sha256:419eea245e7ec0fe664eb7e85e7ff97dcdb2513ca4f6b45a8ec4a3346904f95a", size = 42828, upload-time = "2025-12-15T16:51:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4d/46a53ccfbb39fd0b493fd4496eb76f3ebc15bb3e45d8c2e695a27587edf5/librt-0.7.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d44a1b1ba44cbd2fc3cb77992bef6d6fdb1028849824e1dd5e4d746e1f7f7f0b", size = 55745, upload-time = "2025-12-15T16:51:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2b/3ac7f5212b1828bf4f979cf87f547db948d3e28421d7a430d4db23346ce4/librt-0.7.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c9cab4b3de1f55e6c30a84c8cee20e4d3b2476f4d547256694a1b0163da4fe32", size = 57166, upload-time = "2025-12-15T16:51:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/99/6523509097cbe25f363795f0c0d1c6a3746e30c2994e25b5aefdab119b21/librt-0.7.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2857c875f1edd1feef3c371fbf830a61b632fb4d1e57160bb1e6a3206e6abe67", size = 165833, upload-time = "2025-12-15T16:51:49.443Z" }, + { url = "https://files.pythonhosted.org/packages/fe/35/323611e59f8fe032649b4fb7e77f746f96eb7588fcbb31af26bae9630571/librt-0.7.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b370a77be0a16e1ad0270822c12c21462dc40496e891d3b0caf1617c8cc57e20", size = 174818, upload-time = "2025-12-15T16:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/40fb2bb21616c6e06b6a64022802228066e9a31618f493e03f6b9661548a/librt-0.7.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d05acd46b9a52087bfc50c59dfdf96a2c480a601e8898a44821c7fd676598f74", size = 189607, upload-time = "2025-12-15T16:51:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/32/48/1b47c7d5d28b775941e739ed2bfe564b091c49201b9503514d69e4ed96d7/librt-0.7.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70969229cb23d9c1a80e14225838d56e464dc71fa34c8342c954fc50e7516dee", size = 184585, upload-time = "2025-12-15T16:51:54.027Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/ee135dfb5d3b54d5d9001dbe483806229c6beac3ee2ba1092582b7efeb1b/librt-0.7.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4450c354b89dbb266730893862dbff06006c9ed5b06b6016d529b2bf644fc681", size = 178249, upload-time = "2025-12-15T16:51:55.248Z" }, + { url = "https://files.pythonhosted.org/packages/04/87/d5b84ec997338be26af982bcd6679be0c1db9a32faadab1cf4bb24f9e992/librt-0.7.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:adefe0d48ad35b90b6f361f6ff5a1bd95af80c17d18619c093c60a20e7a5b60c", size = 199851, upload-time = "2025-12-15T16:51:56.933Z" }, + { url = "https://files.pythonhosted.org/packages/86/63/ba1333bf48306fe398e3392a7427ce527f81b0b79d0d91618c4610ce9d15/librt-0.7.4-cp313-cp313-win32.whl", hash = "sha256:21ea710e96c1e050635700695095962a22ea420d4b3755a25e4909f2172b4ff2", size = 43249, upload-time = "2025-12-15T16:51:58.498Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/de2c6df06cdfa9308c080e6b060fe192790b6a48a47320b215e860f0e98c/librt-0.7.4-cp313-cp313-win_amd64.whl", hash = "sha256:772e18696cf5a64afee908662fbcb1f907460ddc851336ee3a848ef7684c8e1e", size = 49417, upload-time = "2025-12-15T16:51:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/8ee0949efc389691381ed686185e43536c20e7ad880c122dd1f31e65c658/librt-0.7.4-cp313-cp313-win_arm64.whl", hash = "sha256:52e34c6af84e12921748c8354aa6acf1912ca98ba60cdaa6920e34793f1a0788", size = 42824, upload-time = "2025-12-15T16:52:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/74/81/6921e65c8708eb6636bbf383aa77e6c7dad33a598ed3b50c313306a2da9d/librt-0.7.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4f1ee004942eaaed6e06c087d93ebc1c67e9a293e5f6b9b5da558df6bf23dc5d", size = 55191, upload-time = "2025-12-15T16:52:01.97Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d6/3eb864af8a8de8b39cc8dd2e9ded1823979a27795d72c4eea0afa8c26c9f/librt-0.7.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d854c6dc0f689bad7ed452d2a3ecff58029d80612d336a45b62c35e917f42d23", size = 56898, upload-time = "2025-12-15T16:52:03.356Z" }, + { url = "https://files.pythonhosted.org/packages/49/bc/b1d4c0711fdf79646225d576faee8747b8528a6ec1ceb6accfd89ade7102/librt-0.7.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4f7339d9e445280f23d63dea842c0c77379c4a47471c538fc8feedab9d8d063", size = 163725, upload-time = "2025-12-15T16:52:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/61c41cd8f0a6a41fc99ea78a2205b88187e45ba9800792410ed62f033584/librt-0.7.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39003fc73f925e684f8521b2dbf34f61a5deb8a20a15dcf53e0d823190ce8848", size = 172469, upload-time = "2025-12-15T16:52:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/4ee18b4d57f01444230bc18cf59103aeab8f8c0f45e84e0e540094df1df1/librt-0.7.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb15ee29d95875ad697d449fe6071b67f730f15a6961913a2b0205015ca0843", size = 186804, upload-time = "2025-12-15T16:52:07.192Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/009e8ba3fbf830c936842da048eda1b34b99329f402e49d88fafff6525d1/librt-0.7.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:02a69369862099e37d00765583052a99d6a68af7e19b887e1b78fee0146b755a", size = 181807, upload-time = "2025-12-15T16:52:08.554Z" }, + { url = "https://files.pythonhosted.org/packages/85/26/51ae25f813656a8b117c27a974f25e8c1e90abcd5a791ac685bf5b489a1b/librt-0.7.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ec72342cc4d62f38b25a94e28b9efefce41839aecdecf5e9627473ed04b7be16", size = 175595, upload-time = "2025-12-15T16:52:10.186Z" }, + { url = "https://files.pythonhosted.org/packages/48/93/36d6c71f830305f88996b15c8e017aa8d1e03e2e947b40b55bbf1a34cf24/librt-0.7.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:776dbb9bfa0fc5ce64234b446995d8d9f04badf64f544ca036bd6cff6f0732ce", size = 196504, upload-time = "2025-12-15T16:52:11.472Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/8299e70862bb9d704735bf132c6be09c17b00fbc7cda0429a9df222fdc1b/librt-0.7.4-cp314-cp314-win32.whl", hash = "sha256:0f8cac84196d0ffcadf8469d9ded4d4e3a8b1c666095c2a291e22bf58e1e8a9f", size = 39738, upload-time = "2025-12-15T16:52:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/656b0126e4e0f8e2725cd2d2a1ec40f71f37f6f03f135a26b663c0e1a737/librt-0.7.4-cp314-cp314-win_amd64.whl", hash = "sha256:037f5cb6fe5abe23f1dc058054d50e9699fcc90d0677eee4e4f74a8677636a1a", size = 45976, upload-time = "2025-12-15T16:52:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/60/86/465ff07b75c1067da8fa7f02913c4ead096ef106cfac97a977f763783bfb/librt-0.7.4-cp314-cp314-win_arm64.whl", hash = "sha256:a5deebb53d7a4d7e2e758a96befcd8edaaca0633ae71857995a0f16033289e44", size = 39073, upload-time = "2025-12-15T16:52:15.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a0/24941f85960774a80d4b3c2aec651d7d980466da8101cae89e8b032a3e21/librt-0.7.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b4c25312c7f4e6ab35ab16211bdf819e6e4eddcba3b2ea632fb51c9a2a97e105", size = 57369, upload-time = "2025-12-15T16:52:16.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/a0/ddb259cae86ab415786c1547d0fe1b40f04a7b089f564fd5c0242a3fafb2/librt-0.7.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:618b7459bb392bdf373f2327e477597fff8f9e6a1878fffc1b711c013d1b0da4", size = 59230, upload-time = "2025-12-15T16:52:18.259Z" }, + { url = "https://files.pythonhosted.org/packages/31/11/77823cb530ab8a0c6fac848ac65b745be446f6f301753b8990e8809080c9/librt-0.7.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1437c3f72a30c7047f16fd3e972ea58b90172c3c6ca309645c1c68984f05526a", size = 183869, upload-time = "2025-12-15T16:52:19.457Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ce/157db3614cf3034b3f702ae5ba4fefda4686f11eea4b7b96542324a7a0e7/librt-0.7.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c96cb76f055b33308f6858b9b594618f1b46e147a4d03a4d7f0c449e304b9b95", size = 194606, upload-time = "2025-12-15T16:52:20.795Z" }, + { url = "https://files.pythonhosted.org/packages/30/ef/6ec4c7e3d6490f69a4fd2803516fa5334a848a4173eac26d8ee6507bff6e/librt-0.7.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28f990e6821204f516d09dc39966ef8b84556ffd648d5926c9a3f681e8de8906", size = 206776, upload-time = "2025-12-15T16:52:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/ad/22/750b37bf549f60a4782ab80e9d1e9c44981374ab79a7ea68670159905918/librt-0.7.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc4aebecc79781a1b77d7d4e7d9fe080385a439e198d993b557b60f9117addaf", size = 203205, upload-time = "2025-12-15T16:52:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/2e8a0f584412a93df5faad46c5fa0a6825fdb5eba2ce482074b114877f44/librt-0.7.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:022cc673e69283a42621dd453e2407cf1647e77f8bd857d7ad7499901e62376f", size = 196696, upload-time = "2025-12-15T16:52:24.951Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ca/7bf78fa950e43b564b7de52ceeb477fb211a11f5733227efa1591d05a307/librt-0.7.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2b3ca211ae8ea540569e9c513da052699b7b06928dcda61247cb4f318122bdb5", size = 217191, upload-time = "2025-12-15T16:52:26.194Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/3732b0e8424ae35ad5c3166d9dd5bcdae43ce98775e0867a716ff5868064/librt-0.7.4-cp314-cp314t-win32.whl", hash = "sha256:8a461f6456981d8c8e971ff5a55f2e34f4e60871e665d2f5fde23ee74dea4eeb", size = 40276, upload-time = "2025-12-15T16:52:27.54Z" }, + { url = "https://files.pythonhosted.org/packages/35/d6/d8823e01bd069934525fddb343189c008b39828a429b473fb20d67d5cd36/librt-0.7.4-cp314-cp314t-win_amd64.whl", hash = "sha256:721a7b125a817d60bf4924e1eec2a7867bfcf64cfc333045de1df7a0629e4481", size = 46772, upload-time = "2025-12-15T16:52:28.653Z" }, + { url = "https://files.pythonhosted.org/packages/36/e9/a0aa60f5322814dd084a89614e9e31139702e342f8459ad8af1984a18168/librt-0.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:76b2ba71265c0102d11458879b4d53ccd0b32b0164d14deb8d2b598a018e502f", size = 39724, upload-time = "2025-12-15T16:52:29.836Z" }, ] [[package]] name = "locust" -version = "2.42.5" +version = "2.43.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1425,7 +1232,6 @@ dependencies = [ { name = "flask-login" }, { name = "gevent" }, { name = "geventhttpclient" }, - { name = "locust-cloud" }, { name = "msgpack" }, { name = "psutil" }, { name = "pytest" }, @@ -1434,30 +1240,12 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pyzmq" }, { name = "requests" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/69/076f6a1eb4e5813eea864f5a9a5311385c5cc71c46377ed7cec824eca0a1/locust-2.42.5.tar.gz", hash = "sha256:83b8cfc38bd88b3d9daf9790be24239356ccd1160d9b357fa9c7af32907a8860", size = 1418586, upload-time = "2025-11-20T16:45:44.806Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/c5/7d7bd50ac744bc209a4bcbeb74660d7ae450a44441737efe92ee9d8ea6a7/locust-2.43.3.tar.gz", hash = "sha256:b5d2c48f8f7d443e3abdfdd6ec2f7aebff5cd74fab986bcf1e95b375b5c5a54b", size = 1445349, upload-time = "2026-02-12T09:55:34.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/a9/ce92490466f719e658b5815a4778669c12a4b907ff7ca51e0a94baedf5b2/locust-2.42.5-py3-none-any.whl", hash = "sha256:fa654eb501bf4bad665310ead59c9d7b209d057717795fbbcc977f61af1fdf64", size = 1437168, upload-time = "2025-11-20T16:45:42.855Z" }, -] - -[[package]] -name = "locust-cloud" -version = "1.29.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configargparse" }, - { name = "gevent" }, - { name = "platformdirs" }, - { name = "python-engineio" }, - { name = "python-socketio", extra = ["client"] }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/51/7367b8f13df5fdda001b717574091ea223820be0e2d22caa0e9cfefba556/locust_cloud-1.29.3.tar.gz", hash = "sha256:1b88c1fa8eeb73557f3ab25e16b037283df9a48282be8e91fb4fae84ef7bb367", size = 457304, upload-time = "2025-11-26T13:37:19.318Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/31/4850f0ac5e109007278dbf7d1368565ce43b589aa74e6c6c481c597968db/locust_cloud-1.29.3-py3-none-any.whl", hash = "sha256:1898f09287865b1590d44d5eb71586a40a3895c52bf9bfc826fd6a384fb389d5", size = 413421, upload-time = "2025-11-26T13:37:16.613Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d2/dc5379876d3a481720803653ea4d219f0c26f2d2b37c9243baaa16d0bc79/locust-2.43.3-py3-none-any.whl", hash = "sha256:e032c119b54a9d984cb74a936ee83cfd7d68b3c76c8f308af63d04f11396b553", size = 1463473, upload-time = "2026-02-12T09:55:31.727Z" }, ] [[package]] @@ -1478,16 +1266,6 @@ version = "2.1.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6d/7c/59a3248f411813f8ccba92a55feaac4bf360d29e2ff05ee7d8e1ef2d7dbf/MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", size = 19132, upload-time = "2023-06-02T21:43:45.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/1d/713d443799d935f4d26a4f1510c9e61b1d288592fb869845e5cc92a1e055/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", size = 17846, upload-time = "2023-06-02T21:42:33.954Z" }, - { url = "https://files.pythonhosted.org/packages/f7/9c/86cbd8e0e1d81f0ba420f20539dd459c50537c7751e28102dbfee2b6f28c/MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", size = 13720, upload-time = "2023-06-02T21:42:35.102Z" }, - { url = "https://files.pythonhosted.org/packages/a6/56/f1d4ee39e898a9e63470cbb7fae1c58cce6874f25f54220b89213a47f273/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", size = 26498, upload-time = "2023-06-02T21:42:36.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/d9ed2c0971e1435b8a62354b18d3060b66c8cb1d368399ec0b9baa7c0ee5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", size = 25691, upload-time = "2023-06-02T21:42:37.778Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b7/c5ba9b7ad9ad21fc4a60df226615cf43ead185d328b77b0327d603d00cc5/MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", size = 25366, upload-time = "2023-06-02T21:42:39.441Z" }, - { url = "https://files.pythonhosted.org/packages/71/61/f5673d7aac2cf7f203859008bb3fc2b25187aa330067c5e9955e5c5ebbab/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", size = 30505, upload-time = "2023-06-02T21:42:41.088Z" }, - { url = "https://files.pythonhosted.org/packages/47/26/932140621773bfd4df3223fbdd9e78de3477f424f0d2987c313b1cb655ff/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", size = 29616, upload-time = "2023-06-02T21:42:42.273Z" }, - { url = "https://files.pythonhosted.org/packages/3c/c8/74d13c999cbb49e3460bf769025659a37ef4a8e884de629720ab4e42dcdb/MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", size = 29891, upload-time = "2023-06-02T21:42:43.635Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/4db3b1abc5a1fe7295aa0683eafd13832084509c3b8236f3faf8dd4eff75/MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", size = 16525, upload-time = "2023-06-02T21:42:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/c4aebb8a14a1d39d5135eb8233a0b95831cdc42c4088358449c3ed657044/MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", size = 17083, upload-time = "2023-06-02T21:42:46.948Z" }, { url = "https://files.pythonhosted.org/packages/fe/09/c31503cb8150cf688c1534a7135cc39bb9092f8e0e6369ec73494d16ee0e/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", size = 17862, upload-time = "2023-06-02T21:42:48.569Z" }, { url = "https://files.pythonhosted.org/packages/c0/c7/171f5ac6b065e1425e8fabf4a4dfbeca76fd8070072c6a41bd5c07d90d8b/MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", size = 13738, upload-time = "2023-06-02T21:42:49.727Z" }, { url = "https://files.pythonhosted.org/packages/a2/f7/9175ad1b8152092f7c3b78c513c1bdfe9287e0564447d1c2d3d1a2471540/MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", size = 28891, upload-time = "2023-06-02T21:42:51.33Z" }, @@ -1510,85 +1288,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/44/dbaf65876e258facd65f586dde158387ab89963e7f2235551afc9c2e24c2/MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", size = 16979, upload-time = "2023-09-07T16:00:57.77Z" }, ] -[[package]] -name = "matplotlib" -version = "3.8.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "contourpy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "cycler", marker = "python_full_version < '3.11'" }, - { name = "fonttools", marker = "python_full_version < '3.11'" }, - { name = "kiwisolver", marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "pyparsing", marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/ab/38a0e94cb01dacb50f06957c2bed1c83b8f9dac6618988a37b2487862944/matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1", size = 35866957, upload-time = "2023-11-17T21:16:40.15Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/d0/fc5f6796a1956f5b9a33555611d01a3cec038f000c3d70ecb051b1631ac4/matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7", size = 7590640, upload-time = "2023-11-17T21:17:02.834Z" }, - { url = "https://files.pythonhosted.org/packages/57/44/007b592809f50883c910db9ec4b81b16dfa0136407250fb581824daabf03/matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367", size = 7484350, upload-time = "2023-11-17T21:17:12.281Z" }, - { url = "https://files.pythonhosted.org/packages/01/87/c7b24f3048234fe10184560263be2173311376dc3d1fa329de7f012d6ce5/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18", size = 11382388, upload-time = "2023-11-17T21:17:26.461Z" }, - { url = "https://files.pythonhosted.org/packages/19/e5/a4ea514515f270224435c69359abb7a3d152ed31b9ee3ba5e63017461945/matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31", size = 11611959, upload-time = "2023-11-17T21:17:40.541Z" }, - { url = "https://files.pythonhosted.org/packages/09/23/ab5a562c9acb81e351b084bea39f65b153918417fb434619cf5a19f44a55/matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a", size = 9536938, upload-time = "2023-11-17T21:17:49.925Z" }, - { url = "https://files.pythonhosted.org/packages/46/37/b5e27ab30ecc0a3694c8a78287b5ef35dad0c3095c144fcc43081170bfd6/matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a", size = 7643836, upload-time = "2023-11-17T21:17:58.379Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0d/53afb186adafc7326d093b8333e8a79974c495095771659f4304626c4bc7/matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63", size = 7593458, upload-time = "2023-11-17T21:18:06.141Z" }, - { url = "https://files.pythonhosted.org/packages/ce/25/a557ee10ac9dce1300850024707ce1850a6958f1673a9194be878b99d631/matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8", size = 7486840, upload-time = "2023-11-17T21:18:13.706Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/72712b3895ee180f6e342638a8591c31912fbcc09ce9084cc256da16d0a0/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6", size = 11387332, upload-time = "2023-11-17T21:18:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/92/1a/cd3e0c90d1a763ad90073e13b189b4702f11becf4e71dbbad70a7a149811/matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788", size = 11616911, upload-time = "2023-11-17T21:18:35.27Z" }, - { url = "https://files.pythonhosted.org/packages/78/4a/bad239071477305a3758eb4810615e310a113399cddd7682998be9f01e97/matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0", size = 9549260, upload-time = "2023-11-17T21:18:44.836Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/27fd341e4510257789f19a4b4be8bb90d1113b8f176c3dab562b4f21466e/matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717", size = 7645742, upload-time = "2023-11-17T21:18:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1b/864d28d5a72d586ac137f4ca54d5afc8b869720e30d508dbd9adcce4d231/matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627", size = 7590988, upload-time = "2023-11-17T21:19:01.119Z" }, - { url = "https://files.pythonhosted.org/packages/9a/b0/dd2b60f2dd90fbc21d1d3129c36a453c322d7995d5e3589f5b3c59ee528d/matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4", size = 7483594, upload-time = "2023-11-17T21:19:09.865Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/9942533ad9f96753bde0e5a5d48eacd6c21de8ea1ad16570e31bda8a017f/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d", size = 11380843, upload-time = "2023-11-17T21:19:20.46Z" }, - { url = "https://files.pythonhosted.org/packages/fc/52/bfd36eb4745a3b21b3946c2c3a15679b620e14574fe2b98e9451b65ef578/matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331", size = 11604608, upload-time = "2023-11-17T21:19:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/6d/8c/0cdfbf604d4ea3dfa77435176c51e233cc408ad8f3efbf8d2c9f57cbdafb/matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213", size = 9545252, upload-time = "2023-11-17T21:19:42.271Z" }, - { url = "https://files.pythonhosted.org/packages/2e/51/c77a14869b7eb9d6fb440e811b754fc3950d6868c38ace57d0632b674415/matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630", size = 7645067, upload-time = "2023-11-17T21:19:50.091Z" }, -] - [[package]] name = "matplotlib" version = "3.10.5" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler", marker = "python_full_version >= '3.11'" }, - { name = "fonttools", marker = "python_full_version >= '3.11'" }, - { name = "kiwisolver", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", marker = "python_full_version >= '3.11'" }, - { name = "pyparsing", marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094, upload-time = "2025-07-31T18:07:36.507Z" }, - { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464, upload-time = "2025-07-31T18:07:38.864Z" }, - { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163, upload-time = "2025-07-31T18:07:41.141Z" }, - { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635, upload-time = "2025-07-31T18:07:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036, upload-time = "2025-07-31T18:07:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529, upload-time = "2025-07-31T18:07:49.553Z" }, { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, @@ -1631,9 +1347,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932, upload-time = "2025-07-31T18:09:15.335Z" }, { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003, upload-time = "2025-07-31T18:09:17.416Z" }, { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849, upload-time = "2025-07-31T18:09:19.673Z" }, - { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360, upload-time = "2025-07-31T18:09:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462, upload-time = "2025-07-31T18:09:23.504Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802, upload-time = "2025-07-31T18:09:25.256Z" }, { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, @@ -1648,6 +1361,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, + { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, + { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, + { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/1bcc98a66de7b2455dfb292f271452cac9edc4e870796e0d87033524d790/ml_dtypes-0.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5103856a225465371fe119f2fef737402b705b810bd95ad5f348e6e1a6ae21af", size = 663781, upload-time = "2025-07-29T18:38:42.984Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2c/bd2a79ba7c759ee192b5601b675b180a3fd6ccf48ffa27fe1782d280f1a7/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cae435a68861660af81fa3c5af16b70ca11a17275c5b662d9c6f58294e0f113", size = 4956217, upload-time = "2025-07-29T18:38:44.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6936283b56d74fbec431ca57ce58a90a908fdbd14d4e2d22eea6d72bb208a7b7", size = 4933109, upload-time = "2025-07-29T18:38:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/bc/24/054036dbe32c43295382c90a1363241684c4d6aaa1ecc3df26bd0c8d5053/ml_dtypes-0.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:d0f730a17cf4f343b2c7ad50cee3bd19e969e793d2be6ed911f43086460096e4", size = 208187, upload-time = "2025-07-29T18:38:48.24Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/7dc3ec6794a4a9004c765e0c341e32355840b698f73fd2daff46f128afc1/ml_dtypes-0.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2db74788fc01914a3c7f7da0763427280adfc9cd377e9604b6b64eb8097284bd", size = 161559, upload-time = "2025-07-29T18:38:50.493Z" }, + { url = "https://files.pythonhosted.org/packages/12/91/e6c7a0d67a152b9330445f9f0cf8ae6eee9b83f990b8c57fe74631e42a90/ml_dtypes-0.5.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93c36a08a6d158db44f2eb9ce3258e53f24a9a4a695325a689494f0fdbc71770", size = 689321, upload-time = "2025-07-29T18:38:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6c/b7b94b84a104a5be1883305b87d4c6bd6ae781504474b4cca067cb2340ec/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e44a3761f64bc009d71ddb6d6c71008ba21b53ab6ee588dadab65e2fa79eafc", size = 5274495, upload-time = "2025-07-29T18:38:53.797Z" }, + { url = "https://files.pythonhosted.org/packages/5b/38/6266604dffb43378055394ea110570cf261a49876fc48f548dfe876f34cc/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdf40d2aaabd3913dec11840f0d0ebb1b93134f99af6a0a4fd88ffe924928ab4", size = 5285422, upload-time = "2025-07-29T18:38:56.603Z" }, + { url = "https://files.pythonhosted.org/packages/7c/88/8612ff177d043a474b9408f0382605d881eeb4125ba89d4d4b3286573a83/ml_dtypes-0.5.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:aec640bd94c4c85c0d11e2733bd13cbb10438fb004852996ec0efbc6cacdaf70", size = 661182, upload-time = "2025-07-29T18:38:58.414Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2b/0569a5e88b29240d373e835107c94ae9256fb2191d3156b43b2601859eff/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bda32ce212baa724e03c68771e5c69f39e584ea426bfe1a701cb01508ffc7035", size = 4956187, upload-time = "2025-07-29T18:39:00.611Z" }, + { url = "https://files.pythonhosted.org/packages/51/66/273c2a06ae44562b104b61e6b14444da00061fd87652506579d7eb2c40b1/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c205cac07d24a29840c163d6469f61069ce4b065518519216297fc2f261f8db9", size = 4930911, upload-time = "2025-07-29T18:39:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/93/ab/606be3e87dc0821bd360c8c1ee46108025c31a4f96942b63907bb441b87d/ml_dtypes-0.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:cd7c0bb22d4ff86d65ad61b5dd246812e8993fbc95b558553624c33e8b6903ea", size = 216664, upload-time = "2025-07-29T18:39:03.927Z" }, + { url = "https://files.pythonhosted.org/packages/30/a2/e900690ca47d01dffffd66375c5de8c4f8ced0f1ef809ccd3b25b3e6b8fa/ml_dtypes-0.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:9d55ea7f7baf2aed61bf1872116cefc9d0c3693b45cae3916897ee27ef4b835e", size = 160203, upload-time = "2025-07-29T18:39:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/53/21/783dfb51f40d2660afeb9bccf3612b99f6a803d980d2a09132b0f9d216ab/ml_dtypes-0.5.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:e12e29764a0e66a7a31e9b8bf1de5cc0423ea72979f45909acd4292de834ccd3", size = 689324, upload-time = "2025-07-29T18:39:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/f7/a82d249c711abf411ac027b7163f285487f5e615c3e0716c61033ce996ab/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19f6c3a4f635c2fc9e2aa7d91416bd7a3d649b48350c51f7f715a09370a90d93", size = 5275917, upload-time = "2025-07-29T18:39:09.339Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3c/541c4b30815ab90ebfbb51df15d0b4254f2f9f1e2b4907ab229300d5e6f2/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ab039ffb40f3dc0aeeeba84fd6c3452781b5e15bef72e2d10bcb33e4bbffc39", size = 5285284, upload-time = "2025-07-29T18:39:11.532Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -1659,88 +1409,94 @@ wheels = [ [[package]] name = "msgpack" -version = "1.0.7" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/d5/5662032db1571110b5b51647aed4b56dfbd01bfae789fa566a2be1f385d1/msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87", size = 166311, upload-time = "2023-09-28T13:20:36.726Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/3a/2e2e902afcd751738e38d88af976fc4010b16e8e821945f4cbf32f75f9c3/msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862", size = 304827, upload-time = "2023-09-28T13:18:30.258Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/490792a524a82e855bdf3885ecb73d7b3a0b17744b3cf4a40aea13ceca38/msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329", size = 234959, upload-time = "2023-09-28T13:18:32.146Z" }, - { url = "https://files.pythonhosted.org/packages/ad/72/d39ed43bfb2ec6968d768318477adb90c474bdc59b2437170c6697ee4115/msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b", size = 231970, upload-time = "2023-09-28T13:18:34.134Z" }, - { url = "https://files.pythonhosted.org/packages/a2/90/2d769e693654f036acfb462b54dacb3ae345699999897ca34f6bd9534fe9/msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6", size = 522440, upload-time = "2023-09-28T13:18:35.866Z" }, - { url = "https://files.pythonhosted.org/packages/46/95/d0440400485eab1bf50f1efe5118967b539f3191d994c3dfc220657594cd/msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee", size = 530797, upload-time = "2023-09-28T13:18:37.653Z" }, - { url = "https://files.pythonhosted.org/packages/76/33/35df717bc095c6e938b3c65ed117b95048abc24d1614427685123fb2f0af/msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d", size = 520372, upload-time = "2023-09-28T13:18:39.685Z" }, - { url = "https://files.pythonhosted.org/packages/af/d1/abbdd58a43827fbec5d98427a7a535c620890289b9d927154465313d6967/msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d", size = 527287, upload-time = "2023-09-28T13:18:41.051Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ac/66625b05091b97ca2c7418eb2d2af152f033d969519f9315556a4ed800fe/msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1", size = 560715, upload-time = "2023-09-28T13:18:42.883Z" }, - { url = "https://files.pythonhosted.org/packages/de/4e/a0e8611f94bac32d2c1c4ad05bb1c0ae61132e3398e0b44a93e6d7830968/msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681", size = 532614, upload-time = "2023-09-28T13:18:44.679Z" }, - { url = "https://files.pythonhosted.org/packages/9b/07/0b3f089684ca330602b2994248eda2898a7232e4b63882b9271164ef672e/msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9", size = 216340, upload-time = "2023-09-28T13:18:46.588Z" }, - { url = "https://files.pythonhosted.org/packages/4b/14/c62fbc8dff118f1558e43b9469d56a1f37bbb35febadc3163efaedd01500/msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415", size = 222828, upload-time = "2023-09-28T13:18:47.875Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b3/309de40dc7406b7f3492332c5ee2b492a593c2a9bb97ea48ebf2f5279999/msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84", size = 305096, upload-time = "2023-09-28T13:18:49.678Z" }, - { url = "https://files.pythonhosted.org/packages/15/56/a677cd761a2cefb2e3ffe7e684633294dccb161d78e8ea6da9277e45b4a2/msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93", size = 235210, upload-time = "2023-09-28T13:18:51.039Z" }, - { url = "https://files.pythonhosted.org/packages/f5/4e/1ab4a982cbd90f988e49f849fc1212f2c04a59870c59daabf8950617e2aa/msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8", size = 231952, upload-time = "2023-09-28T13:18:52.871Z" }, - { url = "https://files.pythonhosted.org/packages/6d/74/bd02044eb628c7361ad2bd8c1a6147af5c6c2bbceb77b3b1da20f4a8a9c5/msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46", size = 549511, upload-time = "2023-09-28T13:18:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/df/09/dee50913ba5cc047f7fd7162f09453a676e7935c84b3bf3a398e12108677/msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b", size = 557980, upload-time = "2023-09-28T13:18:56.058Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/78a7d87f5f8ffe4c32167afa15d4957db649bab4822f909d8d765339bbab/msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e", size = 545547, upload-time = "2023-09-28T13:18:57.396Z" }, - { url = "https://files.pythonhosted.org/packages/d4/53/698c10913947f97f6fe7faad86a34e6aa1b66cea2df6f99105856bd346d9/msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002", size = 554669, upload-time = "2023-09-28T13:18:58.957Z" }, - { url = "https://files.pythonhosted.org/packages/f5/3f/9730c6cb574b15d349b80cd8523a7df4b82058528339f952ea1c32ac8a10/msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c", size = 583353, upload-time = "2023-09-28T13:19:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/4c/bc/dc184d943692671149848438fb3bed3a3de288ce7998cb91bc98f40f201b/msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e", size = 557455, upload-time = "2023-09-28T13:19:03.201Z" }, - { url = "https://files.pythonhosted.org/packages/cf/7b/1bc69d4a56c8d2f4f2dfbe4722d40344af9a85b6fb3b09cfb350ba6a42f6/msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1", size = 216367, upload-time = "2023-09-28T13:19:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/c8dd23050eefa3d9b9c5b8329ed3308c2f2f80f65825e9ea4b7fa621cdab/msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82", size = 222860, upload-time = "2023-09-28T13:19:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/d7/47/20dff6b4512cf3575550c8801bc53fe7d540f4efef9c5c37af51760fcdcf/msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b", size = 305759, upload-time = "2023-09-28T13:19:08.148Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8a/34f1726d2c9feccec3d946776e9bce8f20ae09d8b91899fc20b296c942af/msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4", size = 235330, upload-time = "2023-09-28T13:19:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f6/e64c72577d6953789c3cb051b059a4b56317056b3c65013952338ed8a34e/msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee", size = 232537, upload-time = "2023-09-28T13:19:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/89/75/1ed3a96e12941873fd957e016cc40c0c178861a872bd45e75b9a188eb422/msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5", size = 546561, upload-time = "2023-09-28T13:19:12.779Z" }, - { url = "https://files.pythonhosted.org/packages/e5/0a/c6a1390f9c6a31da0fecbbfdb86b1cb39ad302d9e24f9cca3d9e14c364f0/msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672", size = 559009, upload-time = "2023-09-28T13:19:14.373Z" }, - { url = "https://files.pythonhosted.org/packages/a5/74/99f6077754665613ea1f37b3d91c10129f6976b7721ab4d0973023808e5a/msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075", size = 543882, upload-time = "2023-09-28T13:19:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7e/dc0dc8de2bf27743b31691149258f9b1bd4bf3c44c105df3df9b97081cd1/msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba", size = 546949, upload-time = "2023-09-28T13:19:18.114Z" }, - { url = "https://files.pythonhosted.org/packages/78/61/91bae9474def032f6c333d62889bbeda9e1554c6b123375ceeb1767efd78/msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c", size = 579836, upload-time = "2023-09-28T13:19:19.729Z" }, - { url = "https://files.pythonhosted.org/packages/5d/4d/d98592099d4f18945f89cf3e634dc0cb128bb33b1b93f85a84173d35e181/msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5", size = 556587, upload-time = "2023-09-28T13:19:21.666Z" }, - { url = "https://files.pythonhosted.org/packages/5e/44/6556ffe169bf2c0e974e2ea25fb82a7e55ebcf52a81b03a5e01820de5f84/msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9", size = 216509, upload-time = "2023-09-28T13:19:23.161Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c1/63903f30d51d165e132e5221a2a4a1bbfab7508b68131c871d70bffac78a/msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf", size = 223287, upload-time = "2023-09-28T13:19:25.097Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, + { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, + { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, + { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, + { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, - { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -1763,34 +1519,81 @@ wheels = [ [[package]] name = "numpy" -version = "1.26.4" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] @@ -1808,40 +1611,46 @@ wheels = [ [[package]] name = "onnx" -version = "1.16.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ml-dtypes" }, { name = "numpy" }, { name = "protobuf" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fe/0978403c8d710ece2f34006367e78de80410743fe0e7680c8f33f2dab20d/onnx-1.16.0.tar.gz", hash = "sha256:237c6987c6c59d9f44b6136f5819af79574f8d96a760a1fa843bede11f3822f7", size = 12303017, upload-time = "2024-03-25T15:33:46.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/2f/c619eb65769357e9b6de9212c9a821ab39cd484448e5d6b3fb5fb0a64c6d/onnx-1.19.1.tar.gz", hash = "sha256:737524d6eb3907d3499ea459c6f01c5a96278bb3a0f2ff8ae04786fb5d7f1ed5", size = 12033525, upload-time = "2025-10-10T04:01:34.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/0b/f4705e4a3fa6fd0de971302fdae17ad176b024eca8c24360f0e37c00f9df/onnx-1.16.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:9eadbdce25b19d6216f426d6d99b8bc877a65ed92cbef9707751c6669190ba4f", size = 16514483, upload-time = "2024-03-25T15:25:07.947Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1c/50310a559857951fc6e069cf5d89deebe34287997d1c5928bca435456f62/onnx-1.16.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:034ae21a2aaa2e9c14119a840d2926d213c27aad29e5e3edaa30145a745048e1", size = 15012939, upload-time = "2024-03-25T15:25:11.632Z" }, - { url = "https://files.pythonhosted.org/packages/ef/6e/96be6692ebcd8da568084d753f386ce08efa1f99b216f346ee281edd6cc3/onnx-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec22a43d74eb1f2303373e2fbe7fbcaa45fb225f4eb146edfed1356ada7a9aea", size = 15791856, upload-time = "2024-03-25T15:25:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/49/5f/d8e1a24247f506a77cbe22341c72ca91bea3b468c5d6bca2047d885ea3c6/onnx-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298f28a2b5ac09145fa958513d3d1e6b349ccf86a877dbdcccad57713fe360b3", size = 15922279, upload-time = "2024-03-25T15:25:18.939Z" }, - { url = "https://files.pythonhosted.org/packages/cb/14/562e4ac22cdf41f4465e3b114ef1a9467d513eeff0b9c2285c2da5db6ed1/onnx-1.16.0-cp310-cp310-win32.whl", hash = "sha256:66300197b52beca08bc6262d43c103289c5d45fde43fb51922ed1eb83658cf0c", size = 14335703, upload-time = "2024-03-25T15:25:22.611Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e2/471ff83b3862967791d67f630000afce038756afbdf0665a3d767677c851/onnx-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:ae0029f5e47bf70a1a62e7f88c80bca4ef39b844a89910039184221775df5e43", size = 14435099, upload-time = "2024-03-25T15:25:25.05Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/7accf3f93eee498711f0b7f07f6e93906e031622473e85ce9cd3578f6a92/onnx-1.16.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:f51179d4af3372b4f3800c558d204b592c61e4b4a18b8f61e0eea7f46211221a", size = 16514376, upload-time = "2024-03-25T15:25:27.899Z" }, - { url = "https://files.pythonhosted.org/packages/cc/24/a328236b594d5fea23f70a3a8139e730cb43334f0b24693831c47c9064f0/onnx-1.16.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:5202559070afec5144332db216c20f2fff8323cf7f6512b0ca11b215eacc5bf3", size = 15012839, upload-time = "2024-03-25T15:25:31.16Z" }, - { url = "https://files.pythonhosted.org/packages/80/12/57187bab3f830a47fa65eafe4fbaef01dfdf5042cf82a41fa440fab68766/onnx-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77579e7c15b4df39d29465b216639a5f9b74026bdd9e4b6306cd19a32dcfe67c", size = 15791944, upload-time = "2024-03-25T15:25:34.778Z" }, - { url = "https://files.pythonhosted.org/packages/df/48/63f68b65d041aedffab41eea930563ca52aab70dbaa7d4820501618c1a70/onnx-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e60ca76ac24b65c25860d0f2d2cdd96d6320d062a01dd8ce87c5743603789b8", size = 15922450, upload-time = "2024-03-25T15:25:37.983Z" }, - { url = "https://files.pythonhosted.org/packages/08/1b/4bdf4534f5ff08973725ba5409f95bbf64e2789cd20be615880dae689973/onnx-1.16.0-cp311-cp311-win32.whl", hash = "sha256:81b4ee01bc554e8a2b11ac6439882508a5377a1c6b452acd69a1eebb83571117", size = 14335808, upload-time = "2024-03-25T15:25:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d0/0514d02d2e84e7bb48a105877eae4065e54d7dabb60d0b60214fe2677346/onnx-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7449241e70b847b9c3eb8dae622df8c1b456d11032a9d7e26e0ee8a698d5bf86", size = 14434905, upload-time = "2024-03-25T15:25:42.905Z" }, - { url = "https://files.pythonhosted.org/packages/42/87/577adadda30ee08041e81ef02a331ca9d1a8df93a2e4c4c53ec56fbbc2ac/onnx-1.16.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:03a627488b1a9975d95d6a55582af3e14c7f3bb87444725b999935ddd271d352", size = 16516304, upload-time = "2024-03-25T15:25:45.875Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1b/6e1ea37e081cc49a28f0e4d3830b4c8525081354cf9f5529c6c92268fc77/onnx-1.16.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c392faeabd9283ee344ccb4b067d1fea9dfc614fa1f0de7c47589efd79e15e78", size = 15016538, upload-time = "2024-03-25T15:25:49.396Z" }, - { url = "https://files.pythonhosted.org/packages/6d/07/f8fefd5eb0984be42ef677f0b7db7527edc4529224a34a3c31f7b12ec80d/onnx-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0efeb46985de08f0efe758cb54ad3457e821a05c2eaf5ba2ccb8cd1602c08084", size = 15790415, upload-time = "2024-03-25T15:25:51.929Z" }, - { url = "https://files.pythonhosted.org/packages/11/71/c219ce6d4b5205c77405af7f2de2511ad4eeffbfeb77a422151e893de0ea/onnx-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddf14a3d32234f23e44abb73a755cb96a423fac7f004e8f046f36b10214151ee", size = 15922224, upload-time = "2024-03-25T15:25:55.049Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/554a6e5741b42406c5b1970d04685d7f2012019d4178408ed4b3ec953033/onnx-1.16.0-cp312-cp312-win32.whl", hash = "sha256:62a2e27ae8ba5fc9b4a2620301446a517b5ffaaf8566611de7a7c2160f5bcf4c", size = 14336234, upload-time = "2024-03-25T15:25:57.998Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/8aecec497010ad34e7656408df1868d94483c5c56bc991f4088c06150896/onnx-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:3e0860fea94efde777e81a6f68f65761ed5e5f3adea2e050d7fbe373a9ae05b3", size = 14436591, upload-time = "2024-03-25T15:26:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/36/07/0019c72924909e4f64b9199770630ab7b8d7914b912b03230e68f5eda7ae/onnx-1.19.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:17aaf5832126de0a5197a5864e4f09a764dd7681d3035135547959b4b6b77a09", size = 18320936, upload-time = "2025-10-10T04:00:04.235Z" }, + { url = "https://files.pythonhosted.org/packages/af/2f/5c47acf740dc35f0decc640844260fbbdc0efa0565657c93fd7ff30f13f3/onnx-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01b292a4d0b197c45d8184545bbc8ae1df83466341b604187c1b05902cb9c920", size = 18044269, upload-time = "2025-10-10T04:00:07.449Z" }, + { url = "https://files.pythonhosted.org/packages/d5/61/6c457ee8c3a62a3cad0a4bfa4c5436bb3ac4df90c3551d40bee1224b5b51/onnx-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1839af08ab4a909e4af936b8149c27f8c64b96138981024e251906e0539d8bf9", size = 18218092, upload-time = "2025-10-10T04:00:11.135Z" }, + { url = "https://files.pythonhosted.org/packages/54/d5/ab832e1369505e67926a70e9a102061f89ad01f91aa296c4b1277cb81b25/onnx-1.19.1-cp311-cp311-win32.whl", hash = "sha256:0bdbb676e3722bd32f9227c465d552689f49086f986a696419d865cb4e70b989", size = 16344809, upload-time = "2025-10-10T04:00:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b5/6eb4611d24b85002f878ba8476b4cecbe6f9784c0236a3c5eff85236cc0a/onnx-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:1346853df5c1e3ebedb2e794cf2a51e0f33759affd655524864ccbcddad7035b", size = 16464319, upload-time = "2025-10-10T04:00:18.235Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ff/f0e1f06420c70e20d497fec7c94a864d069943b6312bedd4224c0ab946f8/onnx-1.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:2d69c280c0e665b7f923f499243b9bb84fe97970b7a4668afa0032045de602c8", size = 16437503, upload-time = "2025-10-10T04:00:21.247Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/f6c5b2cffef8c29e739616d1415aea22f7b7ef1f19c17f02b7cff71f5498/onnx-1.19.1-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:3612193a89ddbce5c4e86150869b9258780a82fb8c4ca197723a4460178a6ce9", size = 18327840, upload-time = "2025-10-10T04:00:24.259Z" }, + { url = "https://files.pythonhosted.org/packages/93/20/0568ebd52730287ae80cac8ac893a7301c793ea1630984e2519ee92b02a9/onnx-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c2fd2f744e7a3880ad0c262efa2edf6d965d0bd02b8f327ec516ad4cb0f2f15", size = 18042539, upload-time = "2025-10-10T04:00:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/14/fd/cd7a0fd10a04f8cc5ae436b63e0022e236fe51b9dbb8ee6317fd48568c72/onnx-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:485d3674d50d789e0ee72fa6f6e174ab81cb14c772d594f992141bd744729d8a", size = 18218271, upload-time = "2025-10-10T04:00:30.495Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/cc8b8c05469fe08384b446304ad7e6256131ca0463bf6962366eebec98c0/onnx-1.19.1-cp312-cp312-win32.whl", hash = "sha256:638bc56ff1a5718f7441e887aeb4e450f37a81c6eac482040381b140bd9ba601", size = 16345111, upload-time = "2025-10-10T04:00:34.982Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/d1cb16693598a512c2cf9ffe0841d8d8fd2c83ae8e889efd554f5aa427cf/onnx-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:bc7e2e4e163e679721e547958b5a7db875bf822cad371b7c1304aa4401a7c7a4", size = 16465621, upload-time = "2025-10-10T04:00:39.107Z" }, + { url = "https://files.pythonhosted.org/packages/90/32/da116cc61fdef334782aa7f87a1738431dd1af1a5d1a44bd95d6d51ad260/onnx-1.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:17c215b1c0f20fe93b4cbe62668247c1d2294b9bc7f6be0ca9ced28e980c07b7", size = 16437505, upload-time = "2025-10-10T04:00:42.255Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b8/ab1fdfe2e8502f4dc4289fc893db35816bd20d080d8370f86e74dda5f598/onnx-1.19.1-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:4e5f938c68c4dffd3e19e4fd76eb98d298174eb5ebc09319cdd0ec5fe50050dc", size = 18327815, upload-time = "2025-10-10T04:00:45.682Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/eb875745a4b92aea10e5e32aa2830f409c4d7b6f7b48ca1c4eaad96636c5/onnx-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:86e20a5984b017feeef2dbf4ceff1c7c161ab9423254968dd77d3696c38691d0", size = 18041464, upload-time = "2025-10-10T04:00:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/8586135f40dbe4989cec4d413164bc8fc5c73d37c566f33f5ea3a7f2b6f6/onnx-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d9c467f0f29993c12f330736af87972f30adb8329b515f39d63a0db929cb2c", size = 18218244, upload-time = "2025-10-10T04:00:51.891Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/4201254b8683129db5da3fb55aa1f7e56d0a8d45c66ce875dec21ca1ff25/onnx-1.19.1-cp313-cp313-win32.whl", hash = "sha256:65eee353a51b4e4ca3e797784661e5376e2b209f17557e04921eac9166a8752e", size = 16345330, upload-time = "2025-10-10T04:00:54.858Z" }, + { url = "https://files.pythonhosted.org/packages/69/67/c6d239afbcdbeb6805432969b908b5c9f700c96d332b34e3f99518d76caf/onnx-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3bc87e38b53554b1fc9ef7b275c81c6f5c93c90a91935bb0aa8d4d498a6d48e", size = 16465567, upload-time = "2025-10-10T04:00:57.893Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/89f1e40f5bc54595ff0dcf5391ce19e578b528973ccc74dd99800196d30d/onnx-1.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:e41496f400afb980ec643d80d5164753a88a85234fa5c06afdeebc8b7d1ec252", size = 16437562, upload-time = "2025-10-10T04:01:00.703Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/b186ccbc8fe7e93643a6a6d40bbf2bb6ce4fb9469bbd3453c77e270c50ad/onnx-1.19.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:5f6274abf0fd74e80e78ecbb44bd44509409634525c89a9b38276c8af47dc0a2", size = 18355703, upload-time = "2025-10-10T04:01:03.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/f1/22ee4d8b8f9fa4cb1d1b9579da3b4b5187ddab33846ec5ac744af02c0e2b/onnx-1.19.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07dcd4d83584eb4bf8f21ac04c82643712e5e93ac2a0ed10121ec123cb127e1e", size = 18047830, upload-time = "2025-10-10T04:01:06.552Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/8f3d51e3a095d42cdf2039a590cff06d024f2a10efbd0b1a2a6b3825f019/onnx-1.19.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1975860c3e720db25d37f1619976582828264bdcc64fa7511c321ac4fc01add3", size = 18221126, upload-time = "2025-10-10T04:01:09.77Z" }, + { url = "https://files.pythonhosted.org/packages/4f/0d/f9d6c2237083f1aac14b37f0b03b0d81f1147a8e2af0c3828165e0a6a67b/onnx-1.19.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9807d0e181f6070ee3a6276166acdc571575d1bd522fc7e89dba16fd6e7ffed9", size = 16465560, upload-time = "2025-10-10T04:01:13.212Z" }, + { url = "https://files.pythonhosted.org/packages/36/70/8418a58faa7d606d6a92cab69ae8d361b3b3969bf7e7e9a65a86d5d1b674/onnx-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6ee83e6929d75005482d9f304c502ac7c9b8d6db153aa6b484dae74d0f28570", size = 18042812, upload-time = "2025-10-10T04:01:15.919Z" }, ] [[package]] name = "onnxruntime" -version = "1.23.2" +version = "1.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, @@ -1849,36 +1658,33 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, - { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, - { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, - { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, - { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, - { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, - { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, - { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, - { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, - { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d9757c62a0f96b5193f8d447a141eefd14498c404cc5caf1a6f3233cf102/onnxruntime-1.24.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:79b3119ab9f4f3817062e6dbe7f4a44937de93905e3a31ba34313d18cb49e7be", size = 17212018, upload-time = "2026-02-05T17:32:13.986Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/b3305c39144e19dbe8791802076b29b4b592b09de03d0e340c1314bfd408/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86bc43e922b1f581b3de26a3dc402149c70e5542fceb5bec6b3a85542dbeb164", size = 15018703, upload-time = "2026-02-05T17:30:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/d273b75fe7825ea3feed321dd540aef33d8a1380ddd8ac3bb70a8ed000fe/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cabe71ca14dcfbf812d312aab0a704507ac909c137ee6e89e4908755d0fc60e", size = 17096352, upload-time = "2026-02-05T17:31:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/0616101a3938bfe2918ea60b581a9bbba61ffc255c63388abb0885f7ce18/onnxruntime-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:3273c330f5802b64b4103e87b5bbc334c0355fff1b8935d8910b0004ce2f20c8", size = 12493235, upload-time = "2026-02-05T17:32:04.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/30/437de870e4e1c6d237a2ca5e11f54153531270cb5c745c475d6e3d5c5dcf/onnxruntime-1.24.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7307aab9e2e879c0171f37e0eb2808a5b4aec7ba899bb17c5f0cedfc301a8ac2", size = 17211043, upload-time = "2026-02-05T17:32:16.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/60/004401cd86525101ad8aa9eec301327426555d7a77fac89fd991c3c7aae6/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780add442ce2d4175fafb6f3102cdc94243acffa3ab16eacc03dd627cc7b1b54", size = 15016224, upload-time = "2026-02-05T17:30:56.791Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a1/43ad01b806a1821d1d6f98725edffcdbad54856775643718e9124a09bfbe/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6119526eda12613f0d0498e2ae59563c247c370c9cef74c2fc93133dde157", size = 17098191, upload-time = "2026-02-05T17:31:31.87Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/5beb65270864037d5c8fb25cfe6b23c48b618d1f4d06022d425cbf29bd9c/onnxruntime-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0af2f1cfcfff9094971c7eb1d1dfae7ccf81af197493c4dc4643e4342c0946", size = 12493108, upload-time = "2026-02-05T17:32:07.076Z" }, + { url = "https://files.pythonhosted.org/packages/95/77/7172ecfcbdabd92f338e694f38c325f6fab29a38fa0a8c3d1c85b9f4617c/onnxruntime-1.24.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:82e367770e8fba8a87ba9f4c04bb527e6d4d7204540f1390f202c27a3b759fb4", size = 17211381, upload-time = "2026-02-05T17:31:09.601Z" }, + { url = "https://files.pythonhosted.org/packages/79/5b/532a0d75b93bbd0da0e108b986097ebe164b84fbecfdf2ddbf7c8a3a2e83/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1099f3629832580fedf415cfce2462a56cc9ca2b560d6300c24558e2ac049134", size = 15016000, upload-time = "2026-02-05T17:31:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b5/40606c7bce0702975a077bc6668cd072cd77695fc5c0b3fcf59bdb1fe65e/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6361dda4270f3939a625670bd67ae0982a49b7f923207450e28433abc9c3a83b", size = 17097637, upload-time = "2026-02-05T17:31:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/9e8f7933796b466241b934585723c700d8fb6bde2de856e65335193d7c93/onnxruntime-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:bd1e4aefe73b6b99aa303cd72562ab6de3cccb09088100f8ad1c974be13079c7", size = 12492467, upload-time = "2026-02-05T17:32:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8a/ee07d86e35035f9fed42497af76435f5a613d4e8b6c537ea0f8ef9fa85da/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88a2b54dca00c90fca6303eedf13d49b5b4191d031372c2e85f5cffe4d86b79e", size = 15025407, upload-time = "2026-02-05T17:31:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/ab3e1dda4b126313d240e1aaa87792ddb1f5ba6d03ca2f093a7c4af8c323/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dfbba602da840615ed5b431facda4b3a43b5d8276cf9e0dbf13d842df105838", size = 17099810, upload-time = "2026-02-05T17:31:37.537Z" }, + { url = "https://files.pythonhosted.org/packages/87/23/167d964414cee2af9c72af323b28d2c4cb35beed855c830a23f198265c79/onnxruntime-1.24.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:890c503ca187bc883c3aa72c53f2a604ec8e8444bdd1bf6ac243ec6d5e085202", size = 17214004, upload-time = "2026-02-05T17:31:11.917Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/6e5558fdd51027d6830cf411bc003ae12c64054826382e2fab89e99486a0/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da1b84b3bdeec543120df169e5e62a1445bf732fc2c7fb036c2f8a4090455e8", size = 15017034, upload-time = "2026-02-05T17:31:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/3cb1c9eaae1103265ed7eb00a3eaeb0d9ba51dc88edc398b7071c9553bed/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:557753ec345efa227c6a65139f3d29c76330fcbd54cc10dd1b64232ebb939c13", size = 17097531, upload-time = "2026-02-05T17:31:40.303Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/4522b199c12db7c5b46aaf265ee0d741abe65ea912f6c0aaa2cc18a4654d/onnxruntime-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:ea4942104805e868f3ddddfa1fbb58b04503a534d489ab2d1452bbfa345c78c2", size = 12795556, upload-time = "2026-02-05T17:32:11.886Z" }, + { url = "https://files.pythonhosted.org/packages/a1/53/3b8969417276b061ff04502ccdca9db4652d397abbeb06c9f6ae05cec9ca/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea8963a99e0f10489acdf00ef3383c3232b7e44aa497b063c63be140530d9f85", size = 15025434, upload-time = "2026-02-05T17:31:06.942Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/cfcf009eb38d90cc628c087b6506b3dfe1263387f3cbbf8d272af4fef957/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34488aa760fb5c2e6d06a7ca9241124eb914a6a06f70936a14c669d1b3df9598", size = 17099815, upload-time = "2026-02-05T17:31:43.092Z" }, ] [[package]] name = "onnxruntime-gpu" -version = "1.19.2" -source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" } +version = "1.24.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, @@ -1886,20 +1692,41 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a49740e079e7c5215830d30cde3df792e903df007aa0b0fd7aa797937061b27a" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b895920bb5e4241299f68874e0becdc2635ea0142939c11e7ff5ae5b28993613" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:562fc7c755393eaad9751e56149339dd201ffbfdb3ef5f43ff21d0619ba9045f" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp311-cp311-win_amd64.whl", hash = "sha256:522f7495918176cb8c1a3c78bde7152d984f7096acc786c73a27643af8af87c9" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:554a02a3fac0119707eb87327908afd21c4e6f0fa5bf9a034398f098adc316c5" }, - { url = "https://aiinfra.pkgs.visualstudio.com/2692857e-05ef-43b4-ba9c-ccf1c22c437c/_packaging/9387c3aa-d9ad-4513-968c-383f6f7f53b8/pypi/download/onnxruntime-gpu/1.19.2/onnxruntime_gpu-1.19.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c6165a405027e3c0f11d189ae7013b5d66919b3381f9bfb3405c0c0cf07968" }, + { url = "https://files.pythonhosted.org/packages/ca/c7/07d06175f1124fc89e8b7da30d70eb8e0e1400d90961ae1cbea9da69e69b/onnxruntime_gpu-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac4bfc90c376516b13d709764ab257e4e3d78639bf6a2ccfc826e9db4a5c7ddf", size = 252616647, upload-time = "2026-02-05T17:24:02.993Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/47c2a873bf5fc307cda696e8a8cb54b7c709f5a4b3f9e2b4a636066a63c2/onnxruntime_gpu-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:ccd800875cb6c04ce623154c7fa312da21631ef89a9543c9a21593817cfa3473", size = 207089749, upload-time = "2026-02-05T17:23:59.5Z" }, + { url = "https://files.pythonhosted.org/packages/db/a8/fb1a36a052321a839cc9973f6cfd630709412a24afff2d7315feb3efc4b8/onnxruntime_gpu-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:710bf83751e6761584ad071102af3cbffd4b42bb77b2e3caacfb54ffbaa0666b", size = 252628733, upload-time = "2026-02-05T17:24:12.926Z" }, + { url = "https://files.pythonhosted.org/packages/52/65/48f694b81a963f3ee575041d5f2879b15268f5e7e14d90c3e671836c9646/onnxruntime_gpu-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:b128a42b3fa098647765ba60c2af9d4bf839181307cfac27da649364feb37f7b", size = 207089008, upload-time = "2026-02-05T17:24:07.126Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e7/4e19062e95d3701c0d32c228aa848ba4a1cc97651e53628d978dba8e1267/onnxruntime_gpu-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db9acb0d0e59d93b4fa6b7fd44284ece4408d0acee73235d43ed343f8cee7ee5", size = 252629216, upload-time = "2026-02-05T17:24:24.604Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/223d7120d8a98b07c104ddecfb0cc2536188e566a4e9c2dee7572453f89c/onnxruntime_gpu-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:59fdb40743f0722f3b859209f649ea160ca6bb42799e43f49b70a3ec5fc8c4ad", size = 207089285, upload-time = "2026-02-05T17:24:18.497Z" }, + { url = "https://files.pythonhosted.org/packages/ac/82/3159e57f09d7e6c8ad47d8ba8d5bd7494f383bc1071481cf38c9c8142bf9/onnxruntime_gpu-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88ca04e1dffea2d4c3c79cf4de7f429e99059d085f21b3e775a8d36380cd5186", size = 252633977, upload-time = "2026-02-05T17:24:33.568Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b4/51ad0ab878ff1456a831a0566b4db982a904e22f138e4b2c5f021bac517f/onnxruntime_gpu-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ced66900b1f48bddb62b5233925c3b56f8e008e2c34ebf8c060b20cae5842bcf", size = 252629039, upload-time = "2026-02-05T17:24:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/46/336d4e09a6af66532eedde5c8f03a73eaa91a046b408522259ab6a604363/onnxruntime_gpu-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:129f6ae8b331a6507759597cd317b23e94aed6ead1da951f803c3328f2990b0c", size = 209487551, upload-time = "2026-02-05T17:24:26.373Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" }, +] + +[[package]] +name = "onnxruntime-migraphx" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/da/ca7ebc1a8d1193c97ceb9a05fad50f675eb955dc51beb7eb9ba89c8e7db0/onnxruntime_migraphx-1.24.2-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:a2b434fb8880cac2b268950bdf279f33741d29c1f1c5461d27af835e8e288043", size = 20339710, upload-time = "2026-02-21T07:25:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/8c83ec45a9365b4256495ca55eea30da7f03b02177b6da423c7da1ff5f6a/onnxruntime_migraphx-1.24.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ec814818da952bda3062e26f56c88bb713c00491ef91f86716c8d7346f9bc31b", size = 20341883, upload-time = "2026-02-21T07:25:17.86Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/4776ac68dbc46ca02c9a14cc9e5c496017f47a18cedf606cc38f4911b96a/onnxruntime_migraphx-1.24.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:20e497538362170af639b03a40249d7ed61b873ac354f20d732b90252206e320", size = 20342422, upload-time = "2026-02-21T07:25:22.526Z" }, + { url = "https://files.pythonhosted.org/packages/76/44/db9035204a3363f9c0a4822c68e9a7520c13ef8d261f96b89b1375106dab/onnxruntime_migraphx-1.24.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:9d7f1b1a2b9651143a2080b4f42ee99eead02023de1855d1b8a02199a9c179aa", size = 20343783, upload-time = "2026-02-21T07:25:29.155Z" }, ] [[package]] name = "onnxruntime-openvino" -version = "1.18.0" +version = "1.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, @@ -1907,10 +1734,12 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/57/e9a080f2477b2a4c16925f766e4615fc545098b0f4e20cf8ad803e7a9672/onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331", size = 41971800, upload-time = "2024-06-25T06:30:37.042Z" }, - { url = "https://files.pythonhosted.org/packages/34/7d/b75913bce58f4ee9bf6a02d1b513b9fc82303a496ec698e6fb1f9d597cb4/onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75", size = 5963263, upload-time = "2024-06-24T13:38:15.906Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d3/8299b7285dc8fa7bd986b6f0d7c50b7f0fd13db50dd3b88b93ec269b1e08/onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba", size = 41971927, upload-time = "2024-06-25T06:30:43.765Z" }, - { url = "https://files.pythonhosted.org/packages/88/d9/ca0bfd7ed37153d9664ccdcfb4d0e5b1963563553b05cb4338b46968feb2/onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c", size = 5963464, upload-time = "2024-06-24T13:38:18.437Z" }, + { url = "https://files.pythonhosted.org/packages/99/16/69ca742f0b65c40d4de3ff44bb6abc23c47b23e932bc901116176ae69922/onnxruntime_openvino-1.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3007c803634cc69c6d52af1dea7ce729d9bb62b9a11070fd2f959119199007a8", size = 84430935, upload-time = "2026-02-26T13:44:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/619bb416bbfc40aebdd493fd6800d2637359294fe683d8a6bae3ff8d869a/onnxruntime_openvino-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:8042698232bf67f1f6b219c2b07728d7ae7ddff17d8524588de3675480609aef", size = 13655357, upload-time = "2026-02-26T13:44:35.555Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/17ba72de2df0fcba349937d2788f154397bbc2d1a2d67772a97e26f6bc5f/onnxruntime_openvino-1.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d617fac2f59a6ab5ea59a788c3e1592240a129642519aaeaa774761dfe35150e", size = 84433207, upload-time = "2026-02-26T13:44:41.395Z" }, + { url = "https://files.pythonhosted.org/packages/59/37/d301f2c68b19a9485ed5db3047e0fb52478f3e73eb08c7d2a7c61be7cc1c/onnxruntime_openvino-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:f186335a9c9b255633275290da7521d3d4d14c7773fee3127bfa040234d3fa5a", size = 13658075, upload-time = "2026-02-26T13:44:44.905Z" }, + { url = "https://files.pythonhosted.org/packages/08/07/f225999919f56506b603aaa3ff837ad563ab26f86906ed7fa7e5abcd849e/onnxruntime_openvino-1.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c3bb73e68ac27f4891af8a595c1faf574ec68b772e6583c90a0b997a1822782", size = 84433183, upload-time = "2026-02-26T13:44:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/3e/92/46ae2cd565961a89189900f385bb2f13a9fa731ea4674001d23720fbb1e0/onnxruntime_openvino-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:434bf49aa71393c577a456c9d76c98e6d6958a833fa0876793e3d5437b5a511a", size = 13658485, upload-time = "2026-02-26T13:44:53.889Z" }, ] [[package]] @@ -1932,100 +1761,88 @@ wheels = [ [[package]] name = "opencv-python-headless" -version = "4.11.0.86" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/53/2c50afa0b1e05ecdb4603818e85f7d174e683d874ef63a6abe3ac92220c8/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca", size = 37326460, upload-time = "2025-01-16T13:52:57.015Z" }, - { url = "https://files.pythonhosted.org/packages/3b/43/68555327df94bb9b59a1fd645f63fafb0762515344d2046698762fc19d58/opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81", size = 56723330, upload-time = "2025-01-16T13:55:45.731Z" }, - { url = "https://files.pythonhosted.org/packages/45/be/1438ce43ebe65317344a87e4b150865c5585f4c0db880a34cdae5ac46881/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb", size = 29487060, upload-time = "2025-01-16T13:51:59.625Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5c/c139a7876099916879609372bfa513b7f1257f7f1a908b0bdc1c2328241b/opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b", size = 49969856, upload-time = "2025-01-16T13:53:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/95/dd/ed1191c9dc91abcc9f752b499b7928aacabf10567bb2c2535944d848af18/opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b", size = 29324425, upload-time = "2025-01-16T13:52:49.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/8a/69176a64335aed183529207ba8bc3d329c2999d852b4f3818027203f50e6/opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca", size = 39402386, upload-time = "2025-01-16T13:52:56.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, ] [[package]] name = "orjson" -version = "3.11.4" +version = "3.11.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/30/5aed63d5af1c8b02fbd2a8d83e2a6c8455e30504c50dbf08c8b51403d873/orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", size = 243870, upload-time = "2025-10-24T15:48:28.908Z" }, - { url = "https://files.pythonhosted.org/packages/44/1f/da46563c08bef33c41fd63c660abcd2184b4d2b950c8686317d03b9f5f0c/orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", size = 130622, upload-time = "2025-10-24T15:48:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/02/bd/b551a05d0090eab0bf8008a13a14edc0f3c3e0236aa6f5b697760dd2817b/orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", size = 129344, upload-time = "2025-10-24T15:48:32.71Z" }, - { url = "https://files.pythonhosted.org/packages/87/6c/9ddd5e609f443b2548c5e7df3c44d0e86df2c68587a0e20c50018cdec535/orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", size = 136633, upload-time = "2025-10-24T15:48:34.128Z" }, - { url = "https://files.pythonhosted.org/packages/95/f2/9f04f2874c625a9fb60f6918c33542320661255323c272e66f7dcce14df2/orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", size = 137695, upload-time = "2025-10-24T15:48:35.654Z" }, - { url = "https://files.pythonhosted.org/packages/d2/c2/c7302afcbdfe8a891baae0e2cee091583a30e6fa613e8bdf33b0e9c8a8c7/orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", size = 136879, upload-time = "2025-10-24T15:48:37.483Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3a/b31c8f0182a3e27f48e703f46e61bb769666cd0dac4700a73912d07a1417/orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff", size = 136374, upload-time = "2025-10-24T15:48:38.624Z" }, - { url = "https://files.pythonhosted.org/packages/29/d0/fd9ab96841b090d281c46df566b7f97bc6c8cd9aff3f3ebe99755895c406/orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", size = 140519, upload-time = "2025-10-24T15:48:39.756Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ce/36eb0f15978bb88e33a3480e1a3fb891caa0f189ba61ce7713e0ccdadabf/orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", size = 406522, upload-time = "2025-10-24T15:48:41.198Z" }, - { url = "https://files.pythonhosted.org/packages/85/11/e8af3161a288f5c6a00c188fc729c7ba193b0cbc07309a1a29c004347c30/orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", size = 149790, upload-time = "2025-10-24T15:48:42.664Z" }, - { url = "https://files.pythonhosted.org/packages/ea/96/209d52db0cf1e10ed48d8c194841e383e23c2ced5a2ee766649fe0e32d02/orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", size = 140040, upload-time = "2025-10-24T15:48:44.042Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0e/526db1395ccb74c3d59ac1660b9a325017096dc5643086b38f27662b4add/orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", size = 135955, upload-time = "2025-10-24T15:48:45.495Z" }, - { url = "https://files.pythonhosted.org/packages/e6/69/18a778c9de3702b19880e73c9866b91cc85f904b885d816ba1ab318b223c/orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", size = 131577, upload-time = "2025-10-24T15:48:46.609Z" }, - { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498, upload-time = "2025-10-24T15:48:48.101Z" }, - { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961, upload-time = "2025-10-24T15:48:49.571Z" }, - { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321, upload-time = "2025-10-24T15:48:50.713Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207, upload-time = "2025-10-24T15:48:52.193Z" }, - { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323, upload-time = "2025-10-24T15:48:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440, upload-time = "2025-10-24T15:48:56.326Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680, upload-time = "2025-10-24T15:48:57.476Z" }, - { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160, upload-time = "2025-10-24T15:48:59.631Z" }, - { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318, upload-time = "2025-10-24T15:49:00.834Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330, upload-time = "2025-10-24T15:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580, upload-time = "2025-10-24T15:49:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846, upload-time = "2025-10-24T15:49:04.761Z" }, - { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781, upload-time = "2025-10-24T15:49:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391, upload-time = "2025-10-24T15:49:07.355Z" }, - { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" }, - { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" }, - { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" }, - { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" }, - { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" }, - { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" }, - { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" }, - { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" }, - { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" }, - { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" }, - { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" }, - { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" }, - { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" }, - { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" }, - { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" }, - { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" }, - { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" }, - { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" }, - { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" }, + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] [[package]] @@ -2048,70 +1865,89 @@ wheels = [ [[package]] name = "pillow" -version = "10.4.0" +version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, - { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, - { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, - { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, - { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, - { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, - { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, - { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, - { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, - { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, - { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, - { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, - { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, - { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, - { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, - { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, - { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, - { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, - { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] @@ -2137,16 +1973,17 @@ wheels = [ [[package]] name = "protobuf" -version = "4.25.2" +version = "6.33.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/a5/05ea470f4e793c9408bc975ce1c6957447e3134ce7f7a58c13be8b2c216f/protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e", size = 380282, upload-time = "2024-01-10T19:37:42.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/2f/01f63896ddf22cbb0173ab51f54fde70b0208ca6c2f5e8416950977930e1/protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6", size = 392408, upload-time = "2024-01-10T19:37:23.466Z" }, - { url = "https://files.pythonhosted.org/packages/c1/00/c3ae19cabb36cfabc94ff0b102aac21b471c9f91a1357f8aafffb9efe8e0/protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9", size = 413397, upload-time = "2024-01-10T19:37:26.321Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/0017aefacf23273d4efd1154ef958a27eed9c177c4cc09d2d4ba398fb47f/protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d", size = 394159, upload-time = "2024-01-10T19:37:28.932Z" }, - { url = "https://files.pythonhosted.org/packages/23/17/405ba44f60a693dfe96c7a18e843707cffa0fcfad80bd8fc4f227f499ea5/protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62", size = 293698, upload-time = "2024-01-10T19:37:30.666Z" }, - { url = "https://files.pythonhosted.org/packages/81/9e/63501b8d5b4e40c7260049836bd15ec3270c936e83bc57b85e4603cc212c/protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020", size = 294609, upload-time = "2024-01-10T19:37:32.777Z" }, - { url = "https://files.pythonhosted.org/packages/ff/52/5d23df1fe3b368133ec3e2436fb3dd4ccedf44c8d5ac7f4a88087c75180b/protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830", size = 156463, upload-time = "2024-01-10T19:37:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, ] [[package]] @@ -2169,12 +2006,6 @@ version = "1.3.0.post6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4a/b2/550fe500e49c464d73fabcb8cb04d47e4885d6ca4cfc1f5b0a125a95b19a/pyclipper-1.3.0.post6.tar.gz", hash = "sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c", size = 165909, upload-time = "2024-10-18T12:23:09.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/34/0dca299fe41e9a92e78735502fed5238a4ac734755e624488df9b2eeec46/pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa0f5e78cfa8262277bb3d0225537b3c2a90ef68fd90a229d5d24cf49955dcf4", size = 269504, upload-time = "2024-10-18T12:21:55.735Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5b/81528b08134b3c2abdfae821e1eff975c0703802d41974b02dfb2e101c55/pyclipper-1.3.0.post6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a01f182d8938c1dc515e8508ed2442f7eebd2c25c7d5cb29281f583c1a8008a4", size = 142599, upload-time = "2024-10-18T12:21:57.401Z" }, - { url = "https://files.pythonhosted.org/packages/84/a4/3e304f6c0d000382cd54d4a1e5f0d8fc28e1ae97413a2ec1016a7b840319/pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:640f20975727994d4abacd07396f564e9e5665ba5cb66ceb36b300c281f84fa4", size = 912209, upload-time = "2024-10-18T12:21:59.408Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6a/28ec55cc3f972368b211fca017e081cf5a71009d1b8ec3559767cda5b289/pyclipper-1.3.0.post6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63002f6bb0f1efa87c0b81634cbb571066f237067e23707dabf746306c92ba5", size = 929511, upload-time = "2024-10-18T12:22:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c4/56/c326f3454c5f30a31f58a5c3154d891fce58ad73ccbf1d3f4aacfcbd344d/pyclipper-1.3.0.post6-cp310-cp310-win32.whl", hash = "sha256:106b8622cd9fb07d80cbf9b1d752334c55839203bae962376a8c59087788af26", size = 100126, upload-time = "2024-10-18T12:22:02.83Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e6/f8239af6346848b20a3448c554782fe59298ab06c1d040490242dc7e3c26/pyclipper-1.3.0.post6-cp310-cp310-win_amd64.whl", hash = "sha256:9699e98862dadefd0bea2360c31fa61ca553c660cbf6fb44993acde1b959f58f", size = 110470, upload-time = "2024-10-18T12:22:04.411Z" }, { url = "https://files.pythonhosted.org/packages/50/a9/66ca5f252dcac93ca076698591b838ba17f9729591edf4b74fef7fbe1414/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef", size = 270930, upload-time = "2024-10-18T12:22:06.066Z" }, { url = "https://files.pythonhosted.org/packages/59/fe/2ab5818b3504e179086e54a37ecc245525d069267b8c31b18ec3d0830cbf/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa", size = 143411, upload-time = "2024-10-18T12:22:07.598Z" }, { url = "https://files.pythonhosted.org/packages/09/f7/b58794f643e033a6d14da7c70f517315c3072f3c5fccdf4232fa8c8090c1/pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0", size = 951754, upload-time = "2024-10-18T12:22:08.966Z" }, @@ -2228,19 +2059,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, @@ -2319,14 +2137,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, @@ -2369,31 +2179,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload-time = "2023-07-30T15:06:59.829Z" }, ] -[[package]] -name = "pyreadline3" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465, upload-time = "2022-01-24T20:05:11.66Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203, upload-time = "2022-01-24T20:05:10.442Z" }, -] - [[package]] name = "pytest" -version = "9.0.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -2401,7 +2200,6 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -2471,24 +2269,24 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] name = "python-socketio" -version = "5.15.0" +version = "5.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bidict" }, { name = "python-engineio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/a8/5f7c805dd6d0d6cba91d3ea215b4b88889d1b99b71a53c932629daba53f1/python_socketio-5.15.0.tar.gz", hash = "sha256:d0403ababb59aa12fd5adcfc933a821113f27bd77761bc1c54aad2e3191a9b69", size = 126439, upload-time = "2025-11-22T18:50:21.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/55/5d8af5884283b58e4405580bcd84af1d898c457173c708736e065f10ca4a/python_socketio-5.16.0.tar.gz", hash = "sha256:f79403c7f1ba8b84460aa8fe4c671414c8145b21a501b46b676f3740286356fd", size = 127120, upload-time = "2025-12-24T23:51:48.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/fa/1ef2f8537272a2f383d72b9301c3ef66a49710b3bb7dcb2bd138cf2920d1/python_socketio-5.15.0-py3-none-any.whl", hash = "sha256:e93363102f4da6d8e7a8872bf4908b866c40f070e716aa27132891e643e2687c", size = 79451, upload-time = "2025-11-22T18:50:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/28/d2/2ccc2b69a187b80fda3152745670cfba936704f296a9fa54c6c8ac694d12/python_socketio-5.16.0-py3-none-any.whl", hash = "sha256:d95802961e15c7bd54ecf884c6e7644f81be8460f0a02ee66b473df58088ee8a", size = 79607, upload-time = "2025-12-24T23:51:47.2Z" }, ] [package.optional-dependencies] @@ -2497,28 +2295,23 @@ client = [ { name = "websocket-client" }, ] -[[package]] -name = "pytokens" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, -] - [[package]] name = "pywin32" -version = "306" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/dc/28c668097edfaf4eac4617ef7adf081b9cf50d254672fcf399a70f5efc41/pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d", size = 8506422, upload-time = "2023-03-26T03:27:46.303Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d6/891894edec688e72c2e308b3243fad98b4066e1839fd2fe78f04129a9d31/pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8", size = 9226392, upload-time = "2023-03-26T03:27:53.591Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689, upload-time = "2023-03-25T23:50:08.499Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547, upload-time = "2023-03-25T23:50:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324, upload-time = "2023-03-25T23:50:30.904Z" }, - { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705, upload-time = "2023-03-25T23:50:40.279Z" }, - { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429, upload-time = "2023-03-25T23:50:50.222Z" }, - { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145, upload-time = "2023-03-25T23:51:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] @@ -2527,15 +2320,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, @@ -2567,51 +2351,60 @@ wheels = [ [[package]] name = "pyzmq" -version = "25.1.2" +version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/33/1a3683fc9a4bd64d8ccc0290da75c8f042184a1a49c146d28398414d3341/pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226", size = 1402339, upload-time = "2023-12-05T07:34:47.976Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f4/901edb48b2b2c00ad73de0db2ee76e24ce5903ef815ad0ad10e14555d989/pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4", size = 1872310, upload-time = "2023-12-05T07:48:13.713Z" }, - { url = "https://files.pythonhosted.org/packages/5e/46/2de69c7c79fd78bf4c22a9e8165fa6312f5d49410f1be6ddab51a6fe7236/pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0", size = 1249619, upload-time = "2023-12-05T07:50:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f5/d6b9755713843bf9701ae86bf6fd97ec294a52cf2af719cd14fdf9392f65/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e", size = 897360, upload-time = "2023-12-05T07:42:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/7c/88/c1aef8820f12e710d136024d231e70e24684a01314aa1814f0758960ba01/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872", size = 1156959, upload-time = "2023-12-05T07:44:29.904Z" }, - { url = "https://files.pythonhosted.org/packages/82/1b/b25d2c4ac3b4dae238c98e63395dbb88daf11968b168948d3c6289c3e95c/pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d", size = 1100585, upload-time = "2023-12-05T07:45:05.518Z" }, - { url = "https://files.pythonhosted.org/packages/67/bf/6bc0977acd934b66eacab79cec303ecf08ae4a6150d57c628aa919615488/pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75", size = 1109267, upload-time = "2023-12-05T07:39:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/64/fb/4f07424e56c6a5fb47306d9ba744c3c250250c2e7272f9c81efbf8daaccf/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6", size = 1431853, upload-time = "2023-12-05T07:41:09.261Z" }, - { url = "https://files.pythonhosted.org/packages/a2/10/2b88c1d4beb59a1d45c13983c4b7c5dcd6ef7988db3c03d23b0cabc5adca/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979", size = 1766212, upload-time = "2023-12-05T07:49:05.926Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ab/c9a22eacfd5bd82620501ae426a3dd6ffa374ac335b21e54209d7a93d3fb/pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08", size = 1653737, upload-time = "2023-12-05T07:49:09.096Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e5/71bd89e47eedb7ebec31ef9a49dcdb0517dbbb063bd5de363980a6911eb1/pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886", size = 906288, upload-time = "2023-12-05T07:42:05.509Z" }, - { url = "https://files.pythonhosted.org/packages/9d/5f/2defc8a579e8b5679d92720ab3a4cb93e3a77d923070bf4c1a103d3ae478/pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6", size = 1170923, upload-time = "2023-12-05T07:44:54.296Z" }, - { url = "https://files.pythonhosted.org/packages/35/de/7579518bc58cebf92568b48e354a702fb52525d0fab166dc544f2a0615dc/pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c", size = 1870360, upload-time = "2023-12-05T07:48:16.153Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f9/58b6cc9a110b1832f666fa6b5a67dc4d520fabfc680ca87a8167b2061d5d/pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1", size = 1249008, upload-time = "2023-12-05T07:50:40.442Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4a/ac6469c01813cb3652ab4e30ec4a37815cc9949afc18af33f64e2ec704aa/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348", size = 904394, upload-time = "2023-12-05T07:42:27.815Z" }, - { url = "https://files.pythonhosted.org/packages/77/b7/8cee519b11bdd3f76c1a6eb537ab13c1bfef2964d725717705c86f524e4c/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642", size = 1161453, upload-time = "2023-12-05T07:44:32.003Z" }, - { url = "https://files.pythonhosted.org/packages/b6/1d/c35a956a44b333b064ae1b1c588c2dfa0e01b7ec90884c1972bfcef119c3/pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840", size = 1105501, upload-time = "2023-12-05T07:45:07.18Z" }, - { url = "https://files.pythonhosted.org/packages/18/d1/b3d1e985318ed7287737ea9e6b6e21748cc7c89accc2443347cd2c8d5f0f/pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d", size = 1109513, upload-time = "2023-12-05T07:39:53.338Z" }, - { url = "https://files.pythonhosted.org/packages/14/9b/341cdfb47440069010101403298dc24d449150370c6cb322e73bfa1949bd/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b", size = 1433541, upload-time = "2023-12-05T07:41:10.786Z" }, - { url = "https://files.pythonhosted.org/packages/fa/52/c6d4e76e020c554e965459d41a98201b4d45277a288648f53a4e5a2429cc/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b", size = 1766133, upload-time = "2023-12-05T07:49:11.204Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6d/0cbd8dd5b8979fd6b9cf1852ed067b9d2cd6fa0c09c3bafe6874d2d2e03c/pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3", size = 1653636, upload-time = "2023-12-05T07:49:13.787Z" }, - { url = "https://files.pythonhosted.org/packages/f5/af/d90eed9cf3840685d54d4a35d5f9e242a8a48b5410d41146f14c1e098302/pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097", size = 904865, upload-time = "2023-12-05T07:42:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/20/d2/09443dc73053ad01c846d7fb77e09fe9d93c09d4e900215f3c8b7b56bfec/pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9", size = 1171332, upload-time = "2023-12-05T07:44:56.111Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f0/d71cf69dc039c9adc8b625efc3bad3684f3660a570e47f0f0c64df787b41/pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a", size = 1871111, upload-time = "2023-12-05T07:48:17.868Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/d365773edf56ad71993579ee574105f02f83530caf600ebf28bea15d88d0/pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e", size = 1248844, upload-time = "2023-12-05T07:50:42.922Z" }, - { url = "https://files.pythonhosted.org/packages/72/55/cc3163e20f40615a49245fa7041badec6103e8ee7e482dbb0feea00a7b84/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27", size = 899373, upload-time = "2023-12-05T07:42:29.595Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/ae292bd85deda637230970bbc53c1dc53696a99e82fc7cd6d373ec173853/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30", size = 1160901, upload-time = "2023-12-05T07:44:33.819Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/6e291eafbbbc66d0e87658dd21383ec2b4ab35edcfb283902c580a6db76f/pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee", size = 1101147, upload-time = "2023-12-05T07:45:10.058Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f1/e296d5a507eac519d1fe1382851b1a4575f690bc2b2d2c8eca2ed7e4bd1f/pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537", size = 1105315, upload-time = "2023-12-05T07:39:55.851Z" }, - { url = "https://files.pythonhosted.org/packages/56/63/5c2abb556ab4cf013d98e01782d5bd642238a0ed9b019e965a7d7e957f56/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181", size = 1427747, upload-time = "2023-12-05T07:41:13.219Z" }, - { url = "https://files.pythonhosted.org/packages/b1/71/5dba5f6b12ef54fb977c9b9279075e151c04fc0dd6851e9663d9e66b593f/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe", size = 1762221, upload-time = "2023-12-05T07:49:16.352Z" }, - { url = "https://files.pythonhosted.org/packages/cf/49/54d7e8bb3df82a3509325b11491d33450dc91580d4826b62fa5e554bb9cf/pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737", size = 1649505, upload-time = "2023-12-05T07:49:18.952Z" }, - { url = "https://files.pythonhosted.org/packages/34/14/58e5037229bc37963e2ce804c2c075a3a541e3f84bf1c231e7c9779d36f1/pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d", size = 954891, upload-time = "2023-12-05T07:42:09.208Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2d/04fab685ef3a8e6e955220fd2a54dc99efaee960a88675bf5c92cd277164/pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7", size = 1252773, upload-time = "2023-12-05T07:44:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fe/ed38fe12c540bafc1cae32c3ff638e9df32528f5cf91b5e400e6a8f5b3ec/pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05", size = 963654, upload-time = "2023-12-05T07:47:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/44/97/a760a2dff0672c408f22f726f2ea10a7a516ffa5001ca5a3641e355a45f9/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8", size = 609436, upload-time = "2023-12-05T07:42:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/41/81/ace39daa19c78b2f4fc12ef217d9d5f1ac658d5828d692bbbb68240cd55b/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e", size = 843396, upload-time = "2023-12-05T07:44:43.727Z" }, - { url = "https://files.pythonhosted.org/packages/4c/43/150b0b203f5461a9aeadaa925c55167e2b4215c9322b6911a64360d2243e/pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4", size = 800856, upload-time = "2023-12-05T07:45:21.117Z" }, - { url = "https://files.pythonhosted.org/packages/5f/91/a618b56aaabe40dddcd25db85624d7408768fd32f5bfcf81bc0af5b1ce75/pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d", size = 413836, upload-time = "2023-12-05T07:53:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] [[package]] @@ -2621,8 +2414,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "opencv-python-headless" }, - { name = "scikit-learn", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scikit-learn" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/2d/bab8babd9dc9a9e4df6eb115540cee4322c1a74078fb6f3b3ebc452a22b3/qudida-0.0.4.tar.gz", hash = "sha256:db198e2887ab0c9aa0023e565afbff41dfb76b361f85fd5e13f780d75ba18cc8", size = 3100, upload-time = "2021-08-09T16:47:55.807Z" } @@ -2632,7 +2424,7 @@ wheels = [ [[package]] name = "rapidocr" -version = "3.4.2" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, @@ -2648,12 +2440,12 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/83/5b8c8075954c5b61d938b8954710d986134c4ca7c32a841ad7d8c844cf6c/rapidocr-3.4.2-py3-none-any.whl", hash = "sha256:17845fa8cc9a20a935111e59482f2214598bba1547000cfd960d8924dd4522a5", size = 15056674, upload-time = "2025-10-11T14:43:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/0d025466f0f84552634f2a94c018df34568fe55cc97184a6bb2c719c5b3a/rapidocr-3.6.0-py3-none-any.whl", hash = "sha256:d16b43872fc4dfa1e60996334dcd0dc3e3f1f64161e2332bc1873b9f65754e6b", size = 15067340, upload-time = "2026-01-28T14:45:04.271Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2661,22 +2453,22 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "rich" -version = "14.2.0" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] @@ -2689,7 +2481,6 @@ dependencies = [ { name = "ruamel-yaml" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/db/76b40afe343f8a8c5222300da425e0dace30ce639a94776468b1d157311b/rknn_toolkit_lite2-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:821e80c95e6838308c133915660b1a6ae78bb8d079b2cbbd46a02dae61192d33", size = 559386, upload-time = "2025-04-09T09:39:54.414Z" }, { url = "https://files.pythonhosted.org/packages/c1/3d/e80e1742420f62cb628d40a8bf547d6f7c9dbe4e13dcb7b7e7c0b5620e74/rknn_toolkit_lite2-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda74f1179e15fccb8726054a24898982522784b65bb340b20146955d254e800", size = 569160, upload-time = "2025-04-09T09:39:56.149Z" }, { url = "https://files.pythonhosted.org/packages/ff/db/64c756f3f06b219e92ff4f0fd4e000870ee49f214d505ff01c8b0275e26d/rknn_toolkit_lite2-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1e4ec691fed900c0e6fde5e7d8eeba17f806aa45092b63b361ee775e2c1b50e", size = 527458, upload-time = "2025-04-09T09:39:58.881Z" }, ] @@ -2712,15 +2503,6 @@ version = "0.2.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301, upload-time = "2024-10-20T10:12:35.876Z" }, - { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728, upload-time = "2024-10-20T10:12:37.858Z" }, - { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230, upload-time = "2024-10-20T10:12:39.457Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712, upload-time = "2024-10-20T10:12:41.119Z" }, - { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936, upload-time = "2024-10-21T11:26:37.419Z" }, - { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580, upload-time = "2024-10-21T11:26:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393, upload-time = "2024-12-11T19:58:13.873Z" }, - { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326, upload-time = "2024-10-20T10:12:42.967Z" }, - { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079, upload-time = "2024-10-20T10:12:44.117Z" }, { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, @@ -2752,33 +2534,32 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.6" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, - { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, - { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, - { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, - { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, - { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, - { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, - { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, - { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, - { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, - { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] [[package]] name = "scikit-image" -version = "0.22.0" +version = "0.25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "imageio" }, @@ -2787,94 +2568,41 @@ dependencies = [ { name = "numpy" }, { name = "packaging" }, { name = "pillow" }, - { name = "scipy", version = "1.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, { name = "tifffile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/c1/a49da20845f0f0e1afbb1c2586d406dc0acb84c26ae293bad6d7e7f718bc/scikit_image-0.22.0.tar.gz", hash = "sha256:018d734df1d2da2719087d15f679d19285fce97cd37695103deadfaef2873236", size = 22685018, upload-time = "2023-10-03T21:36:34.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/8c/381ae42b37cf3e9e99a1deb3ffe76ca5ff5dd18ffa368293476164507fad/scikit_image-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74ec5c1d4693506842cc7c9487c89d8fc32aed064e9363def7af08b8f8cbb31d", size = 13905039, upload-time = "2023-10-03T21:35:27.279Z" }, - { url = "https://files.pythonhosted.org/packages/16/06/4bfba08f5cce26d5070bb2cf4e3f9f479480978806355d1c5bea6f26a17c/scikit_image-0.22.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:a05ae4fe03d802587ed8974e900b943275548cde6a6807b785039d63e9a7a5ff", size = 13279212, upload-time = "2023-10-03T21:35:30.864Z" }, - { url = "https://files.pythonhosted.org/packages/74/57/dbf744ca00eea2a09b1848c9dec28a43978c16dc049b1fba949cb050bedf/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a92dca3d95b1301442af055e196a54b5a5128c6768b79fc0a4098f1d662dee6", size = 14091779, upload-time = "2023-10-03T21:35:34.273Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/49f5a0ce8ddcdbdac5ac69c129654938cc6de0a936303caa6cad495ceb2a/scikit_image-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3663d063d8bf2fb9bdfb0ca967b9ee3b6593139c860c7abc2d2351a8a8863938", size = 14682042, upload-time = "2023-10-03T21:35:37.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/f0/18895318109f9b508f2310f136922e455a453550826a8240b412063c2528/scikit_image-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebdbdc901bae14dab637f8d5c99f6d5cc7aaf4a3b6f4003194e003e9f688a6fc", size = 24492345, upload-time = "2023-10-03T21:35:41.122Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d9/dc99e527d1a0050f0353d2fff3548273b4df6151884806e324f26572fd6b/scikit_image-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95d6da2d8a44a36ae04437c76d32deb4e3c993ffc846b394b9949fd8ded73cb2", size = 13883619, upload-time = "2023-10-03T21:35:44.88Z" }, - { url = "https://files.pythonhosted.org/packages/80/37/7670020b112ff9a47e49b1e36f438d000db5b632aab8a8fd7e6be545d065/scikit_image-0.22.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2c6ef454a85f569659b813ac2a93948022b0298516b757c9c6c904132be327e2", size = 13264761, upload-time = "2023-10-03T21:35:48.865Z" }, - { url = "https://files.pythonhosted.org/packages/ad/85/dadf1194793ac1c895370f3ed048bb91dda083775b42e11d9672a50494d5/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87872f067444ee90a00dd49ca897208308645382e8a24bd3e76f301af2352cd", size = 14070710, upload-time = "2023-10-03T21:35:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/d4/34/e27bf2bfe7b52b884b49bd71ea91ff81e4737246735ee5ea383314c31876/scikit_image-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5c378db54e61b491b9edeefff87e49fcf7fdf729bb93c777d7a5f15d36f743e", size = 14664172, upload-time = "2023-10-03T21:35:55.752Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d0/a3f60c9f57ed295b3076e4acdb29a37bbd8823452562ab2ad51b03d6f377/scikit_image-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:2bcb74adb0634258a67f66c2bb29978c9a3e222463e003b67ba12056c003971b", size = 24491321, upload-time = "2023-10-03T21:35:58.847Z" }, - { url = "https://files.pythonhosted.org/packages/da/a4/b0b69bde4d6360e801d647691591dc9967a25a18a4c63ecf7f87d94e3fac/scikit_image-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:003ca2274ac0fac252280e7179ff986ff783407001459ddea443fe7916e38cff", size = 13968808, upload-time = "2023-10-03T21:36:02.526Z" }, - { url = "https://files.pythonhosted.org/packages/e4/65/3c0f77e7a9bae100a8f7f5cebde410fca1a3cf64e1ecdd343666e27b11d4/scikit_image-0.22.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:cf3c0c15b60ae3e557a0c7575fbd352f0c3ce0afca562febfe3ab80efbeec0e9", size = 13323763, upload-time = "2023-10-03T21:36:05.504Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ed/7faf9f7a55d5b3095d33990a85603b66866cce2a608b27f0e1487d70a451/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b23908dd4d120e6aecb1ed0277563e8cbc8d6c0565bdc4c4c6475d53608452", size = 13877233, upload-time = "2023-10-03T21:36:08.352Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/09d06f36ce71fa276e1d9453fb4b04250a7038292b13b8c273a5a1a8f7c0/scikit_image-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be79d7493f320a964f8fcf603121595ba82f84720de999db0fcca002266a549a", size = 14954814, upload-time = "2023-10-03T21:36:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/dc/35/e6327ae498c6f557cb0a7c3fc284effe7958d2d1c43fb61cd77804fc2c4f/scikit_image-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:722b970aa5da725dca55252c373b18bbea7858c1cdb406e19f9b01a4a73b30b2", size = 25004857, upload-time = "2023-10-03T21:36:15.457Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.11.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/00/835e3d280fdd7784e76bdef91dd9487582d7951a7254f59fc8004fc8b213/scikit-learn-1.3.2.tar.gz", hash = "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05", size = 7510251, upload-time = "2023-10-23T13:47:55.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/53/570b55a6e10b8694ac1e3024d2df5cd443f1b4ff6d28430845da8b9019b3/scikit_learn-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1", size = 10209999, upload-time = "2023-10-23T13:46:30.373Z" }, - { url = "https://files.pythonhosted.org/packages/70/d0/50ace22129f79830e3cf682d0a2bd4843ef91573299d43112d52790163a8/scikit_learn-1.3.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a", size = 9479353, upload-time = "2023-10-23T13:46:34.368Z" }, - { url = "https://files.pythonhosted.org/packages/8f/46/fcc35ed7606c50d3072eae5a107a45cfa5b7f5fa8cc48610edd8cc8e8550/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c", size = 10304705, upload-time = "2023-10-23T13:46:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0b/26ad95cf0b747be967b15fb71a06f5ac67aba0fd2f9cd174de6edefc4674/scikit_learn-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161", size = 10827807, upload-time = "2023-10-23T13:46:41.59Z" }, - { url = "https://files.pythonhosted.org/packages/69/8a/cf17d6443f5f537e099be81535a56ab68a473f9393fbffda38cd19899fc8/scikit_learn-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c", size = 9255427, upload-time = "2023-10-23T13:46:44.826Z" }, - { url = "https://files.pythonhosted.org/packages/08/5d/e5acecd6e99a6b656e42e7a7b18284e2f9c9f512e8ed6979e1e75d25f05f/scikit_learn-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66", size = 10116376, upload-time = "2023-10-23T13:46:48.147Z" }, - { url = "https://files.pythonhosted.org/packages/40/c6/2e91eefb757822e70d351e02cc38d07c137212ae7c41ac12746415b4860a/scikit_learn-1.3.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157", size = 9383415, upload-time = "2023-10-23T13:46:51.324Z" }, - { url = "https://files.pythonhosted.org/packages/fa/fd/b3637639e73bb72b12803c5245f2a7299e09b2acd85a0f23937c53369a1c/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb", size = 10279163, upload-time = "2023-10-23T13:46:54.642Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/d3ff6091406bc2207e0adb832ebd15e40ac685811c7e2e3b432bfd969b71/scikit_learn-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433", size = 10884422, upload-time = "2023-10-23T13:46:58.087Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ba/ce9bd1cd4953336a0e213b29cb80bb11816f2a93de8c99f88ef0b446ad0c/scikit_learn-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b", size = 9207060, upload-time = "2023-10-23T13:47:00.948Z" }, - { url = "https://files.pythonhosted.org/packages/26/7e/2c3b82c8c29aa384c8bf859740419278627d2cdd0050db503c8840e72477/scikit_learn-1.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028", size = 9979322, upload-time = "2023-10-23T13:47:03.977Z" }, - { url = "https://files.pythonhosted.org/packages/cf/fc/6c52ffeb587259b6b893b7cac268f1eb1b5426bcce1aa20e53523bfe6944/scikit_learn-1.3.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5", size = 9270688, upload-time = "2023-10-23T13:47:07.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a7/6f4ae76f72ae9de162b97acbf1f53acbe404c555f968d13da21e4112a002/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525", size = 10280398, upload-time = "2023-10-23T13:47:10.796Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b7/ee35904c07a0666784349529412fbb9814a56382b650d30fd9d6be5e5054/scikit_learn-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c", size = 10796478, upload-time = "2023-10-23T13:47:14.077Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6b/db949ed5ac367987b1f250f070f340b7715d22f0c9c965bdf07de6ca75a3/scikit_learn-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107", size = 9133979, upload-time = "2023-10-23T13:47:17.389Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, ] [[package]] name = "scikit-learn" version = "1.7.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/84/5f4af978fff619706b8961accac84780a6d298d82a8873446f72edb4ead0/scikit_learn-1.7.1.tar.gz", hash = "sha256:24b3f1e976a4665aa74ee0fcaac2b8fccc6ae77c8e07ab25da3ba6d3292b9802", size = 7190445, upload-time = "2025-07-18T08:01:54.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/88/0dd5be14ef19f2d80a77780be35a33aa94e8a3b3223d80bee8892a7832b4/scikit_learn-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:406204dd4004f0517f0b23cf4b28c6245cbd51ab1b6b78153bc784def214946d", size = 9338868, upload-time = "2025-07-18T08:01:00.25Z" }, - { url = "https://files.pythonhosted.org/packages/fd/52/3056b6adb1ac58a0bc335fc2ed2fcf599974d908855e8cb0ca55f797593c/scikit_learn-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:16af2e44164f05d04337fd1fc3ae7c4ea61fd9b0d527e22665346336920fe0e1", size = 8655943, upload-time = "2025-07-18T08:01:02.974Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a4/e488acdece6d413f370a9589a7193dac79cd486b2e418d3276d6ea0b9305/scikit_learn-1.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f2e78e56a40c7587dea9a28dc4a49500fa2ead366869418c66f0fd75b80885c", size = 9652056, upload-time = "2025-07-18T08:01:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/18/41/bceacec1285b94eb9e4659b24db46c23346d7e22cf258d63419eb5dec6f7/scikit_learn-1.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b62b76ad408a821475b43b7bb90a9b1c9a4d8d125d505c2df0539f06d6e631b1", size = 9473691, upload-time = "2025-07-18T08:01:07.006Z" }, - { url = "https://files.pythonhosted.org/packages/12/7b/e1ae4b7e1dd85c4ca2694ff9cc4a9690970fd6150d81b975e6c5c6f8ee7c/scikit_learn-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:9963b065677a4ce295e8ccdee80a1dd62b37249e667095039adcd5bce6e90deb", size = 8900873, upload-time = "2025-07-18T08:01:09.332Z" }, { url = "https://files.pythonhosted.org/packages/b4/bd/a23177930abd81b96daffa30ef9c54ddbf544d3226b8788ce4c3ef1067b4/scikit_learn-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90c8494ea23e24c0fb371afc474618c1019dc152ce4a10e4607e62196113851b", size = 9334838, upload-time = "2025-07-18T08:01:11.239Z" }, { url = "https://files.pythonhosted.org/packages/8d/a1/d3a7628630a711e2ac0d1a482910da174b629f44e7dd8cfcd6924a4ef81a/scikit_learn-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:bb870c0daf3bf3be145ec51df8ac84720d9972170786601039f024bf6d61a518", size = 8651241, upload-time = "2025-07-18T08:01:13.234Z" }, { url = "https://files.pythonhosted.org/packages/26/92/85ec172418f39474c1cd0221d611345d4f433fc4ee2fc68e01f524ccc4e4/scikit_learn-1.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40daccd1b5623f39e8943ab39735cadf0bdce80e67cdca2adcb5426e987320a8", size = 9718677, upload-time = "2025-07-18T08:01:15.649Z" }, @@ -2897,60 +2625,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/20/f4777fcd5627dc6695fa6b92179d0edb7a3ac1b91bcd9a1c7f64fa7ade23/scikit_learn-1.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b1bd1d919210b6a10b7554b717c9000b5485aa95a1d0f177ae0d7ee8ec750da5", size = 9277310, upload-time = "2025-07-18T08:01:52.547Z" }, ] -[[package]] -name = "scipy" -version = "1.11.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/1f/91144ba78dccea567a6466262922786ffc97be1e9b06ed9574ef0edc11e1/scipy-1.11.4.tar.gz", hash = "sha256:90a2b78e7f5733b9de748f589f09225013685f9b218275257f8a8168ededaeaa", size = 56336202, upload-time = "2023-11-18T21:06:08.277Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/c6/a32add319475d21f89733c034b99c81b3a7c6c7c19f96f80c7ca3ff1bbd4/scipy-1.11.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc9a714581f561af0848e6b69947fda0614915f072dfd14142ed1bfe1b806710", size = 37293259, upload-time = "2023-11-18T21:01:18.805Z" }, - { url = "https://files.pythonhosted.org/packages/de/0d/4fa68303568c70fd56fbf40668b6c6807cfee4cad975f07d80bdd26d013e/scipy-1.11.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cf00bd2b1b0211888d4dc75656c0412213a8b25e80d73898083f402b50f47e41", size = 29760656, upload-time = "2023-11-18T21:01:41.815Z" }, - { url = "https://files.pythonhosted.org/packages/13/e5/8012be7857db6cbbbdbeea8a154dbacdfae845e95e1e19c028e82236d4a0/scipy-1.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9999c008ccf00e8fbcce1236f85ade5c569d13144f77a1946bef8863e8f6eb4", size = 32922489, upload-time = "2023-11-18T21:01:50.637Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9e/80e2205d138960a49caea391f3710600895dd8292b6868dc9aff7aa593f9/scipy-1.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:933baf588daa8dc9a92c20a0be32f56d43faf3d1a60ab11b3f08c356430f6e56", size = 36442040, upload-time = "2023-11-18T21:02:00.119Z" }, - { url = "https://files.pythonhosted.org/packages/69/60/30a9c3fbe5066a3a93eefe3e2d44553df13587e6f792e1bff20dfed3d17e/scipy-1.11.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8fce70f39076a5aa62e92e69a7f62349f9574d8405c0a5de6ed3ef72de07f446", size = 36643257, upload-time = "2023-11-18T21:02:06.798Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ec/b46756f80e3f4c5f0989f6e4492c2851f156d9c239d554754a3c8cffd4e2/scipy-1.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:6550466fbeec7453d7465e74d4f4b19f905642c89a7525571ee91dd7adabb5a3", size = 44149285, upload-time = "2023-11-18T21:02:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/1aefbd5e54ebd8c6163ccf7f73e5d17bc8cb38738d312befc524fce84bb4/scipy-1.11.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f313b39a7e94f296025e3cffc2c567618174c0b1dde173960cf23808f9fae4be", size = 37159197, upload-time = "2023-11-18T21:02:21.959Z" }, - { url = "https://files.pythonhosted.org/packages/4b/48/20e77ddb1f473d4717a7d4d3fc8d15557f406f7708496054c59f635b7734/scipy-1.11.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1b7c3dca977f30a739e0409fb001056484661cb2541a01aba0bb0029f7b68db8", size = 29675057, upload-time = "2023-11-18T21:02:28.169Z" }, - { url = "https://files.pythonhosted.org/packages/75/2e/a781862190d0e7e76afa74752ef363488a9a9d6ea86e46d5e5506cee8df6/scipy-1.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00150c5eae7b610c32589dda259eacc7c4f1665aedf25d921907f4d08a951b1c", size = 32882747, upload-time = "2023-11-18T21:02:33.683Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d4/d62ce38ba00dc67d7ec4ec5cc19d36958d8ed70e63778715ad626bcbc796/scipy-1.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:530f9ad26440e85766509dbf78edcfe13ffd0ab7fec2560ee5c36ff74d6269ff", size = 36402732, upload-time = "2023-11-18T21:02:39.762Z" }, - { url = "https://files.pythonhosted.org/packages/88/86/827b56aea1ed04adbb044a675672a73c84d81076a350092bbfcfc1ae723b/scipy-1.11.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e347b14fe01003d3b78e196e84bd3f48ffe4c8a7b8a1afbcb8f5505cb710993", size = 36622138, upload-time = "2023-11-18T21:02:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/43/d0/f3cd75b62e1b90f48dbf091261b2fc7ceec14a700e308c50f6a69c83d337/scipy-1.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:acf8ed278cc03f5aff035e69cb511741e0418681d25fbbb86ca65429c4f4d9cd", size = 44095631, upload-time = "2023-11-18T21:02:52.859Z" }, - { url = "https://files.pythonhosted.org/packages/df/64/8a690570485b636da614acff35fd725fcbc487f8b1fa9bdb12871b77412f/scipy-1.11.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:028eccd22e654b3ea01ee63705681ee79933652b2d8f873e7949898dda6d11b6", size = 37053653, upload-time = "2023-11-18T21:03:00.107Z" }, - { url = "https://files.pythonhosted.org/packages/5e/43/abf331745a7e5f4af51f13d40e2a72f516048db41ecbcf3ac6f86ada54a3/scipy-1.11.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c6ff6ef9cc27f9b3db93a6f8b38f97387e6e0591600369a297a50a8e96e835d", size = 29641601, upload-time = "2023-11-18T21:03:06.708Z" }, - { url = "https://files.pythonhosted.org/packages/47/9b/62d0ec086dd2871009da8769c504bec6e39b80f4c182c6ead0fcebd8b323/scipy-1.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b030c6674b9230d37c5c60ab456e2cf12f6784596d15ce8da9365e70896effc4", size = 32272137, upload-time = "2023-11-18T21:03:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/08/77/f90f7306d755ac68bd159c50bb86fffe38400e533e8c609dd8484bd0f172/scipy-1.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad669df80528aeca5f557712102538f4f37e503f0c5b9541655016dd0932ca79", size = 35777534, upload-time = "2023-11-18T21:03:21.451Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/b9f6938090c37b5092969ba1c67118e9114e8e6ef9d197251671444e839c/scipy-1.11.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce7fff2e23ab2cc81ff452a9444c215c28e6305f396b2ba88343a567feec9660", size = 35963721, upload-time = "2023-11-18T21:03:27.85Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a1/357e4cd43af2748e1e0407ae0e9a5ea8aaaa6b702833c81be11670dcbad8/scipy-1.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:36750b7733d960d7994888f0d148d31ea3017ac15eef664194b4ef68d36a4a97", size = 43730653, upload-time = "2023-11-18T21:03:34.758Z" }, -] - [[package]] name = "scipy" version = "1.16.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } wheels = [ @@ -3028,14 +2708,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422, upload-time = "2025-05-19T11:04:41.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fa/f18025c95b86116dd8f1ec58cab078bd59ab51456b448136ca27463be533/shapely-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8ccc872a632acb7bdcb69e5e78df27213f7efd195882668ffba5405497337c6", size = 1825117, upload-time = "2025-05-19T11:03:43.547Z" }, - { url = "https://files.pythonhosted.org/packages/c7/65/46b519555ee9fb851234288be7c78be11e6260995281071d13abf2c313d0/shapely-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f24f2ecda1e6c091da64bcbef8dd121380948074875bd1b247b3d17e99407099", size = 1628541, upload-time = "2025-05-19T11:03:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/29/51/0b158a261df94e33505eadfe737db9531f346dfa60850945ad25fd4162f1/shapely-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45112a5be0b745b49e50f8829ce490eb67fefb0cea8d4f8ac5764bfedaa83d2d", size = 2948453, upload-time = "2025-05-19T11:03:46.681Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4f/6c9bb4bd7b1a14d7051641b9b479ad2a643d5cbc382bcf5bd52fd0896974/shapely-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c10ce6f11904d65e9bbb3e41e774903c944e20b3f0b282559885302f52f224a", size = 3057029, upload-time = "2025-05-19T11:03:48.346Z" }, - { url = "https://files.pythonhosted.org/packages/89/0b/ad1b0af491d753a83ea93138eee12a4597f763ae12727968d05934fe7c78/shapely-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:61168010dfe4e45f956ffbbaf080c88afce199ea81eb1f0ac43230065df320bd", size = 3894342, upload-time = "2025-05-19T11:03:49.602Z" }, - { url = "https://files.pythonhosted.org/packages/7d/96/73232c5de0b9fdf0ec7ddfc95c43aaf928740e87d9f168bff0e928d78c6d/shapely-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cacf067cdff741cd5c56a21c52f54ece4e4dad9d311130493a791997da4a886b", size = 4056766, upload-time = "2025-05-19T11:03:51.252Z" }, - { url = "https://files.pythonhosted.org/packages/43/cc/eec3c01f754f5b3e0c47574b198f9deb70465579ad0dad0e1cef2ce9e103/shapely-2.1.1-cp310-cp310-win32.whl", hash = "sha256:23b8772c3b815e7790fb2eab75a0b3951f435bc0fce7bb146cb064f17d35ab4f", size = 1523744, upload-time = "2025-05-19T11:03:52.624Z" }, - { url = "https://files.pythonhosted.org/packages/50/fc/a7187e6dadb10b91e66a9e715d28105cde6489e1017cce476876185a43da/shapely-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c7b2b6143abf4fa77851cef8ef690e03feade9a0d48acd6dc41d9e0e78d7ca6", size = 1703061, upload-time = "2025-05-19T11:03:54.695Z" }, { url = "https://files.pythonhosted.org/packages/19/97/2df985b1e03f90c503796ad5ecd3d9ed305123b64d4ccb54616b30295b29/shapely-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:587a1aa72bc858fab9b8c20427b5f6027b7cbc92743b8e2c73b9de55aa71c7a7", size = 1819368, upload-time = "2025-05-19T11:03:55.937Z" }, { url = "https://files.pythonhosted.org/packages/56/17/504518860370f0a28908b18864f43d72f03581e2b6680540ca668f07aa42/shapely-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fa5c53b0791a4b998f9ad84aad456c988600757a96b0a05e14bba10cebaaaea", size = 1625362, upload-time = "2025-05-19T11:03:57.06Z" }, { url = "https://files.pythonhosted.org/packages/36/a1/9677337d729b79fce1ef3296aac6b8ef4743419086f669e8a8070eff8f40/shapely-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aabecd038841ab5310d23495253f01c2a82a3aedae5ab9ca489be214aa458aa7", size = 2999005, upload-time = "2025-05-19T11:03:58.692Z" }, @@ -3102,14 +2774,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.41.2" +version = "0.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867, upload-time = "2024-10-27T08:20:02.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259, upload-time = "2024-10-27T08:20:00.052Z" }, + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, ] [[package]] @@ -3147,36 +2820,77 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.22.1" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] [[package]] @@ -3202,23 +2916,23 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250913" +version = "2.32.4.20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, ] [[package]] name = "types-setuptools" -version = "80.9.0.20250822" +version = "82.0.0.20260210" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/90/796ac8c774a7f535084aacbaa6b7053d16fff5c630eff87c3ecff7896c37/types_setuptools-82.0.0.20260210.tar.gz", hash = "sha256:d9719fbbeb185254480ade1f25327c4654f8c00efda3fec36823379cebcdee58", size = 44768, upload-time = "2026-02-10T04:22:02.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" }, + { url = "https://files.pythonhosted.org/packages/3e/54/3489432b1d9bc713c9d8aa810296b8f5b0088403662959fb63a8acdbd4fc/types_setuptools-82.0.0.20260210-py3-none-any.whl", hash = "sha256:5124a7daf67f195c6054e0f00f1d97c69caad12fdcf9113eba33eff0bce8cd2b", size = 68433, upload-time = "2026-02-10T04:22:00.876Z" }, ] [[package]] @@ -3262,25 +2976,24 @@ wheels = [ [[package]] name = "urllib3" -version = "2.1.0" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/dd/a6b232f449e1bc71802a5b7950dc3675d32c6dbc2a1bd6d71f065551adb6/urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54", size = 263900, upload-time = "2023-11-13T12:29:45.049Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/94/c31f58c7a7f470d5665935262ebd7455c7e4c7782eb525658d3dbf4b9403/urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", size = 104579, upload-time = "2023-11-13T12:29:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.40.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/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [package.optional-dependencies] @@ -3296,28 +3009,40 @@ standard = [ [[package]] name = "uvloop" -version = "0.19.0" +version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/16/728cc5dde368e6eddb299c5aec4d10eaf25335a5af04e8c0abd68e2e9d32/uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd", size = 2318492, upload-time = "2023-10-22T22:03:57.665Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/c2/27bf858a576b1fa35b5c2c2029c8cec424a8789e87545ed2f25466d1f21d/uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e", size = 1443484, upload-time = "2023-10-22T22:02:54.169Z" }, - { url = "https://files.pythonhosted.org/packages/4e/35/05b6064b93f4113412d1fd92bdcb6018607e78ae94d1712e63e533f9b2fa/uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428", size = 793850, upload-time = "2023-10-22T22:02:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/aa/56/b62ab4e10458ce96bb30c98d327c127f989d3bb4ef899e4c410c739f7ef6/uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8", size = 3418601, upload-time = "2023-10-22T22:02:58.717Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ed/12729fba5e3b7e02ee70b3ea230b88e60a50375cf63300db22607694d2f0/uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849", size = 3416731, upload-time = "2023-10-22T22:03:01.043Z" }, - { url = "https://files.pythonhosted.org/packages/a2/23/80381a2d728d2a0c36e2eef202f5b77428990004d8fbdd3865558ff49fa5/uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957", size = 4128572, upload-time = "2023-10-22T22:03:02.874Z" }, - { url = "https://files.pythonhosted.org/packages/6b/23/1ee41a15e1ad15182e2bd12cbfd37bcb6802f01d6bbcaddf6ca136cbb308/uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd", size = 4129235, upload-time = "2023-10-22T22:03:05.361Z" }, - { url = "https://files.pythonhosted.org/packages/41/2a/608ad69f27f51280098abee440c33e921d3ad203e2c86f7262e241e49c99/uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef", size = 1357681, upload-time = "2023-10-22T22:03:07.158Z" }, - { url = "https://files.pythonhosted.org/packages/13/00/d0923d66d80c8717983493a4d7af747ce47f1c2147d82df057a846ba6bff/uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2", size = 746421, upload-time = "2023-10-22T22:03:09.4Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c7/e494c367b0c6e6453f9bed5a78548f5b2ff49add36302cd915a91d347d88/uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1", size = 3481000, upload-time = "2023-10-22T22:03:11.755Z" }, - { url = "https://files.pythonhosted.org/packages/86/cc/1829b3f740e4cb1baefff8240a1c6fc8db9e3caac7b93169aec7d4386069/uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24", size = 3476361, upload-time = "2023-10-22T22:03:13.841Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4c/ca87e8f5a30629ffa2038c20907c8ab455c5859ff10e810227b76e60d927/uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533", size = 4169571, upload-time = "2023-10-22T22:03:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a9/f947a00c47b1c87c937cac2423243a41ba08f0fb76d04eb0d1d170606e0a/uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12", size = 4170459, upload-time = "2023-10-22T22:03:17.988Z" }, - { url = "https://files.pythonhosted.org/packages/85/57/6736733bb0e86a4b5380d04082463b289c0baecaa205934ba81e8a1d5ea4/uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650", size = 1355376, upload-time = "2023-10-22T22:03:20.075Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0c/51339463da912ed34b48d470538d98a91660749b2db56902f23db9b42fdd/uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec", size = 745031, upload-time = "2023-10-22T22:03:21.404Z" }, - { url = "https://files.pythonhosted.org/packages/e6/fc/f0daaf19f5b2116a2d26eb9f98c4a45084aea87bf03c33bcca7aa1ff36e5/uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc", size = 4077630, upload-time = "2023-10-22T22:03:23.568Z" }, - { url = "https://files.pythonhosted.org/packages/fd/96/fdc318ffe82ae567592b213ec2fcd8ecedd927b5da068cf84d56b28c51a4/uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6", size = 4159957, upload-time = "2023-10-22T22:03:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/71/bc/092068ae7fc16dcf20f3e389126ba7800cee75ffba83f78bf1d167aee3cd/uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593", size = 4014951, upload-time = "2023-10-22T22:03:27.055Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f2/6ce1e73933eb038c89f929e26042e64b2cb8d4453410153eed14918ca9a8/uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3", size = 4100911, upload-time = "2023-10-22T22:03:29.39Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] @@ -3329,18 +3054,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/66/79/0ee412e1228aaf6f9568aa180b43cb482472de52560fbd7c283c786534af/watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", size = 37098, upload-time = "2023-10-13T13:06:39.809Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/85/ea2a035b7d86bf0a29ee1c32bc2df8ad4da77e6602806e679d9735ff28cb/watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", size = 428182, upload-time = "2023-10-13T13:04:34.803Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/240e5eb3ff0ee3da3b028ac5be2019c407bdd0f3fdb02bd75fdf3bd10aff/watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", size = 418275, upload-time = "2023-10-13T13:04:36.632Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/ecd0dfb04443a1900cd3952d7ea6493bf655c2db9a0d3736a5d98a15da39/watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", size = 1379785, upload-time = "2023-10-13T13:04:38.641Z" }, - { url = "https://files.pythonhosted.org/packages/41/0e/3333b986b1889bb71f0e44b3fac0591824a679619b8b8ddd70ff8858edc4/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", size = 1349374, upload-time = "2023-10-13T13:04:41.711Z" }, - { url = "https://files.pythonhosted.org/packages/18/c4/ad5ad16cad900a29aaa792e0ed121ff70d76f74062b051661090d88c6dfd/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", size = 1348033, upload-time = "2023-10-13T13:04:43.324Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d2/769254ff04ba88ceb179a6e892606ac4da17338eb010e85ca7a9c3339234/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", size = 1464393, upload-time = "2023-10-13T13:04:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/14/d0/662800e778ca20e7664dd5df57751aa79ef18b6abb92224b03c8c2e852a6/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", size = 1542953, upload-time = "2023-10-13T13:04:46.714Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4b/b90dcdc3bbaf3bb2db733e1beea2d01566b601c15fcf8e71dfcc8686c097/watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", size = 1346961, upload-time = "2023-10-13T13:04:48.072Z" }, - { url = "https://files.pythonhosted.org/packages/92/ff/75cc1b30c5abcad13a2a72e75625ec619c7a393028a111d7d24dba578d5e/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", size = 1464393, upload-time = "2023-10-13T13:04:49.638Z" }, - { url = "https://files.pythonhosted.org/packages/9a/65/12cbeb363bf220482a559c48107edfd87f09248f55e1ac315a36c2098a0f/watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", size = 1463409, upload-time = "2023-10-13T13:04:51.762Z" }, - { url = "https://files.pythonhosted.org/packages/f2/08/92e28867c66f0d9638bb131feca739057efc48dbcd391fd7f0a55507e470/watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", size = 268101, upload-time = "2023-10-13T13:04:53.78Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ea/80527adf1ad51488a96fc201715730af5879f4dfeccb5e2069ff82d890d4/watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", size = 279675, upload-time = "2023-10-13T13:04:55.113Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/2667286003dd305b81d3a3aa824d3dfc63dacbf2a96faae09e72d953c430/watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", size = 428210, upload-time = "2023-10-13T13:04:56.894Z" }, { url = "https://files.pythonhosted.org/packages/a3/87/6793ac60d2e20c9c1883aec7431c2e7b501ee44a839f6da1b747c13baa23/watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", size = 418196, upload-time = "2023-10-13T13:04:58.19Z" }, { url = "https://files.pythonhosted.org/packages/5d/12/e1d1d220c5b99196eea38c9a878964f30a2b55ec9d72fd713191725b35e8/watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", size = 1380287, upload-time = "2023-10-13T13:04:59.923Z" }, @@ -3367,10 +3080,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/e4/8d2b3c67364671b0e1c0ce383895a5415f45ecb3e8586982deff4a8e85c9/watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", size = 266789, upload-time = "2023-10-13T13:05:35.606Z" }, { url = "https://files.pythonhosted.org/packages/da/f2/6b1de38aeb21eb9dac1ae6a1ee4521566e79690117032036c737cfab52fa/watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", size = 280292, upload-time = "2023-10-13T13:05:37.357Z" }, { url = "https://files.pythonhosted.org/packages/5a/a5/7aba9435beb863c2490bae3173a45f42044ac7a48155d3dd42ab49cfae45/watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", size = 268026, upload-time = "2023-10-13T13:05:38.591Z" }, - { url = "https://files.pythonhosted.org/packages/62/66/7463ceb43eabc6deaa795c7969ff4d4fd938de54e655035483dfd1e97c84/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", size = 429092, upload-time = "2023-10-13T13:06:21.419Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a3/42686af3a089f34aba35c39abac852869661938dae7025c1a0580dfe0fbf/watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", size = 419188, upload-time = "2023-10-13T13:06:22.934Z" }, - { url = "https://files.pythonhosted.org/packages/37/17/4825999346f15d650f4c69093efa64fb040fbff4f706a20e8c4745f64070/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", size = 1350366, upload-time = "2023-10-13T13:06:24.254Z" }, - { url = "https://files.pythonhosted.org/packages/70/76/8d124e14cf51af4d6bba926c7473f253c6efd1539ba62577f079a2d71537/watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", size = 1346270, upload-time = "2023-10-13T13:06:25.742Z" }, ] [[package]] @@ -3397,17 +3106,6 @@ version = "12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload-time = "2023-10-21T14:21:11.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/b9/360b86ded0920a93bff0db4e4b0aa31370b0208ca240b2e98d62aad8d082/websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", size = 124025, upload-time = "2023-10-21T14:19:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d3/1eca0d8fb6f0665c96f0dc7c0d0ec8aa1a425e8c003e0c18e1451f65d177/websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", size = 121261, upload-time = "2023-10-21T14:19:30.203Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/f6c3ecf7f1bfd9209e13949db027d7fdea2faf090c69b5f2d17d1d796d96/websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", size = 121328, upload-time = "2023-10-21T14:19:31.765Z" }, - { url = "https://files.pythonhosted.org/packages/74/4d/f88eeceb23cb587c4aeca779e3f356cf54817af2368cb7f2bd41f93c8360/websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", size = 130925, upload-time = "2023-10-21T14:19:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/16/17/f63d9ee6ffd9afbeea021d5950d6e8db84cd4aead306c6c2ca523805699e/websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", size = 129930, upload-time = "2023-10-21T14:19:35.109Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/c7a7504f5bf74d6ee0533f6fc7d30d8f4b79420ab179d1df2484b07602eb/websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", size = 130245, upload-time = "2023-10-21T14:19:36.761Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6a/3600c7771eb31116d2e77383d7345618b37bb93709d041e328c08e2a8eb3/websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", size = 134966, upload-time = "2023-10-21T14:19:38.481Z" }, - { url = "https://files.pythonhosted.org/packages/22/26/df77c4b7538caebb78c9b97f43169ef742a4f445e032a5ea1aaef88f8f46/websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", size = 134196, upload-time = "2023-10-21T14:19:40.264Z" }, - { url = "https://files.pythonhosted.org/packages/e5/18/18ce9a4a08203c8d0d3d561e3ea4f453daf32f099601fc831e60c8a9b0f2/websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", size = 134822, upload-time = "2023-10-21T14:19:41.836Z" }, - { url = "https://files.pythonhosted.org/packages/45/51/1f823a341fc20a880e67ae62f6c38c4880a24a4b60fbe544a38f516f39a1/websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", size = 124454, upload-time = "2023-10-21T14:19:43.639Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/5ec054cfcf23adfc88d39359b85e81d043af8a141e3ac8ce40f45a5ce5f4/websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", size = 124974, upload-time = "2023-10-21T14:19:44.934Z" }, { url = "https://files.pythonhosted.org/packages/02/73/9c1e168a2e7fdf26841dc98f5f5502e91dea47428da7690a08101f616169/websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", size = 124047, upload-time = "2023-10-21T14:19:46.519Z" }, { url = "https://files.pythonhosted.org/packages/e4/2d/9a683359ad2ed11b2303a7a94800db19c61d33fa3bde271df09e99936022/websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", size = 121282, upload-time = "2023-10-21T14:19:47.739Z" }, { url = "https://files.pythonhosted.org/packages/95/aa/75fa3b893142d6d98a48cb461169bd268141f2da8bfca97392d6462a02eb/websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", size = 121325, upload-time = "2023-10-21T14:19:49.4Z" }, @@ -3430,11 +3128,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload-time = "2023-10-21T14:20:13.451Z" }, { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload-time = "2023-10-21T14:20:14.719Z" }, { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload-time = "2023-10-21T14:20:15.817Z" }, - { url = "https://files.pythonhosted.org/packages/43/8b/554a8a8bb6da9dd1ce04c44125e2192af7b7beebf6e3dbfa5d0e285cc20f/websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", size = 121110, upload-time = "2023-10-21T14:20:48.335Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8e/58b8812940d746ad74d395fb069497255cb5ef50748dfab1e8b386b1f339/websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", size = 123216, upload-time = "2023-10-21T14:20:50.083Z" }, - { url = "https://files.pythonhosted.org/packages/81/ee/272cb67ace1786ce6d9f39d47b3c55b335e8b75dd1972a7967aad39178b6/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", size = 122821, upload-time = "2023-10-21T14:20:51.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/03/387fc902b397729df166763e336f4e5cec09fe7b9d60f442542c94a21be1/websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", size = 122768, upload-time = "2023-10-21T14:20:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/50/f0/5939fbc9bc1979d79a774ce5b7c4b33c0cefe99af22fb70f7462d0919640/websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", size = 125009, upload-time = "2023-10-21T14:20:54.419Z" }, { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload-time = "2023-10-21T14:21:10.075Z" }, ] @@ -3483,12 +3176,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/87/03/6b85c1df2dca1b9acca38b423d1e226d8ffdf30ebd78bcb398c511de8b54/zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309", size = 293914, upload-time = "2023-10-05T11:24:38.943Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/ec/c1e7ce928dc10bfe02c6da7e964342d941aaf168f96f8084636167ea50d2/zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb", size = 202417, upload-time = "2023-10-05T11:24:25.141Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/12f269ad049fc40a7a3ab85445d7855b6bc6f1e774c5ca9dd6f5c32becb3/zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92", size = 202528, upload-time = "2023-10-05T11:24:27.336Z" }, - { url = "https://files.pythonhosted.org/packages/7f/85/3a35144509eb4a5a2208b48ae8d116a969d67de62cc6513d85602144d9cd/zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3", size = 247532, upload-time = "2023-10-05T11:49:20.587Z" }, - { url = "https://files.pythonhosted.org/packages/50/d6/6176aaa1f6588378f5a5a4a9c6ad50a36824e902b2f844ca8de7f1b0c4a7/zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd", size = 241703, upload-time = "2023-10-05T11:25:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/4f/20/94d4f221989b4bbdd09004b2afb329958e776b7015b7ea8bc915327e195a/zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41", size = 247078, upload-time = "2023-10-05T11:25:48.235Z" }, - { url = "https://files.pythonhosted.org/packages/97/7e/b790b4ab9605010816a91df26a715f163e228d60eb36c947c3118fb65190/zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f", size = 204155, upload-time = "2023-10-05T11:37:56.715Z" }, { url = "https://files.pythonhosted.org/packages/4a/0b/1d8817b8a3631384a26ff7faa4c1f3e6726f7e4950c3442721cfef2c95eb/zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1", size = 202441, upload-time = "2023-10-05T11:24:20.414Z" }, { url = "https://files.pythonhosted.org/packages/3e/1f/43557bb2b6e8537002a5a26af9b899171e26ddfcdf17a00ff729b00c036b/zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736", size = 202530, upload-time = "2023-10-05T11:24:22.975Z" }, { url = "https://files.pythonhosted.org/packages/37/a1/5d2b265f4b7371630cad5873d0873965e35ca3de993d11b9336c720f7259/zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605", size = 249584, upload-time = "2023-10-05T11:49:22.978Z" }, diff --git a/misc/release/archive-version.js b/misc/release/archive-version.js index 1a66963dad..5c0ed9f22f 100755 --- a/misc/release/archive-version.js +++ b/misc/release/archive-version.js @@ -1,6 +1,12 @@ #! /usr/bin/env node const { readFileSync, writeFileSync } = require('node:fs'); +const asVersion = (item) => { + const { label, url } = item; + const [major, minor, patch] = label.substring(1).split('.').map(Number); + return { major, minor, patch, label, url }; +}; + const nextVersion = process.argv[2]; if (!nextVersion) { console.log('Usage: archive-version.js '); @@ -8,10 +14,32 @@ if (!nextVersion) { } const filename = './docs/static/archived-versions.json'; -const oldVersions = JSON.parse(readFileSync(filename)); -const newVersions = [ - { label: `v${nextVersion}`, url: `https://docs.v${nextVersion}.archive.immich.app` }, - ...oldVersions, -]; +let versions = JSON.parse(readFileSync(filename)); +const newVersion = { + label: `v${nextVersion}`, + url: `https://docs.v${nextVersion}.archive.immich.app`, +}; -writeFileSync(filename, JSON.stringify(newVersions, null, 2) + '\n'); +let lastVersion = asVersion(newVersion); +for (const item of versions) { + const version = asVersion(item); + // only keep the latest patch version for each minor release + if ( + lastVersion.major === version.major && + lastVersion.minor === version.minor && + lastVersion.patch >= version.patch + ) { + versions = versions.filter((item) => item.label !== version.label); + console.log( + `Removed ${version.label} (replaced with ${lastVersion.label})` + ); + continue; + } + + lastVersion = version; +} + +writeFileSync( + filename, + JSON.stringify([newVersion, ...versions], null, 2) + '\n' +); diff --git a/misc/release/pump-version.sh b/misc/release/pump-version.sh index 2dc772ca91..6be0ddebb9 100755 --- a/misc/release/pump-version.sh +++ b/misc/release/pump-version.sh @@ -61,26 +61,23 @@ fi if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER" - jq --arg version "$NEXT_SERVER" '.version = $version' server/package.json > server/package.json.tmp && mv server/package.json.tmp server/package.json + + pnpm version "$NEXT_SERVER" --no-git-tag-version + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix i18n + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix cli + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e + pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix open-api/typescript-sdk + + # copy version to open-api spec pnpm install --frozen-lockfile --prefix server pnpm --prefix server run build - ( cd ./open-api && bash ./bin/generate-open-api.sh ) - jq --arg version "$NEXT_SERVER" '.version = $version' open-api/typescript-sdk/package.json > open-api/typescript-sdk/package.json.tmp && mv open-api/typescript-sdk/package.json.tmp open-api/typescript-sdk/package.json + uv version --directory machine-learning "$NEXT_SERVER" - # TODO use $SERVER_PUMP once we pass 2.2.x - CURRENT_CLI_VERSION=$(jq -r '.version' cli/package.json) - CLI_PATCH_VERSION=$(echo "$CURRENT_CLI_VERSION" | awk -F. '{print $1"."$2"."($3+1)}') - jq --arg version "$CLI_PATCH_VERSION" '.version = $version' cli/package.json > cli/package.json.tmp && mv cli/package.json.tmp cli/package.json - pnpm install --frozen-lockfile --prefix cli - - jq --arg version "$NEXT_SERVER" '.version = $version' web/package.json > web/package.json.tmp && mv web/package.json.tmp web/package.json - pnpm install --frozen-lockfile --prefix web - - jq --arg version "$NEXT_SERVER" '.version = $version' e2e/package.json > e2e/package.json.tmp && mv e2e/package.json.tmp e2e/package.json - pnpm install --frozen-lockfile --prefix e2e - uvx --from=toml-cli toml set --toml-path=machine-learning/pyproject.toml project.version "$NEXT_SERVER" + ./misc/release/archive-version.js "$NEXT_SERVER" fi if [ "$CURRENT_MOBILE" != "$NEXT_MOBILE" ]; then @@ -90,7 +87,7 @@ fi sed -i "s/\"android\.injected\.version\.name\" => \"$CURRENT_SERVER\",/\"android\.injected\.version\.name\" => \"$NEXT_SERVER\",/" mobile/android/fastlane/Fastfile sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml +perl -i -p0e "s/(CFBundleShortVersionString<\/key>\s*)$CURRENT_SERVER(<\/string>)/\${1}$NEXT_SERVER\${2}/s" mobile/ios/Runner/Info.plist -./misc/release/archive-version.js "$NEXT_SERVER" echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV" diff --git a/mise.toml b/mise.toml index 0b61f3c26a..a87b1c3a29 100644 --- a/mise.toml +++ b/mise.toml @@ -1,15 +1,28 @@ experimental_monorepo_root = true +[monorepo] +config_roots = [ + "plugins", + "server", + "cli", + "deployment", + "mobile", + "e2e", + "web", + "docs", + ".github", +] + [tools] -node = "24.11.1" +node = "24.13.1" flutter = "3.35.7" -pnpm = "10.24.0" -terragrunt = "0.93.10" -opentofu = "1.10.7" -java = "25.0.1" +pnpm = "10.30.0" +terragrunt = "0.98.0" +opentofu = "1.11.4" +java = "21.0.2" [tools."github:CQLabs/homebrew-dcm"] -version = "1.30.0" +version = "1.35.1" bin = "dcm" postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" @@ -24,14 +37,13 @@ run = "pnpm install --filter @immich/sdk --frozen-lockfile" [tasks."sdk:build"] dir = "open-api/typescript-sdk" -env._.path = "./node_modules/.bin" -run = "tsc" +run = "pnpm run build" # i18n tasks [tasks."i18n:format"] dir = "i18n" -run = { task = ":i18n:format-fix" } +run = "pnpm run format" [tasks."i18n:format-fix"] dir = "i18n" -run = "pnpm dlx sort-json *.json" +run = "pnpm run format:fix" diff --git a/mobile/.fvmrc b/mobile/.fvmrc deleted file mode 100644 index e8b4151592..0000000000 --- a/mobile/.fvmrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "flutter": "3.35.7" -} \ No newline at end of file diff --git a/mobile/.gitignore b/mobile/.gitignore index 484c3f0afc..04eb74fddd 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -55,8 +55,5 @@ default.isar default.isar.lock libisar.so -# FVM Version -.fvm/ - # Translation file -lib/generated/ \ No newline at end of file +lib/generated/ diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 3092c4565f..eafbef8102 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -2,7 +2,9 @@ "dart.flutterSdkPath": ".fvm/versions/3.35.7", "dart.lineLength": 120, "[dart]": { - "editor.rulers": [120] + "editor.rulers": [ + 120 + ] }, "search.exclude": { "**/.fvm": true diff --git a/mobile/README.md b/mobile/README.md index 59b2d9340c..1f0860ced6 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -4,10 +4,12 @@ The Immich mobile app is a Flutter-based solution leveraging the Isar Database f ## Setup -1. Setup Flutter toolchain using FVM. -2. Run `flutter pub get` to install the dependencies. -3. Run `make translation` to generate the translation file. -4. Run `fvm flutter run` to start the app. +1. [Install mise](https://mise.jdx.dev/installing-mise.html). +2. Change to the immich directory and trust the mise config with `mise trust`. +3. Install tools with mise: `mise install`. +4. Run `flutter pub get` to install the dependencies. +5. Run `make translation` to generate the translation file. +6. Run `flutter run` to start the app. ## Translation @@ -29,7 +31,7 @@ dcm analyze lib ``` [DCM](https://dcm.dev/) is a vendor tool that needs to be downloaded manually to run locally. -Immich was provided an open source license. +Immich was provided an open source license. To use it, it is important that you do not have an active free tier license (can be verified with `dcm license`). If you have write-access to the Immich repository directly, running dcm in your clone should just work. If you are working on a clone of a fork, you need to connect to the main Immich repository as remote first: diff --git a/mobile/android/app/CMakeLists.txt b/mobile/android/app/CMakeLists.txt index 1569f1859e..133bde4fc0 100644 --- a/mobile/android/app/CMakeLists.txt +++ b/mobile/android/app/CMakeLists.txt @@ -8,3 +8,5 @@ project(native_buffer LANGUAGES C) add_library(native_buffer SHARED src/main/cpp/native_buffer.c ) + +target_link_libraries(native_buffer jnigraphics) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 3c2125e24e..4999f9a7f9 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -31,7 +31,7 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 35 - ndkVersion = "28.1.13356709" + ndkVersion = "28.2.13676358" compileOptions { sourceCompatibility JavaVersion.VERSION_17 @@ -48,6 +48,7 @@ android { } buildFeatures { + buildConfig true compose true } @@ -105,8 +106,11 @@ dependencies { def serialization_version = '1.8.1' def compose_version = '1.1.1' def gson_version = '2.10.1' + def okhttp_version = '4.12.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation 'org.chromium.net:cronet-embedded:143.7445.0' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.concurrent:concurrent-futures:$concurrent_version" @@ -127,6 +131,7 @@ dependencies { implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.material3:material3:1.2.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" + implementation "com.google.android.material:material:1.12.0" } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c6e04e5a10..db3859ab6e 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -27,7 +27,8 @@ + android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false" + android:networkSecurityConfig="@xml/network_security_config"> @@ -116,7 +117,10 @@ android:pathPrefix="/albums/" /> + android:pathPrefix="/people/" /> + diff --git a/mobile/android/app/src/main/cpp/native_buffer.c b/mobile/android/app/src/main/cpp/native_buffer.c index 3720d025f6..bcc9d5c7c8 100644 --- a/mobile/android/app/src/main/cpp/native_buffer.c +++ b/mobile/android/app/src/main/cpp/native_buffer.c @@ -1,40 +1,38 @@ #include #include +#include JNIEXPORT jlong JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_allocateNative( - JNIEnv *env, jclass clazz, jint size) { - void *ptr = malloc(size); - return (jlong) ptr; -} - -JNIEXPORT jlong JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_allocateNative( +Java_app_alextran_immich_NativeBuffer_allocate( JNIEnv *env, jclass clazz, jint size) { void *ptr = malloc(size); return (jlong) ptr; } JNIEXPORT void JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_freeNative( +Java_app_alextran_immich_NativeBuffer_free( JNIEnv *env, jclass clazz, jlong address) { free((void *) address); } +JNIEXPORT jlong JNICALL +Java_app_alextran_immich_NativeBuffer_realloc( + JNIEnv *env, jclass clazz, jlong address, jint size) { + void *ptr = realloc((void *) address, size); + return (jlong) ptr; +} + +JNIEXPORT jobject JNICALL +Java_app_alextran_immich_NativeBuffer_wrap( + JNIEnv *env, jclass clazz, jlong address, jint capacity) { + return (*env)->NewDirectByteBuffer(env, (void *) address, capacity); +} + JNIEXPORT void JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_freeNative( - JNIEnv *env, jclass clazz, jlong address) { - free((void *) address); -} - -JNIEXPORT jobject JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_wrapAsBuffer( - JNIEnv *env, jclass clazz, jlong address, jint capacity) { - return (*env)->NewDirectByteBuffer(env, (void *) address, capacity); -} - -JNIEXPORT jobject JNICALL -Java_app_alextran_immich_images_ThumbnailsImpl_wrapAsBuffer( - JNIEnv *env, jclass clazz, jlong address, jint capacity) { - return (*env)->NewDirectByteBuffer(env, (void *) address, capacity); +Java_app_alextran_immich_NativeBuffer_copy( + JNIEnv *env, jclass clazz, jobject buffer, jlong destAddress, jint offset, jint length) { + void *src = (*env)->GetDirectBufferAddress(env, buffer); + if (src != NULL) { + memcpy((void *) destAddress, (char *) src + offset, length); + } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/HttpSSLOptionsPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/HttpSSLOptionsPlugin.kt deleted file mode 100644 index 44d2aee2ce..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/HttpSSLOptionsPlugin.kt +++ /dev/null @@ -1,146 +0,0 @@ -package app.alextran.immich - -import android.annotation.SuppressLint -import android.content.Context -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import java.io.ByteArrayInputStream -import java.net.InetSocketAddress -import java.net.Socket -import java.security.KeyStore -import java.security.cert.X509Certificate -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.HttpsURLConnection -import javax.net.ssl.KeyManager -import javax.net.ssl.KeyManagerFactory -import javax.net.ssl.SSLContext -import javax.net.ssl.SSLEngine -import javax.net.ssl.SSLSession -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509ExtendedTrustManager - -/** - * Android plugin for Dart `HttpSSLOptions` - */ -class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { - private var methodChannel: MethodChannel? = null - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - methodChannel = MethodChannel(messenger, "immich/httpSSLOptions") - methodChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - try { - when (call.method) { - "apply" -> { - val args = call.arguments>()!! - - var tm: Array? = null - if (args[0] as Boolean) { - tm = arrayOf(AllowSelfSignedTrustManager(args[1] as? String)) - } - - var km: Array? = null - if (args[2] != null) { - val cert = ByteArrayInputStream(args[2] as ByteArray) - val password = (args[3] as String).toCharArray() - val keyStore = KeyStore.getInstance("PKCS12") - keyStore.load(cert, password) - val keyManagerFactory = - KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) - keyManagerFactory.init(keyStore, null) - km = keyManagerFactory.keyManagers - } - - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(km, tm, null) - HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) - - HttpsURLConnection.setDefaultHostnameVerifier(AllowSelfSignedHostnameVerifier(args[1] as? String)) - - result.success(true) - } - - else -> result.notImplemented() - } - } catch (e: Throwable) { - result.error("error", e.message, null) - } - } - - @SuppressLint("CustomX509TrustManager") - class AllowSelfSignedTrustManager(private val serverHost: String?) : X509ExtendedTrustManager() { - private val defaultTrustManager: X509ExtendedTrustManager = getDefaultTrustManager() - - override fun checkClientTrusted(chain: Array?, authType: String?) = - defaultTrustManager.checkClientTrusted(chain, authType) - - override fun checkClientTrusted( - chain: Array?, authType: String?, socket: Socket? - ) = defaultTrustManager.checkClientTrusted(chain, authType, socket) - - override fun checkClientTrusted( - chain: Array?, authType: String?, engine: SSLEngine? - ) = defaultTrustManager.checkClientTrusted(chain, authType, engine) - - override fun checkServerTrusted(chain: Array?, authType: String?) { - if (serverHost == null) return - defaultTrustManager.checkServerTrusted(chain, authType) - } - - override fun checkServerTrusted( - chain: Array?, authType: String?, socket: Socket? - ) { - if (serverHost == null) return - val socketAddress = socket?.remoteSocketAddress - if (socketAddress is InetSocketAddress && socketAddress.hostName == serverHost) return - defaultTrustManager.checkServerTrusted(chain, authType, socket) - } - - override fun checkServerTrusted( - chain: Array?, authType: String?, engine: SSLEngine? - ) { - if (serverHost == null || engine?.peerHost == serverHost) return - defaultTrustManager.checkServerTrusted(chain, authType, engine) - } - - override fun getAcceptedIssuers(): Array = defaultTrustManager.acceptedIssuers - - private fun getDefaultTrustManager(): X509ExtendedTrustManager { - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - factory.init(null as KeyStore?) - return factory.trustManagers.filterIsInstance().first() - } - } - - class AllowSelfSignedHostnameVerifier(private val serverHost: String?) : HostnameVerifier { - companion object { - private val _defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier() - } - - override fun verify(hostname: String?, session: SSLSession?): Boolean { - if (serverHost == null || hostname == serverHost) { - return true - } else { - return _defaultHostnameVerifier.verify(hostname, session) - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 4383b3098d..a85929a0e9 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -9,9 +9,13 @@ import app.alextran.immich.background.BackgroundWorkerFgHostApi import app.alextran.immich.background.BackgroundWorkerLockApi import app.alextran.immich.connectivity.ConnectivityApi import app.alextran.immich.connectivity.ConnectivityApiImpl +import app.alextran.immich.core.HttpClientManager import app.alextran.immich.core.ImmichPlugin -import app.alextran.immich.images.ThumbnailApi -import app.alextran.immich.images.ThumbnailsImpl +import app.alextran.immich.core.NetworkApiPlugin +import app.alextran.immich.images.LocalImageApi +import app.alextran.immich.images.LocalImagesImpl +import app.alextran.immich.images.RemoteImageApi +import app.alextran.immich.images.RemoteImagesImpl import app.alextran.immich.sync.NativeSyncApi import app.alextran.immich.sync.NativeSyncApiImpl26 import app.alextran.immich.sync.NativeSyncApiImpl30 @@ -26,6 +30,9 @@ class MainActivity : FlutterFragmentActivity() { companion object { fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) { + HttpClientManager.initialize(ctx) + flutterEngine.plugins.add(NetworkApiPlugin()) + val messenger = flutterEngine.dartExecutor.binaryMessenger val backgroundEngineLockImpl = BackgroundEngineLock(ctx) BackgroundWorkerLockApi.setUp(messenger, backgroundEngineLockImpl) @@ -36,12 +43,13 @@ class MainActivity : FlutterFragmentActivity() { NativeSyncApiImpl30(ctx) } NativeSyncApi.setUp(messenger, nativeSyncApiImpl) - ThumbnailApi.setUp(messenger, ThumbnailsImpl(ctx)) + LocalImageApi.setUp(messenger, LocalImagesImpl(ctx)) + RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx)) + BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx)) ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx)) flutterEngine.plugins.add(BackgroundServicePlugin()) - flutterEngine.plugins.add(HttpSSLOptionsPlugin()) flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(nativeSyncApiImpl) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/NativeBuffer.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/NativeBuffer.kt new file mode 100644 index 0000000000..a9011f3047 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/NativeBuffer.kt @@ -0,0 +1,52 @@ +package app.alextran.immich + +import java.nio.ByteBuffer + +const val INITIAL_BUFFER_SIZE = 32 * 1024 + +object NativeBuffer { + init { + System.loadLibrary("native_buffer") + } + + @JvmStatic + external fun allocate(size: Int): Long + + @JvmStatic + external fun free(address: Long) + + @JvmStatic + external fun realloc(address: Long, size: Int): Long + + @JvmStatic + external fun wrap(address: Long, capacity: Int): ByteBuffer + + @JvmStatic + external fun copy(buffer: ByteBuffer, destAddress: Long, offset: Int, length: Int) +} + +class NativeByteBuffer(initialCapacity: Int) { + var pointer = NativeBuffer.allocate(initialCapacity) + var capacity = initialCapacity + var offset = 0 + + inline fun ensureHeadroom() { + if (offset == capacity) { + capacity *= 2 + pointer = NativeBuffer.realloc(pointer, capacity) + } + } + + inline fun wrapRemaining() = NativeBuffer.wrap(pointer + offset, capacity - offset) + + inline fun advance(bytesRead: Int) { + offset += bytesRead + } + + inline fun free() { + if (pointer != 0L) { + NativeBuffer.free(pointer) + pointer = 0L + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt new file mode 100644 index 0000000000..ee92c2120e --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/HttpClientManager.kt @@ -0,0 +1,149 @@ +package app.alextran.immich.core + +import android.content.Context +import app.alextran.immich.BuildConfig +import okhttp3.Cache +import okhttp3.ConnectionPool +import okhttp3.Dispatcher +import okhttp3.OkHttpClient +import java.io.ByteArrayInputStream +import java.io.File +import java.net.Socket +import java.security.KeyStore +import java.security.Principal +import java.security.PrivateKey +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509KeyManager +import javax.net.ssl.X509TrustManager + +const val CERT_ALIAS = "client_cert" +const val USER_AGENT = "Immich_Android_${BuildConfig.VERSION_NAME}" + +/** + * Manages a shared OkHttpClient with SSL configuration support. + */ +object HttpClientManager { + private const val CACHE_SIZE_BYTES = 100L * 1024 * 1024 // 100MiB + private const val KEEP_ALIVE_CONNECTIONS = 10 + private const val KEEP_ALIVE_DURATION_MINUTES = 5L + private const val MAX_REQUESTS_PER_HOST = 64 + + private var initialized = false + private val clientChangedListeners = mutableListOf<() -> Unit>() + + private lateinit var client: OkHttpClient + + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + val isMtls: Boolean get() = keyStore.containsAlias(CERT_ALIAS) + + fun initialize(context: Context) { + if (initialized) return + synchronized(this) { + if (initialized) return + + val cacheDir = File(File(context.cacheDir, "okhttp"), "api") + client = build(cacheDir) + initialized = true + } + } + + fun setKeyEntry(clientData: ByteArray, password: CharArray) { + synchronized(this) { + val wasMtls = isMtls + val tmpKeyStore = KeyStore.getInstance("PKCS12").apply { + ByteArrayInputStream(clientData).use { stream -> load(stream, password) } + } + val tmpAlias = tmpKeyStore.aliases().asSequence().firstOrNull { tmpKeyStore.isKeyEntry(it) } + ?: throw IllegalArgumentException("No private key found in PKCS12") + val key = tmpKeyStore.getKey(tmpAlias, password) + val chain = tmpKeyStore.getCertificateChain(tmpAlias) + + if (wasMtls) { + keyStore.deleteEntry(CERT_ALIAS) + } + keyStore.setKeyEntry(CERT_ALIAS, key, null, chain) + if (wasMtls != isMtls) { + clientChangedListeners.forEach { it() } + } + } + } + + fun deleteKeyEntry() { + synchronized(this) { + if (!isMtls) { + return + } + + keyStore.deleteEntry(CERT_ALIAS) + clientChangedListeners.forEach { it() } + } + } + + @JvmStatic + fun getClient(): OkHttpClient { + return client + } + + fun addClientChangedListener(listener: () -> Unit) { + synchronized(this) { clientChangedListeners.add(listener) } + } + + private fun build(cacheDir: File): OkHttpClient { + val connectionPool = ConnectionPool( + maxIdleConnections = KEEP_ALIVE_CONNECTIONS, + keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES, + timeUnit = TimeUnit.MINUTES + ) + + val managerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + managerFactory.init(null as KeyStore?) + val trustManager = managerFactory.trustManagers.filterIsInstance().first() + + val sslContext = SSLContext.getInstance("TLS") + .apply { init(arrayOf(DynamicKeyManager()), arrayOf(trustManager), null) } + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) + + return OkHttpClient.Builder() + .addInterceptor { chain -> + chain.proceed(chain.request().newBuilder().header("User-Agent", USER_AGENT).build()) + } + .connectionPool(connectionPool) + .dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST }) + .cache(Cache(cacheDir.apply { mkdirs() }, CACHE_SIZE_BYTES)) + .sslSocketFactory(sslContext.socketFactory, trustManager) + .build() + } + + // Reads from the key store rather than taking a snapshot at initialization time + private class DynamicKeyManager : X509KeyManager { + override fun getClientAliases(keyType: String, issuers: Array?): Array? = + if (isMtls) arrayOf(CERT_ALIAS) else null + + override fun chooseClientAlias( + keyTypes: Array, + issuers: Array?, + socket: Socket? + ): String? = + if (isMtls) CERT_ALIAS else null + + override fun getCertificateChain(alias: String): Array? = + keyStore.getCertificateChain(alias)?.map { it as X509Certificate }?.toTypedArray() + + override fun getPrivateKey(alias: String): PrivateKey? = + keyStore.getKey(alias, null) as? PrivateKey + + override fun getServerAliases(keyType: String, issuers: Array?): Array? = + null + + override fun chooseServerAlias( + keyType: String, + issuers: Array?, + socket: Socket? + ): String? = null + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt new file mode 100644 index 0000000000..1e7156a147 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt @@ -0,0 +1,253 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.core + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object NetworkPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ClientCertData ( + val data: ByteArray, + val password: String +) + { + companion object { + fun fromList(pigeonVar_list: List): ClientCertData { + val data = pigeonVar_list[0] as ByteArray + val password = pigeonVar_list[1] as String + return ClientCertData(data, password) + } + } + fun toList(): List { + return listOf( + data, + password, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is ClientCertData) { + return false + } + if (this === other) { + return true + } + return NetworkPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class ClientCertPrompt ( + val title: String, + val message: String, + val cancel: String, + val confirm: String +) + { + companion object { + fun fromList(pigeonVar_list: List): ClientCertPrompt { + val title = pigeonVar_list[0] as String + val message = pigeonVar_list[1] as String + val cancel = pigeonVar_list[2] as String + val confirm = pigeonVar_list[3] as String + return ClientCertPrompt(title, message, cancel, confirm) + } + } + fun toList(): List { + return listOf( + title, + message, + cancel, + confirm, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is ClientCertPrompt) { + return false + } + if (this === other) { + return true + } + return NetworkPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class NetworkPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + ClientCertData.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + ClientCertPrompt.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is ClientCertData -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is ClientCertPrompt -> { + stream.write(130) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface NetworkApi { + fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) + fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) + fun removeCertificate(callback: (Result) -> Unit) + + companion object { + /** The codec used by NetworkApi. */ + val codec: MessageCodec by lazy { + NetworkPigeonCodec() + } + /** Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val clientDataArg = args[0] as ClientCertData + api.addCertificate(clientDataArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(NetworkPigeonUtils.wrapError(error)) + } else { + reply.reply(NetworkPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val promptTextArg = args[0] as ClientCertPrompt + api.selectCertificate(promptTextArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(NetworkPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(NetworkPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.removeCertificate{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(NetworkPigeonUtils.wrapError(error)) + } else { + reply.reply(NetworkPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt new file mode 100644 index 0000000000..4f25896b2f --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/NetworkApiPlugin.kt @@ -0,0 +1,159 @@ +package app.alextran.immich.core + +import android.app.Activity +import android.content.Context +import android.net.Uri +import android.os.OperationCanceledException +import android.text.InputType +import android.view.ContextThemeWrapper +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding + +class NetworkApiPlugin : FlutterPlugin, ActivityAware { + private var networkApi: NetworkApiImpl? = null + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + networkApi = NetworkApiImpl(binding.applicationContext) + NetworkApi.setUp(binding.binaryMessenger, networkApi) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + NetworkApi.setUp(binding.binaryMessenger, null) + networkApi = null + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + networkApi?.onAttachedToActivity(binding) + } + + override fun onDetachedFromActivityForConfigChanges() { + networkApi?.onDetachedFromActivityForConfigChanges() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + networkApi?.onReattachedToActivityForConfigChanges(binding) + } + + override fun onDetachedFromActivity() { + networkApi?.onDetachedFromActivity() + } +} + +private class NetworkApiImpl(private val context: Context) : NetworkApi { + private var activity: Activity? = null + private var pendingCallback: ((Result) -> Unit)? = null + private var filePicker: ActivityResultLauncher>? = null + private var promptText: ClientCertPrompt? = null + + fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + (binding.activity as? ComponentActivity)?.let { componentActivity -> + filePicker = componentActivity.registerForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> uri?.let { handlePickedFile(it) } ?: pendingCallback?.invoke(Result.failure(OperationCanceledException())) } + } + } + + fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + } + + fun onDetachedFromActivity() { + activity = null + } + + override fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) { + try { + HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray()) + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) { + val picker = filePicker ?: return callback(Result.failure(IllegalStateException("No activity"))) + pendingCallback = callback + this.promptText = promptText + picker.launch(arrayOf("application/x-pkcs12", "application/x-pem-file")) + } + + override fun removeCertificate(callback: (Result) -> Unit) { + HttpClientManager.deleteKeyEntry() + callback(Result.success(Unit)) + } + + private fun handlePickedFile(uri: Uri) { + val callback = pendingCallback ?: return + pendingCallback = null + + try { + val data = context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + ?: throw IllegalStateException("Could not read file") + + val activity = activity ?: throw IllegalStateException("No activity") + promptForPassword(activity) { password -> + promptText = null + if (password == null) { + callback(Result.failure(OperationCanceledException())) + return@promptForPassword + } + try { + HttpClientManager.setKeyEntry(data, password.toCharArray()) + callback(Result.success(ClientCertData(data, password))) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + private fun promptForPassword(activity: Activity, callback: (String?) -> Unit) { + val themedContext = ContextThemeWrapper(activity, com.google.android.material.R.style.Theme_Material3_DayNight_Dialog) + val density = activity.resources.displayMetrics.density + val horizontalPadding = (24 * density).toInt() + + val textInputLayout = TextInputLayout(themedContext).apply { + hint = "Password" + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply { + setMargins(horizontalPadding, 0, horizontalPadding, 0) + } + } + + val editText = TextInputEditText(textInputLayout.context).apply { + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) + } + textInputLayout.addView(editText) + + val container = FrameLayout(themedContext).apply { addView(textInputLayout) } + + val text = promptText!! + MaterialAlertDialogBuilder(themedContext) + .setTitle(text.title) + .setMessage(text.message) + .setView(container) + .setPositiveButton(text.confirm) { _, _ -> callback(editText.text.toString()) } + .setNegativeButton(text.cancel) { _, _ -> callback(null) } + .setOnCancelListener { callback(null) } + .show() + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt similarity index 74% rename from mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt index ae2cca4d7b..7d998c2f48 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt @@ -13,7 +13,7 @@ import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -private object ThumbnailsPigeonUtils { +private object LocalImagesPigeonUtils { fun wrapResult(result: Any?): List { return listOf(result) @@ -47,7 +47,7 @@ class FlutterError ( override val message: String? = null, val details: Any? = null ) : Throwable() -private open class ThumbnailsPigeonCodec : StandardMessageCodec() { +private open class LocalImagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return super.readValueOfType(type, buffer) } @@ -58,22 +58,22 @@ private open class ThumbnailsPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface ThumbnailApi { - fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result>) -> Unit) - fun cancelImageRequest(requestId: Long) +interface LocalImageApi { + fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result?>) -> Unit) + fun cancelRequest(requestId: Long) fun getThumbhash(thumbhash: String, callback: (Result>) -> Unit) companion object { - /** The codec used by ThumbnailApi. */ + /** The codec used by LocalImageApi. */ val codec: MessageCodec by lazy { - ThumbnailsPigeonCodec() + LocalImagesPigeonCodec() } - /** Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`. */ + /** Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") { + fun setUp(binaryMessenger: BinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List @@ -82,13 +82,14 @@ interface ThumbnailApi { val widthArg = args[2] as Long val heightArg = args[3] as Long val isVideoArg = args[4] as Boolean - api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result> -> + val preferEncodedArg = args[5] as Boolean + api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result?> -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(ThumbnailsPigeonUtils.wrapError(error)) + reply.reply(LocalImagesPigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(ThumbnailsPigeonUtils.wrapResult(data)) + reply.reply(LocalImagesPigeonUtils.wrapResult(data)) } } } @@ -97,16 +98,16 @@ interface ThumbnailApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List val requestIdArg = args[0] as Long val wrapped: List = try { - api.cancelImageRequest(requestIdArg) + api.cancelRequest(requestIdArg) listOf(null) } catch (exception: Throwable) { - ThumbnailsPigeonUtils.wrapError(exception) + LocalImagesPigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -115,7 +116,7 @@ interface ThumbnailApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> val args = message as List @@ -123,10 +124,10 @@ interface ThumbnailApi { api.getThumbhash(thumbhashArg) { result: Result> -> val error = result.exceptionOrNull() if (error != null) { - reply.reply(ThumbnailsPigeonUtils.wrapError(error)) + reply.reply(LocalImagesPigeonUtils.wrapError(error)) } else { val data = result.getOrNull() - reply.reply(ThumbnailsPigeonUtils.wrapResult(data)) + reply.reply(LocalImagesPigeonUtils.wrapResult(data)) } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt similarity index 67% rename from mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt index a9d602c19c..3babad2e37 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbnailsImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt @@ -11,8 +11,10 @@ import android.os.OperationCanceledException import android.provider.MediaStore.Images import android.provider.MediaStore.Video import android.util.Size -import java.nio.ByteBuffer +import androidx.annotation.RequiresApi +import app.alextran.immich.NativeBuffer import kotlin.math.* +import java.io.IOException import java.util.concurrent.Executors import com.bumptech.glide.Glide import com.bumptech.glide.Priority @@ -26,10 +28,42 @@ import java.util.concurrent.Future data class Request( val taskFuture: Future<*>, val cancellationSignal: CancellationSignal, - val callback: (Result>) -> Unit + val callback: (Result?>) -> Unit ) -class ThumbnailsImpl(context: Context) : ThumbnailApi { +@RequiresApi(Build.VERSION_CODES.Q) +inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap { + return ImageDecoder.decodeBitmap(this) { decoder, info, _ -> + if (target.width > 0 && target.height > 0) { + val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) + decoder.setTargetSampleSize(sample) + } + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)) + } +} + +fun Bitmap.toNativeBuffer(): Map { + val size = width * height * 4 + val pointer = NativeBuffer.allocate(size) + try { + val buffer = NativeBuffer.wrap(pointer, size) + copyPixelsToBuffer(buffer) + return mapOf( + "pointer" to pointer, + "width" to width.toLong(), + "height" to height.toLong(), + "rowBytes" to (width * 4).toLong() + ) + } catch (e: Exception) { + NativeBuffer.free(pointer) + throw e + } finally { + recycle() + } +} + +class LocalImagesImpl(context: Context) : LocalImageApi { private val ctx: Context = context.applicationContext private val resolver: ContentResolver = ctx.contentResolver private val requestThread = Executors.newSingleThreadExecutor() @@ -38,21 +72,8 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { private val requestMap = ConcurrentHashMap() companion object { - val CANCELLED = Result.success>(mapOf()) + val CANCELLED = Result.success?>(null) val OPTIONS = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 } - - init { - System.loadLibrary("native_buffer") - } - - @JvmStatic - external fun allocateNative(size: Int): Long - - @JvmStatic - external fun freeNative(pointer: Long) - - @JvmStatic - external fun wrapAsBuffer(address: Long, capacity: Int): ByteBuffer } override fun getThumbhash(thumbhash: String, callback: (Result>) -> Unit) { @@ -63,7 +84,8 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { val res = mapOf( "pointer" to image.pointer, "width" to image.width.toLong(), - "height" to image.height.toLong() + "height" to image.height.toLong(), + "rowBytes" to (image.width * 4).toLong() ) callback(Result.success(res)) } catch (e: Exception) { @@ -78,12 +100,17 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { width: Long, height: Long, isVideo: Boolean, - callback: (Result>) -> Unit + preferEncoded: Boolean, + callback: (Result?>) -> Unit ) { val signal = CancellationSignal() val task = threadPool.submit { try { - getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal) + if (preferEncoded) { + getEncodedImageInternal(assetId, callback, signal) + } else { + getThumbnailBufferInternal(assetId, width, height, isVideo, callback, signal) + } } catch (e: Exception) { when (e) { is OperationCanceledException -> callback(CANCELLED) @@ -98,7 +125,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { requestMap[requestId] = request } - override fun cancelImageRequest(requestId: Long) { + override fun cancelRequest(requestId: Long) { val request = requestMap.remove(requestId) ?: return request.taskFuture.cancel(false) request.cancellationSignal.cancel() @@ -112,12 +139,41 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { } } + private fun getEncodedImageInternal( + assetId: String, + callback: (Result?>) -> Unit, + signal: CancellationSignal + ) { + signal.throwIfCanceled() + val id = assetId.toLong() + val uri = ContentUris.withAppendedId(Images.Media.EXTERNAL_CONTENT_URI, id) + + signal.throwIfCanceled() + val bytes = resolver.openInputStream(uri)?.use { it.readBytes() } + ?: throw IOException("Could not read image data for $assetId") + + signal.throwIfCanceled() + val pointer = NativeBuffer.allocate(bytes.size) + try { + val buffer = NativeBuffer.wrap(pointer, bytes.size) + buffer.put(bytes) + signal.throwIfCanceled() + callback(Result.success(mapOf( + "pointer" to pointer, + "length" to bytes.size.toLong() + ))) + } catch (e: Exception) { + NativeBuffer.free(pointer) + throw e + } + } + private fun getThumbnailBufferInternal( assetId: String, width: Long, height: Long, isVideo: Boolean, - callback: (Result>) -> Unit, + callback: (Result?>) -> Unit, signal: CancellationSignal ) { signal.throwIfCanceled() @@ -131,31 +187,12 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { decodeImage(id, size, signal) } - processBitmap(bitmap, callback, signal) - } - - private fun processBitmap( - bitmap: Bitmap, callback: (Result>) -> Unit, signal: CancellationSignal - ) { - signal.throwIfCanceled() - val actualWidth = bitmap.width - val actualHeight = bitmap.height - - val size = actualWidth * actualHeight * 4 - val pointer = allocateNative(size) - try { signal.throwIfCanceled() - val buffer = wrapAsBuffer(pointer, size) - bitmap.copyPixelsToBuffer(buffer) - bitmap.recycle() + val res = bitmap.toNativeBuffer() signal.throwIfCanceled() - val res = mapOf( - "pointer" to pointer, "width" to actualWidth.toLong(), "height" to actualHeight.toLong() - ) callback(Result.success(res)) } catch (e: Exception) { - freeNative(pointer) callback(if (e is OperationCanceledException) CANCELLED else Result.failure(e)) } } @@ -191,16 +228,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { private fun decodeSource(uri: Uri, target: Size, signal: CancellationSignal): Bitmap { signal.throwIfCanceled() return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val source = ImageDecoder.createSource(resolver, uri) - signal.throwIfCanceled() - ImageDecoder.decodeBitmap(source) { decoder, info, _ -> - if (target.width > 0 && target.height > 0) { - val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) - decoder.setTargetSampleSize(sample) - } - decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE - decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)) - } + ImageDecoder.createSource(resolver, uri).decodeBitmap(target) } else { val ref = Glide.with(ctx).asBitmap().priority(Priority.IMMEDIATE).load(uri).disallowHardwareConfig() diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt new file mode 100644 index 0000000000..a04dedb676 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt @@ -0,0 +1,124 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.images + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object RemoteImagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } +} +private open class RemoteImagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return super.readValueOfType(type, buffer) + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + super.writeValue(stream, value) + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface RemoteImageApi { + fun requestImage(url: String, headers: Map, requestId: Long, preferEncoded: Boolean, callback: (Result?>) -> Unit) + fun cancelRequest(requestId: Long) + fun clearCache(callback: (Result) -> Unit) + + companion object { + /** The codec used by RemoteImageApi. */ + val codec: MessageCodec by lazy { + RemoteImagesPigeonCodec() + } + /** Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val urlArg = args[0] as String + val headersArg = args[1] as Map + val requestIdArg = args[2] as Long + val preferEncodedArg = args[3] as Boolean + api.requestImage(urlArg, headersArg, requestIdArg, preferEncodedArg) { result: Result?> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(RemoteImagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val requestIdArg = args[0] as Long + val wrapped: List = try { + api.cancelRequest(requestIdArg) + listOf(null) + } catch (exception: Throwable) { + RemoteImagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.clearCache{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(RemoteImagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt new file mode 100644 index 0000000000..6b15f33414 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -0,0 +1,483 @@ +package app.alextran.immich.images + +import android.content.Context +import android.os.CancellationSignal +import android.os.OperationCanceledException +import app.alextran.immich.INITIAL_BUFFER_SIZE +import app.alextran.immich.NativeBuffer +import app.alextran.immich.NativeByteBuffer +import app.alextran.immich.core.HttpClientManager +import app.alextran.immich.core.USER_AGENT +import kotlinx.coroutines.* +import okhttp3.Cache +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.chromium.net.CronetEngine +import org.chromium.net.CronetException +import org.chromium.net.UrlRequest +import org.chromium.net.UrlResponseInfo +import java.io.EOFException +import java.io.File +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors + + +private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024 + +private class RemoteRequest(val cancellationSignal: CancellationSignal) + +class RemoteImagesImpl(context: Context) : RemoteImageApi { + private val requestMap = ConcurrentHashMap() + + init { + ImageFetcherManager.initialize(context) + } + + companion object { + val CANCELLED = Result.success?>(null) + } + + override fun requestImage( + url: String, + headers: Map, + requestId: Long, + @Suppress("UNUSED_PARAMETER") preferEncoded: Boolean, // always returns encoded; setting has no effect on Android + callback: (Result?>) -> Unit + ) { + val signal = CancellationSignal() + requestMap[requestId] = RemoteRequest(signal) + + ImageFetcherManager.fetch( + url, + headers, + signal, + onSuccess = { buffer -> + requestMap.remove(requestId) + if (signal.isCanceled) { + NativeBuffer.free(buffer.pointer) + return@fetch callback(CANCELLED) + } + + callback( + Result.success( + mapOf( + "pointer" to buffer.pointer, + "length" to buffer.offset.toLong() + ) + ) + ) + }, + onFailure = { e -> + requestMap.remove(requestId) + val result = if (signal.isCanceled) CANCELLED else Result.failure(e) + callback(result) + } + ) + } + + override fun cancelRequest(requestId: Long) { + requestMap.remove(requestId)?.cancellationSignal?.cancel() + } + + override fun clearCache(callback: (Result) -> Unit) { + CoroutineScope(Dispatchers.IO).launch { + try { + ImageFetcherManager.clearCache(callback) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + } +} + +private object ImageFetcherManager { + private lateinit var appContext: Context + private lateinit var cacheDir: File + private lateinit var fetcher: ImageFetcher + private var initialized = false + + fun initialize(context: Context) { + if (initialized) return + synchronized(this) { + if (initialized) return + appContext = context.applicationContext + cacheDir = context.cacheDir + fetcher = build() + HttpClientManager.addClientChangedListener(::invalidate) + initialized = true + } + } + + fun fetch( + url: String, + headers: Map, + signal: CancellationSignal, + onSuccess: (NativeByteBuffer) -> Unit, + onFailure: (Exception) -> Unit, + ) { + fetcher.fetch(url, headers, signal, onSuccess, onFailure) + } + + fun clearCache(onCleared: (Result) -> Unit) { + fetcher.clearCache(onCleared) + } + + private fun invalidate() { + synchronized(this) { + val oldFetcher = fetcher + fetcher = build() + oldFetcher.drain() + } + } + + private fun build(): ImageFetcher { + return if (HttpClientManager.isMtls) { + OkHttpImageFetcher.create(cacheDir) + } else { + CronetImageFetcher(appContext, cacheDir) + } + } +} + +private sealed interface ImageFetcher { + fun fetch( + url: String, + headers: Map, + signal: CancellationSignal, + onSuccess: (NativeByteBuffer) -> Unit, + onFailure: (Exception) -> Unit, + ) + + fun drain() + + fun clearCache(onCleared: (Result) -> Unit) +} + +private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetcher { + private val ctx = context + private var engine: CronetEngine + private val executor = Executors.newFixedThreadPool(4) + private val stateLock = Any() + private var activeCount = 0 + private var draining = false + private var onCacheCleared: ((Result) -> Unit)? = null + private val storageDir = File(cacheDir, "cronet").apply { mkdirs() } + + init { + engine = build(context) + } + + override fun fetch( + url: String, + headers: Map, + signal: CancellationSignal, + onSuccess: (NativeByteBuffer) -> Unit, + onFailure: (Exception) -> Unit, + ) { + synchronized(stateLock) { + if (draining) { + onFailure(IllegalStateException("Engine is draining")) + return + } + activeCount++ + } + + val callback = FetchCallback(onSuccess, onFailure, ::onComplete) + val requestBuilder = engine.newUrlRequestBuilder(url, callback, executor) + headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) } + val request = requestBuilder.build() + signal.setOnCancelListener(request::cancel) + request.start() + } + + private fun build(ctx: Context): CronetEngine { + return CronetEngine.Builder(ctx) + .enableHttp2(true) + .enableQuic(true) + .enableBrotli(true) + .setStoragePath(storageDir.absolutePath) + .setUserAgent(USER_AGENT) + .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, CACHE_SIZE_BYTES) + .build() + } + + private fun onComplete() { + val didDrain = synchronized(stateLock) { + activeCount-- + draining && activeCount == 0 + } + if (didDrain) { + onDrained() + } + } + + override fun drain() { + val didDrain = synchronized(stateLock) { + if (draining) return + draining = true + activeCount == 0 + } + if (didDrain) { + onDrained() + } + } + + private fun onDrained() { + engine.shutdown() + val onCacheCleared = synchronized(stateLock) { + val onCacheCleared = onCacheCleared + this.onCacheCleared = null + onCacheCleared + } + if (onCacheCleared == null) { + executor.shutdown() + } else { + CoroutineScope(Dispatchers.IO).launch { + val result = runCatching { deleteFolderAndGetSize(storageDir.toPath()) } + // Cronet is very good at self-repair, so it shouldn't fail here regardless of clear result + engine = build(ctx) + synchronized(stateLock) { draining = false } + onCacheCleared(result) + } + } + } + + override fun clearCache(onCleared: (Result) -> Unit) { + synchronized(stateLock) { + if (onCacheCleared != null) { + return onCleared(Result.success(-1)) + } + onCacheCleared = onCleared + } + drain() + } + + private class FetchCallback( + private val onSuccess: (NativeByteBuffer) -> Unit, + private val onFailure: (Exception) -> Unit, + private val onComplete: () -> Unit, + ) : UrlRequest.Callback() { + private var buffer: NativeByteBuffer? = null + private var wrapped: ByteBuffer? = null + private var error: Exception? = null + + override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newUrl: String) { + request.followRedirect() + } + + override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) { + if (info.httpStatusCode !in 200..299) { + error = IOException("HTTP ${info.httpStatusCode}: ${info.httpStatusText}") + return request.cancel() + } + + try { + val contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0 + if (contentLength > 0) { + buffer = NativeByteBuffer(contentLength + 1) + wrapped = NativeBuffer.wrap(buffer!!.pointer, contentLength + 1) + request.read(wrapped) + } else { + buffer = NativeByteBuffer(INITIAL_BUFFER_SIZE) + request.read(buffer!!.wrapRemaining()) + } + } catch (e: Exception) { + error = e + return request.cancel() + } + } + + override fun onReadCompleted( + request: UrlRequest, + info: UrlResponseInfo, + byteBuffer: ByteBuffer + ) { + try { + val buf = if (wrapped == null) { + buffer!!.run { + advance(byteBuffer.position()) + ensureHeadroom() + wrapRemaining() + } + } else { + wrapped + } + request.read(buf) + } catch (e: Exception) { + error = e + return request.cancel() + } + } + + override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) { + wrapped?.let { buffer!!.advance(it.position()) } + onSuccess(buffer!!) + onComplete() + } + + override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) { + buffer?.free() + onFailure(error) + onComplete() + } + + override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) { + buffer?.free() + onFailure(error ?: OperationCanceledException()) + onComplete() + } + } + + suspend fun deleteFolderAndGetSize(root: Path): Long = withContext(Dispatchers.IO) { + var totalSize = 0L + + Files.walkFileTree(root, object : SimpleFileVisitor() { + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + totalSize += attrs.size() + Files.delete(file) + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + if (dir != root) { + Files.delete(dir) + } + return FileVisitResult.CONTINUE + } + }) + + totalSize + } +} + +private class OkHttpImageFetcher private constructor( + private val client: OkHttpClient, +) : ImageFetcher { + private val stateLock = Any() + private var activeCount = 0 + private var draining = false + + companion object { + fun create(cacheDir: File): OkHttpImageFetcher { + val dir = File(cacheDir, "okhttp") + + val client = HttpClientManager.getClient().newBuilder() + .cache(Cache(File(dir, "thumbnails"), CACHE_SIZE_BYTES)) + .build() + + return OkHttpImageFetcher(client) + } + } + + private fun onComplete() { + val shouldClose = synchronized(stateLock) { + activeCount-- + draining && activeCount == 0 + } + if (shouldClose) { + client.cache?.close() + } + } + + override fun fetch( + url: String, + headers: Map, + signal: CancellationSignal, + onSuccess: (NativeByteBuffer) -> Unit, + onFailure: (Exception) -> Unit, + ) { + synchronized(stateLock) { + if (draining) { + return onFailure(IllegalStateException("Client is draining")) + } + activeCount++ + } + + val requestBuilder = Request.Builder().url(url) + headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) } + val call = client.newCall(requestBuilder.build()) + signal.setOnCancelListener(call::cancel) + + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + onFailure(e) + onComplete() + } + + override fun onResponse(call: Call, response: Response) { + response.use { + if (!response.isSuccessful) { + return onFailure(IOException("HTTP ${response.code}: ${response.message}")).also { onComplete() } + } + + val body = response.body + ?: return onFailure(IOException("Empty response body")).also { onComplete() } + + if (call.isCanceled()) { + onFailure(OperationCanceledException()) + return onComplete() + } + + body.source().use { source -> + val length = body.contentLength().toInt() + val buffer = NativeByteBuffer(if (length > 0) length else INITIAL_BUFFER_SIZE) + try { + if (length > 0) { + val wrapped = NativeBuffer.wrap(buffer.pointer, length) + while (wrapped.hasRemaining()) { + if (call.isCanceled()) throw OperationCanceledException() + if (source.read(wrapped) == -1) throw EOFException() + } + buffer.advance(length) + } else { + while (true) { + if (call.isCanceled()) throw OperationCanceledException() + val bytesRead = source.read(buffer.wrapRemaining()) + if (bytesRead == -1) break + buffer.advance(bytesRead) + buffer.ensureHeadroom() + } + } + onSuccess(buffer) + } catch (e: Exception) { + buffer.free() + onFailure(e) + } + onComplete() + } + } + } + }) + } + + override fun drain() { + val shouldClose = synchronized(stateLock) { + if (draining) return + draining = true + activeCount == 0 + } + if (shouldClose) { + client.cache?.close() + } + } + + override fun clearCache(onCleared: (Result) -> Unit) { + try { + val size = client.cache!!.size() + client.cache!!.evictAll() + onCleared(Result.success(size)) + } catch (e: Exception) { + onCleared(Result.failure(e)) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbHash.java b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbHash.java index 3af76b5763..02b11b61da 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbHash.java +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/ThumbHash.java @@ -7,6 +7,8 @@ package app.alextran.immich.images; import java.nio.ByteBuffer; +import app.alextran.immich.NativeBuffer; + // modified to use native allocations public final class ThumbHash { /** @@ -56,8 +58,8 @@ public final class ThumbHash { int w = Math.round(ratio > 1.0f ? 32.0f : 32.0f * ratio); int h = Math.round(ratio > 1.0f ? 32.0f / ratio : 32.0f); int size = w * h * 4; - long pointer = ThumbnailsImpl.allocateNative(size); - ByteBuffer rgba = ThumbnailsImpl.wrapAsBuffer(pointer, size); + long pointer = NativeBuffer.allocate(size); + ByteBuffer rgba = NativeBuffer.wrap(pointer, size); int cx_stop = Math.max(lx, hasAlpha ? 5 : 3); int cy_stop = Math.max(ly, hasAlpha ? 5 : 3); float[] fx = new float[cx_stop]; 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 d3282f4dfd..29c197c2b6 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 @@ -78,6 +78,21 @@ class FlutterError ( val details: Any? = null ) : Throwable() +enum class PlatformAssetPlaybackStyle(val raw: Int) { + UNKNOWN(0), + IMAGE(1), + VIDEO(2), + IMAGE_ANIMATED(3), + LIVE_PHOTO(4), + VIDEO_LOOPING(5); + + companion object { + fun ofRaw(raw: Int): PlatformAssetPlaybackStyle? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class PlatformAsset ( val id: String, @@ -92,7 +107,8 @@ data class PlatformAsset ( val isFavorite: Boolean, val adjustmentTime: Long? = null, val latitude: Double? = null, - val longitude: Double? = null + val longitude: Double? = null, + val playbackStyle: PlatformAssetPlaybackStyle ) { companion object { @@ -110,7 +126,8 @@ data class PlatformAsset ( val adjustmentTime = pigeonVar_list[10] as Long? val latitude = pigeonVar_list[11] as Double? val longitude = pigeonVar_list[12] as Double? - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude) + val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle + return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle) } } fun toList(): List { @@ -128,6 +145,7 @@ data class PlatformAsset ( adjustmentTime, latitude, longitude, + playbackStyle, ) } override fun equals(other: Any?): Boolean { @@ -252,50 +270,102 @@ data class HashResult ( override fun hashCode(): Int = toList().hashCode() } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class CloudIdResult ( + val assetId: String, + val error: String? = null, + val cloudId: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): CloudIdResult { + val assetId = pigeonVar_list[0] as String + val error = pigeonVar_list[1] as String? + val cloudId = pigeonVar_list[2] as String? + return CloudIdResult(assetId, error, cloudId) + } + } + fun toList(): List { + return listOf( + assetId, + error, + cloudId, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is CloudIdResult) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlatformAsset.fromList(it) + return (readValue(buffer) as Long?)?.let { + PlatformAssetPlaybackStyle.ofRaw(it.toInt()) } } 130.toByte() -> { return (readValue(buffer) as? List)?.let { - PlatformAlbum.fromList(it) + PlatformAsset.fromList(it) } } 131.toByte() -> { return (readValue(buffer) as? List)?.let { - SyncDelta.fromList(it) + PlatformAlbum.fromList(it) } } 132.toByte() -> { + return (readValue(buffer) as? List)?.let { + SyncDelta.fromList(it) + } + } + 133.toByte() -> { return (readValue(buffer) as? List)?.let { HashResult.fromList(it) } } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { + CloudIdResult.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is PlatformAsset -> { + is PlatformAssetPlaybackStyle -> { stream.write(129) - writeValue(stream, value.toList()) + writeValue(stream, value.raw) } - is PlatformAlbum -> { + is PlatformAsset -> { stream.write(130) writeValue(stream, value.toList()) } - is SyncDelta -> { + is PlatformAlbum -> { stream.write(131) writeValue(stream, value.toList()) } - is HashResult -> { + is SyncDelta -> { stream.write(132) writeValue(stream, value.toList()) } + is HashResult -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is CloudIdResult -> { + stream.write(134) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -315,6 +385,7 @@ interface NativeSyncApi { fun hashAssets(assetIds: List, allowNetworkAccess: Boolean, callback: (Result>) -> Unit) fun cancelHashing() fun getTrashedAssets(): Map> + fun getCloudIdForAssetIds(assetIds: List): List companion object { /** The codec used by NativeSyncApi. */ @@ -508,6 +579,23 @@ interface NativeSyncApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val assetIdsArg = args[0] as List + val wrapped: List = try { + listOf(api.getCloudIdForAssetIds(assetIdsArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } 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 b374ef50f0..173d81613a 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 @@ -4,16 +4,23 @@ import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.database.Cursor +import androidx.exifinterface.media.ExifInterface +import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.util.Base64 +import android.util.Log import androidx.core.database.getStringOrNull import app.alextran.immich.core.ImmichPlugin +import com.bumptech.glide.Glide +import com.bumptech.glide.load.ImageHeaderParser +import com.bumptech.glide.load.ImageHeaderParserUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore @@ -21,13 +28,14 @@ import kotlinx.coroutines.sync.withPermit import java.io.File import java.security.MessageDigest import kotlin.coroutines.cancellation.CancellationException -import kotlin.coroutines.coroutineContext sealed class AssetResult { data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() data class InvalidAsset(val assetId: String) : AssetResult() } +private const val TAG = "NativeSyncApiImplBase" + @SuppressLint("InlinedApi") open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { private val ctx: Context = context.applicationContext @@ -39,6 +47,13 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { private val hashSemaphore = Semaphore(MAX_CONCURRENT_HASH_OPERATIONS) private const val HASHING_CANCELLED_CODE = "HASH_CANCELLED" + // MediaStore.Files.FileColumns.SPECIAL_FORMAT — S Extensions 21+ + // https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT + private const val SPECIAL_FORMAT_COLUMN = "_special_format" + private const val SPECIAL_FORMAT_GIF = 1 + private const val SPECIAL_FORMAT_MOTION_PHOTO = 2 + private const val SPECIAL_FORMAT_ANIMATED_WEBP = 3 + const val MEDIA_SELECTION = "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" val MEDIA_SELECTION_ARGS = arrayOf( @@ -60,9 +75,15 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { add(MediaStore.MediaColumns.DURATION) add(MediaStore.MediaColumns.ORIENTATION) // IS_FAVORITE is only available on Android 11 and above - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { add(MediaStore.MediaColumns.IS_FAVORITE) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add(SPECIAL_FORMAT_COLUMN) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Fallback: read XMP from MediaStore to detect Motion Photos + add(MediaStore.MediaColumns.XMP) + } }.toTypedArray() const val HASH_BUFFER_SIZE = 2 * 1024 * 1024 @@ -109,9 +130,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { val orientationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION) val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE) + val specialFormatColumn = c.getColumnIndex(SPECIAL_FORMAT_COLUMN) + val xmpColumn = c.getColumnIndex(MediaStore.MediaColumns.XMP) while (c.moveToNext()) { - val id = c.getLong(idColumn).toString() + val numericId = c.getLong(idColumn) + val id = numericId.toString() val name = c.getStringOrNull(nameColumn) val bucketId = c.getStringOrNull(bucketIdColumn) val path = c.getStringOrNull(dataColumn) @@ -125,10 +149,11 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { continue } - val mediaType = when (c.getInt(mediaTypeColumn)) { - MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1 - MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2 - else -> 0 + val rawMediaType = c.getInt(mediaTypeColumn) + val assetType: Long = when (rawMediaType) { + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> 1L + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> 2L + else -> 0L } // Date taken is milliseconds since epoch, Date added is seconds since epoch val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000)) @@ -138,15 +163,19 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { val width = c.getInt(widthColumn).toLong() val height = c.getInt(heightColumn).toLong() // Duration is milliseconds - val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 + val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L else c.getLong(durationColumn) / 1000 val orientation = c.getInt(orientationColumn) val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0 + val playbackStyle = detectPlaybackStyle( + numericId, rawMediaType, specialFormatColumn, xmpColumn, c + ) + val asset = PlatformAsset( id, name, - mediaType.toLong(), + assetType, createdAt, modifiedAt, width, @@ -154,6 +183,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { duration, orientation.toLong(), isFavorite, + playbackStyle = playbackStyle, ) yield(AssetResult.ValidAsset(asset, bucketId)) } @@ -161,6 +191,81 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { } } + /** + * Detects the playback style for an asset using _special_format (API 33+) + * or XMP / MIME / RIFF header fallbacks (pre-33). + */ + @SuppressLint("NewApi") + private fun detectPlaybackStyle( + assetId: Long, + rawMediaType: Int, + specialFormatColumn: Int, + xmpColumn: Int, + cursor: Cursor + ): PlatformAssetPlaybackStyle { + // video currently has no special formats, so we can short circuit and avoid unnecessary work + if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO) { + return PlatformAssetPlaybackStyle.VIDEO + } + + // API 33+: use _special_format from cursor + if (specialFormatColumn != -1) { + val specialFormat = cursor.getInt(specialFormatColumn) + return when { + specialFormat == SPECIAL_FORMAT_MOTION_PHOTO -> PlatformAssetPlaybackStyle.LIVE_PHOTO + specialFormat == SPECIAL_FORMAT_GIF || specialFormat == SPECIAL_FORMAT_ANIMATED_WEBP -> PlatformAssetPlaybackStyle.IMAGE_ANIMATED + rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> PlatformAssetPlaybackStyle.IMAGE + else -> PlatformAssetPlaybackStyle.UNKNOWN + } + } + + if (rawMediaType != MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) { + return PlatformAssetPlaybackStyle.UNKNOWN + } + + // Pre-API 33 fallback + val uri = ContentUris.withAppendedId( + MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), + assetId + ) + + // Read XMP from cursor (API 30+) or ExifInterface stream (pre-30) + val xmp: String? = if (xmpColumn != -1) { + cursor.getBlob(xmpColumn)?.toString(Charsets.UTF_8) + } else { + try { + ctx.contentResolver.openInputStream(uri)?.use { stream -> + ExifInterface(stream).getAttribute(ExifInterface.TAG_XMP) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to read XMP for asset $assetId", e) + null + } + } + + if (xmp != null && "Camera:MotionPhoto" in xmp) { + return PlatformAssetPlaybackStyle.LIVE_PHOTO + } + + try { + ctx.contentResolver.openInputStream(uri)?.use { stream -> + val glide = Glide.get(ctx) + val type = ImageHeaderParserUtils.getType( + glide.registry.imageHeaderParsers, + stream, + glide.arrayPool + ) + if (type == ImageHeaderParser.ImageType.GIF || type == ImageHeaderParser.ImageType.ANIMATED_WEBP) { + return PlatformAssetPlaybackStyle.IMAGE_ANIMATED + } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to parse image header for asset $assetId", e) + } + + return PlatformAssetPlaybackStyle.IMAGE + } + fun getAlbums(): List { val albums = mutableListOf() val albumsCount = mutableMapOf() @@ -298,7 +403,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { var bytesRead: Int val buffer = ByteArray(HASH_BUFFER_SIZE) while (inputStream.read(buffer).also { bytesRead = it } > 0) { - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() digest.update(buffer, 0, bytesRead) } } ?: return HashResult(assetId, "Cannot open input stream for asset", null) @@ -316,4 +421,10 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { hashTask?.cancel() hashTask = null } + + // This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs + @Suppress("unused", "UNUSED_PARAMETER") + fun getCloudIdForAssetIds(assetIds: List): List { + return emptyList() + } } 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 index 54ccb1ddfb..c55db8da93 100644 --- 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 @@ -101,7 +101,7 @@ class ImmichAPI(cfg: ServerConfig) { } suspend fun fetchImage(asset: Asset): Bitmap = withContext(Dispatchers.IO) { - val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview")) + val url = buildRequestURL("/assets/${asset.id}/thumbnail", listOf("size" to "preview", "edited" to "true")) val connection = url.openConnection() val data = connection.getInputStream().readBytes() BitmapFactory.decodeByteArray(data, 0, data.size) diff --git a/mobile/android/app/src/main/res/xml/network_security_config.xml b/mobile/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..8a76775f86 --- /dev/null +++ b/mobile/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 33fe8f978b..14a6b4b660 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" => 3029, - "android.injected.version.name" => "2.4.0", + "android.injected.version.code" => 3037, + "android.injected.version.name" => "2.5.6", } ) 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/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index 8353b1c6f4..3c5c284c3e 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -3,7 +3,99 @@ import 'dart:convert'; import 'dart:io'; -const _kReservedWords = ['continue']; +const _kReservedWords = [ + 'abstract', + 'as', + 'assert', + 'async', + 'await', + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'covariant', + 'default', + 'deferred', + 'do', + 'dynamic', + 'else', + 'enum', + 'export', + 'extends', + 'extension', + 'external', + 'factory', + 'false', + 'final', + 'finally', + 'for', + 'Function', + 'get', + 'hide', + 'if', + 'implements', + 'import', + 'in', + 'interface', + 'is', + 'late', + 'library', + 'mixin', + 'new', + 'null', + 'on', + 'operator', + 'part', + 'required', + 'rethrow', + 'return', + 'sealed', + 'set', + 'show', + 'static', + 'super', + 'switch', + 'sync', + 'this', + 'throw', + 'true', + 'try', + 'typedef', + 'var', + 'void', + 'when', + 'while', + 'with', + 'yield', +]; + +const _kIntParamNames = [ + 'count', + 'number', + 'amount', + 'total', + 'index', + 'size', + 'length', + 'width', + 'height', + 'year', + 'month', + 'day', + 'hour', + 'minute', + 'second', + 'page', + 'limit', + 'offset', + 'max', + 'min', + 'id', + 'num', + 'quantity', +]; void main() async { final sourceFile = File('../i18n/en.json'); @@ -15,49 +107,258 @@ void main() async { final outputDir = Directory('lib/generated'); await outputDir.create(recursive: true); - final outputFile = File('lib/generated/intl_keys.g.dart'); - await _generate(sourceFile, outputFile); + final content = await sourceFile.readAsString(); + final translations = json.decode(content) as Map; + + final outputFile = File('lib/generated/translations.g.dart'); + await _generateTranslations(translations, outputFile); print('Generated ${outputFile.path}'); } -Future _generate(File source, File output) async { - final content = await source.readAsString(); - final translations = json.decode(content) as Map; +class TranslationNode { + final String key; + final String? value; + final Map children; + final List params; + + const TranslationNode({ + required this.key, + this.value, + Map? children, + List? params, + }) : children = children ?? const {}, + params = params ?? const []; + + bool get isLeaf => value != null; + bool get hasParams => params.isNotEmpty; +} + +class TranslationParam { + final String name; + final String type; + + const TranslationParam(this.name, this.type); +} + +Future _generateTranslations(Map translations, File output) async { + final root = _buildTranslationTree('', translations); final buffer = StringBuffer(''' // DO NOT EDIT. This is code generated via generate_keys.dart -abstract class IntlKeys { -'''); +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/message_format.dart'; - _writeKeys(buffer, translations); - buffer.writeln('}'); - - await output.writeAsString(buffer.toString()); +extension TranslationsExtension on BuildContext { + Translations get t => Translations.of(this); } -void _writeKeys( - StringBuffer buffer, - Map map, [ - String prefix = '', -]) { - for (final entry in map.entries) { - final key = entry.key; - final value = entry.value; +class StaticTranslations { + StaticTranslations._(); + static final instance = Translations._(null); +} - if (value is Map) { - _writeKeys(buffer, value, prefix.isEmpty ? key : '${prefix}_$key'); - } else { - final name = _cleanName(prefix.isEmpty ? key : '${prefix}_$key'); - final path = prefix.isEmpty ? key : '$prefix.$key'.replaceAll('_', '.'); - buffer.writeln(' static const $name = \'$path\';'); +abstract class _BaseTranslations { + BuildContext? get _context; + + String _t(String key, [Map? args]) { + if (key.isEmpty) return ''; + try { + final translated = key.tr(context: _context); + return args != null + ? MessageFormat(translated, locale: Intl.defaultLocale ?? 'en').format(args) + : translated; + } catch (e) { + return key; } } } -String _cleanName(String name) { - name = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); - if (RegExp(r'^[0-9]').hasMatch(name)) name = 'k_$name'; - if (_kReservedWords.contains(name)) name = '${name}_'; +class Translations extends _BaseTranslations { + @override + final BuildContext? _context; + Translations._(this._context); + + static Translations of(BuildContext context) { + context.locale; + return Translations._(context); + } + +'''); + + _generateClassMembers(buffer, root, ' '); + buffer.writeln('}'); + _generateNestedClasses(buffer, root); + + await output.writeAsString(buffer.toString()); +} + +TranslationNode _buildTranslationTree(String key, dynamic value) { + if (value is Map) { + final children = {}; + for (final entry in value.entries) { + children[entry.key] = _buildTranslationTree(entry.key, entry.value); + } + return TranslationNode(key: key, children: children); + } else { + final stringValue = value.toString(); + final params = _extractParams(stringValue); + return TranslationNode(key: key, value: stringValue, params: params); + } +} + +List _extractParams(String value) { + final params = {}; + + final icuRegex = RegExp(r'\{(\w+),\s*(plural|select|number|date|time)([^}]*(?:\{[^}]*\}[^}]*)*)\}'); + for (final match in icuRegex.allMatches(value)) { + final name = match.group(1)!; + final icuType = match.group(2)!; + final icuContent = match.group(3) ?? ''; + + if (params.containsKey(name)) continue; + + String type; + if (icuType == 'plural' || icuType == 'number') { + type = 'int'; + } else if (icuType == 'select') { + final hasTrueFalse = RegExp(r',\s*(true|false)\s*\{').hasMatch(icuContent); + type = hasTrueFalse ? 'bool' : 'String'; + } else { + type = 'String'; + } + + params[name] = TranslationParam(name, type); + } + + var cleanedValue = value; + var depth = 0; + var icuStart = -1; + + for (var i = 0; i < value.length; i++) { + if (value[i] == '{') { + if (depth == 0) icuStart = i; + depth++; + } else if (value[i] == '}') { + depth--; + if (depth == 0 && icuStart >= 0) { + final block = value.substring(icuStart, i + 1); + if (RegExp(r'^\{\w+,').hasMatch(block)) { + cleanedValue = cleanedValue.replaceFirst(block, ''); + } + icuStart = -1; + } + } + } + + final simpleRegex = RegExp(r'\{(\w+)\}'); + for (final match in simpleRegex.allMatches(cleanedValue)) { + final name = match.group(1)!; + + if (params.containsKey(name)) continue; + + String type; + if (_kIntParamNames.contains(name.toLowerCase())) { + type = 'int'; + } else { + type = 'Object'; + } + + params[name] = TranslationParam(name, type); + } + + return params.values.toList(); +} + +void _generateClassMembers(StringBuffer buffer, TranslationNode node, String indent, [String keyPrefix = '']) { + final sortedKeys = node.children.keys.toList()..sort(); + + for (final childKey in sortedKeys) { + final child = node.children[childKey]!; + final dartName = _escapeName(childKey); + final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; + + if (child.isLeaf) { + if (child.hasParams) { + _generateMethod(buffer, dartName, fullKey, child.params, indent); + } else { + _generateGetter(buffer, dartName, fullKey, indent); + } + } else { + final className = _toNestedClassName(keyPrefix, childKey); + buffer.writeln('${indent}late final $dartName = $className._(_context);'); + } + } +} + +void _generateGetter(StringBuffer buffer, String dartName, String translationKey, String indent) { + buffer.writeln('${indent}String get $dartName => _t(\'$translationKey\');'); +} + +void _generateMethod( + StringBuffer buffer, + String dartName, + String translationKey, + List params, + String indent, +) { + final paramList = params.map((p) => 'required ${p.type} ${_escapeName(p.name)}').join(', '); + final argsMap = params.map((p) => '\'${p.name}\': ${_escapeName(p.name)}').join(', '); + buffer.writeln('${indent}String $dartName({$paramList}) => _t(\'$translationKey\', {$argsMap});'); +} + +void _generateNestedClasses(StringBuffer buffer, TranslationNode node, [String keyPrefix = '']) { + final sortedKeys = node.children.keys.toList()..sort(); + + for (final childKey in sortedKeys) { + final child = node.children[childKey]!; + final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; + + if (!child.isLeaf && child.children.isNotEmpty) { + final className = _toNestedClassName(keyPrefix, childKey); + buffer.writeln(); + buffer.writeln('class $className extends _BaseTranslations {'); + buffer.writeln(' @override'); + buffer.writeln(' final BuildContext? _context;'); + buffer.writeln(' $className._(this._context);'); + _generateClassMembers(buffer, child, ' ', fullKey); + buffer.writeln('}'); + _generateNestedClasses(buffer, child, fullKey); + } + } +} + +String _toNestedClassName(String prefix, String key) { + final parts = []; + if (prefix.isNotEmpty) { + parts.addAll(prefix.split('.')); + } + parts.add(key); + + final result = StringBuffer('_'); + for (final part in parts) { + final words = part.split('_'); + for (final word in words) { + if (word.isNotEmpty) { + result.write(word[0].toUpperCase()); + if (word.length > 1) { + result.write(word.substring(1).toLowerCase()); + } + } + } + } + result.write('Translations'); + + return result.toString(); +} + +String _escapeName(String name) { + if (_kReservedWords.contains(name)) { + return '$name\$'; + } + if (RegExp(r'^[0-9]').hasMatch(name)) { + return 'k$name'; + } return name; } diff --git a/mobile/dcm_global.yaml b/mobile/dcm_global.yaml index c33846e674..ffe77eede8 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.36.0' diff --git a/mobile/drift_schemas/main/drift_schema_v15.json b/mobile/drift_schemas/main/drift_schema_v15.json new file mode 100644 index 0000000000..8c56e7fa4c --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v15.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":12,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":13,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"key","getter_name":"key","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(UserMetadataKey.values)","dart_type_name":"UserMetadataKey"}},{"name":"value","getter_name":"value","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userMetadataConverter","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id","key"]}},{"id":14,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":15,"references":[1],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"double","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"lens","getter_name":"lens","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":16,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":17,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":18,"references":[0],"type":"table","data":{"name":"memory_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(MemoryTypeEnum.values)","dart_type_name":"MemoryTypeEnum"}},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_saved","getter_name":"isSaved","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_saved\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_saved\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"memory_at","getter_name":"memoryAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"seen_at","getter_name":"seenAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"show_at","getter_name":"showAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"hide_at","getter_name":"hideAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":19,"references":[1,18],"type":"table","data":{"name":"memory_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"memory_id","getter_name":"memoryId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES memory_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES memory_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","memory_id"]}},{"id":20,"references":[0],"type":"table","data":{"name":"person_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"face_asset_id","getter_name":"faceAssetId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_hidden","getter_name":"isHidden","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_hidden\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_hidden\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"color","getter_name":"color","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"birth_date","getter_name":"birthDate","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":21,"references":[1,20],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":22,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":23,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":24,"references":[15],"type":"index","data":{"on":15,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":25,"references":[23],"type":"index","data":{"on":23,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":26,"references":[23],"type":"index","data":{"on":23,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v16.json b/mobile/drift_schemas/main/drift_schema_v16.json new file mode 100644 index 0000000000..417a3a0f20 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v16.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"stack_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"primary_asset_id","getter_name":"primaryAssetId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":13,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":14,"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":15,"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":16,"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":17,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":18,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":19,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":20,"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":21,"references":[1,20],"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":22,"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":23,"references":[1,22],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":24,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":25,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":26,"references":[16],"type":"index","data":{"on":16,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":27,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":28,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v17.json b/mobile/drift_schemas/main/drift_schema_v17.json new file mode 100644 index 0000000000..a26b7b57ad --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v17.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" 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":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":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":13,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":14,"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":15,"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":16,"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":17,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":18,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":19,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":20,"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":21,"references":[1,20],"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":22,"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":23,"references":[1,22],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":24,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":25,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":26,"references":[16],"type":"index","data":{"on":16,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":27,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":28,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v18.json b/mobile/drift_schemas/main/drift_schema_v18.json new file mode 100644 index 0000000000..8d9efd3db6 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v18.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" 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":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":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":8,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":9,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":10,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":13,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":14,"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":15,"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":16,"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":17,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":18,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":19,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":20,"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":21,"references":[1,20],"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":22,"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":23,"references":[1,22],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":24,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":25,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":26,"references":[16],"type":"index","data":{"on":16,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":27,"references":[19],"type":"index","data":{"on":19,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":28,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":29,"references":[25],"type":"index","data":{"on":25,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v19.json b/mobile/drift_schemas/main/drift_schema_v19.json new file mode 100644 index 0000000000..405650a41f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v19.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" 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":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":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[6],"type":"index","data":{"on":6,"name":"idx_local_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_remote_album_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)","unique":false,"columns":[]}},{"id":9,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":10,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":11,"references":[2],"type":"index","data":{"on":2,"name":"idx_stack_primary_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)","unique":false,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":13,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":14,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":15,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":16,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_stack_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)","unique":false,"columns":[]}},{"id":17,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_day","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))","unique":false,"columns":[]}},{"id":18,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_month","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))","unique":false,"columns":[]}},{"id":19,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"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":21,"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":22,"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":23,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":24,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":25,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":26,"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":27,"references":[1,26],"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":28,"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":29,"references":[1,28],"type":"table","data":{"name":"asset_face_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"person_id","getter_name":"personId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES person_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES person_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"image_width","getter_name":"imageWidth","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"image_height","getter_name":"imageHeight","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x1","getter_name":"boundingBoxX1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y1","getter_name":"boundingBoxY1","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_x2","getter_name":"boundingBoxX2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"bounding_box_y2","getter_name":"boundingBoxY2","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":30,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":31,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":32,"references":[21],"type":"index","data":{"on":21,"name":"idx_partner_shared_with_id","sql":"CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)","unique":false,"columns":[]}},{"id":33,"references":[22],"type":"index","data":{"on":22,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":34,"references":[23],"type":"index","data":{"on":23,"name":"idx_remote_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":35,"references":[25],"type":"index","data":{"on":25,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":36,"references":[28],"type":"index","data":{"on":28,"name":"idx_person_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)","unique":false,"columns":[]}},{"id":37,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_person_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)","unique":false,"columns":[]}},{"id":38,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)","unique":false,"columns":[]}},{"id":39,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":40,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v20.json b/mobile/drift_schemas/main/drift_schema_v20.json new file mode 100644 index 0000000000..f85af83439 --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v20.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" 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":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":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[6],"type":"index","data":{"on":6,"name":"idx_local_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_remote_album_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)","unique":false,"columns":[]}},{"id":9,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":10,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":11,"references":[2],"type":"index","data":{"on":2,"name":"idx_stack_primary_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)","unique":false,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":13,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":14,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":15,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":16,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_stack_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)","unique":false,"columns":[]}},{"id":17,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_day","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))","unique":false,"columns":[]}},{"id":18,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_month","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))","unique":false,"columns":[]}},{"id":19,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"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":21,"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":22,"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":23,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":24,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":25,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":26,"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":27,"references":[1,26],"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":28,"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":29,"references":[1,28],"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":[]},{"name":"is_visible","getter_name":"isVisible","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_visible\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_visible\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":30,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":31,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":32,"references":[21],"type":"index","data":{"on":21,"name":"idx_partner_shared_with_id","sql":"CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)","unique":false,"columns":[]}},{"id":33,"references":[22],"type":"index","data":{"on":22,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":34,"references":[23],"type":"index","data":{"on":23,"name":"idx_remote_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":35,"references":[25],"type":"index","data":{"on":25,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":36,"references":[28],"type":"index","data":{"on":28,"name":"idx_person_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)","unique":false,"columns":[]}},{"id":37,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_person_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)","unique":false,"columns":[]}},{"id":38,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)","unique":false,"columns":[]}},{"id":39,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":40,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/drift_schemas/main/drift_schema_v21.json b/mobile/drift_schemas/main/drift_schema_v21.json new file mode 100644 index 0000000000..4a6654ba4f --- /dev/null +++ b/mobile/drift_schemas/main/drift_schema_v21.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"live_photo_video_id","getter_name":"livePhotoVideoId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}},{"name":"stack_id","getter_name":"stackId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"library_id","getter_name":"libraryId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_edited","getter_name":"isEdited","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_edited\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_edited\" 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":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":[]},{"name":"i_cloud_id","getter_name":"iCloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":"playback_style","getter_name":"playbackStyle","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetPlaybackStyle.values)","dart_type_name":"AssetPlaybackStyle"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[0,1],"type":"table","data":{"name":"remote_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('\\'\\'')","default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"thumbnail_asset_id","getter_name":"thumbnailAssetId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_activity_enabled","getter_name":"isActivityEnabled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_activity_enabled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_activity_enabled\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","default_client_dart":null,"dsl_features":[]},{"name":"order","getter_name":"order","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumAssetOrder.values)","dart_type_name":"AlbumAssetOrder"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"linked_remote_album_id","getter_name":"linkedRemoteAlbumId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":6,"references":[3,5],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":7,"references":[6],"type":"index","data":{"on":6,"name":"idx_local_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_remote_album_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)","unique":false,"columns":[]}},{"id":9,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":10,"references":[3],"type":"index","data":{"on":3,"name":"idx_local_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)","unique":false,"columns":[]}},{"id":11,"references":[2],"type":"index","data":{"on":2,"name":"idx_stack_primary_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)","unique":false,"columns":[]}},{"id":12,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_owner_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)","unique":false,"columns":[]}},{"id":13,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n","unique":true,"columns":[]}},{"id":14,"references":[1],"type":"index","data":{"on":1,"name":"UQ_remote_assets_owner_library_checksum","sql":"CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n","unique":true,"columns":[]}},{"id":15,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)","unique":false,"columns":[]}},{"id":16,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_stack_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)","unique":false,"columns":[]}},{"id":17,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_day","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))","unique":false,"columns":[]}},{"id":18,"references":[1],"type":"index","data":{"on":1,"name":"idx_remote_asset_local_date_time_month","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))","unique":false,"columns":[]}},{"id":19,"references":[],"type":"table","data":{"name":"auth_user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"has_profile_image","getter_name":"hasProfileImage","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"has_profile_image\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"has_profile_image\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"profile_changed_at","getter_name":"profileChangedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"avatar_color","getter_name":"avatarColor","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AvatarColor.values)","dart_type_name":"AvatarColor"}},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"pin_code","getter_name":"pinCode","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":20,"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":21,"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":22,"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":23,"references":[1,4],"type":"table","data":{"name":"remote_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":24,"references":[4,0],"type":"table","data":{"name":"remote_album_user_entity","was_declared_in_moor":false,"columns":[{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"role","getter_name":"role","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AlbumUserRole.values)","dart_type_name":"AlbumUserRole"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["album_id","user_id"]}},{"id":25,"references":[1],"type":"table","data":{"name":"remote_asset_cloud_id_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":"cloud_id","getter_name":"cloudId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"adjustment_time","getter_name":"adjustmentTime","moor_type":"dateTime","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":26,"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":27,"references":[1,26],"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":28,"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":29,"references":[1,28],"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":[]},{"name":"is_visible","getter_name":"isVisible","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_visible\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_visible\" IN (0, 1))"},"default_dart":"const CustomExpression('1')","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":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":30,"references":[],"type":"table","data":{"name":"store_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"string_value","getter_name":"stringValue","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"int_value","getter_name":"intValue","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":31,"references":[],"type":"table","data":{"name":"trashed_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":"album_id","getter_name":"albumId","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":[]},{"name":"source","getter_name":"source","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TrashOrigin.values)","dart_type_name":"TrashOrigin"}},{"name":"playback_style","getter_name":"playbackStyle","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetPlaybackStyle.values)","dart_type_name":"AssetPlaybackStyle"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id","album_id"]}},{"id":32,"references":[21],"type":"index","data":{"on":21,"name":"idx_partner_shared_with_id","sql":"CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)","unique":false,"columns":[]}},{"id":33,"references":[22],"type":"index","data":{"on":22,"name":"idx_lat_lng","sql":"CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)","unique":false,"columns":[]}},{"id":34,"references":[23],"type":"index","data":{"on":23,"name":"idx_remote_album_asset_album_asset","sql":"CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)","unique":false,"columns":[]}},{"id":35,"references":[25],"type":"index","data":{"on":25,"name":"idx_remote_asset_cloud_id","sql":"CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)","unique":false,"columns":[]}},{"id":36,"references":[28],"type":"index","data":{"on":28,"name":"idx_person_owner_id","sql":"CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)","unique":false,"columns":[]}},{"id":37,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_person_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)","unique":false,"columns":[]}},{"id":38,"references":[29],"type":"index","data":{"on":29,"name":"idx_asset_face_asset_id","sql":"CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)","unique":false,"columns":[]}},{"id":39,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_checksum","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)","unique":false,"columns":[]}},{"id":40,"references":[31],"type":"index","data":{"on":31,"name":"idx_trashed_local_asset_album","sql":"CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)","unique":false,"columns":[]}}]} \ No newline at end of file diff --git a/mobile/fonts/GoogleSans/GoogleSans-Bold.ttf b/mobile/fonts/GoogleSans/GoogleSans-Bold.ttf new file mode 100644 index 0000000000..71b847f80f Binary files /dev/null and b/mobile/fonts/GoogleSans/GoogleSans-Bold.ttf differ diff --git a/mobile/fonts/GoogleSans/GoogleSans-Italic.ttf b/mobile/fonts/GoogleSans/GoogleSans-Italic.ttf new file mode 100644 index 0000000000..1f9059a58c Binary files /dev/null and b/mobile/fonts/GoogleSans/GoogleSans-Italic.ttf differ diff --git a/mobile/fonts/GoogleSans/GoogleSans-Medium.ttf b/mobile/fonts/GoogleSans/GoogleSans-Medium.ttf new file mode 100644 index 0000000000..8b9aebc952 Binary files /dev/null and b/mobile/fonts/GoogleSans/GoogleSans-Medium.ttf differ diff --git a/mobile/fonts/GoogleSans/GoogleSans-Regular.ttf b/mobile/fonts/GoogleSans/GoogleSans-Regular.ttf new file mode 100644 index 0000000000..cc37c3f38d Binary files /dev/null and b/mobile/fonts/GoogleSans/GoogleSans-Regular.ttf differ diff --git a/mobile/fonts/GoogleSans/GoogleSans-SemiBold.ttf b/mobile/fonts/GoogleSans/GoogleSans-SemiBold.ttf new file mode 100644 index 0000000000..b80284d2ea Binary files /dev/null and b/mobile/fonts/GoogleSans/GoogleSans-SemiBold.ttf differ diff --git a/mobile/fonts/GoogleSansCode/GoogleSansCode-Medium.ttf b/mobile/fonts/GoogleSansCode/GoogleSansCode-Medium.ttf new file mode 100644 index 0000000000..5e7f46b979 Binary files /dev/null and b/mobile/fonts/GoogleSansCode/GoogleSansCode-Medium.ttf differ diff --git a/mobile/fonts/GoogleSansCode/GoogleSansCode-Regular.ttf b/mobile/fonts/GoogleSansCode/GoogleSansCode-Regular.ttf new file mode 100644 index 0000000000..5c520addd9 Binary files /dev/null and b/mobile/fonts/GoogleSansCode/GoogleSansCode-Regular.ttf differ diff --git a/mobile/fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf b/mobile/fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf new file mode 100644 index 0000000000..a03c7f0440 Binary files /dev/null and b/mobile/fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf differ diff --git a/mobile/ios/.gitignore b/mobile/ios/.gitignore index f1a46a2fef..63e84080df 100644 --- a/mobile/ios/.gitignore +++ b/mobile/ios/.gitignore @@ -33,4 +33,5 @@ Runner/GeneratedPluginRegistrant.* !default.perspectivev3 fastlane/report.xml -Gemfile.lock \ No newline at end of file +Gemfile.lock +certs/ \ No newline at end of file diff --git a/mobile/ios/Podfile b/mobile/ios/Podfile index ca0166a382..a236b027f5 100644 --- a/mobile/ios/Podfile +++ b/mobile/ios/Podfile @@ -121,4 +121,6 @@ post_install do |installer| end # End of the permission_handler configuration end + system("defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES") + system("defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES") end diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index d869aa9c08..e1ec4aff07 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -6,41 +6,10 @@ PODS: - FlutterMacOS - connectivity_plus (0.0.1): - Flutter - - device_info_plus (0.0.1): + - cupertino_http (0.0.1): - Flutter - - DKImagePickerController/Core (4.3.9): - - DKImagePickerController/ImageDataManager - - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.9) - - DKImagePickerController/PhotoGallery (4.3.9): - - DKImagePickerController/Core - - DKPhotoGallery - - DKImagePickerController/Resource (4.3.9) - - DKPhotoGallery (0.0.19): - - DKPhotoGallery/Core (= 0.0.19) - - DKPhotoGallery/Model (= 0.0.19) - - DKPhotoGallery/Preview (= 0.0.19) - - DKPhotoGallery/Resource (= 0.0.19) - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Core (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Preview - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Model (0.0.19): - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Preview (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Resource - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Resource (0.0.19): - - SDWebImage - - SwiftyGif - - file_picker (0.0.1): - - DKImagePickerController/PhotoGallery + - FlutterMacOS + - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) - flutter_local_notifications (0.0.1): @@ -77,6 +46,8 @@ PODS: - Flutter - network_info_plus (0.0.1): - Flutter + - objective_c (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -88,9 +59,6 @@ PODS: - Flutter - FlutterMacOS - SAMKeychain (1.5.3) - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) - share_handler_ios (0.0.14): - Flutter - share_handler_ios/share_handler_ios_models (= 0.0.14) @@ -126,7 +94,6 @@ PODS: - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree - - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter - wakelock_plus (0.0.1): @@ -136,8 +103,8 @@ DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) @@ -154,6 +121,7 @@ DEPENDENCIES: - maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -169,13 +137,9 @@ DEPENDENCIES: SPEC REPOS: trunk: - - DKImagePickerController - - DKPhotoGallery - MapLibre - SAMKeychain - - SDWebImage - sqlite3 - - SwiftyGif EXTERNAL SOURCES: background_downloader: @@ -184,10 +148,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/bonsoir_darwin/darwin" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cupertino_http: + :path: ".symlinks/plugins/cupertino_http/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" - file_picker: - :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter flutter_local_notifications: @@ -220,6 +184,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/native_video_player/ios" network_info_plus: :path: ".symlinks/plugins/network_info_plus/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -249,10 +215,8 @@ SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe - DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c - DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf @@ -270,12 +234,12 @@ SPEC CHECKSUMS: maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f native_video_player: b65c58951ede2f93d103a25366bdebca95081265 network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a @@ -283,10 +247,9 @@ SPEC CHECKSUMS: sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 - SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 -PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45 +PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe COCOAPODS: 1.16.2 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 599e7990f4..22a7abcbac 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -29,9 +29,12 @@ FAC6F89B2D287C890078CB2F /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FAC6F8902D287C890078CB2F /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; FAC6F8B72D287F120078CB2F /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC6F8B52D287F120078CB2F /* ShareViewController.swift */; }; FAC6F8B92D287F120078CB2F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAC6F8B32D287F120078CB2F /* MainInterface.storyboard */; }; + FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F12F1197D8006016CB /* LocalImages.g.swift */; }; + FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F22F1197D8006016CB /* RemoteImages.g.swift */; }; + FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F52F11980E006016CB /* LocalImagesImpl.swift */; }; + FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */; }; + FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */; }; FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; }; - FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; }; - FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; }; FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; }; FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; }; FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; }; @@ -118,9 +121,12 @@ FAC6F8B42D287F120078CB2F /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; FAC6F8B52D287F120078CB2F /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; + FE5499F12F1197D8006016CB /* LocalImages.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImages.g.swift; sourceTree = ""; }; + FE5499F22F1197D8006016CB /* RemoteImages.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImages.g.swift; sourceTree = ""; }; + FE5499F52F11980E006016CB /* LocalImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalImagesImpl.swift; sourceTree = ""; }; + FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = ""; }; + FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = ""; }; FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = ""; }; - FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnails.g.swift; sourceTree = ""; }; - FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailsImpl.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -321,9 +327,12 @@ FED3B1952E253E9B0030FD97 /* Images */ = { isa = PBXGroup; children = ( + FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */, + FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */, + FE5499F52F11980E006016CB /* LocalImagesImpl.swift */, + FE5499F12F1197D8006016CB /* LocalImages.g.swift */, + FE5499F22F1197D8006016CB /* RemoteImages.g.swift */, FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */, - FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */, - FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */, ); path = Images; sourceTree = ""; @@ -600,12 +609,15 @@ 65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */, 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */, + FE5499F32F1197D8006016CB /* LocalImages.g.swift in Sources */, + FE5499F62F11980E006016CB /* LocalImagesImpl.swift in Sources */, + FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */, + FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */, B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, + FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */, FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */, B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, - FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */, B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */, - FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */, 65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */, @@ -733,7 +745,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -877,7 +889,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -907,7 +919,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -941,7 +953,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -984,7 +996,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1024,7 +1036,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1063,7 +1075,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1107,7 +1119,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -1148,7 +1160,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 233; + CURRENT_PROJECT_VERSION = 240; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index ff8a53ff4b..4962230c22 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "18497b68fdbb3a09528d260a0a0e1e7e61c8c53d", - "version" : "7.8.0" + "revision" : "aa0079aeb82a4bf00324561a40bffe68c6fe1c26", + "version" : "7.9.0" } }, { @@ -24,17 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/sqlite-data", "state" : { - "revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-case-paths", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-case-paths", - "state" : { - "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", - "version" : "1.7.2" + "revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d", + "version" : "1.5.1" } }, { @@ -132,8 +123,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-structured-queries", "state" : { - "revision" : "1447ea20550f6f02c4b48cc80931c3ed40a9c756", - "version" : "0.25.0" + "revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66", + "version" : "0.30.0" } }, { @@ -145,15 +136,6 @@ "version" : "602.0.0" } }, - { - "identity" : "swift-tagged", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-tagged", - "state" : { - "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", - "version" : "0.10.0" - } - }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 4e4cb2ed13..f842285b23 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -15,12 +15,12 @@ import UIKit ) -> Bool { // Required for flutter_local_notification if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - AppDelegate.registerPlugins(with: controller.engine) + AppDelegate.registerPlugins(with: controller.engine, controller: controller) BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) BackgroundServicePlugin.registerBackgroundProcessing() @@ -51,10 +51,13 @@ import UIKit return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - public static func registerPlugins(with engine: FlutterEngine) { + public static func registerPlugins(with engine: FlutterEngine, controller: FlutterViewController?) { NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!) - ThumbnailApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ThumbnailApiImpl()) + LocalImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: LocalImageApiImpl()) + RemoteImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: RemoteImageApiImpl()) BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl()) + ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl()) + NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller)) } public static func cancelPlugins(with engine: FlutterEngine) { diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index 7dc450d76e..85e1a55d3d 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { // Register plugins in the new engine GeneratedPluginRegistrant.register(with: engine) // Register custom plugins - AppDelegate.registerPlugins(with: engine) + AppDelegate.registerPlugins(with: engine, controller: nil) flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) diff --git a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift index 0261cb26fb..f104314fae 100644 --- a/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift +++ b/mobile/ios/Runner/Connectivity/ConnectivityApiImpl.swift @@ -1,6 +1,60 @@ +import Network class ConnectivityApiImpl: ConnectivityApi { + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "ConnectivityMonitor") + private var currentPath: NWPath? + + init() { + monitor.pathUpdateHandler = { [weak self] path in + self?.currentPath = path + } + monitor.start(queue: queue) + // Get initial state synchronously + currentPath = monitor.currentPath + } + + deinit { + monitor.cancel() + } + func getCapabilities() throws -> [NetworkCapability] { - [] + guard let path = currentPath else { + return [] + } + + guard path.status == .satisfied else { + return [] + } + + var capabilities: [NetworkCapability] = [] + + if path.usesInterfaceType(.wifi) { + capabilities.append(.wifi) + } + + if path.usesInterfaceType(.cellular) { + capabilities.append(.cellular) + } + + // Check for VPN - iOS reports VPN as .other interface type in many cases + // or through the path's expensive property when on cellular with VPN + if path.usesInterfaceType(.other) { + capabilities.append(.vpn) + } + + // Determine if connection is unmetered: + // - Must be on WiFi (not cellular) + // - Must not be expensive (rules out personal hotspot) + // - Must not be constrained (Low Data Mode) + // Note: VPN over cellular should still be considered metered + let isOnCellular = path.usesInterfaceType(.cellular) + let isOnWifi = path.usesInterfaceType(.wifi) + + if isOnWifi && !isOnCellular && !path.isExpensive && !path.isConstrained { + capabilities.append(.unmetered) + } + + return capabilities } } diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift new file mode 100644 index 0000000000..0f678ce4a4 --- /dev/null +++ b/mobile/ios/Runner/Core/Network.g.swift @@ -0,0 +1,284 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsNetwork(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashNetwork(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashNetwork(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashNetwork(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Generated class from Pigeon that represents data sent in messages. +struct ClientCertData: Hashable { + var data: FlutterStandardTypedData + var password: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertData? { + let data = pigeonVar_list[0] as! FlutterStandardTypedData + let password = pigeonVar_list[1] as! String + + return ClientCertData( + data: data, + password: password + ) + } + func toList() -> [Any?] { + return [ + data, + password, + ] + } + static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool { + return deepEqualsNetwork(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashNetwork(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct ClientCertPrompt: Hashable { + var title: String + var message: String + var cancel: String + var confirm: String + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertPrompt? { + let title = pigeonVar_list[0] as! String + let message = pigeonVar_list[1] as! String + let cancel = pigeonVar_list[2] as! String + let confirm = pigeonVar_list[3] as! String + + return ClientCertPrompt( + title: title, + message: message, + cancel: cancel, + confirm: confirm + ) + } + func toList() -> [Any?] { + return [ + title, + message, + cancel, + confirm, + ] + } + static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool { + return deepEqualsNetwork(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashNetwork(value: toList(), hasher: &hasher) + } +} + +private class NetworkPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return ClientCertData.fromList(self.readValue() as! [Any?]) + case 130: + return ClientCertPrompt.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class NetworkPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? ClientCertData { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? ClientCertPrompt { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class NetworkPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return NetworkPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return NetworkPigeonCodecWriter(data: data) + } +} + +class NetworkPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = NetworkPigeonCodec(readerWriter: NetworkPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NetworkApi { + func addCertificate(clientData: ClientCertData, completion: @escaping (Result) -> Void) + func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) + func removeCertificate(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NetworkApiSetup { + static var codec: FlutterStandardMessageCodec { NetworkPigeonCodec.shared } + /// Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let addCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + addCertificateChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let clientDataArg = args[0] as! ClientCertData + api.addCertificate(clientData: clientDataArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + addCertificateChannel.setMessageHandler(nil) + } + let selectCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + selectCertificateChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let promptTextArg = args[0] as! ClientCertPrompt + api.selectCertificate(promptText: promptTextArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + selectCertificateChannel.setMessageHandler(nil) + } + let removeCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + removeCertificateChannel.setMessageHandler { _, reply in + api.removeCertificate { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + removeCertificateChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Core/NetworkApiImpl.swift b/mobile/ios/Runner/Core/NetworkApiImpl.swift new file mode 100644 index 0000000000..d67c392a3a --- /dev/null +++ b/mobile/ios/Runner/Core/NetworkApiImpl.swift @@ -0,0 +1,157 @@ +import Foundation +import UniformTypeIdentifiers + +enum ImportError: Error { + case noFile + case noViewController + case keychainError(OSStatus) + case cancelled +} + +class NetworkApiImpl: NetworkApi { + weak var viewController: UIViewController? + private var activeImporter: CertImporter? + + init(viewController: UIViewController?) { + self.viewController = viewController + } + + func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) { + let importer = CertImporter(promptText: promptText, completion: { [weak self] result in + self?.activeImporter = nil + completion(result.map { ClientCertData(data: FlutterStandardTypedData(bytes: $0.0), password: $0.1) }) + }, viewController: viewController) + activeImporter = importer + importer.load() + } + + func removeCertificate(completion: @escaping (Result) -> Void) { + let status = clearCerts() + if status == errSecSuccess || status == errSecItemNotFound { + return completion(.success(())) + } + completion(.failure(ImportError.keychainError(status))) + } + + func addCertificate(clientData: ClientCertData, completion: @escaping (Result) -> Void) { + let status = importCert(clientData: clientData.data.data, password: clientData.password) + if status == errSecSuccess { + return completion(.success(())) + } + completion(.failure(ImportError.keychainError(status))) + } +} + +private class CertImporter: NSObject, UIDocumentPickerDelegate { + private let promptText: ClientCertPrompt + private var completion: ((Result<(Data, String), Error>) -> Void) + private weak var viewController: UIViewController? + + init(promptText: ClientCertPrompt, completion: (@escaping (Result<(Data, String), Error>) -> Void), viewController: UIViewController?) { + self.promptText = promptText + self.completion = completion + self.viewController = viewController + } + + func load() { + guard let vc = viewController else { return completion(.failure(ImportError.noViewController)) } + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [ + UTType(filenameExtension: "p12")!, + UTType(filenameExtension: "pfx")!, + ]) + picker.delegate = self + picker.allowsMultipleSelection = false + vc.present(picker, animated: true) + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { + return completion(.failure(ImportError.noFile)) + } + + Task { @MainActor in + do { + let data = try readSecurityScoped(url: url) + guard let password = await promptForPassword() else { + return completion(.failure(ImportError.cancelled)) + } + let status = importCert(clientData: data, password: password) + if status != errSecSuccess { + return completion(.failure(ImportError.keychainError(status))) + } + + await URLSessionManager.shared.session.flush() + self.completion(.success((data, password))) + } catch { + completion(.failure(error)) + } + } + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + completion(.failure(ImportError.cancelled)) + } + + private func promptForPassword() async -> String? { + guard let vc = viewController else { return nil } + + return await withCheckedContinuation { continuation in + let alert = UIAlertController( + title: promptText.title, + message: promptText.message, + preferredStyle: .alert + ) + + alert.addTextField { $0.isSecureTextEntry = true } + + alert.addAction(UIAlertAction(title: promptText.cancel, style: .cancel) { _ in + continuation.resume(returning: nil) + }) + + alert.addAction(UIAlertAction(title: promptText.confirm, style: .default) { _ in + continuation.resume(returning: alert.textFields?.first?.text ?? "") + }) + + vc.present(alert, animated: true) + } + } + + private func readSecurityScoped(url: URL) throws -> Data { + guard url.startAccessingSecurityScopedResource() else { + throw ImportError.noFile + } + defer { url.stopAccessingSecurityScopedResource() } + return try Data(contentsOf: url) + } +} + +private func importCert(clientData: Data, password: String) -> OSStatus { + let options = [kSecImportExportPassphrase: password] as CFDictionary + var items: CFArray? + let status = SecPKCS12Import(clientData as CFData, options, &items) + + guard status == errSecSuccess, + let array = items as? [[String: Any]], + let first = array.first, + let identity = first[kSecImportItemIdentity as String] else { + return status + } + + clearCerts() + + let addQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecValueRef as String: identity, + kSecAttrLabel as String: CLIENT_CERT_LABEL, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, + ] + return SecItemAdd(addQuery as CFDictionary, nil) +} + +@discardableResult private func clearCerts() -> OSStatus { + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: CLIENT_CERT_LABEL, + ] + return SecItemDelete(deleteQuery as CFDictionary) +} diff --git a/mobile/ios/Runner/Core/URLSessionManager.swift b/mobile/ios/Runner/Core/URLSessionManager.swift new file mode 100644 index 0000000000..73145dbce5 --- /dev/null +++ b/mobile/ios/Runner/Core/URLSessionManager.swift @@ -0,0 +1,87 @@ +import Foundation + +let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity" + +/// Manages a shared URLSession with SSL configuration support. +class URLSessionManager: NSObject { + static let shared = URLSessionManager() + + let session: URLSession + private let configuration = { + let config = URLSessionConfiguration.default + + let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("api", isDirectory: true) + try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + + config.urlCache = URLCache( + memoryCapacity: 0, + diskCapacity: 1024 * 1024 * 1024, + directory: cacheDir + ) + + config.httpMaximumConnectionsPerHost = 64 + config.timeoutIntervalForRequest = 60 + config.timeoutIntervalForResource = 300 + + let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" + config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"] + + return config + }() + + private override init() { + session = URLSession(configuration: configuration, delegate: URLSessionManagerDelegate(), delegateQueue: nil) + super.init() + } +} + +class URLSessionManagerDelegate: NSObject, URLSessionTaskDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + handleChallenge(challenge, completionHandler: completionHandler) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + handleChallenge(challenge, completionHandler: completionHandler) + } + + func handleChallenge( + _ challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + switch challenge.protectionSpace.authenticationMethod { + case NSURLAuthenticationMethodClientCertificate: handleClientCertificate(completion: completionHandler) + default: completionHandler(.performDefaultHandling, nil) + } + } + + private func handleClientCertificate( + completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + let query: [String: Any] = [ + kSecClass as String: kSecClassIdentity, + kSecAttrLabel as String: CLIENT_CERT_LABEL, + kSecReturnRef as String: true, + ] + + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + if status == errSecSuccess, let identity = item { + let credential = URLCredential(identity: identity as! SecIdentity, + certificates: nil, + persistence: .forSession) + return completion(.useCredential, credential) + } + completion(.performDefaultHandling, nil) + } +} diff --git a/mobile/ios/Runner/Images/ImageProcessing.swift b/mobile/ios/Runner/Images/ImageProcessing.swift new file mode 100644 index 0000000000..2270bbffac --- /dev/null +++ b/mobile/ios/Runner/Images/ImageProcessing.swift @@ -0,0 +1,7 @@ +import Foundation + +enum ImageProcessing { + static let queue = DispatchQueue(label: "thumbnail.processing", qos: .userInitiated, attributes: .concurrent) + static let semaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2) + static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) +} diff --git a/mobile/ios/Runner/Images/Thumbnails.g.swift b/mobile/ios/Runner/Images/LocalImages.g.swift similarity index 64% rename from mobile/ios/Runner/Images/Thumbnails.g.swift rename to mobile/ios/Runner/Images/LocalImages.g.swift index fbaef294d3..146950cd51 100644 --- a/mobile/ios/Runner/Images/Thumbnails.g.swift +++ b/mobile/ios/Runner/Images/LocalImages.g.swift @@ -47,41 +47,41 @@ private func nilOrValue(_ value: Any?) -> T? { } -private class ThumbnailsPigeonCodecReader: FlutterStandardReader { +private class LocalImagesPigeonCodecReader: FlutterStandardReader { } -private class ThumbnailsPigeonCodecWriter: FlutterStandardWriter { +private class LocalImagesPigeonCodecWriter: FlutterStandardWriter { } -private class ThumbnailsPigeonCodecReaderWriter: FlutterStandardReaderWriter { +private class LocalImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { override func reader(with data: Data) -> FlutterStandardReader { - return ThumbnailsPigeonCodecReader(data: data) + return LocalImagesPigeonCodecReader(data: data) } override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return ThumbnailsPigeonCodecWriter(data: data) + return LocalImagesPigeonCodecWriter(data: data) } } -class ThumbnailsPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = ThumbnailsPigeonCodec(readerWriter: ThumbnailsPigeonCodecReaderWriter()) +class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = LocalImagesPigeonCodec(readerWriter: LocalImagesPigeonCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol ThumbnailApi { - func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], Error>) -> Void) - func cancelImageRequest(requestId: Int64) throws +protocol LocalImageApi { + func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) + func cancelRequest(requestId: Int64) throws func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class ThumbnailApiSetup { - static var codec: FlutterStandardMessageCodec { ThumbnailsPigeonCodec.shared } - /// Sets up an instance of `ThumbnailApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ThumbnailApi?, messageChannelSuffix: String = "") { +class LocalImageApiSetup { + static var codec: FlutterStandardMessageCodec { LocalImagesPigeonCodec.shared } + /// Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { requestImageChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -90,7 +90,8 @@ class ThumbnailApiSetup { let widthArg = args[2] as! Int64 let heightArg = args[3] as! Int64 let isVideoArg = args[4] as! Bool - api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg) { result in + let preferEncodedArg = args[5] as! Bool + api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in switch result { case .success(let res): reply(wrapResult(res)) @@ -102,22 +103,22 @@ class ThumbnailApiSetup { } else { requestImageChannel.setMessageHandler(nil) } - let cancelImageRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - cancelImageRequestChannel.setMessageHandler { message, reply in + cancelRequestChannel.setMessageHandler { message, reply in let args = message as! [Any?] let requestIdArg = args[0] as! Int64 do { - try api.cancelImageRequest(requestId: requestIdArg) + try api.cancelRequest(requestId: requestIdArg) reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - cancelImageRequestChannel.setMessageHandler(nil) + cancelRequestChannel.setMessageHandler(nil) } - let getThumbhashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let getThumbhashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { getThumbhashChannel.setMessageHandler { message, reply in let args = message as! [Any?] diff --git a/mobile/ios/Runner/Images/LocalImagesImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift new file mode 100644 index 0000000000..303ff5bc33 --- /dev/null +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -0,0 +1,221 @@ +import Accelerate +import Flutter +import MobileCoreServices +import Photos + +class LocalImageRequest { + weak var workItem: DispatchWorkItem? + var isCancelled = false + let callback: (Result<[String: Int64]?, any Error>) -> Void + + init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) { + self.callback = callback + } +} + +class LocalImageApiImpl: LocalImageApi { + private static let imageManager = PHImageManager.default() + private static let fetchOptions = { + let fetchOptions = PHFetchOptions() + fetchOptions.fetchLimit = 1 + fetchOptions.wantsIncrementalChangeDetails = false + return fetchOptions + }() + private static let requestOptions = { + let requestOptions = PHImageRequestOptions() + requestOptions.isNetworkAccessAllowed = true + requestOptions.deliveryMode = .highQualityFormat + requestOptions.resizeMode = .fast + requestOptions.isSynchronous = true + requestOptions.version = .current + return requestOptions + }() + + private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated) + private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated) + private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default) + + private static var rgbaFormat = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), + renderingIntent: .defaultIntent + )! + private static var requests = [Int64: LocalImageRequest]() + private static let assetCache = { + let assetCache = NSCache() + assetCache.countLimit = 10000 + return assetCache + }() + + func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { + ImageProcessing.queue.async { + guard let data = Data(base64Encoded: thumbhash) + else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} + + let (width, height, pointer) = thumbHashToRGBA(hash: data) + completion(.success([ + "pointer": Int64(Int(bitPattern: pointer.baseAddress)), + "width": Int64(width), + "height": Int64(height), + "rowBytes": Int64(width * 4) + ])) + } + } + + func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { + let request = LocalImageRequest(callback: completion) + let item = DispatchWorkItem { + if request.isCancelled { + return completion(ImageProcessing.cancelledResult) + } + + ImageProcessing.semaphore.wait() + defer { + ImageProcessing.semaphore.signal() + } + + if request.isCancelled { + return completion(ImageProcessing.cancelledResult) + } + + guard let asset = Self.requestAsset(assetId: assetId) + else { + Self.remove(requestId: requestId) + completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) + return + } + + if request.isCancelled { + return completion(ImageProcessing.cancelledResult) + } + + if preferEncoded { + let dataOptions = PHImageRequestOptions() + dataOptions.isNetworkAccessAllowed = true + dataOptions.isSynchronous = true + dataOptions.version = .current + + var imageData: Data? + Self.imageManager.requestImageDataAndOrientation( + for: asset, + options: dataOptions, + resultHandler: { (data, _, _, _) in + imageData = data + } + ) + + if request.isCancelled { + Self.remove(requestId: requestId) + return completion(ImageProcessing.cancelledResult) + } + + guard let data = imageData else { + Self.remove(requestId: requestId) + return completion(.failure(PigeonError(code: "", message: "Could not get image data for \(assetId)", details: nil))) + } + + let length = data.count + let pointer = malloc(length)! + data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length) + + if request.isCancelled { + free(pointer) + Self.remove(requestId: requestId) + return completion(ImageProcessing.cancelledResult) + } + + request.callback(.success([ + "pointer": Int64(Int(bitPattern: pointer)), + "length": Int64(length), + ])) + Self.remove(requestId: requestId) + return + } + + var image: UIImage? + Self.imageManager.requestImage( + for: asset, + targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize, + contentMode: .aspectFill, + options: Self.requestOptions, + resultHandler: { (_image, info) -> Void in + image = _image + } + ) + + if request.isCancelled { + return completion(ImageProcessing.cancelledResult) + } + + guard let image = image, + let cgImage = image.cgImage else { + Self.remove(requestId: requestId) + return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) + } + + if request.isCancelled { + return completion(ImageProcessing.cancelledResult) + } + + do { + let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat) + + if request.isCancelled { + buffer.free() + return completion(ImageProcessing.cancelledResult) + } + + request.callback(.success([ + "pointer": Int64(Int(bitPattern: buffer.data)), + "width": Int64(buffer.width), + "height": Int64(buffer.height), + "rowBytes": Int64(buffer.rowBytes) + ])) + Self.remove(requestId: requestId) + } catch { + Self.remove(requestId: requestId) + return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", details: nil))) + } + } + + request.workItem = item + Self.add(requestId: requestId, request: request) + ImageProcessing.queue.async(execute: item) + } + + func cancelRequest(requestId: Int64) { + Self.cancel(requestId: requestId) + } + + private static func add(requestId: Int64, request: LocalImageRequest) -> Void { + requestQueue.sync { requests[requestId] = request } + } + + private static func remove(requestId: Int64) -> Void { + requestQueue.sync { requests[requestId] = nil } + } + + private static func cancel(requestId: Int64) -> Void { + requestQueue.async { + guard let request = requests.removeValue(forKey: requestId) else { return } + request.isCancelled = true + guard let item = request.workItem else { return } + if item.isCancelled { + cancelQueue.async { request.callback(ImageProcessing.cancelledResult) } + } + } + } + + private static func requestAsset(assetId: String) -> PHAsset? { + var asset: PHAsset? + assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) } + if asset != nil { return asset } + + guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject + else { return nil } + assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } + return asset + } +} diff --git a/mobile/ios/Runner/Images/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift new file mode 100644 index 0000000000..5123a12f3e --- /dev/null +++ b/mobile/ios/Runner/Images/RemoteImages.g.swift @@ -0,0 +1,135 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + + +private class RemoteImagesPigeonCodecReader: FlutterStandardReader { +} + +private class RemoteImagesPigeonCodecWriter: FlutterStandardWriter { +} + +private class RemoteImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return RemoteImagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return RemoteImagesPigeonCodecWriter(data: data) + } +} + +class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = RemoteImagesPigeonCodec(readerWriter: RemoteImagesPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol RemoteImageApi { + func requestImage(url: String, headers: [String: String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) + func cancelRequest(requestId: Int64) throws + func clearCache(completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class RemoteImageApiSetup { + static var codec: FlutterStandardMessageCodec { RemoteImagesPigeonCodec.shared } + /// Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + requestImageChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let urlArg = args[0] as! String + let headersArg = args[1] as! [String: String] + let requestIdArg = args[2] as! Int64 + let preferEncodedArg = args[3] as! Bool + api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + requestImageChannel.setMessageHandler(nil) + } + let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + cancelRequestChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let requestIdArg = args[0] as! Int64 + do { + try api.cancelRequest(requestId: requestIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + cancelRequestChannel.setMessageHandler(nil) + } + let clearCacheChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearCacheChannel.setMessageHandler { _, reply in + api.clearCache { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + clearCacheChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift new file mode 100644 index 0000000000..fe318800b8 --- /dev/null +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -0,0 +1,153 @@ +import Accelerate +import Flutter +import MobileCoreServices +import Photos + +class RemoteImageRequest { + weak var task: URLSessionDataTask? + let id: Int64 + var isCancelled = false + let completion: (Result<[String: Int64]?, any Error>) -> Void + + init(id: Int64, task: URLSessionDataTask, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { + self.id = id + self.task = task + self.completion = completion + } +} + +class RemoteImageApiImpl: NSObject, RemoteImageApi { + private static var lock = os_unfair_lock() + private static var requests = [Int64: RemoteImageRequest]() + private static var rgbaFormat = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), + renderingIntent: .perceptual + )! + private static let decodeOptions = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceCreateThumbnailFromImageAlways: true + ] as CFDictionary + + func requestImage(url: String, headers: [String : String], requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { + var urlRequest = URLRequest(url: URL(string: url)!) + urlRequest.cachePolicy = .returnCacheDataElseLoad + for (key, value) in headers { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + + let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in + Self.handleCompletion(requestId: requestId, encoded: preferEncoded, data: data, response: response, error: error) + } + + let request = RemoteImageRequest(id: requestId, task: task, completion: completion) + + os_unfair_lock_lock(&Self.lock) + Self.requests[requestId] = request + os_unfair_lock_unlock(&Self.lock) + + task.resume() + } + + private static func handleCompletion(requestId: Int64, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { + os_unfair_lock_lock(&Self.lock) + guard let request = requests[requestId] else { + return os_unfair_lock_unlock(&Self.lock) + } + requests[requestId] = nil + os_unfair_lock_unlock(&Self.lock) + + if let error = error { + if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + return request.completion(.failure(error)) + } + + if request.isCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + + guard let data = data else { + return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil))) + } + + ImageProcessing.queue.async { + ImageProcessing.semaphore.wait() + defer { ImageProcessing.semaphore.signal() } + + if request.isCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + + // Return raw encoded bytes when requested (for animated images) + if encoded { + let length = data.count + let pointer = malloc(length)! + data.copyBytes(to: pointer.assumingMemoryBound(to: UInt8.self), count: length) + + if request.isCancelled { + free(pointer) + return request.completion(ImageProcessing.cancelledResult) + } + + return request.completion( + .success([ + "pointer": Int64(Int(bitPattern: pointer)), + "length": Int64(length), + ])) + } + + guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), + let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { + return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) + } + + if request.isCancelled { + return request.completion(ImageProcessing.cancelledResult) + } + + do { + let buffer = try vImage_Buffer(cgImage: cgImage, format: rgbaFormat) + + if request.isCancelled { + buffer.free() + return request.completion(ImageProcessing.cancelledResult) + } + + request.completion( + .success([ + "pointer": Int64(Int(bitPattern: buffer.data)), + "width": Int64(buffer.width), + "height": Int64(buffer.height), + "rowBytes": Int64(buffer.rowBytes), + ])) + } catch { + return request.completion(.failure(PigeonError(code: "", message: "Failed to convert image for request: \(error)", details: nil))) + } + } + } + + func cancelRequest(requestId: Int64) { + os_unfair_lock_lock(&Self.lock) + let request = Self.requests[requestId] + os_unfair_lock_unlock(&Self.lock) + + guard let request = request else { return } + request.isCancelled = true + request.task?.cancel() + } + + func clearCache(completion: @escaping (Result) -> Void) { + Task { + let cache = URLSessionManager.shared.session.configuration.urlCache! + let cacheSize = Int64(cache.currentDiskUsage) + cache.removeAllCachedResponses() + completion(.success(cacheSize)) + } + } +} diff --git a/mobile/ios/Runner/Images/ThumbnailsImpl.swift b/mobile/ios/Runner/Images/ThumbnailsImpl.swift deleted file mode 100644 index 452ca62377..0000000000 --- a/mobile/ios/Runner/Images/ThumbnailsImpl.swift +++ /dev/null @@ -1,211 +0,0 @@ -import CryptoKit -import Flutter -import MobileCoreServices -import Photos - -class Request { - weak var workItem: DispatchWorkItem? - var isCancelled = false - let callback: (Result<[String: Int64], any Error>) -> Void - - init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) { - self.callback = callback - } -} - -class ThumbnailApiImpl: ThumbnailApi { - private static let imageManager = PHImageManager.default() - private static let fetchOptions = { - let fetchOptions = PHFetchOptions() - fetchOptions.fetchLimit = 1 - fetchOptions.wantsIncrementalChangeDetails = false - return fetchOptions - }() - private static let requestOptions = { - let requestOptions = PHImageRequestOptions() - requestOptions.isNetworkAccessAllowed = true - requestOptions.deliveryMode = .highQualityFormat - requestOptions.resizeMode = .fast - requestOptions.isSynchronous = true - requestOptions.version = .current - return requestOptions - }() - - private static let assetQueue = DispatchQueue(label: "thumbnail.assets", qos: .userInitiated) - private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated) - private static let cancelQueue = DispatchQueue(label: "thumbnail.cancellation", qos: .default) - private static let processingQueue = DispatchQueue(label: "thumbnail.processing", qos: .userInteractive, attributes: .concurrent) - - private static let rgbColorSpace = CGColorSpaceCreateDeviceRGB() - private static let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue).rawValue - private static var requests = [Int64: Request]() - private static let cancelledResult = Result<[String: Int64], any Error>.success([:]) - private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2) - private static let assetCache = { - let assetCache = NSCache() - assetCache.countLimit = 10000 - return assetCache - }() - private static let activitySemaphore = DispatchSemaphore(value: 1) - private static let willResignActiveObserver = NotificationCenter.default.addObserver( - forName: UIApplication.willResignActiveNotification, - object: nil, - queue: .main - ) { _ in - processingQueue.suspend() - activitySemaphore.wait() - } - private static let didBecomeActiveObserver = NotificationCenter.default.addObserver( - forName: UIApplication.didBecomeActiveNotification, - object: nil, - queue: .main - ) { _ in - processingQueue.resume() - activitySemaphore.signal() - } - - func getThumbhash(thumbhash: String, completion: @escaping (Result<[String : Int64], any Error>) -> Void) { - Self.processingQueue.async { - guard let data = Data(base64Encoded: thumbhash) - else { return completion(.failure(PigeonError(code: "", message: "Invalid base64 string: \(thumbhash)", details: nil)))} - - let (width, height, pointer) = thumbHashToRGBA(hash: data) - self.waitForActiveState() - completion(.success(["pointer": Int64(Int(bitPattern: pointer.baseAddress)), "width": Int64(width), "height": Int64(height)])) - } - } - - func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { - let request = Request(callback: completion) - let item = DispatchWorkItem { - if request.isCancelled { - return completion(Self.cancelledResult) - } - - Self.concurrencySemaphore.wait() - defer { - Self.concurrencySemaphore.signal() - } - - if request.isCancelled { - return completion(Self.cancelledResult) - } - - guard let asset = Self.requestAsset(assetId: assetId) - else { - Self.removeRequest(requestId: requestId) - completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) - return - } - - if request.isCancelled { - return completion(Self.cancelledResult) - } - - var image: UIImage? - Self.imageManager.requestImage( - for: asset, - targetSize: width > 0 && height > 0 ? CGSize(width: Double(width), height: Double(height)) : PHImageManagerMaximumSize, - contentMode: .aspectFill, - options: Self.requestOptions, - resultHandler: { (_image, info) -> Void in - image = _image - } - ) - - if request.isCancelled { - return completion(Self.cancelledResult) - } - - guard let image = image, - let cgImage = image.cgImage else { - Self.removeRequest(requestId: requestId) - return completion(.failure(PigeonError(code: "", message: "Could not get pixel data for \(assetId)", details: nil))) - } - - let pointer = UnsafeMutableRawPointer.allocate( - byteCount: Int(cgImage.width) * Int(cgImage.height) * 4, - alignment: MemoryLayout.alignment - ) - - if request.isCancelled { - pointer.deallocate() - return completion(Self.cancelledResult) - } - - guard let context = CGContext( - data: pointer, - width: cgImage.width, - height: cgImage.height, - bitsPerComponent: 8, - bytesPerRow: cgImage.width * 4, - space: Self.rgbColorSpace, - bitmapInfo: Self.bitmapInfo - ) else { - pointer.deallocate() - Self.removeRequest(requestId: requestId) - return completion(.failure(PigeonError(code: "", message: "Could not create context for \(assetId)", details: nil))) - } - - if request.isCancelled { - pointer.deallocate() - return completion(Self.cancelledResult) - } - - context.interpolationQuality = .none - context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)) - - if request.isCancelled { - pointer.deallocate() - return completion(Self.cancelledResult) - } - - self.waitForActiveState() - completion(.success(["pointer": Int64(Int(bitPattern: pointer)), "width": Int64(cgImage.width), "height": Int64(cgImage.height)])) - Self.removeRequest(requestId: requestId) - } - - request.workItem = item - Self.addRequest(requestId: requestId, request: request) - Self.processingQueue.async(execute: item) - } - - func cancelImageRequest(requestId: Int64) { - Self.cancelRequest(requestId: requestId) - } - - private static func addRequest(requestId: Int64, request: Request) -> Void { - requestQueue.sync { requests[requestId] = request } - } - - private static func removeRequest(requestId: Int64) -> Void { - requestQueue.sync { requests[requestId] = nil } - } - - private static func cancelRequest(requestId: Int64) -> Void { - requestQueue.async { - guard let request = requests.removeValue(forKey: requestId) else { return } - request.isCancelled = true - guard let item = request.workItem else { return } - if item.isCancelled { - cancelQueue.async { request.callback(Self.cancelledResult) } - } - } - } - - private static func requestAsset(assetId: String) -> PHAsset? { - var asset: PHAsset? - assetQueue.sync { asset = assetCache.object(forKey: assetId as NSString) } - if asset != nil { return asset } - - guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: Self.fetchOptions).firstObject - else { return nil } - assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } - return asset - } - - func waitForActiveState() { - Self.activitySemaphore.wait() - Self.activitySemaphore.signal() - } -} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b597912d0f..1557d7f701 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -80,7 +80,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.2.1 + 2.5.6 CFBundleSignature ???? CFBundleURLTypes @@ -107,7 +107,7 @@ CFBundleVersion - 233 + 240 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index c1cc98014b..6bba25d94b 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -128,6 +128,15 @@ func deepHashMessages(value: Any?, hasher: inout Hasher) { +enum PlatformAssetPlaybackStyle: Int { + case unknown = 0 + case image = 1 + case video = 2 + case imageAnimated = 3 + case livePhoto = 4 + case videoLooping = 5 +} + /// Generated class from Pigeon that represents data sent in messages. struct PlatformAsset: Hashable { var id: String @@ -143,6 +152,7 @@ struct PlatformAsset: Hashable { var adjustmentTime: Int64? = nil var latitude: Double? = nil var longitude: Double? = nil + var playbackStyle: PlatformAssetPlaybackStyle // swift-format-ignore: AlwaysUseLowerCamelCase @@ -160,6 +170,7 @@ struct PlatformAsset: Hashable { let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10]) let latitude: Double? = nilOrValue(pigeonVar_list[11]) let longitude: Double? = nilOrValue(pigeonVar_list[12]) + let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle return PlatformAsset( id: id, @@ -174,7 +185,8 @@ struct PlatformAsset: Hashable { isFavorite: isFavorite, adjustmentTime: adjustmentTime, latitude: latitude, - longitude: longitude + longitude: longitude, + playbackStyle: playbackStyle ) } func toList() -> [Any?] { @@ -192,6 +204,7 @@ struct PlatformAsset: Hashable { adjustmentTime, latitude, longitude, + playbackStyle, ] } static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { @@ -312,17 +325,58 @@ struct HashResult: Hashable { } } +/// Generated class from Pigeon that represents data sent in messages. +struct CloudIdResult: Hashable { + var assetId: String + var error: String? = nil + var cloudId: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CloudIdResult? { + let assetId = pigeonVar_list[0] as! String + let error: String? = nilOrValue(pigeonVar_list[1]) + let cloudId: String? = nilOrValue(pigeonVar_list[2]) + + return CloudIdResult( + assetId: assetId, + error: error, + cloudId: cloudId + ) + } + func toList() -> [Any?] { + return [ + assetId, + error, + cloudId, + ] + } + static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + private class MessagesPigeonCodecReader: FlutterStandardReader { override func readValue(ofType type: UInt8) -> Any? { switch type { case 129: - return PlatformAsset.fromList(self.readValue() as! [Any?]) + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt) + } + return nil case 130: - return PlatformAlbum.fromList(self.readValue() as! [Any?]) + return PlatformAsset.fromList(self.readValue() as! [Any?]) case 131: - return SyncDelta.fromList(self.readValue() as! [Any?]) + return PlatformAlbum.fromList(self.readValue() as! [Any?]) case 132: + return SyncDelta.fromList(self.readValue() as! [Any?]) + case 133: return HashResult.fromList(self.readValue() as! [Any?]) + case 134: + return CloudIdResult.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -331,18 +385,24 @@ private class MessagesPigeonCodecReader: FlutterStandardReader { private class MessagesPigeonCodecWriter: FlutterStandardWriter { override func writeValue(_ value: Any) { - if let value = value as? PlatformAsset { + if let value = value as? PlatformAssetPlaybackStyle { super.writeByte(129) - super.writeValue(value.toList()) - } else if let value = value as? PlatformAlbum { + super.writeValue(value.rawValue) + } else if let value = value as? PlatformAsset { super.writeByte(130) super.writeValue(value.toList()) - } else if let value = value as? SyncDelta { + } else if let value = value as? PlatformAlbum { super.writeByte(131) super.writeValue(value.toList()) - } else if let value = value as? HashResult { + } else if let value = value as? SyncDelta { super.writeByte(132) super.writeValue(value.toList()) + } else if let value = value as? HashResult { + super.writeByte(133) + super.writeValue(value.toList()) + } else if let value = value as? CloudIdResult { + super.writeByte(134) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -377,6 +437,7 @@ protocol NativeSyncApi { func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) func cancelHashing() throws func getTrashedAssets() throws -> [String: [PlatformAsset]] + func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -560,5 +621,22 @@ class NativeSyncApiSetup { } else { getTrashedAssetsChannel.setMessageHandler(nil) } + let getCloudIdForAssetIdsChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getCloudIdForAssetIdsChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let assetIdsArg = args[0] as! [String] + do { + let result = try api.getCloudIdForAssetIds(assetIds: assetIdsArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getCloudIdForAssetIdsChannel.setMessageHandler(nil) + } } } diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index 03493f57ca..8022fb06d2 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -19,31 +19,31 @@ struct AssetWrapper: Hashable, Equatable { class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { static let name = "NativeSyncApi" - + static func register(with registrar: any FlutterPluginRegistrar) { let instance = NativeSyncApiImpl() NativeSyncApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) registrar.publish(instance) } - + func detachFromEngine(for registrar: any FlutterPluginRegistrar) { super.detachFromEngine() } - + private let defaults: UserDefaults private let changeTokenKey = "immich:changeToken" private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] private let recoveredAlbumSubType = 1000000219 - + private var hashTask: Task? private static let hashCancelledCode = "HASH_CANCELLED" private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil)) - - + + init(with defaults: UserDefaults = .standard) { self.defaults = defaults } - + @available(iOS 16, *) private func getChangeToken() -> PHPersistentChangeToken? { guard let data = defaults.data(forKey: changeTokenKey) else { @@ -51,7 +51,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data) } - + @available(iOS 16, *) private func saveChangeToken(token: PHPersistentChangeToken) -> Void { guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { @@ -59,18 +59,18 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { } defaults.set(data, forKey: changeTokenKey) } - + func clearSyncCheckpoint() -> Void { defaults.removeObject(forKey: changeTokenKey) } - + func checkpointSync() { guard #available(iOS 16, *) else { return } saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) } - + func shouldFullSync() -> Bool { guard #available(iOS 16, *), PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized, @@ -78,36 +78,36 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { // When we do not have access to photo library, older iOS version or No token available, fallback to full sync return true } - + guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else { // Cannot fetch persistent changes return true } - + return false } - + func getAlbums() throws -> [PlatformAlbum] { var albums: [PlatformAlbum] = [] - + albumTypes.forEach { type in let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) for i in 0.. SyncDelta { guard #available(iOS 16, *) else { throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) } - + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil) } - + guard let storedToken = getChangeToken() else { // No token exists, definitely need a full sync print("MediaManager::getMediaChanges: No token found") throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil) } - + let currentToken = PHPhotoLibrary.shared().currentChangeToken if storedToken == currentToken { return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:]) } - + do { let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) - + var updatedAssets: Set = [] var deletedAssets: Set = [] - + for change in changes { guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue } - + let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers) deletedAssets.formUnion(details.deletedLocalIdentifiers) - + if (updated.isEmpty) { continue } - + let options = PHFetchOptions() options.includeHiddenAssets = false let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options) for i in 0..) -> [String: [String]] { guard !assets.isEmpty else { return [:] } - + var albumAssets: [String: [String]] = [:] - + for type in albumTypes { let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) collections.enumerateObjects { (album, _, _) in @@ -211,13 +212,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { } return albumAssets } - + func getAssetIdsForAlbum(albumId: String) throws -> [String] { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return [] } - + var ids: [String] = [] let options = PHFetchOptions() options.includeHiddenAssets = false @@ -227,13 +228,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { } return ids } - + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return 0 } - + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) let options = PHFetchOptions() options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) @@ -241,32 +242,32 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { let assets = getAssetsFromAlbum(in: album, options: options) return Int64(assets.count) } - + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) guard let album = collections.firstObject else { return [] } - + let options = PHFetchOptions() options.includeHiddenAssets = false if(updatedTimeCond != nil) { let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!)) options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) } - + let result = getAssetsFromAlbum(in: album, options: options) if(result.count == 0) { return [] } - + var assets: [PlatformAsset] = [] result.enumerateObjects { (asset, _, _) in assets.append(asset.toPlatformAsset()) } return assets } - + func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) { if let prevTask = hashTask { prevTask.cancel() @@ -284,11 +285,11 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { missingAssetIds.remove(asset.localIdentifier) assets.append(asset) } - + if Task.isCancelled { return self?.completeWhenActive(for: completion, with: Self.hashCancelled) } - + await withTaskGroup(of: HashResult?.self) { taskGroup in var results = [HashResult]() results.reserveCapacity(assets.count) @@ -301,28 +302,28 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess) } } - + for await result in taskGroup { guard let result = result else { return self?.completeWhenActive(for: completion, with: Self.hashCancelled) } results.append(result) } - + for missing in missingAssetIds { results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil)) } - + return self?.completeWhenActive(for: completion, with: .success(results)) } } } - + func cancelHashing() { hashTask?.cancel() hashTask = nil } - + private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? { class RequestRef { var id: PHAssetResourceDataRequestID? @@ -332,21 +333,21 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { if Task.isCancelled { return nil } - + guard let resource = asset.getResource() else { return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil) } - + if Task.isCancelled { return nil } - + let options = PHAssetResourceRequestOptions() options.isNetworkAccessAllowed = allowNetworkAccess - + return await withCheckedContinuation { continuation in var hasher = Insecure.SHA1() - + requestRef.id = PHAssetResourceManager.default().requestData( for: resource, options: options, @@ -377,11 +378,11 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { PHAssetResourceManager.default().cancelDataRequest(requestId) }) } - + func getTrashedAssets() throws -> [String: [PlatformAsset]] { throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil) } - + private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult { // Ensure to actually getting all assets for the Recents album if (album.assetCollectionSubtype == .smartAlbumUserLibrary) { @@ -390,4 +391,28 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { return PHAsset.fetchAssets(in: album, options: options) } } + + func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] { + guard #available(iOS 16, *) else { + return assetIds.map { CloudIdResult(assetId: $0) } + } + + var mappings: [CloudIdResult] = [] + let result = PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: assetIds) + for (key, value) in result { + switch value { + case .success(let cloudIdentifier): + let cloudId = cloudIdentifier.stringValue + // Ignores invalid cloud ids of the format "GUID:ID:". Valid Ids are of the form "GUID:ID:HASH" + if !cloudId.hasSuffix(":") { + mappings.append(CloudIdResult(assetId: key, cloudId: cloudId)) + } else { + mappings.append(CloudIdResult(assetId: key, error: "Incomplete Cloud Id: \(cloudId)")) + } + case .failure(let error): + mappings.append(CloudIdResult(assetId: key, error: "Error getting Cloud Id: \(error.localizedDescription)")) + } + } + return mappings; + } } diff --git a/mobile/ios/Runner/Sync/PHAssetExtensions.swift b/mobile/ios/Runner/Sync/PHAssetExtensions.swift index f555d75bd0..0fc1dfc701 100644 --- a/mobile/ios/Runner/Sync/PHAssetExtensions.swift +++ b/mobile/ios/Runner/Sync/PHAssetExtensions.swift @@ -1,6 +1,17 @@ import Photos extension PHAsset { + var platformPlaybackStyle: PlatformAssetPlaybackStyle { + switch playbackStyle { + case .image: return .image + case .imageAnimated: return .imageAnimated + case .livePhoto: return .livePhoto + case .video: return .video + case .videoLooping: return .videoLooping + @unknown default: return .unknown + } + } + func toPlatformAsset() -> PlatformAsset { return PlatformAsset( id: localIdentifier, @@ -15,7 +26,8 @@ extension PHAsset { isFavorite: isFavorite, adjustmentTime: adjustmentTimestamp, latitude: location?.coordinate.latitude, - longitude: location?.coordinate.longitude + longitude: location?.coordinate.longitude, + playbackStyle: platformPlaybackStyle ) } @@ -26,7 +38,7 @@ extension PHAsset { var filename: String? { return value(forKey: "filename") as? String } - + var adjustmentTimestamp: Int64? { if let date = value(forKey: "adjustmentTimestamp") as? Date { return Int64(date.timeIntervalSince1970) diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift index 19ff3d38ba..6ae2d502f8 100644 --- a/mobile/ios/WidgetExtension/ImmichAPI.swift +++ b/mobile/ios/WidgetExtension/ImmichAPI.swift @@ -225,7 +225,7 @@ class ImmichAPI { } func fetchImage(asset: Asset) async throws(FetchError) -> UIImage { - let thumbnailParams = [URLQueryItem(name: "size", value: "preview")] + let thumbnailParams = [URLQueryItem(name: "size", value: "preview"), URLQueryItem(name: "edited", value: "true")] let assetEndpoint = "/assets/" + asset.id + "/thumbnail" guard diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d167d5fb2d..9c31ced00d 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -44,7 +44,7 @@ def get_version_from_pubspec end # Helper method to configure code signing for all targets - def configure_code_signing(bundle_id_suffix: "") + def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:) bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}" # Runner (main app) @@ -54,7 +54,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix} AppStore", + profile_name: profile_name_main, targets: ["Runner"] ) @@ -65,7 +65,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension AppStore", + profile_name: profile_name_share, targets: ["ShareExtension"] ) @@ -76,7 +76,7 @@ end team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, code_sign_identity: CODE_SIGN_IDENTITY, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget", - profile_name: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget AppStore", + profile_name: profile_name_widget, targets: ["WidgetExtension"] ) end @@ -87,7 +87,10 @@ end bundle_id_suffix: "", configuration: "Release", distribute_external: true, - version_number: nil + version_number: nil, + profile_name_main:, + profile_name_share:, + profile_name_widget: ) bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}" app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}" @@ -115,9 +118,9 @@ end xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", export_options: { provisioningProfiles: { - "#{app_identifier}" => "#{app_identifier} AppStore", - "#{app_identifier}.ShareExtension" => "#{app_identifier}.ShareExtension AppStore", - "#{app_identifier}.Widget" => "#{app_identifier}.Widget AppStore" + "#{app_identifier}" => profile_name_main, + "#{app_identifier}.ShareExtension" => profile_name_share, + "#{app_identifier}.Widget" => profile_name_widget }, signingStyle: "manual", signingCertificate: CODE_SIGN_IDENTITY @@ -136,20 +139,35 @@ end lane :gha_testflight_dev do api_key = get_api_key - # Install development provisioning profiles - install_provisioning_profile(path: "profile_dev.mobileprovision") - install_provisioning_profile(path: "profile_dev_share.mobileprovision") - install_provisioning_profile(path: "profile_dev_widget.mobileprovision") + # Download and install provisioning profiles from App Store Connect + # Certificate is imported by GHA workflow into build.keychain + # Capture profile names after each sigh call + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true) + main_profile_name = lane_context[SharedValues::SIGH_NAME] - # Configure code signing for dev bundle IDs - configure_code_signing(bundle_id_suffix: "development") + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true) + share_profile_name = lane_context[SharedValues::SIGH_NAME] + + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true) + widget_profile_name = lane_context[SharedValues::SIGH_NAME] + + # Configure code signing for dev bundle IDs using the downloaded profile names + configure_code_signing( + bundle_id_suffix: "development", + profile_name_main: main_profile_name, + profile_name_share: share_profile_name, + profile_name_widget: widget_profile_name + ) # Build and upload build_and_upload( api_key: api_key, bundle_id_suffix: "development", configuration: "Profile", - distribute_external: false + distribute_external: false, + profile_name_main: main_profile_name, + profile_name_share: share_profile_name, + profile_name_widget: widget_profile_name ) end @@ -157,20 +175,33 @@ end lane :gha_release_prod do api_key = get_api_key - # Install provisioning profiles - install_provisioning_profile(path: "profile.mobileprovision") - install_provisioning_profile(path: "profile_share.mobileprovision") - install_provisioning_profile(path: "profile_widget.mobileprovision") + # Download and install provisioning profiles from App Store Connect + # Certificate is imported by GHA workflow into build.keychain + sigh(api_key: api_key, app_identifier: BASE_BUNDLE_ID, force: true) + main_profile_name = lane_context[SharedValues::SIGH_NAME] + + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.ShareExtension", force: true) + share_profile_name = lane_context[SharedValues::SIGH_NAME] + + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.Widget", force: true) + widget_profile_name = lane_context[SharedValues::SIGH_NAME] # Configure code signing for production bundle IDs - configure_code_signing + configure_code_signing( + profile_name_main: main_profile_name, + profile_name_share: share_profile_name, + profile_name_widget: widget_profile_name + ) # Build and upload with version number build_and_upload( api_key: api_key, version_number: get_version_from_pubspec, distribute_external: false, + profile_name_main: main_profile_name, + profile_name_share: share_profile_name, + profile_name_widget: widget_profile_name ) end @@ -215,13 +246,26 @@ end # Use the same build process as production, just skip the upload # This ensures PR builds validate the same way as production builds - # Install provisioning profiles (use development profiles for PR builds) - install_provisioning_profile(path: "profile_dev.mobileprovision") - install_provisioning_profile(path: "profile_dev_share.mobileprovision") - install_provisioning_profile(path: "profile_dev_widget.mobileprovision") + api_key = get_api_key + + # Download and install provisioning profiles from App Store Connect + # Certificate is imported by GHA workflow into build.keychain + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true) + main_profile_name = lane_context[SharedValues::SIGH_NAME] + + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true) + share_profile_name = lane_context[SharedValues::SIGH_NAME] + + sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true) + widget_profile_name = lane_context[SharedValues::SIGH_NAME] # Configure code signing for dev bundle IDs - configure_code_signing(bundle_id_suffix: "development") + configure_code_signing( + bundle_id_suffix: "development", + profile_name_main: main_profile_name, + profile_name_share: share_profile_name, + profile_name_widget: widget_profile_name + ) # Build the app (same as gha_testflight_dev but without upload) build_app( @@ -233,9 +277,9 @@ end xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", export_options: { provisioningProfiles: { - "#{BASE_BUNDLE_ID}.development" => "#{BASE_BUNDLE_ID}.development AppStore", - "#{BASE_BUNDLE_ID}.development.ShareExtension" => "#{BASE_BUNDLE_ID}.development.ShareExtension AppStore", - "#{BASE_BUNDLE_ID}.development.Widget" => "#{BASE_BUNDLE_ID}.development.Widget AppStore" + "#{BASE_BUNDLE_ID}.development" => main_profile_name, + "#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name, + "#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name }, signingStyle: "manual", signingCertificate: CODE_SIGN_IDENTITY diff --git a/mobile/ios/fastlane/README.md b/mobile/ios/fastlane/README.md index 5fc8101b3a..9ba39c0a34 100644 --- a/mobile/ios/fastlane/README.md +++ b/mobile/ios/fastlane/README.md @@ -39,6 +39,14 @@ iOS Release to TestFlight iOS Manual Release +### ios gha_build_only + +```sh +[bundle exec] fastlane ios gha_build_only +``` + +iOS Build Only (no TestFlight upload) + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index cc408548d2..9d28941b8f 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -4,6 +4,8 @@ const int noDbId = -9223372036854775808; // from Isar const double downloadCompleted = -1; const double downloadFailed = -2; +const String kMobileMetadataKey = "mobile-app"; + // Number of log entries to retain on app start const int kLogTruncateLimit = 2000; diff --git a/mobile/lib/constants/enums.dart b/mobile/lib/constants/enums.dart index 91ca50a2c0..32ef9bbbed 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -1,4 +1,11 @@ -enum SortOrder { asc, desc } +enum SortOrder { + asc, + desc; + + SortOrder reverse() { + return this == SortOrder.asc ? SortOrder.desc : SortOrder.asc; + } +} enum TextSearchType { context, filename, description, ocr } @@ -7,3 +14,9 @@ enum AssetVisibilityEnum { timeline, hidden, archive, locked } enum SortUserBy { id } enum ActionSource { timeline, viewer } + +enum CleanupStep { selectDate, scan, delete } + +enum AssetKeepType { none, photosOnly, videosOnly } + +enum AssetDateAggregation { start, end } diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index f3c24384b0..e20f037beb 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -51,4 +51,4 @@ const Map locales = { const String translationsPath = 'assets/i18n'; -const List localesNotSupportedByOverpass = [Locale('el', 'GR'), Locale('sr', 'Cyrl')]; +const List localesNotSupportedByAppFont = [Locale('el', 'GR'), Locale('sr', 'Cyrl')]; diff --git a/mobile/lib/domain/models/asset/asset_metadata.model.dart b/mobile/lib/domain/models/asset/asset_metadata.model.dart new file mode 100644 index 0000000000..fc29da3db0 --- /dev/null +++ b/mobile/lib/domain/models/asset/asset_metadata.model.dart @@ -0,0 +1,62 @@ +enum RemoteAssetMetadataKey { + mobileApp("mobile-app"); + + final String key; + + const RemoteAssetMetadataKey(this.key); +} + +abstract class RemoteAssetMetadataValue { + const RemoteAssetMetadataValue(); + + Map toJson(); +} + +class RemoteAssetMetadataItem { + final RemoteAssetMetadataKey key; + final RemoteAssetMetadataValue value; + + const RemoteAssetMetadataItem({required this.key, required this.value}); + + Map toJson() { + return {'key': key.key, 'value': value}; + } +} + +class RemoteAssetMobileAppMetadata extends RemoteAssetMetadataValue { + final String? cloudId; + final String? createdAt; + final String? adjustmentTime; + final String? latitude; + final String? longitude; + + const RemoteAssetMobileAppMetadata({ + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + + @override + Map toJson() { + final map = {}; + if (cloudId != null) { + map["iCloudId"] = cloudId; + } + if (createdAt != null) { + map["createdAt"] = createdAt; + } + if (adjustmentTime != null) { + map["adjustmentTime"] = adjustmentTime; + } + if (latitude != null) { + map["latitude"] = latitude; + } + if (longitude != null) { + map["longitude"] = longitude; + } + + return map; + } +} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index 5774a13c90..5dd34c04ba 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -11,6 +11,10 @@ enum AssetType { enum AssetState { local, remote, merged } +// do not change! +// keep in sync with PlatformAssetPlaybackStyle +enum AssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping } + sealed class BaseAsset { final String name; final String? checksum; @@ -22,6 +26,7 @@ sealed class BaseAsset { final int? durationInSeconds; final bool isFavorite; final String? livePhotoVideoId; + final bool isEdited; const BaseAsset({ required this.name, @@ -34,6 +39,7 @@ sealed class BaseAsset { this.durationInSeconds, this.isFavorite = false, this.livePhotoVideoId, + required this.isEdited, }); bool get isImage => type == AssetType.image; @@ -41,6 +47,14 @@ sealed class BaseAsset { bool get isMotionPhoto => livePhotoVideoId != null; + AssetPlaybackStyle get playbackStyle { + if (isVideo) return AssetPlaybackStyle.video; + if (isMotionPhoto) return AssetPlaybackStyle.livePhoto; + if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated; + if (isImage) return AssetPlaybackStyle.image; + return AssetPlaybackStyle.unknown; + } + Duration get duration { final durationInSeconds = this.durationInSeconds; if (durationInSeconds != null) { @@ -71,6 +85,7 @@ sealed class BaseAsset { height: ${height ?? ""}, durationInSeconds: ${durationInSeconds ?? ""}, isFavorite: $isFavorite, + isEdited: $isEdited, }'''; } @@ -85,7 +100,8 @@ sealed class BaseAsset { width == other.width && height == other.height && durationInSeconds == other.durationInSeconds && - isFavorite == other.isFavorite; + isFavorite == other.isFavorite && + isEdited == other.isEdited; } return false; } @@ -99,6 +115,7 @@ sealed class BaseAsset { width.hashCode ^ height.hashCode ^ durationInSeconds.hashCode ^ - isFavorite.hashCode; + isFavorite.hashCode ^ + isEdited.hashCode; } } diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart index ba64cc40b8..6766f4c3a2 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -3,7 +3,10 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { final String id; final String? remoteAssetId; + final String? cloudId; final int orientation; + @override + final AssetPlaybackStyle playbackStyle; final DateTime? adjustmentTime; final double? latitude; @@ -12,6 +15,7 @@ class LocalAsset extends BaseAsset { const LocalAsset({ required this.id, String? remoteId, + this.cloudId, required super.name, super.checksum, required super.type, @@ -23,9 +27,11 @@ class LocalAsset extends BaseAsset { super.isFavorite = false, super.livePhotoVideoId, this.orientation = 0, + required this.playbackStyle, this.adjustmentTime, this.latitude, this.longitude, + required super.isEdited, }) : remoteAssetId = remoteId; @override @@ -53,12 +59,15 @@ class LocalAsset extends BaseAsset { width: ${width ?? ""}, height: ${height ?? ""}, durationInSeconds: ${durationInSeconds ?? ""}, - remoteId: ${remoteId ?? ""} + playbackStyle: $playbackStyle, + remoteId: ${remoteId ?? ""}, + cloudId: ${cloudId ?? ""}, + checksum: ${checksum ?? ""}, isFavorite: $isFavorite, - orientation: $orientation, - adjustmentTime: $adjustmentTime, - latitude: ${latitude ?? ""}, - longitude: ${longitude ?? ""}, + orientation: $orientation, + adjustmentTime: $adjustmentTime, + latitude: ${latitude ?? ""}, + longitude: ${longitude ?? ""}, }'''; } @@ -69,7 +78,9 @@ class LocalAsset extends BaseAsset { if (identical(this, other)) return true; return super == other && id == other.id && + cloudId == other.cloudId && orientation == other.orientation && + playbackStyle == other.playbackStyle && adjustmentTime == other.adjustmentTime && latitude == other.latitude && longitude == other.longitude; @@ -81,6 +92,7 @@ class LocalAsset extends BaseAsset { id.hashCode ^ remoteId.hashCode ^ orientation.hashCode ^ + playbackStyle.hashCode ^ adjustmentTime.hashCode ^ latitude.hashCode ^ longitude.hashCode; @@ -88,6 +100,7 @@ class LocalAsset extends BaseAsset { LocalAsset copyWith({ String? id, String? remoteId, + String? cloudId, String? name, String? checksum, AssetType? type, @@ -98,13 +111,16 @@ class LocalAsset extends BaseAsset { int? durationInSeconds, bool? isFavorite, int? orientation, + AssetPlaybackStyle? playbackStyle, DateTime? adjustmentTime, double? latitude, double? longitude, + bool? isEdited, }) { return LocalAsset( id: id ?? this.id, remoteId: remoteId ?? this.remoteId, + cloudId: cloudId ?? this.cloudId, name: name ?? this.name, checksum: checksum ?? this.checksum, type: type ?? this.type, @@ -115,9 +131,11 @@ class LocalAsset extends BaseAsset { durationInSeconds: durationInSeconds ?? this.durationInSeconds, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + playbackStyle: playbackStyle ?? this.playbackStyle, adjustmentTime: adjustmentTime ?? this.adjustmentTime, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart index 4974dc9118..43d49506e3 100644 --- a/mobile/lib/domain/models/asset/remote_asset.model.dart +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -28,6 +28,7 @@ class RemoteAsset extends BaseAsset { this.visibility = AssetVisibility.timeline, super.livePhotoVideoId, this.stackId, + required super.isEdited, }) : localAssetId = localId; @override @@ -104,6 +105,7 @@ class RemoteAsset extends BaseAsset { AssetVisibility? visibility, String? livePhotoVideoId, String? stackId, + bool? isEdited, }) { return RemoteAsset( id: id ?? this.id, @@ -122,6 +124,7 @@ class RemoteAsset extends BaseAsset { visibility: visibility ?? this.visibility, livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, stackId: stackId ?? this.stackId, + isEdited: isEdited ?? this.isEdited, ); } } diff --git a/mobile/lib/domain/models/events.model.dart b/mobile/lib/domain/models/events.model.dart index b3ab756414..9bbe00852e 100644 --- a/mobile/lib/domain/models/events.model.dart +++ b/mobile/lib/domain/models/events.model.dart @@ -16,9 +16,8 @@ class ScrollToDateEvent extends Event { } // Asset Viewer Events -class ViewerOpenBottomSheetEvent extends Event { - final bool activitiesMode; - const ViewerOpenBottomSheetEvent({this.activitiesMode = false}); +class ViewerShowDetailsEvent extends Event { + const ViewerShowDetailsEvent(); } class ViewerReloadAssetEvent extends Event { @@ -30,3 +29,8 @@ class MultiSelectToggleEvent extends Event { final bool isEnabled; const MultiSelectToggleEvent(this.isEnabled); } + +// Map Events +class MapMarkerReloadEvent extends Event { + const MapMarkerReloadEvent(); +} diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart index 46e2352ac8..d0f78b59de 100644 --- a/mobile/lib/domain/models/exif.model.dart +++ b/mobile/lib/domain/models/exif.model.dart @@ -6,6 +6,7 @@ class ExifInfo { final String? orientation; final String? timeZone; final DateTime? dateTimeOriginal; + final int? rating; // GPS final double? latitude; @@ -46,6 +47,7 @@ class ExifInfo { this.orientation, this.timeZone, this.dateTimeOriginal, + this.rating, this.isFlipped = false, this.latitude, this.longitude, @@ -71,6 +73,7 @@ class ExifInfo { other.orientation == orientation && other.timeZone == timeZone && other.dateTimeOriginal == dateTimeOriginal && + other.rating == rating && other.latitude == latitude && other.longitude == longitude && other.city == city && @@ -94,6 +97,7 @@ class ExifInfo { isFlipped.hashCode ^ timeZone.hashCode ^ dateTimeOriginal.hashCode ^ + rating.hashCode ^ latitude.hashCode ^ longitude.hashCode ^ city.hashCode ^ @@ -118,6 +122,7 @@ orientation: ${orientation ?? 'NA'}, isFlipped: $isFlipped, timeZone: ${timeZone ?? 'NA'}, dateTimeOriginal: ${dateTimeOriginal ?? 'NA'}, +rating: ${rating ?? 'NA'}, latitude: ${latitude ?? 'NA'}, longitude: ${longitude ?? 'NA'}, city: ${city ?? 'NA'}, @@ -140,6 +145,7 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, String? orientation, String? timeZone, DateTime? dateTimeOriginal, + int? rating, double? latitude, double? longitude, String? city, @@ -161,6 +167,7 @@ exposureSeconds: ${exposureSeconds ?? 'NA'}, orientation: orientation ?? this.orientation, timeZone: timeZone ?? this.timeZone, dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + rating: rating ?? this.rating, isFlipped: isFlipped ?? this.isFlipped, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index a18644cd2a..00545aa01a 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -73,6 +73,9 @@ enum StoreKey { autoPlayVideo._(139), albumGridView._(140), + // Image viewer navigation settings + tapToNavigate._(141), + // Experimental stuff photoManagerCustomFilter._(1000), betaPromptShown._(1001), @@ -82,7 +85,16 @@ enum StoreKey { useWifiForUploadPhotos._(1005), needBetaMigration._(1006), // TODO: Remove this after patching open-api - shouldResetSync._(1007); + shouldResetSync._(1007), + + // Free up space + cleanupKeepFavorites._(1008), + cleanupKeepMediaType._(1009), + cleanupKeepAlbumIds._(1010), + cleanupCutoffDaysAgo._(1011), + cleanupDefaultsInitialized._(1012), + + syncMigrationStatus._(1013); const StoreKey._(this.id); final int id; diff --git a/mobile/lib/domain/models/tag.model.dart b/mobile/lib/domain/models/tag.model.dart new file mode 100644 index 0000000000..357367b13e --- /dev/null +++ b/mobile/lib/domain/models/tag.model.dart @@ -0,0 +1,29 @@ +import 'package:openapi/api.dart'; + +class Tag { + final String id; + final String value; + + const Tag({required this.id, required this.value}); + + @override + String toString() { + return 'Tag(id: $id, value: $value)'; + } + + @override + bool operator ==(covariant Tag other) { + if (identical(this, other)) return true; + + return other.id == id && other.value == value; + } + + @override + int get hashCode { + return id.hashCode ^ value.hashCode; + } + + static Tag fromDto(TagResponseDto dto) { + return Tag(id: dto.id, value: dto.value); + } +} diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index eb78ea0c8e..198733b3c8 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -4,7 +4,6 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped}); @@ -99,9 +98,7 @@ class AssetService { height = fetched?.height?.toDouble(); } - final exif = await getExif(asset); - final isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation); - return (width: width, height: height, isFlipped: isFlipped); + return (width: width, height: height, isFlipped: false); } Future> getPlaces(String userId) { diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 8a237f801a..6de13b6244 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -9,7 +9,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; @@ -20,13 +19,13 @@ import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; @@ -89,7 +88,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { Future init() async { try { - HttpSSLOptions.apply(applyNative: false); + HttpSSLOptions.apply(); await Future.wait( [ @@ -243,13 +242,12 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } if (Platform.isIOS) { - return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); + return _ref?.read(driftBackupProvider.notifier).startBackupWithURLSession(currentUser.id); } - final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? []; return _ref - ?.read(uploadServiceProvider) - .startBackupWithHttpClient(currentUser.id, networkCapabilities.isUnmetered, _cancellationToken); + ?.read(foregroundUploadServiceProvider) + .uploadCandidates(currentUser.id, _cancellationToken, useSequentialUpload: true); }, (error, stack) { dPrint(() => "Error in backup zone $error, $stack"); diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index 5e81643fc5..6781507566 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -40,6 +40,9 @@ class HashService { _log.info("Starting hashing of assets"); final Stopwatch stopwatch = Stopwatch()..start(); try { + // Migrate hashes from cloud ID to local ID so we don't have to re-hash them + // await _localAssetRepository.reconcileHashesFromCloudId(); + // Sorted by backupSelection followed by isCloud final localAlbums = await _localAlbumRepository.getBackupAlbums(); diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index c49ac49cce..029482978a 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/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/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; @@ -18,6 +19,8 @@ import 'package:logging/logging.dart'; class LocalSyncService { final DriftLocalAlbumRepository _localAlbumRepository; + // ignore: unused_field + final DriftLocalAssetRepository _localAssetRepository; final NativeSyncApi _nativeSyncApi; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final LocalFilesManagerRepository _localFilesManager; @@ -26,11 +29,13 @@ class LocalSyncService { LocalSyncService({ required DriftLocalAlbumRepository localAlbumRepository, + required DriftLocalAssetRepository localAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required LocalFilesManagerRepository localFilesManager, required StorageRepository storageRepository, required NativeSyncApi nativeSyncApi, }) : _localAlbumRepository = localAlbumRepository, + _localAssetRepository = localAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository, _localFilesManager = localFilesManager, _storageRepository = storageRepository, @@ -47,6 +52,12 @@ class LocalSyncService { _log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing"); } } + + if (CurrentPlatform.isIOS) { + // final assets = await _localAssetRepository.getEmptyCloudIdAssets(); + // await _mapIosCloudIds(assets); + } + if (full || await _nativeSyncApi.shouldFullSync()) { _log.fine("Full sync request from ${full ? "user" : "native"}"); return await fullSync(); @@ -63,8 +74,9 @@ class LocalSyncService { final deviceAlbums = await _nativeSyncApi.getAlbums(); await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); + final newAssets = delta.updates.toLocalAssets(); await _localAlbumRepository.processDelta( - updates: delta.updates.toLocalAssets(), + updates: newAssets, deletes: delta.deletes, assetAlbums: delta.assetAlbums, ); @@ -92,6 +104,8 @@ class LocalSyncService { } await updateAlbum(dbAlbum, album); } + + await _mapIosCloudIds(newAssets); } await _nativeSyncApi.checkpointSync(); } catch (e, s) { @@ -130,9 +144,12 @@ 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).then((a) => a.toLocalAssets()) + : []; - await _localAlbumRepository.upsert(album, toUpsert: assets.toLocalAssets()); + await _localAlbumRepository.upsert(album, toUpsert: assets); + await _mapIosCloudIds(assets); _log.fine("Successfully added device album ${album.name}"); } catch (e, s) { _log.warning("Error while adding device album", e, s); @@ -202,13 +219,16 @@ class LocalSyncService { return false; } - final newAssets = await _nativeSyncApi.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime); + final newAssets = await _nativeSyncApi + .getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime) + .then((a) => a.toLocalAssets()); await _localAlbumRepository.upsert( deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), - toUpsert: newAssets.toLocalAssets(), + toUpsert: newAssets, ); + await _mapIosCloudIds(newAssets); return true; } catch (e, s) { _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); @@ -240,6 +260,7 @@ class LocalSyncService { if (dbAlbum.assetCount == 0) { _log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB."); await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice); + await _mapIosCloudIds(assetsInDevice); return true; } @@ -277,6 +298,7 @@ class LocalSyncService { } await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete); + await _mapIosCloudIds(assetsToUpsert); return true; } catch (e, s) { @@ -285,6 +307,30 @@ class LocalSyncService { return true; } + // ignore: avoid-unused-parameters + Future _mapIosCloudIds(List assets) async { + // if (!CurrentPlatform.isIOS || assets.isEmpty) { + return; + // } + + // final assetIds = assets.map((a) => a.id).toList(); + // final cloudMapping = {}; + // final cloudIds = await _nativeSyncApi.getCloudIdForAssetIds(assetIds); + // for (int i = 0; i < cloudIds.length; i++) { + // final cloudIdResult = cloudIds[i]; + // if (cloudIdResult.cloudId != null) { + // cloudMapping[cloudIdResult.assetId] = cloudIdResult.cloudId!; + // } else { + // final asset = assets.firstWhereOrNull((a) => a.id == cloudIdResult.assetId); + // _log.fine( + // "Cannot fetch cloudId for asset with id: ${cloudIdResult.assetId}, name: ${asset?.name}, createdAt: ${asset?.createdAt}. Error: ${cloudIdResult.error ?? "unknown"}", + // ); + // } + // } + + // await _localAlbumRepository.updateCloudMapping(cloudMapping); + } + bool _assetsEqual(LocalAsset a, LocalAsset b) { if (CurrentPlatform.isAndroid) { return a.updatedAt.isAtSameMomentAs(b.updatedAt) && @@ -360,6 +406,7 @@ extension on Iterable { name: e.name, updatedAt: tryFromSecondsSinceEpoch(e.updatedAt, isUtc: true) ?? DateTime.timestamp(), assetCount: e.assetCount, + isIosSharedAlbum: e.isCloud, ), ).toList(); } @@ -388,8 +435,19 @@ extension PlatformToLocalAsset on PlatformAsset { durationInSeconds: durationInSeconds, isFavorite: isFavorite, orientation: orientation, + playbackStyle: _toPlaybackStyle(playbackStyle), adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true), latitude: latitude, longitude: longitude, + isEdited: false, ); } + +AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { + PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, + PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, + PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, + PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, + PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, + PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, +}; diff --git a/mobile/lib/domain/services/map.service.dart b/mobile/lib/domain/services/map.service.dart index 8c50a5aaeb..6c64e2817e 100644 --- a/mobile/lib/domain/services/map.service.dart +++ b/mobile/lib/domain/services/map.service.dart @@ -1,5 +1,6 @@ import 'package:immich_mobile/domain/models/map.model.dart'; import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; typedef MapMarkerSource = Future> Function(LatLngBounds? bounds); @@ -11,7 +12,8 @@ class MapFactory { const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository; - MapService remote(String ownerId) => MapService(_mapRepository.remote(ownerId)); + MapService remote(List ownerIds, TimelineMapOptions options) => + MapService(_mapRepository.remote(ownerIds, options)); } class MapService { diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index d45f710d7b..ecfe83e5cb 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -10,6 +10,10 @@ class DriftPeopleService { const DriftPeopleService(this._repository, this._personApiRepository); + Future get(String personId) { + return _repository.get(personId); + } + Future> getAssetPeople(String assetId) { return _repository.getAssetPeople(assetId); } diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 68c72255b0..945ba8eb3f 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -36,16 +37,18 @@ class RemoteAlbumService { AlbumSortMode sortMode, { bool isReverse = false, }) async { + // list of albums sorted ascendingly according to the selected sort mode final List sorted = switch (sortMode) { AlbumSortMode.created => albums.sortedBy((album) => album.createdAt), AlbumSortMode.title => albums.sortedBy((album) => album.name), AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), - AlbumSortMode.mostRecent => await _sortByNewestAsset(albums), - AlbumSortMode.mostOldest => await _sortByOldestAsset(albums), + AlbumSortMode.mostRecent => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.end), + AlbumSortMode.mostOldest => await _sortByAssetDate(albums, aggregation: AssetDateAggregation.start), }; + final effectiveOrder = isReverse ? sortMode.defaultOrder.reverse() : sortMode.defaultOrder; - return (isReverse ? sorted.reversed : sorted).toList(); + return (effectiveOrder == SortOrder.asc ? sorted : sorted.reversed).toList(); } List searchAlbums( @@ -169,46 +172,25 @@ class RemoteAlbumService { return _repository.getAlbumsContainingAsset(assetId); } - Future> _sortByNewestAsset(List albums) async { - // map album IDs to their newest asset dates - final Map> assetTimestampFutures = {}; - for (final album in albums) { - assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id); + Future> _sortByAssetDate( + List albums, { + required AssetDateAggregation aggregation, + }) async { + if (albums.isEmpty) return []; + + final albumIds = albums.map((e) => e.id).toList(); + final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation); + + final albumMap = Map.fromEntries(albums.map((a) => MapEntry(a.id, a))); + + final sortedAlbums = sortedIds.map((id) => albumMap[id]).whereType().toList(); + + if (sortedAlbums.length < albums.length) { + final returnedIdSet = sortedIds.toSet(); + final emptyAlbums = albums.where((a) => !returnedIdSet.contains(a.id)); + sortedAlbums.addAll(emptyAlbums); } - // await all database queries - final entries = await Future.wait( - assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)), - ); - final assetTimestamps = Map.fromEntries(entries); - - final sorted = albums.sorted((a, b) { - final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - return aDate.compareTo(bDate); - }); - - return sorted; - } - - Future> _sortByOldestAsset(List albums) async { - // map album IDs to their oldest asset dates - final Map> assetTimestampFutures = { - for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id), - }; - - // await all database queries - final entries = await Future.wait( - assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)), - ); - final assetTimestamps = Map.fromEntries(entries); - - final sorted = albums.sorted((a, b) { - final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - final bDate = assetTimestamps[b.id] ?? DateTime.fromMillisecondsSinceEpoch(0); - return aDate.compareTo(bDate); - }); - - return sorted.reversed.toList(); + return sortedAlbums; } } diff --git a/mobile/lib/domain/services/search.service.dart b/mobile/lib/domain/services/search.service.dart index 6ccc5a97bf..a3f935c492 100644 --- a/mobile/lib/domain/services/search.service.dart +++ b/mobile/lib/domain/services/search.service.dart @@ -77,6 +77,7 @@ extension on AssetResponseDto { thumbHash: thumbhash, localId: null, type: type.toAssetType(), + isEdited: isEdited, ); } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 2ff0f18fcf..2bda6cd683 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,4 +1,7 @@ +// ignore_for_file: constant_identifier_names + import 'dart:async'; +import 'dart:convert'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; @@ -7,12 +10,21 @@ import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +enum SyncMigrationTask { + v20260128_ResetExifV1, // EXIF table has incorrect width and height information. + v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations. + v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets. +} + class SyncStreamService { final Logger _logger = Logger('SyncStreamService'); @@ -22,6 +34,8 @@ class SyncStreamService { final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final LocalFilesManagerRepository _localFilesManager; final StorageRepository _storageRepository; + final SyncMigrationRepository _syncMigrationRepository; + final ApiService _api; final bool Function()? _cancelChecker; SyncStreamService({ @@ -31,6 +45,8 @@ class SyncStreamService { required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required LocalFilesManagerRepository localFilesManager, required StorageRepository storageRepository, + required SyncMigrationRepository syncMigrationRepository, + required ApiService api, bool Function()? cancelChecker, }) : _syncApiRepository = syncApiRepository, _syncStreamRepository = syncStreamRepository, @@ -38,22 +54,93 @@ class SyncStreamService { _trashedLocalAssetRepository = trashedLocalAssetRepository, _localFilesManager = localFilesManager, _storageRepository = storageRepository, + _syncMigrationRepository = syncMigrationRepository, + _api = api, _cancelChecker = cancelChecker; bool get isCancelled => _cancelChecker?.call() ?? false; Future sync() async { _logger.info("Remote sync request for user"); + final serverVersion = await _api.serverInfoApi.getServerVersion(); + if (serverVersion == null) { + _logger.severe("Cannot perform sync: unable to determine server version"); + return false; + } + + final serverSemVer = SemVer(major: serverVersion.major, minor: serverVersion.minor, patch: serverVersion.patch_); + + final value = Store.get(StoreKey.syncMigrationStatus, "[]"); + final migrations = (jsonDecode(value) as List).cast(); + int previousLength = migrations.length; + await _runPreSyncTasks(migrations, serverSemVer); + + if (migrations.length != previousLength) { + _logger.info("Updated pre-sync migration status: $migrations"); + await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); + } + // Start the sync stream and handle events bool shouldReset = false; - await _syncApiRepository.streamChanges(_handleEvents, onReset: () => shouldReset = true); + await _syncApiRepository.streamChanges( + _handleEvents, + serverVersion: serverSemVer, + onReset: () => shouldReset = true, + ); if (shouldReset) { _logger.info("Resetting sync state as requested by server"); - await _syncApiRepository.streamChanges(_handleEvents); + await _syncApiRepository.streamChanges(_handleEvents, serverVersion: serverSemVer); } + + previousLength = migrations.length; + await _runPostSyncTasks(migrations); + + if (migrations.length != previousLength) { + _logger.info("Updated pre-sync migration status: $migrations"); + await Store.put(StoreKey.syncMigrationStatus, jsonEncode(migrations)); + } + return true; } + Future _runPreSyncTasks(List migrations, SemVer semVer) async { + if (!migrations.contains(SyncMigrationTask.v20260128_ResetExifV1.name)) { + _logger.info("Running pre-sync task: v20260128_ResetExifV1"); + await _syncApiRepository.deleteSyncAck([ + SyncEntityType.assetExifV1, + SyncEntityType.partnerAssetExifV1, + SyncEntityType.albumAssetExifCreateV1, + SyncEntityType.albumAssetExifUpdateV1, + ]); + migrations.add(SyncMigrationTask.v20260128_ResetExifV1.name); + } + + if (!migrations.contains(SyncMigrationTask.v20260128_ResetAssetV1.name) && + semVer >= const SemVer(major: 2, minor: 5, patch: 0)) { + _logger.info("Running pre-sync task: v20260128_ResetAssetV1"); + await _syncApiRepository.deleteSyncAck([ + SyncEntityType.assetV1, + SyncEntityType.partnerAssetV1, + SyncEntityType.albumAssetCreateV1, + SyncEntityType.albumAssetUpdateV1, + ]); + + migrations.add(SyncMigrationTask.v20260128_ResetAssetV1.name); + + if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) { + migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name); + } + } + } + + Future _runPostSyncTasks(List migrations) async { + if (!migrations.contains(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name)) { + _logger.info("Running post-sync task: v20260128_CopyExifWidthHeightToAsset"); + await _syncMigrationRepository.v20260128CopyExifWidthHeightToAsset(); + migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name); + } + } + Future _handleEvents(List events, Function() abort, Function() reset) async { List items = []; for (final event in events) { @@ -118,6 +205,10 @@ class SyncStreamService { return _syncStreamRepository.deleteAssetsV1(data.cast()); case SyncEntityType.assetExifV1: return _syncStreamRepository.updateAssetsExifV1(data.cast()); + case SyncEntityType.assetMetadataV1: + return _syncStreamRepository.updateAssetsMetadataV1(data.cast()); + case SyncEntityType.assetMetadataDeleteV1: + return _syncStreamRepository.deleteAssetsMetadataV1(data.cast()); case SyncEntityType.partnerAssetV1: return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner'); case SyncEntityType.partnerAssetBackfillV1: @@ -195,6 +286,8 @@ class SyncStreamService { return _syncStreamRepository.deletePeopleV1(data.cast()); case SyncEntityType.assetFaceV1: return _syncStreamRepository.updateAssetFacesV1(data.cast()); + case SyncEntityType.assetFaceV2: + return _syncStreamRepository.updateAssetFacesV2(data.cast()); case SyncEntityType.assetFaceDeleteV1: return _syncStreamRepository.deleteAssetFacesV1(data.cast()); default: @@ -243,6 +336,42 @@ class SyncStreamService { } } + Future handleWsAssetEditReadyV1Batch(List batchData) async { + if (batchData.isEmpty) return; + + _logger.info('Processing batch of ${batchData.length} AssetEditReadyV1 events'); + + final List assets = []; + + try { + for (final data in batchData) { + if (data is! Map) { + continue; + } + + final payload = data; + final assetData = payload['asset']; + + if (assetData == null) { + continue; + } + + final asset = SyncAssetV1.fromJson(assetData); + + if (asset != null) { + assets.add(asset); + } + } + + if (assets.isNotEmpty) { + await _syncStreamRepository.updateAssetsV1(assets, debugLabel: 'websocket-edit'); + _logger.info('Successfully processed ${assets.length} edited assets'); + } + } catch (error, stackTrace) { + _logger.severe("Error processing AssetEditReadyV1 websocket batch events", error, stackTrace); + } + } + Future _handleRemoteTrashed(Iterable checksums) async { if (checksums.isEmpty) { return Future.value(); diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 96630f1eba..39aeb867a3 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -11,7 +11,6 @@ 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'; -import 'package:maplibre_gl/maplibre_gl.dart'; typedef TimelineAssetSource = Future> Function(int index, int count); @@ -79,8 +78,11 @@ class TimelineFactory { TimelineService fromAssets(List assets, TimelineOrigin type) => TimelineService(_timelineRepository.fromAssets(assets, type)); - TimelineService map(String userId, LatLngBounds bounds) => - TimelineService(_timelineRepository.map(userId, bounds, groupBy)); + TimelineService fromAssetsWithBuckets(List assets, TimelineOrigin type) => + TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type)); + + TimelineService map(List userIds, TimelineMapOptions options) => + TimelineService(_timelineRepository.map(userIds, options, groupBy)); } class TimelineService { @@ -181,8 +183,8 @@ 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))); + // Preload assets around the given index for asset viewer + Future preloadAssets(int index) => _mutex.run(() => _loadAssets(index, math.min(5, _totalAssets - index))); BaseAsset getRandomAsset() => _buffer.elementAt(math.Random().nextInt(_buffer.length)); @@ -225,6 +227,13 @@ class TimelineService { return _buffer.elementAt(index - _bufferOffset); } + /// Finds the index of an asset by its heroTag within the current buffer. + /// Returns null if the asset is not found in the buffer. + int? getIndex(String heroTag) { + final index = _buffer.indexWhere((a) => a.heroTag == heroTag); + return index >= 0 ? _bufferOffset + index : null; + } + 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 38e249b9f1..6840bae595 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart' as m; import 'package:immich_mobile/domain/utils/sync_linked_album.dart'; import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; @@ -22,8 +23,13 @@ class BackgroundSyncManager { final SyncCallback? onHashingComplete; final SyncErrorCallback? onHashingError; + final SyncCallback? onCloudIdSyncStart; + final SyncCallback? onCloudIdSyncComplete; + final SyncErrorCallback? onCloudIdSyncError; + Cancelable? _syncTask; Cancelable? _syncWebsocketTask; + Cancelable? _cloudIdSyncTask; Cancelable? _deviceAlbumSyncTask; Cancelable? _linkedAlbumSyncTask; Cancelable? _hashTask; @@ -38,6 +44,9 @@ class BackgroundSyncManager { this.onHashingStart, this.onHashingComplete, this.onHashingError, + this.onCloudIdSyncStart, + this.onCloudIdSyncComplete, + this.onCloudIdSyncError, }); Future cancel() async { @@ -55,6 +64,12 @@ class BackgroundSyncManager { _syncWebsocketTask?.cancel(); _syncWebsocketTask = null; + if (_cloudIdSyncTask != null) { + futures.add(_cloudIdSyncTask!.future); + } + _cloudIdSyncTask?.cancel(); + _cloudIdSyncTask = null; + if (_linkedAlbumSyncTask != null) { futures.add(_linkedAlbumSyncTask!.future); } @@ -121,7 +136,6 @@ class BackgroundSyncManager { }); } - // No need to cancel the task, as it can also be run when the user logs out Future hashAssets() { if (_hashTask != null) { return _hashTask!.future; @@ -182,6 +196,16 @@ class BackgroundSyncManager { }); } + Future syncWebsocketEditBatch(List batchData) { + if (_syncWebsocketTask != null) { + return _syncWebsocketTask!.future; + } + _syncWebsocketTask = _handleWsAssetEditReadyV1Batch(batchData); + return _syncWebsocketTask!.whenComplete(() { + _syncWebsocketTask = null; + }); + } + Future syncLinkedAlbum() { if (_linkedAlbumSyncTask != null) { return _linkedAlbumSyncTask!.future; @@ -192,9 +216,33 @@ class BackgroundSyncManager { _linkedAlbumSyncTask = null; }); } + + Future syncCloudIds() { + if (_cloudIdSyncTask != null) { + return _cloudIdSyncTask!.future; + } + + onCloudIdSyncStart?.call(); + + _cloudIdSyncTask = runInIsolateGentle(computation: m.syncCloudIds); + return _cloudIdSyncTask! + .whenComplete(() { + onCloudIdSyncComplete?.call(); + _cloudIdSyncTask = null; + }) + .catchError((error) { + onCloudIdSyncError?.call(error.toString()); + _cloudIdSyncTask = null; + }); + } } Cancelable _handleWsAssetUploadReadyV1Batch(List batchData) => runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData), debugLabel: 'websocket-batch', ); + +Cancelable _handleWsAssetEditReadyV1Batch(List batchData) => runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1Batch(batchData), + debugLabel: 'websocket-edit', +); diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart new file mode 100644 index 0000000000..33a8eca94d --- /dev/null +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -0,0 +1,191 @@ +import 'package:drift/drift.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.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/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:logging/logging.dart'; +// ignore: import_rule_openapi +import 'package:openapi/api.dart' hide AssetVisibility; + +Future syncCloudIds(ProviderContainer ref) async { + if (!CurrentPlatform.isIOS) { + return; + } + final logger = Logger('migrateCloudIds'); + + final db = ref.read(driftProvider); + // Populate cloud IDs for local assets that don't have one yet + await _populateCloudIds(db); + + final serverInfo = await ref.read(serverInfoProvider.notifier).getServerInfo(); + final canUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 4); + if (!canUpdateMetadata) { + logger.fine('Server version does not support asset metadata updates. Skipping cloudId migration.'); + return; + } + final canBulkUpdateMetadata = serverInfo.serverVersion.isAtLeast(major: 2, minor: 5); + + // Wait for remote sync to complete, so we have up-to-date asset metadata entries + try { + await ref.read(syncStreamServiceProvider).sync(); + } catch (e, s) { + logger.fine('Failed to complete remote sync before cloudId migration.', e, s); + return; + } + + // Fetch the mapping for backed up assets that have a cloud ID locally but do not have a cloud ID on the server + final currentUser = ref.read(currentUserProvider); + if (currentUser == null) { + logger.warning('Current user is null. Aborting cloudId migration.'); + return; + } + + final assetApi = ref.read(apiServiceProvider).assetsApi; + + // Process cloud IDs in paginated batches + await _processCloudIdMappingsInBatches(db, currentUser.id, assetApi, canBulkUpdateMetadata, logger); +} + +Future _processCloudIdMappingsInBatches( + Drift drift, + String userId, + AssetsApi assetsApi, + bool canBulkUpdate, + Logger logger, +) async { + const pageSize = 20000; + String? lastLocalId; + final seenRemoteAssetIds = {}; + + while (true) { + final mappings = await _fetchCloudIdMappings(drift, userId, pageSize, lastLocalId); + if (mappings.isEmpty) { + break; + } + + final items = []; + for (final mapping in mappings) { + if (seenRemoteAssetIds.add(mapping.remoteAssetId)) { + items.add( + AssetMetadataBulkUpsertItemDto( + assetId: mapping.remoteAssetId, + key: kMobileMetadataKey, + value: RemoteAssetMobileAppMetadata( + cloudId: mapping.localAsset.cloudId, + createdAt: mapping.localAsset.createdAt.toIso8601String(), + adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), + latitude: mapping.localAsset.latitude?.toString(), + longitude: mapping.localAsset.longitude?.toString(), + ), + ), + ); + } else { + logger.fine('Duplicate remote asset ID found: ${mapping.remoteAssetId}. Skipping duplicate entry.'); + } + } + + if (items.isNotEmpty) { + if (canBulkUpdate) { + await _bulkUpdateCloudIds(assetsApi, items); + } else { + await _sequentialUpdateCloudIds(assetsApi, items); + } + } + + lastLocalId = mappings.last.localAsset.id; + if (mappings.length < pageSize) { + break; + } + } +} + +Future _sequentialUpdateCloudIds(AssetsApi assetsApi, List items) async { + for (final item in items) { + final upsertItem = AssetMetadataUpsertItemDto(key: item.key, value: item.value); + try { + await assetsApi.updateAssetMetadata(item.assetId, AssetMetadataUpsertDto(items: [upsertItem])); + } catch (error, stack) { + Logger('migrateCloudIds').warning('Failed to update metadata for asset ${item.assetId}', error, stack); + } + } +} + +Future _bulkUpdateCloudIds(AssetsApi assetsApi, List items) async { + try { + await assetsApi.updateBulkAssetMetadata(AssetMetadataBulkUpsertDto(items: items)); + } catch (error, stack) { + Logger('migrateCloudIds').warning('Failed to bulk update metadata', error, stack); + } +} + +Future _populateCloudIds(Drift drift) async { + final query = drift.localAssetEntity.selectOnly() + ..addColumns([drift.localAssetEntity.id]) + ..where(drift.localAssetEntity.iCloudId.isNull()); + final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get(); + final cloudMapping = {}; + final cloudIds = await NativeSyncApi().getCloudIdForAssetIds(ids); + for (int i = 0; i < cloudIds.length; i++) { + final cloudIdResult = cloudIds[i]; + if (cloudIdResult.cloudId != null) { + cloudMapping[cloudIdResult.assetId] = cloudIdResult.cloudId!; + } else { + Logger('migrateCloudIds').fine( + "Cannot fetch cloudId for asset with id: ${cloudIdResult.assetId}. Error: ${cloudIdResult.error ?? "unknown"}", + ); + } + } + await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping); +} + +typedef _CloudIdMapping = ({String remoteAssetId, LocalAsset localAsset}); + +Future> _fetchCloudIdMappings(Drift drift, String userId, int limit, String? lastLocalId) async { + final query = + drift.localAssetEntity.select().join([ + innerJoin( + drift.remoteAssetEntity, + drift.localAssetEntity.checksum.equalsExp(drift.remoteAssetEntity.checksum), + ), + leftOuterJoin( + drift.remoteAssetCloudIdEntity, + drift.remoteAssetEntity.id.equalsExp(drift.remoteAssetCloudIdEntity.assetId), + useColumns: false, + ), + ]) + ..where( + // Only select assets that have a local cloud ID but either no remote cloud ID or a mismatched eTag + drift.localAssetEntity.iCloudId.isNotNull() & + drift.remoteAssetEntity.ownerId.equals(userId) & + // Skip locked assets as we cannot update them without unlocking first + drift.remoteAssetEntity.visibility.isNotValue(AssetVisibility.locked.index) & + (drift.remoteAssetCloudIdEntity.cloudId.isNull() | + drift.remoteAssetCloudIdEntity.adjustmentTime.isNotExp(drift.localAssetEntity.adjustmentTime) | + drift.remoteAssetCloudIdEntity.latitude.isNotExp(drift.localAssetEntity.latitude) | + drift.remoteAssetCloudIdEntity.longitude.isNotExp(drift.localAssetEntity.longitude) | + drift.remoteAssetCloudIdEntity.createdAt.isNotExp(drift.localAssetEntity.createdAt)), + ) + ..orderBy([OrderingTerm.asc(drift.localAssetEntity.id)]) + ..limit(limit); + + if (lastLocalId != null) { + query.where(drift.localAssetEntity.id.isBiggerThanValue(lastLocalId)); + } + + return query.map((row) { + return ( + remoteAssetId: row.read(drift.remoteAssetEntity.id)!, + localAsset: row.readTable(drift.localAssetEntity).toDto(), + ); + }).get(); +} diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart index 169032ff5d..5917e127bc 100644 --- a/mobile/lib/extensions/scroll_extensions.dart +++ b/mobile/lib/extensions/scroll_extensions.dart @@ -32,3 +32,125 @@ class FastClampingScrollPhysics extends ClampingScrollPhysics { damping: 80, ); } + +class SnapScrollPhysics extends ScrollPhysics { + static const _minFlingVelocity = 700.0; + static const minSnapDistance = 30.0; + + static final _spring = SpringDescription.withDampingRatio(mass: .5, stiffness: 300); + + const SnapScrollPhysics({super.parent}); + + @override + SnapScrollPhysics applyTo(ScrollPhysics? ancestor) { + return SnapScrollPhysics(parent: buildParent(ancestor)); + } + + @override + Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { + assert( + position is SnapScrollPosition, + 'SnapScrollPhysics can only be used with Scrollables that use a ' + 'controller whose createScrollPosition returns a SnapScrollPosition', + ); + + final snapOffset = (position as SnapScrollPosition).snapOffset; + if (snapOffset <= 0) { + return super.createBallisticSimulation(position, velocity); + } + + if (position.pixels >= snapOffset) { + final simulation = super.createBallisticSimulation(position, velocity); + if (simulation == null || simulation.x(double.infinity) >= snapOffset) { + return simulation; + } + } + + return ScrollSpringSimulation( + _spring, + position.pixels, + target(position, velocity, snapOffset), + velocity, + tolerance: toleranceFor(position), + ); + } + + static double target(ScrollMetrics position, double velocity, double snapOffset) { + if (velocity > _minFlingVelocity) return snapOffset; + if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset; + return position.pixels < minSnapDistance ? 0.0 : snapOffset; + } +} + +class SnapScrollPosition extends ScrollPositionWithSingleContext { + double snapOffset; + + SnapScrollPosition({this.snapOffset = 0.0, required super.physics, required super.context, super.oldPosition}); +} + +class ProxyScrollController extends ScrollController { + final ScrollController scrollController; + + ProxyScrollController({required this.scrollController}); + + SnapScrollPosition get snapPosition => position as SnapScrollPosition; + + @override + ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { + return ProxyScrollPosition( + scrollController: scrollController, + physics: physics, + context: context, + oldPosition: oldPosition, + ); + } + + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } +} + +class ProxyScrollPosition extends SnapScrollPosition { + final ScrollController scrollController; + + ProxyScrollPosition({ + required this.scrollController, + required super.physics, + required super.context, + super.oldPosition, + }); + + @override + double setPixels(double newPixels) { + final overscroll = super.setPixels(newPixels); + if (scrollController.hasClients && scrollController.position.pixels != pixels) { + scrollController.position.forcePixels(pixels); + } + return overscroll; + } + + @override + void forcePixels(double value) { + super.forcePixels(value); + if (scrollController.hasClients && scrollController.position.pixels != pixels) { + scrollController.position.forcePixels(pixels); + } + } + + @override + double get maxScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions + ? scrollController.position.maxScrollExtent + : super.maxScrollExtent; + + @override + double get minScrollExtent => scrollController.hasClients && scrollController.position.hasContentDimensions + ? scrollController.position.minScrollExtent + : super.minScrollExtent; + + @override + double get viewportDimension => scrollController.hasClients && scrollController.position.hasViewportDimension + ? scrollController.position.viewportDimension + : super.viewportDimension; +} diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart index 5f793030c3..40fe9ab1c1 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart @@ -3,6 +3,8 @@ 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'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)') +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)') class AssetFaceEntity extends Table with DriftDefaultsMixin { const AssetFaceEntity(); @@ -26,6 +28,10 @@ class AssetFaceEntity extends Table with DriftDefaultsMixin { TextColumn get sourceType => text()(); + BoolColumn get isVisible => boolean().withDefault(const Constant(true))(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + @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 index 092fcc5859..c97dd545a8 100644 --- a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart @@ -5,11 +5,12 @@ import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.da as i1; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' - as i3; -import 'package:drift/internal/modular.dart' as i4; + as i4; +import 'package:drift/internal/modular.dart' as i5; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' - as i5; + as i6; typedef $$AssetFaceEntityTableCreateCompanionBuilder = i1.AssetFaceEntityCompanion Function({ @@ -23,6 +24,8 @@ typedef $$AssetFaceEntityTableCreateCompanionBuilder = required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + i0.Value isVisible, + i0.Value deletedAt, }); typedef $$AssetFaceEntityTableUpdateCompanionBuilder = i1.AssetFaceEntityCompanion Function({ @@ -36,6 +39,8 @@ typedef $$AssetFaceEntityTableUpdateCompanionBuilder = i0.Value boundingBoxX2, i0.Value boundingBoxY2, i0.Value sourceType, + i0.Value isVisible, + i0.Value deletedAt, }); final class $$AssetFaceEntityTableReferences @@ -51,29 +56,29 @@ final class $$AssetFaceEntityTableReferences super.$_typedResult, ); - static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => - i4.ReadDatabaseContainer(db) - .resultSet('remote_asset_entity') + static i4.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') .createAlias( i0.$_aliasNameGenerator( - i4.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('asset_face_entity') .assetId, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( db, - ).resultSet('remote_asset_entity').id, + ).resultSet('remote_asset_entity').id, ), ); - i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + i4.$$RemoteAssetEntityTableProcessedTableManager get assetId { final $_column = $_itemColumn('asset_id')!; - final manager = i3 + final manager = i4 .$$RemoteAssetEntityTableTableManager( $_db, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( $_db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), ) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); @@ -83,29 +88,29 @@ final class $$AssetFaceEntityTableReferences ); } - static i5.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => - i4.ReadDatabaseContainer(db) - .resultSet('person_entity') + static i6.$PersonEntityTable _personIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('person_entity') .createAlias( i0.$_aliasNameGenerator( - i4.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('asset_face_entity') .personId, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( db, - ).resultSet('person_entity').id, + ).resultSet('person_entity').id, ), ); - i5.$$PersonEntityTableProcessedTableManager? get personId { + i6.$$PersonEntityTableProcessedTableManager? get personId { final $_column = $_itemColumn('person_id'); if ($_column == null) return null; - final manager = i5 + final manager = i6 .$$PersonEntityTableTableManager( $_db, - i4.ReadDatabaseContainer( + i5.ReadDatabaseContainer( $_db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), ) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_personIdTable($_db)); @@ -165,24 +170,34 @@ class $$AssetFaceEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); - i3.$$RemoteAssetEntityTableFilterComposer get assetId { - final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + i0.ColumnFilters get isVisible => $composableBuilder( + column: $table.isVisible, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i4.$$RemoteAssetEntityTableFilterComposer get assetId { + final i4.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableFilterComposer( + }) => i4.$$RemoteAssetEntityTableFilterComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -192,24 +207,24 @@ class $$AssetFaceEntityTableFilterComposer return composer; } - i5.$$PersonEntityTableFilterComposer get personId { - final i5.$$PersonEntityTableFilterComposer composer = $composerBuilder( + i6.$$PersonEntityTableFilterComposer get personId { + final i6.$$PersonEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableFilterComposer( + }) => i6.$$PersonEntityTableFilterComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -269,25 +284,35 @@ class $$AssetFaceEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); - i3.$$RemoteAssetEntityTableOrderingComposer get assetId { - final i3.$$RemoteAssetEntityTableOrderingComposer composer = + i0.ColumnOrderings get isVisible => $composableBuilder( + column: $table.isVisible, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i4.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i4.$$RemoteAssetEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableOrderingComposer( + }) => i4.$$RemoteAssetEntityTableOrderingComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -297,24 +322,24 @@ class $$AssetFaceEntityTableOrderingComposer return composer; } - i5.$$PersonEntityTableOrderingComposer get personId { - final i5.$$PersonEntityTableOrderingComposer composer = $composerBuilder( + i6.$$PersonEntityTableOrderingComposer get personId { + final i6.$$PersonEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableOrderingComposer( + }) => i6.$$PersonEntityTableOrderingComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -372,25 +397,31 @@ class $$AssetFaceEntityTableAnnotationComposer builder: (column) => column, ); - i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { - final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + i0.GeneratedColumn get isVisible => + $composableBuilder(column: $table.isVisible, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i4.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i4.$$RemoteAssetEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.assetId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i3.$$RemoteAssetEntityTableAnnotationComposer( + }) => i4.$$RemoteAssetEntityTableAnnotationComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('remote_asset_entity'), + ).resultSet('remote_asset_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -400,24 +431,24 @@ class $$AssetFaceEntityTableAnnotationComposer return composer; } - i5.$$PersonEntityTableAnnotationComposer get personId { - final i5.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( + i6.$$PersonEntityTableAnnotationComposer get personId { + final i6.$$PersonEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.personId, - referencedTable: i4.ReadDatabaseContainer( + referencedTable: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), getReferencedColumn: (t) => t.id, builder: ( joinBuilder, { $addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer, - }) => i5.$$PersonEntityTableAnnotationComposer( + }) => i6.$$PersonEntityTableAnnotationComposer( $db: $db, - $table: i4.ReadDatabaseContainer( + $table: i5.ReadDatabaseContainer( $db, - ).resultSet('person_entity'), + ).resultSet('person_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -468,6 +499,8 @@ class $$AssetFaceEntityTableTableManager i0.Value boundingBoxX2 = const i0.Value.absent(), i0.Value boundingBoxY2 = const i0.Value.absent(), i0.Value sourceType = const i0.Value.absent(), + i0.Value isVisible = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityCompanion( id: id, assetId: assetId, @@ -479,6 +512,8 @@ class $$AssetFaceEntityTableTableManager boundingBoxX2: boundingBoxX2, boundingBoxY2: boundingBoxY2, sourceType: sourceType, + isVisible: isVisible, + deletedAt: deletedAt, ), createCompanionCallback: ({ @@ -492,6 +527,8 @@ class $$AssetFaceEntityTableTableManager required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + i0.Value isVisible = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityCompanion.insert( id: id, assetId: assetId, @@ -503,6 +540,8 @@ class $$AssetFaceEntityTableTableManager boundingBoxX2: boundingBoxX2, boundingBoxY2: boundingBoxY2, sourceType: sourceType, + isVisible: isVisible, + deletedAt: deletedAt, ), withReferenceMapper: (p0) => p0 .map( @@ -588,6 +627,10 @@ typedef $$AssetFaceEntityTableProcessedTableManager = i1.AssetFaceEntityData, i0.PrefetchHooks Function({bool assetId, bool personId}) >; +i0.Index get idxAssetFacePersonId => i0.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', +); class $AssetFaceEntityTable extends i2.AssetFaceEntity with i0.TableInfo<$AssetFaceEntityTable, i1.AssetFaceEntityData> { @@ -705,6 +748,33 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity type: i0.DriftSqlType.string, requiredDuringInsert: true, ); + static const i0.VerificationMeta _isVisibleMeta = const i0.VerificationMeta( + 'isVisible', + ); + @override + late final i0.GeneratedColumn isVisible = i0.GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const i3.Constant(true), + ); + 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, + ); @override List get $columns => [ id, @@ -717,6 +787,8 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity boundingBoxX2, boundingBoxY2, sourceType, + isVisible, + deletedAt, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -820,6 +892,18 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity } else if (isInserting) { context.missing(_sourceTypeMeta); } + if (data.containsKey('is_visible')) { + context.handle( + _isVisibleMeta, + isVisible.isAcceptableOrUnknown(data['is_visible']!, _isVisibleMeta), + ); + } + if (data.containsKey('deleted_at')) { + context.handle( + _deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta), + ); + } return context; } @@ -869,6 +953,14 @@ class $AssetFaceEntityTable extends i2.AssetFaceEntity i0.DriftSqlType.string, data['${effectivePrefix}source_type'], )!, + isVisible: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), ); } @@ -895,6 +987,8 @@ class AssetFaceEntityData extends i0.DataClass final int boundingBoxX2; final int boundingBoxY2; final String sourceType; + final bool isVisible; + final DateTime? deletedAt; const AssetFaceEntityData({ required this.id, required this.assetId, @@ -906,6 +1000,8 @@ class AssetFaceEntityData extends i0.DataClass required this.boundingBoxX2, required this.boundingBoxY2, required this.sourceType, + required this.isVisible, + this.deletedAt, }); @override Map toColumns(bool nullToAbsent) { @@ -922,6 +1018,10 @@ class AssetFaceEntityData extends i0.DataClass map['bounding_box_x2'] = i0.Variable(boundingBoxX2); map['bounding_box_y2'] = i0.Variable(boundingBoxY2); map['source_type'] = i0.Variable(sourceType); + map['is_visible'] = i0.Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } return map; } @@ -941,6 +1041,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), sourceType: serializer.fromJson(json['sourceType']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), ); } @override @@ -957,6 +1059,8 @@ class AssetFaceEntityData extends i0.DataClass 'boundingBoxX2': serializer.toJson(boundingBoxX2), 'boundingBoxY2': serializer.toJson(boundingBoxY2), 'sourceType': serializer.toJson(sourceType), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), }; } @@ -971,6 +1075,8 @@ class AssetFaceEntityData extends i0.DataClass int? boundingBoxX2, int? boundingBoxY2, String? sourceType, + bool? isVisible, + i0.Value deletedAt = const i0.Value.absent(), }) => i1.AssetFaceEntityData( id: id ?? this.id, assetId: assetId ?? this.assetId, @@ -982,6 +1088,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, ); AssetFaceEntityData copyWithCompanion(i1.AssetFaceEntityCompanion data) { return AssetFaceEntityData( @@ -1009,6 +1117,8 @@ class AssetFaceEntityData extends i0.DataClass sourceType: data.sourceType.present ? data.sourceType.value : this.sourceType, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, ); } @@ -1024,7 +1134,9 @@ class AssetFaceEntityData extends i0.DataClass ..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxY2: $boundingBoxY2, ') - ..write('sourceType: $sourceType') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') ..write(')')) .toString(); } @@ -1041,6 +1153,8 @@ class AssetFaceEntityData extends i0.DataClass boundingBoxX2, boundingBoxY2, sourceType, + isVisible, + deletedAt, ); @override bool operator ==(Object other) => @@ -1055,7 +1169,9 @@ class AssetFaceEntityData extends i0.DataClass other.boundingBoxY1 == this.boundingBoxY1 && other.boundingBoxX2 == this.boundingBoxX2 && other.boundingBoxY2 == this.boundingBoxY2 && - other.sourceType == this.sourceType); + other.sourceType == this.sourceType && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); } class AssetFaceEntityCompanion @@ -1070,6 +1186,8 @@ class AssetFaceEntityCompanion final i0.Value boundingBoxX2; final i0.Value boundingBoxY2; final i0.Value sourceType; + final i0.Value isVisible; + final i0.Value deletedAt; const AssetFaceEntityCompanion({ this.id = const i0.Value.absent(), this.assetId = const i0.Value.absent(), @@ -1081,6 +1199,8 @@ class AssetFaceEntityCompanion this.boundingBoxX2 = const i0.Value.absent(), this.boundingBoxY2 = const i0.Value.absent(), this.sourceType = const i0.Value.absent(), + this.isVisible = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), }); AssetFaceEntityCompanion.insert({ required String id, @@ -1093,6 +1213,8 @@ class AssetFaceEntityCompanion required int boundingBoxX2, required int boundingBoxY2, required String sourceType, + this.isVisible = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), }) : id = i0.Value(id), assetId = i0.Value(assetId), imageWidth = i0.Value(imageWidth), @@ -1113,6 +1235,8 @@ class AssetFaceEntityCompanion i0.Expression? boundingBoxX2, i0.Expression? boundingBoxY2, i0.Expression? sourceType, + i0.Expression? isVisible, + i0.Expression? deletedAt, }) { return i0.RawValuesInsertable({ if (id != null) 'id': id, @@ -1125,6 +1249,8 @@ class AssetFaceEntityCompanion if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, if (sourceType != null) 'source_type': sourceType, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, }); } @@ -1139,6 +1265,8 @@ class AssetFaceEntityCompanion i0.Value? boundingBoxX2, i0.Value? boundingBoxY2, i0.Value? sourceType, + i0.Value? isVisible, + i0.Value? deletedAt, }) { return i1.AssetFaceEntityCompanion( id: id ?? this.id, @@ -1151,6 +1279,8 @@ class AssetFaceEntityCompanion boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, sourceType: sourceType ?? this.sourceType, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, ); } @@ -1187,6 +1317,12 @@ class AssetFaceEntityCompanion if (sourceType.present) { map['source_type'] = i0.Variable(sourceType.value); } + if (isVisible.present) { + map['is_visible'] = i0.Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } return map; } @@ -1202,8 +1338,15 @@ class AssetFaceEntityCompanion ..write('boundingBoxY1: $boundingBoxY1, ') ..write('boundingBoxX2: $boundingBoxX2, ') ..write('boundingBoxY2: $boundingBoxY2, ') - ..write('sourceType: $sourceType') + ..write('sourceType: $sourceType, ') + ..write('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') ..write(')')) .toString(); } } + +i0.Index get idxAssetFaceAssetId => i0.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', +); diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 2dbe05b9d7..77cae5dbbe 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -151,6 +151,7 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { domain.ExifInfo toDto() => domain.ExifInfo( fileSize: fileSize, dateTimeOriginal: dateTimeOriginal, + rating: rating, timeZone: timeZone, make: make, model: model, diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index 707d3326a4..641a5359f6 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -33,6 +33,7 @@ extension LocalAlbumEntityDataHelper on LocalAlbumEntityData { assetCount: assetCount, backupSelection: backupSelection, linkedRemoteAlbumId: linkedRemoteAlbumId, + isIosSharedAlbum: isIosSharedAlbum, ); } } diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart index 53f1a10662..b0f4b1b27f 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -3,6 +3,9 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql( + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', +) class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { const LocalAlbumAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart index 70c298332b..77b2798afb 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -459,6 +459,10 @@ typedef $$LocalAlbumAssetEntityTableProcessedTableManager = i1.LocalAlbumAssetEntityData, i0.PrefetchHooks Function({bool assetId, bool albumId}) >; +i0.Index get idxLocalAlbumAssetAlbumAsset => i0.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', +); class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity with diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index d2455b744e..e1cb5f5597 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)') +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)') class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const LocalAssetEntity(); @@ -16,12 +17,16 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { IntColumn get orientation => integer().withDefault(const Constant(0))(); + TextColumn get iCloudId => text().nullable()(); + DateTimeColumn get adjustmentTime => dateTime().nullable()(); RealColumn get latitude => real().nullable()(); RealColumn get longitude => real().nullable()(); + IntColumn get playbackStyle => intEnum().withDefault(const Constant(0))(); + @override Set get primaryKey => {id}; } @@ -40,8 +45,11 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { width: width, remoteId: remoteId, orientation: orientation, + playbackStyle: playbackStyle, adjustmentTime: adjustmentTime, latitude: latitude, longitude: longitude, + cloudId: iCloudId, + isEdited: false, ); } diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index 22219b1e6e..92ac3d2e35 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -21,9 +21,11 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value iCloudId, i0.Value adjustmentTime, i0.Value latitude, i0.Value longitude, + i0.Value playbackStyle, }); typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i1.LocalAssetEntityCompanion Function({ @@ -38,9 +40,11 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value iCloudId, i0.Value adjustmentTime, i0.Value latitude, i0.Value longitude, + i0.Value playbackStyle, }); class $$LocalAssetEntityTableFilterComposer @@ -108,6 +112,11 @@ class $$LocalAssetEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); + i0.ColumnFilters get iCloudId => $composableBuilder( + column: $table.iCloudId, + builder: (column) => i0.ColumnFilters(column), + ); + i0.ColumnFilters get adjustmentTime => $composableBuilder( column: $table.adjustmentTime, builder: (column) => i0.ColumnFilters(column), @@ -122,6 +131,16 @@ class $$LocalAssetEntityTableFilterComposer column: $table.longitude, builder: (column) => i0.ColumnFilters(column), ); + + i0.ColumnWithTypeConverterFilters< + i2.AssetPlaybackStyle, + i2.AssetPlaybackStyle, + int + > + get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); } class $$LocalAssetEntityTableOrderingComposer @@ -188,6 +207,11 @@ class $$LocalAssetEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); + i0.ColumnOrderings get iCloudId => $composableBuilder( + column: $table.iCloudId, + builder: (column) => i0.ColumnOrderings(column), + ); + i0.ColumnOrderings get adjustmentTime => $composableBuilder( column: $table.adjustmentTime, builder: (column) => i0.ColumnOrderings(column), @@ -202,6 +226,11 @@ class $$LocalAssetEntityTableOrderingComposer column: $table.longitude, builder: (column) => i0.ColumnOrderings(column), ); + + i0.ColumnOrderings get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => i0.ColumnOrderings(column), + ); } class $$LocalAssetEntityTableAnnotationComposer @@ -252,6 +281,9 @@ class $$LocalAssetEntityTableAnnotationComposer builder: (column) => column, ); + i0.GeneratedColumn get iCloudId => + $composableBuilder(column: $table.iCloudId, builder: (column) => column); + i0.GeneratedColumn get adjustmentTime => $composableBuilder( column: $table.adjustmentTime, builder: (column) => column, @@ -262,6 +294,12 @@ class $$LocalAssetEntityTableAnnotationComposer i0.GeneratedColumn get longitude => $composableBuilder(column: $table.longitude, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => column, + ); } class $$LocalAssetEntityTableTableManager @@ -315,9 +353,12 @@ class $$LocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + i0.Value iCloudId = const i0.Value.absent(), i0.Value adjustmentTime = const i0.Value.absent(), i0.Value latitude = const i0.Value.absent(), i0.Value longitude = const i0.Value.absent(), + i0.Value playbackStyle = + const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion( name: name, type: type, @@ -330,9 +371,11 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + iCloudId: iCloudId, adjustmentTime: adjustmentTime, latitude: latitude, longitude: longitude, + playbackStyle: playbackStyle, ), createCompanionCallback: ({ @@ -347,9 +390,12 @@ class $$LocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + i0.Value iCloudId = const i0.Value.absent(), i0.Value adjustmentTime = const i0.Value.absent(), i0.Value latitude = const i0.Value.absent(), i0.Value longitude = const i0.Value.absent(), + i0.Value playbackStyle = + const i0.Value.absent(), }) => i1.LocalAssetEntityCompanion.insert( name: name, type: type, @@ -362,9 +408,11 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + iCloudId: iCloudId, adjustmentTime: adjustmentTime, latitude: latitude, longitude: longitude, + playbackStyle: playbackStyle, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -532,6 +580,17 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity requiredDuringInsert: false, defaultValue: const i4.Constant(0), ); + static const i0.VerificationMeta _iCloudIdMeta = const i0.VerificationMeta( + 'iCloudId', + ); + @override + late final i0.GeneratedColumn iCloudId = i0.GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + ); static const i0.VerificationMeta _adjustmentTimeMeta = const i0.VerificationMeta('adjustmentTime'); @override @@ -566,6 +625,19 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity requiredDuringInsert: false, ); @override + late final i0.GeneratedColumnWithTypeConverter + playbackStyle = + i0.GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0), + ).withConverter( + i1.$LocalAssetEntityTable.$converterplaybackStyle, + ); + @override List get $columns => [ name, type, @@ -578,9 +650,11 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity checksum, isFavorite, orientation, + iCloudId, adjustmentTime, latitude, longitude, + playbackStyle, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -661,6 +735,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity ), ); } + if (data.containsKey('i_cloud_id')) { + context.handle( + _iCloudIdMeta, + iCloudId.isAcceptableOrUnknown(data['i_cloud_id']!, _iCloudIdMeta), + ); + } if (data.containsKey('adjustment_time')) { context.handle( _adjustmentTimeMeta, @@ -740,6 +820,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity i0.DriftSqlType.int, data['${effectivePrefix}orientation'], )!, + iCloudId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), adjustmentTime: attachedDatabase.typeMapping.read( i0.DriftSqlType.dateTime, data['${effectivePrefix}adjustment_time'], @@ -752,6 +836,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity i0.DriftSqlType.double, data['${effectivePrefix}longitude'], ), + playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ), ); } @@ -762,6 +852,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity static i0.JsonTypeConverter2 $convertertype = const i0.EnumIndexConverter(i2.AssetType.values); + static i0.JsonTypeConverter2 + $converterplaybackStyle = const i0.EnumIndexConverter( + i2.AssetPlaybackStyle.values, + ); @override bool get withoutRowId => true; @override @@ -781,9 +875,11 @@ class LocalAssetEntityData extends i0.DataClass final String? checksum; final bool isFavorite; final int orientation; + final String? iCloudId; final DateTime? adjustmentTime; final double? latitude; final double? longitude; + final i2.AssetPlaybackStyle playbackStyle; const LocalAssetEntityData({ required this.name, required this.type, @@ -796,9 +892,11 @@ class LocalAssetEntityData extends i0.DataClass this.checksum, required this.isFavorite, required this.orientation, + this.iCloudId, this.adjustmentTime, this.latitude, this.longitude, + required this.playbackStyle, }); @override Map toColumns(bool nullToAbsent) { @@ -826,6 +924,9 @@ class LocalAssetEntityData extends i0.DataClass } map['is_favorite'] = i0.Variable(isFavorite); map['orientation'] = i0.Variable(orientation); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = i0.Variable(iCloudId); + } if (!nullToAbsent || adjustmentTime != null) { map['adjustment_time'] = i0.Variable(adjustmentTime); } @@ -835,6 +936,11 @@ class LocalAssetEntityData extends i0.DataClass if (!nullToAbsent || longitude != null) { map['longitude'] = i0.Variable(longitude); } + { + map['playback_style'] = i0.Variable( + i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(playbackStyle), + ); + } return map; } @@ -857,9 +963,13 @@ class LocalAssetEntityData extends i0.DataClass checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), orientation: serializer.fromJson(json['orientation']), + iCloudId: serializer.fromJson(json['iCloudId']), adjustmentTime: serializer.fromJson(json['adjustmentTime']), latitude: serializer.fromJson(json['latitude']), longitude: serializer.fromJson(json['longitude']), + playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromJson( + serializer.fromJson(json['playbackStyle']), + ), ); } @override @@ -879,9 +989,13 @@ class LocalAssetEntityData extends i0.DataClass 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), 'orientation': serializer.toJson(orientation), + 'iCloudId': serializer.toJson(iCloudId), 'adjustmentTime': serializer.toJson(adjustmentTime), 'latitude': serializer.toJson(latitude), 'longitude': serializer.toJson(longitude), + 'playbackStyle': serializer.toJson( + i1.$LocalAssetEntityTable.$converterplaybackStyle.toJson(playbackStyle), + ), }; } @@ -897,9 +1011,11 @@ class LocalAssetEntityData extends i0.DataClass i0.Value checksum = const i0.Value.absent(), bool? isFavorite, int? orientation, + i0.Value iCloudId = const i0.Value.absent(), i0.Value adjustmentTime = const i0.Value.absent(), i0.Value latitude = const i0.Value.absent(), i0.Value longitude = const i0.Value.absent(), + i2.AssetPlaybackStyle? playbackStyle, }) => i1.LocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -914,11 +1030,13 @@ class LocalAssetEntityData extends i0.DataClass checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, adjustmentTime: adjustmentTime.present ? adjustmentTime.value : this.adjustmentTime, latitude: latitude.present ? latitude.value : this.latitude, longitude: longitude.present ? longitude.value : this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, ); LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { return LocalAssetEntityData( @@ -939,11 +1057,15 @@ class LocalAssetEntityData extends i0.DataClass orientation: data.orientation.present ? data.orientation.value : this.orientation, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, adjustmentTime: data.adjustmentTime.present ? data.adjustmentTime.value : this.adjustmentTime, latitude: data.latitude.present ? data.latitude.value : this.latitude, longitude: data.longitude.present ? data.longitude.value : this.longitude, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, ); } @@ -961,9 +1083,11 @@ class LocalAssetEntityData extends i0.DataClass ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') ..write('adjustmentTime: $adjustmentTime, ') ..write('latitude: $latitude, ') - ..write('longitude: $longitude') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') ..write(')')) .toString(); } @@ -981,9 +1105,11 @@ class LocalAssetEntityData extends i0.DataClass checksum, isFavorite, orientation, + iCloudId, adjustmentTime, latitude, longitude, + playbackStyle, ); @override bool operator ==(Object other) => @@ -1000,9 +1126,11 @@ class LocalAssetEntityData extends i0.DataClass other.checksum == this.checksum && other.isFavorite == this.isFavorite && other.orientation == this.orientation && + other.iCloudId == this.iCloudId && other.adjustmentTime == this.adjustmentTime && other.latitude == this.latitude && - other.longitude == this.longitude); + other.longitude == this.longitude && + other.playbackStyle == this.playbackStyle); } class LocalAssetEntityCompanion @@ -1018,9 +1146,11 @@ class LocalAssetEntityCompanion final i0.Value checksum; final i0.Value isFavorite; final i0.Value orientation; + final i0.Value iCloudId; final i0.Value adjustmentTime; final i0.Value latitude; final i0.Value longitude; + final i0.Value playbackStyle; const LocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -1033,9 +1163,11 @@ class LocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + this.iCloudId = const i0.Value.absent(), this.adjustmentTime = const i0.Value.absent(), this.latitude = const i0.Value.absent(), this.longitude = const i0.Value.absent(), + this.playbackStyle = const i0.Value.absent(), }); LocalAssetEntityCompanion.insert({ required String name, @@ -1049,9 +1181,11 @@ class LocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + this.iCloudId = const i0.Value.absent(), this.adjustmentTime = const i0.Value.absent(), this.latitude = const i0.Value.absent(), this.longitude = const i0.Value.absent(), + this.playbackStyle = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id); @@ -1067,9 +1201,11 @@ class LocalAssetEntityCompanion i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? orientation, + i0.Expression? iCloudId, i0.Expression? adjustmentTime, i0.Expression? latitude, i0.Expression? longitude, + i0.Expression? playbackStyle, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -1083,9 +1219,11 @@ class LocalAssetEntityCompanion if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, if (orientation != null) 'orientation': orientation, + if (iCloudId != null) 'i_cloud_id': iCloudId, if (adjustmentTime != null) 'adjustment_time': adjustmentTime, if (latitude != null) 'latitude': latitude, if (longitude != null) 'longitude': longitude, + if (playbackStyle != null) 'playback_style': playbackStyle, }); } @@ -1101,9 +1239,11 @@ class LocalAssetEntityCompanion i0.Value? checksum, i0.Value? isFavorite, i0.Value? orientation, + i0.Value? iCloudId, i0.Value? adjustmentTime, i0.Value? latitude, i0.Value? longitude, + i0.Value? playbackStyle, }) { return i1.LocalAssetEntityCompanion( name: name ?? this.name, @@ -1117,9 +1257,11 @@ class LocalAssetEntityCompanion checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + iCloudId: iCloudId ?? this.iCloudId, adjustmentTime: adjustmentTime ?? this.adjustmentTime, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, ); } @@ -1161,6 +1303,9 @@ class LocalAssetEntityCompanion if (orientation.present) { map['orientation'] = i0.Variable(orientation.value); } + if (iCloudId.present) { + map['i_cloud_id'] = i0.Variable(iCloudId.value); + } if (adjustmentTime.present) { map['adjustment_time'] = i0.Variable(adjustmentTime.value); } @@ -1170,6 +1315,13 @@ class LocalAssetEntityCompanion if (longitude.present) { map['longitude'] = i0.Variable(longitude.value); } + if (playbackStyle.present) { + map['playback_style'] = i0.Variable( + i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql( + playbackStyle.value, + ), + ); + } return map; } @@ -1187,10 +1339,17 @@ class LocalAssetEntityCompanion ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') ..write('adjustmentTime: $adjustmentTime, ') ..write('latitude: $latitude, ') - ..write('longitude: $longitude') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') ..write(')')) .toString(); } } + +i0.Index get idxLocalAssetCloudId => i0.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', +); diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift index d1377f6685..73276d1756 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -21,7 +21,13 @@ SELECT rae.owner_id, rae.live_photo_video_id, 0 as orientation, - rae.stack_id + rae.stack_id, + NULL as i_cloud_id, + NULL as latitude, + NULL as longitude, + NULL as adjustmentTime, + rae.is_edited, + 0 as playback_style FROM remote_asset_entity rae LEFT JOIN @@ -53,7 +59,13 @@ SELECT NULL as owner_id, NULL as live_photo_video_id, lae.orientation, - NULL as stack_id + NULL as stack_id, + lae.i_cloud_id, + lae.latitude, + lae.longitude, + lae.adjustment_time, + 0 as is_edited, + lae.playback_style FROM local_asset_entity lae WHERE NOT EXISTS ( @@ -75,14 +87,20 @@ LIMIT $limit; mergedBucket(:group_by AS INTEGER): SELECT COUNT(*) as asset_count, - CASE - 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 + bucket_date FROM ( SELECT - rae.created_at + CASE + WHEN :group_by = 0 THEN COALESCE( + STRFTIME('%Y-%m-%d', rae.local_date_time), + STRFTIME('%Y-%m-%d', rae.created_at, 'localtime') + ) + WHEN :group_by = 1 THEN COALESCE( + STRFTIME('%Y-%m', rae.local_date_time), + STRFTIME('%Y-%m', rae.created_at, 'localtime') + ) + END as bucket_date FROM remote_asset_entity rae LEFT JOIN @@ -97,7 +115,10 @@ FROM ) UNION ALL SELECT - lae.created_at + CASE + WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', lae.created_at, 'localtime') + WHEN :group_by = 1 THEN STRFTIME('%Y-%m', lae.created_at, 'localtime') + END as bucket_date FROM local_asset_entity lae WHERE NOT EXISTS ( diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart index 5a091c349c..c6004eb10d 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart +++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart @@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor { ); $arrayStartIndex += generatedlimit.amountOfVariables; return customSelect( - '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 ($expandeduserIds) 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 rae.owner_id IN ($expandeduserIds)) 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) AND NOT 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 = 2) 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, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style 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 ($expandeduserIds) 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, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) 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) AND NOT 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 = 2) ORDER BY created_at DESC ${generatedlimit.sql}', variables: [ for (var $ in userIds) i0.Variable($), ...generatedlimit.introducedVariables, @@ -62,6 +62,12 @@ class MergedAssetDrift extends i1.ModularAccessor { livePhotoVideoId: row.readNullable('live_photo_video_id'), orientation: row.read('orientation'), stackId: row.readNullable('stack_id'), + iCloudId: row.readNullable('i_cloud_id'), + latitude: row.readNullable('latitude'), + longitude: row.readNullable('longitude'), + adjustmentTime: row.readNullable('adjustmentTime'), + isEdited: row.read('is_edited'), + playbackStyle: row.read('playback_style'), ), ); } @@ -74,7 +80,7 @@ class MergedAssetDrift extends i1.ModularAccessor { final expandeduserIds = $expandVar($arrayStartIndex, userIds.length); $arrayStartIndex += userIds.length; return customSelect( - '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 ($expandeduserIds) 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 WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) 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) AND NOT 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 = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC', + 'SELECT COUNT(*) AS asset_count, bucket_date FROM (SELECT CASE WHEN ?1 = 0 THEN COALESCE(STRFTIME(\'%Y-%m-%d\', rae.local_date_time), STRFTIME(\'%Y-%m-%d\', rae.created_at, \'localtime\')) WHEN ?1 = 1 THEN COALESCE(STRFTIME(\'%Y-%m\', rae.local_date_time), STRFTIME(\'%Y-%m\', rae.created_at, \'localtime\')) END AS bucket_date 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 ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', lae.created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', lae.created_at, \'localtime\') END AS bucket_date FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) 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) AND NOT 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 = 2)) GROUP BY bucket_date ORDER BY bucket_date DESC', variables: [ i0.Variable(groupBy), for (var $ in userIds) i0.Variable($), @@ -129,6 +135,12 @@ class MergedAssetResult { final String? livePhotoVideoId; final int orientation; final String? stackId; + final String? iCloudId; + final double? latitude; + final double? longitude; + final DateTime? adjustmentTime; + final bool isEdited; + final int playbackStyle; MergedAssetResult({ this.remoteId, this.localId, @@ -146,6 +158,12 @@ class MergedAssetResult { this.livePhotoVideoId, required this.orientation, this.stackId, + this.iCloudId, + this.latitude, + this.longitude, + this.adjustmentTime, + required this.isEdited, + required this.playbackStyle, }); } diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index dbc675ee99..1d8dc6d87c 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)') class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 01ec72fe23..76a91f27bf 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -440,6 +440,10 @@ typedef $$PartnerEntityTableProcessedTableManager = i1.PartnerEntityData, i0.PrefetchHooks Function({bool sharedById, bool sharedWithId}) >; +i0.Index get idxPartnerSharedWithId => i0.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', +); class $PartnerEntityTable extends i2.PartnerEntity with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { diff --git a/mobile/lib/infrastructure/entities/person.entity.dart b/mobile/lib/infrastructure/entities/person.entity.dart index f0878e00f8..6e014590ab 100644 --- a/mobile/lib/infrastructure/entities/person.entity.dart +++ b/mobile/lib/infrastructure/entities/person.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)') class PersonEntity extends Table with DriftDefaultsMixin { const PersonEntity(); diff --git a/mobile/lib/infrastructure/entities/person.entity.drift.dart b/mobile/lib/infrastructure/entities/person.entity.drift.dart index ffbd796f4b..02ea48c846 100644 --- a/mobile/lib/infrastructure/entities/person.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/person.entity.drift.dart @@ -455,6 +455,10 @@ typedef $$PersonEntityTableProcessedTableManager = i1.PersonEntityData, i0.PrefetchHooks Function({bool ownerId}) >; +i0.Index get idxPersonOwnerId => i0.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', +); class $PersonEntityTable extends i2.PersonEntity with i0.TableInfo<$PersonEntityTable, i1.PersonEntityData> { diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart index 74b00dd9ee..30e13853d8 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -4,6 +4,7 @@ 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'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)') class RemoteAlbumEntity extends Table with DriftDefaultsMixin { const RemoteAlbumEntity(); diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart index 30a6d0b535..7dc864b978 100644 --- a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart @@ -566,6 +566,10 @@ typedef $$RemoteAlbumEntityTableProcessedTableManager = i1.RemoteAlbumEntityData, i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId}) >; +i0.Index get idxRemoteAlbumOwnerId => i0.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', +); class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> { diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart index e99f5364a4..6d1e88514b 100644 --- a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart @@ -3,6 +3,9 @@ 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'; +@TableIndex.sql( + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', +) class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin { const RemoteAlbumAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart index adf22635c1..a03c4d7e96 100644 --- a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart @@ -441,6 +441,10 @@ typedef $$RemoteAlbumAssetEntityTableProcessedTableManager = i1.RemoteAlbumAssetEntityData, i0.PrefetchHooks Function({bool assetId, bool albumId}) >; +i0.Index get idxRemoteAlbumAssetAlbumAsset => i0.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', +); class $RemoteAlbumAssetEntityTable extends i2.RemoteAlbumAssetEntity with diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index dcc885a2a9..4c8b563616 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -19,6 +19,13 @@ ON remote_asset_entity (owner_id, library_id, checksum) WHERE (library_id IS NOT NULL); ''') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)') +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)') +@TableIndex.sql( + "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))", +) +@TableIndex.sql( + "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))", +) class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const RemoteAssetEntity(); @@ -44,6 +51,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin TextColumn get libraryId => text().nullable()(); + BoolColumn get isEdited => boolean().withDefault(const Constant(false))(); + @override Set get primaryKey => {id}; } @@ -66,5 +75,6 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { livePhotoVideoId: livePhotoVideoId, localId: localId, stackId: stackId, + isEdited: isEdited, ); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index eab7f95f64..8231cfcd8a 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -31,6 +31,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder = required i2.AssetVisibility visibility, i0.Value stackId, i0.Value libraryId, + i0.Value isEdited, }); typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i1.RemoteAssetEntityCompanion Function({ @@ -52,6 +53,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder = i0.Value visibility, i0.Value stackId, i0.Value libraryId, + i0.Value isEdited, }); final class $$RemoteAssetEntityTableReferences @@ -196,6 +198,11 @@ class $$RemoteAssetEntityTableFilterComposer builder: (column) => i0.ColumnFilters(column), ); + i0.ColumnFilters get isEdited => $composableBuilder( + column: $table.isEdited, + builder: (column) => i0.ColumnFilters(column), + ); + i5.$$UserEntityTableFilterComposer get ownerId { final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, @@ -318,6 +325,11 @@ class $$RemoteAssetEntityTableOrderingComposer builder: (column) => i0.ColumnOrderings(column), ); + i0.ColumnOrderings get isEdited => $composableBuilder( + column: $table.isEdited, + builder: (column) => i0.ColumnOrderings(column), + ); + i5.$$UserEntityTableOrderingComposer get ownerId { final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, @@ -417,6 +429,9 @@ class $$RemoteAssetEntityTableAnnotationComposer i0.GeneratedColumn get libraryId => $composableBuilder(column: $table.libraryId, builder: (column) => column); + i0.GeneratedColumn get isEdited => + $composableBuilder(column: $table.isEdited, builder: (column) => column); + i5.$$UserEntityTableAnnotationComposer get ownerId { final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -497,6 +512,7 @@ class $$RemoteAssetEntityTableTableManager const i0.Value.absent(), i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + i0.Value isEdited = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion( name: name, type: type, @@ -516,6 +532,7 @@ class $$RemoteAssetEntityTableTableManager visibility: visibility, stackId: stackId, libraryId: libraryId, + isEdited: isEdited, ), createCompanionCallback: ({ @@ -537,6 +554,7 @@ class $$RemoteAssetEntityTableTableManager required i2.AssetVisibility visibility, i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + i0.Value isEdited = const i0.Value.absent(), }) => i1.RemoteAssetEntityCompanion.insert( name: name, type: type, @@ -556,6 +574,7 @@ class $$RemoteAssetEntityTableTableManager visibility: visibility, stackId: stackId, libraryId: libraryId, + isEdited: isEdited, ), withReferenceMapper: (p0) => p0 .map( @@ -844,6 +863,21 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity type: i0.DriftSqlType.string, requiredDuringInsert: false, ); + static const i0.VerificationMeta _isEditedMeta = const i0.VerificationMeta( + 'isEdited', + ); + @override + late final i0.GeneratedColumn isEdited = i0.GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const i4.Constant(false), + ); @override List get $columns => [ name, @@ -864,6 +898,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity visibility, stackId, libraryId, + isEdited, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -987,6 +1022,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity libraryId.isAcceptableOrUnknown(data['library_id']!, _libraryIdMeta), ); } + if (data.containsKey('is_edited')) { + context.handle( + _isEditedMeta, + isEdited.isAcceptableOrUnknown(data['is_edited']!, _isEditedMeta), + ); + } return context; } @@ -1075,6 +1116,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity i0.DriftSqlType.string, data['${effectivePrefix}library_id'], ), + isEdited: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, ); } @@ -1115,6 +1160,7 @@ class RemoteAssetEntityData extends i0.DataClass final i2.AssetVisibility visibility; final String? stackId; final String? libraryId; + final bool isEdited; const RemoteAssetEntityData({ required this.name, required this.type, @@ -1134,6 +1180,7 @@ class RemoteAssetEntityData extends i0.DataClass required this.visibility, this.stackId, this.libraryId, + required this.isEdited, }); @override Map toColumns(bool nullToAbsent) { @@ -1182,6 +1229,7 @@ class RemoteAssetEntityData extends i0.DataClass if (!nullToAbsent || libraryId != null) { map['library_id'] = i0.Variable(libraryId); } + map['is_edited'] = i0.Variable(isEdited); return map; } @@ -1213,6 +1261,7 @@ class RemoteAssetEntityData extends i0.DataClass ), stackId: serializer.fromJson(json['stackId']), libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), ); } @override @@ -1241,6 +1290,7 @@ class RemoteAssetEntityData extends i0.DataClass ), 'stackId': serializer.toJson(stackId), 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), }; } @@ -1263,6 +1313,7 @@ class RemoteAssetEntityData extends i0.DataClass i2.AssetVisibility? visibility, i0.Value stackId = const i0.Value.absent(), i0.Value libraryId = const i0.Value.absent(), + bool? isEdited, }) => i1.RemoteAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -1288,6 +1339,7 @@ class RemoteAssetEntityData extends i0.DataClass visibility: visibility ?? this.visibility, stackId: stackId.present ? stackId.value : this.stackId, libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, ); RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { return RemoteAssetEntityData( @@ -1319,6 +1371,7 @@ class RemoteAssetEntityData extends i0.DataClass : this.visibility, stackId: data.stackId.present ? data.stackId.value : this.stackId, libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, ); } @@ -1342,7 +1395,8 @@ class RemoteAssetEntityData extends i0.DataClass ..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('visibility: $visibility, ') ..write('stackId: $stackId, ') - ..write('libraryId: $libraryId') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') ..write(')')) .toString(); } @@ -1367,6 +1421,7 @@ class RemoteAssetEntityData extends i0.DataClass visibility, stackId, libraryId, + isEdited, ); @override bool operator ==(Object other) => @@ -1389,7 +1444,8 @@ class RemoteAssetEntityData extends i0.DataClass other.livePhotoVideoId == this.livePhotoVideoId && other.visibility == this.visibility && other.stackId == this.stackId && - other.libraryId == this.libraryId); + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); } class RemoteAssetEntityCompanion @@ -1412,6 +1468,7 @@ class RemoteAssetEntityCompanion final i0.Value visibility; final i0.Value stackId; final i0.Value libraryId; + final i0.Value isEdited; const RemoteAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -1431,6 +1488,7 @@ class RemoteAssetEntityCompanion this.visibility = const i0.Value.absent(), this.stackId = const i0.Value.absent(), this.libraryId = const i0.Value.absent(), + this.isEdited = const i0.Value.absent(), }); RemoteAssetEntityCompanion.insert({ required String name, @@ -1451,6 +1509,7 @@ class RemoteAssetEntityCompanion required i2.AssetVisibility visibility, this.stackId = const i0.Value.absent(), this.libraryId = const i0.Value.absent(), + this.isEdited = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id), @@ -1476,6 +1535,7 @@ class RemoteAssetEntityCompanion i0.Expression? visibility, i0.Expression? stackId, i0.Expression? libraryId, + i0.Expression? isEdited, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -1496,6 +1556,7 @@ class RemoteAssetEntityCompanion if (visibility != null) 'visibility': visibility, if (stackId != null) 'stack_id': stackId, if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, }); } @@ -1518,6 +1579,7 @@ class RemoteAssetEntityCompanion i0.Value? visibility, i0.Value? stackId, i0.Value? libraryId, + i0.Value? isEdited, }) { return i1.RemoteAssetEntityCompanion( name: name ?? this.name, @@ -1538,6 +1600,7 @@ class RemoteAssetEntityCompanion visibility: visibility ?? this.visibility, stackId: stackId ?? this.stackId, libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, ); } @@ -1602,6 +1665,9 @@ class RemoteAssetEntityCompanion if (libraryId.present) { map['library_id'] = i0.Variable(libraryId.value); } + if (isEdited.present) { + map['is_edited'] = i0.Variable(isEdited.value); + } return map; } @@ -1625,7 +1691,8 @@ class RemoteAssetEntityCompanion ..write('livePhotoVideoId: $livePhotoVideoId, ') ..write('visibility: $visibility, ') ..write('stackId: $stackId, ') - ..write('libraryId: $libraryId') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') ..write(')')) .toString(); } @@ -1643,3 +1710,15 @@ i0.Index get idxRemoteAssetChecksum => i0.Index( 'idx_remote_asset_checksum', 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', ); +i0.Index get idxRemoteAssetStackId => i0.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', +); +i0.Index get idxRemoteAssetLocalDateTimeDay => i0.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', +); +i0.Index get idxRemoteAssetLocalDateTimeMonth => i0.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', +); diff --git a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart new file mode 100644 index 0000000000..593931f986 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)') +class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin { + TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get cloudId => text().nullable()(); + + DateTimeColumn get createdAt => dateTime().nullable()(); + + DateTimeColumn get adjustmentTime => dateTime().nullable()(); + + RealColumn get latitude => real().nullable()(); + + RealColumn get longitude => real().nullable()(); + + @override + Set get primaryKey => {assetId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart new file mode 100644 index 0000000000..f86528ee64 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart @@ -0,0 +1,830 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.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; + +typedef $$RemoteAssetCloudIdEntityTableCreateCompanionBuilder = + i1.RemoteAssetCloudIdEntityCompanion Function({ + required String assetId, + i0.Value cloudId, + i0.Value createdAt, + i0.Value adjustmentTime, + i0.Value latitude, + i0.Value longitude, + }); +typedef $$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder = + i1.RemoteAssetCloudIdEntityCompanion Function({ + i0.Value assetId, + i0.Value cloudId, + i0.Value createdAt, + i0.Value adjustmentTime, + i0.Value latitude, + i0.Value longitude, + }); + +final class $$RemoteAssetCloudIdEntityTableReferences + extends + i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAssetCloudIdEntityTable, + i1.RemoteAssetCloudIdEntityData + > { + $$RemoteAssetCloudIdEntityTableReferences( + 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_asset_cloud_id_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]), + ); + } +} + +class $$RemoteAssetCloudIdEntityTableFilterComposer + extends + i0.Composer { + $$RemoteAssetCloudIdEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get cloudId => $composableBuilder( + column: $table.cloudId, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get adjustmentTime => $composableBuilder( + column: $table.adjustmentTime, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => i0.ColumnFilters(column), + ); + + i0.ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, + 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; + } +} + +class $$RemoteAssetCloudIdEntityTableOrderingComposer + extends + i0.Composer { + $$RemoteAssetCloudIdEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get cloudId => $composableBuilder( + column: $table.cloudId, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get adjustmentTime => $composableBuilder( + column: $table.adjustmentTime, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + 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; + } +} + +class $$RemoteAssetCloudIdEntityTableAnnotationComposer + extends + i0.Composer { + $$RemoteAssetCloudIdEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get cloudId => + $composableBuilder(column: $table.cloudId, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get adjustmentTime => $composableBuilder( + column: $table.adjustmentTime, + builder: (column) => column, + ); + + i0.GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + i0.GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, 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; + } +} + +class $$RemoteAssetCloudIdEntityTableTableManager + extends + i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetCloudIdEntityTable, + i1.RemoteAssetCloudIdEntityData, + i1.$$RemoteAssetCloudIdEntityTableFilterComposer, + i1.$$RemoteAssetCloudIdEntityTableOrderingComposer, + i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer, + $$RemoteAssetCloudIdEntityTableCreateCompanionBuilder, + $$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder, + ( + i1.RemoteAssetCloudIdEntityData, + i1.$$RemoteAssetCloudIdEntityTableReferences, + ), + i1.RemoteAssetCloudIdEntityData, + i0.PrefetchHooks Function({bool assetId}) + > { + $$RemoteAssetCloudIdEntityTableTableManager( + i0.GeneratedDatabase db, + i1.$RemoteAssetCloudIdEntityTable table, + ) : super( + i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAssetCloudIdEntityTableFilterComposer( + $db: db, + $table: table, + ), + createOrderingComposer: () => + i1.$$RemoteAssetCloudIdEntityTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value cloudId = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value adjustmentTime = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + }) => i1.RemoteAssetCloudIdEntityCompanion( + assetId: assetId, + cloudId: cloudId, + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ), + createCompanionCallback: + ({ + required String assetId, + i0.Value cloudId = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value adjustmentTime = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + }) => i1.RemoteAssetCloudIdEntityCompanion.insert( + assetId: assetId, + cloudId: cloudId, + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + i1.$$RemoteAssetCloudIdEntityTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({assetId = 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 + .$$RemoteAssetCloudIdEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1 + .$$RemoteAssetCloudIdEntityTableReferences + ._assetIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$RemoteAssetCloudIdEntityTableProcessedTableManager = + i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetCloudIdEntityTable, + i1.RemoteAssetCloudIdEntityData, + i1.$$RemoteAssetCloudIdEntityTableFilterComposer, + i1.$$RemoteAssetCloudIdEntityTableOrderingComposer, + i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer, + $$RemoteAssetCloudIdEntityTableCreateCompanionBuilder, + $$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder, + ( + i1.RemoteAssetCloudIdEntityData, + i1.$$RemoteAssetCloudIdEntityTableReferences, + ), + i1.RemoteAssetCloudIdEntityData, + i0.PrefetchHooks Function({bool assetId}) + >; +i0.Index get idxRemoteAssetCloudId => i0.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', +); + +class $RemoteAssetCloudIdEntityTable extends i2.RemoteAssetCloudIdEntity + with + i0.TableInfo< + $RemoteAssetCloudIdEntityTable, + i1.RemoteAssetCloudIdEntityData + > { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAssetCloudIdEntityTable(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 _cloudIdMeta = const i0.VerificationMeta( + 'cloudId', + ); + @override + late final i0.GeneratedColumn cloudId = i0.GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + ); + static const i0.VerificationMeta _createdAtMeta = const i0.VerificationMeta( + 'createdAt', + ); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn( + 'created_at', + aliasedName, + true, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const i0.VerificationMeta _adjustmentTimeMeta = + const i0.VerificationMeta('adjustmentTime'); + @override + late final i0.GeneratedColumn adjustmentTime = + i0.GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const i0.VerificationMeta _latitudeMeta = const i0.VerificationMeta( + 'latitude', + ); + @override + late final i0.GeneratedColumn latitude = i0.GeneratedColumn( + 'latitude', + aliasedName, + true, + type: i0.DriftSqlType.double, + requiredDuringInsert: false, + ); + static const i0.VerificationMeta _longitudeMeta = const i0.VerificationMeta( + 'longitude', + ); + @override + late final i0.GeneratedColumn longitude = i0.GeneratedColumn( + 'longitude', + aliasedName, + true, + type: i0.DriftSqlType.double, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_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('cloud_id')) { + context.handle( + _cloudIdMeta, + cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } + if (data.containsKey('adjustment_time')) { + context.handle( + _adjustmentTimeMeta, + adjustmentTime.isAcceptableOrUnknown( + data['adjustment_time']!, + _adjustmentTimeMeta, + ), + ); + } + if (data.containsKey('latitude')) { + context.handle( + _latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta), + ); + } + if (data.containsKey('longitude')) { + context.handle( + _longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {assetId}; + @override + i1.RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + i0.DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + i0.DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + $RemoteAssetCloudIdEntityTable createAlias(String alias) { + return $RemoteAssetCloudIdEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = i0.Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = i0.Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = i0.Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = i0.Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = i0.Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + i0.ValueSerializer? serializer, + }) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + i1.RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + i0.Value cloudId = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value adjustmentTime = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + }) => i1.RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + i1.RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value cloudId; + final i0.Value createdAt; + final i0.Value adjustmentTime; + final i0.Value latitude; + final i0.Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.cloudId = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.adjustmentTime = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.adjustmentTime = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + }) : assetId = i0.Value(assetId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? cloudId, + i0.Expression? createdAt, + i0.Expression? adjustmentTime, + i0.Expression? latitude, + i0.Expression? longitude, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + i1.RemoteAssetCloudIdEntityCompanion copyWith({ + i0.Value? assetId, + i0.Value? cloudId, + i0.Value? createdAt, + i0.Value? adjustmentTime, + i0.Value? latitude, + i0.Value? longitude, + }) { + return i1.RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = i0.Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = i0.Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = i0.Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = i0.Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart index be50d7e330..4f90845a45 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)') class StackEntity extends Table with DriftDefaultsMixin { const StackEntity(); diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart index ff7a3c3444..55017f8344 100644 --- a/mobile/lib/infrastructure/entities/stack.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/stack.entity.drift.dart @@ -357,6 +357,10 @@ typedef $$StackEntityTableProcessedTableManager = i1.StackEntityData, i0.PrefetchHooks Function({bool ownerId}) >; +i0.Index get idxStackPrimaryAssetId => i0.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', +); class $StackEntityTable extends i2.StackEntity with i0.TableInfo<$StackEntityTable, i1.StackEntityData> { diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart index 308130b9ea..4a8a374f20 100644 --- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart @@ -4,6 +4,13 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; +enum TrashOrigin { + // do not change this order! + localSync, + remoteSync, + localUser, +} + @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)') class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { @@ -19,6 +26,10 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity IntColumn get orientation => integer().withDefault(const Constant(0))(); + IntColumn get source => intEnum()(); + + IntColumn get playbackStyle => intEnum().withDefault(const Constant(0))(); + @override Set get primaryKey => {id, albumId}; } @@ -36,5 +47,7 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD height: height, width: width, orientation: orientation, + playbackStyle: playbackStyle, + isEdited: false, ); } diff --git a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart index aab226c3a2..84be6289b8 100644 --- a/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.drift.dart @@ -22,6 +22,8 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + required i3.TrashOrigin source, + i0.Value playbackStyle, }); typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder = i1.TrashedLocalAssetEntityCompanion Function({ @@ -37,6 +39,8 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value source, + i0.Value playbackStyle, }); class $$TrashedLocalAssetEntityTableFilterComposer @@ -109,6 +113,22 @@ class $$TrashedLocalAssetEntityTableFilterComposer column: $table.orientation, builder: (column) => i0.ColumnFilters(column), ); + + i0.ColumnWithTypeConverterFilters + get source => $composableBuilder( + column: $table.source, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); + + i0.ColumnWithTypeConverterFilters< + i2.AssetPlaybackStyle, + i2.AssetPlaybackStyle, + int + > + get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); } class $$TrashedLocalAssetEntityTableOrderingComposer @@ -180,6 +200,16 @@ class $$TrashedLocalAssetEntityTableOrderingComposer column: $table.orientation, builder: (column) => i0.ColumnOrderings(column), ); + + i0.ColumnOrderings get source => $composableBuilder( + column: $table.source, + builder: (column) => i0.ColumnOrderings(column), + ); + + i0.ColumnOrderings get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => i0.ColumnOrderings(column), + ); } class $$TrashedLocalAssetEntityTableAnnotationComposer @@ -233,6 +263,15 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer column: $table.orientation, builder: (column) => column, ); + + i0.GeneratedColumnWithTypeConverter get source => + $composableBuilder(column: $table.source, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get playbackStyle => $composableBuilder( + column: $table.playbackStyle, + builder: (column) => column, + ); } class $$TrashedLocalAssetEntityTableTableManager @@ -293,6 +332,9 @@ class $$TrashedLocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + i0.Value source = const i0.Value.absent(), + i0.Value playbackStyle = + const i0.Value.absent(), }) => i1.TrashedLocalAssetEntityCompanion( name: name, type: type, @@ -306,6 +348,8 @@ class $$TrashedLocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + source: source, + playbackStyle: playbackStyle, ), createCompanionCallback: ({ @@ -321,6 +365,9 @@ class $$TrashedLocalAssetEntityTableTableManager i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value orientation = const i0.Value.absent(), + required i3.TrashOrigin source, + i0.Value playbackStyle = + const i0.Value.absent(), }) => i1.TrashedLocalAssetEntityCompanion.insert( name: name, type: type, @@ -334,6 +381,8 @@ class $$TrashedLocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + source: source, + playbackStyle: playbackStyle, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -519,6 +568,30 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity defaultValue: const i4.Constant(0), ); @override + late final i0.GeneratedColumnWithTypeConverter source = + i0.GeneratedColumn( + 'source', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + ).withConverter( + i1.$TrashedLocalAssetEntityTable.$convertersource, + ); + @override + late final i0.GeneratedColumnWithTypeConverter + playbackStyle = + i0.GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0), + ).withConverter( + i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle, + ); + @override List get $columns => [ name, type, @@ -532,6 +605,8 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity checksum, isFavorite, orientation, + source, + playbackStyle, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -682,6 +757,19 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity i0.DriftSqlType.int, data['${effectivePrefix}orientation'], )!, + source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ), + playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle + .fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ), ); } @@ -692,6 +780,12 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity static i0.JsonTypeConverter2 $convertertype = const i0.EnumIndexConverter(i2.AssetType.values); + static i0.JsonTypeConverter2 $convertersource = + const i0.EnumIndexConverter(i3.TrashOrigin.values); + static i0.JsonTypeConverter2 + $converterplaybackStyle = const i0.EnumIndexConverter( + i2.AssetPlaybackStyle.values, + ); @override bool get withoutRowId => true; @override @@ -712,6 +806,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass final String? checksum; final bool isFavorite; final int orientation; + final i3.TrashOrigin source; + final i2.AssetPlaybackStyle playbackStyle; const TrashedLocalAssetEntityData({ required this.name, required this.type, @@ -725,6 +821,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass this.checksum, required this.isFavorite, required this.orientation, + required this.source, + required this.playbackStyle, }); @override Map toColumns(bool nullToAbsent) { @@ -753,6 +851,18 @@ class TrashedLocalAssetEntityData extends i0.DataClass } map['is_favorite'] = i0.Variable(isFavorite); map['orientation'] = i0.Variable(orientation); + { + map['source'] = i0.Variable( + i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source), + ); + } + { + map['playback_style'] = i0.Variable( + i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql( + playbackStyle, + ), + ); + } return map; } @@ -776,6 +886,11 @@ class TrashedLocalAssetEntityData extends i0.DataClass checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), orientation: serializer.fromJson(json['orientation']), + source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromJson( + serializer.fromJson(json['source']), + ), + playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle + .fromJson(serializer.fromJson(json['playbackStyle'])), ); } @override @@ -796,6 +911,14 @@ class TrashedLocalAssetEntityData extends i0.DataClass 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson( + i1.$TrashedLocalAssetEntityTable.$convertersource.toJson(source), + ), + 'playbackStyle': serializer.toJson( + i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toJson( + playbackStyle, + ), + ), }; } @@ -812,6 +935,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass i0.Value checksum = const i0.Value.absent(), bool? isFavorite, int? orientation, + i3.TrashOrigin? source, + i2.AssetPlaybackStyle? playbackStyle, }) => i1.TrashedLocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -827,6 +952,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, ); TrashedLocalAssetEntityData copyWithCompanion( i1.TrashedLocalAssetEntityCompanion data, @@ -850,6 +977,10 @@ class TrashedLocalAssetEntityData extends i0.DataClass orientation: data.orientation.present ? data.orientation.value : this.orientation, + source: data.source.present ? data.source.value : this.source, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, ); } @@ -867,7 +998,9 @@ class TrashedLocalAssetEntityData extends i0.DataClass ..write('albumId: $albumId, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') ..write(')')) .toString(); } @@ -886,6 +1019,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass checksum, isFavorite, orientation, + source, + playbackStyle, ); @override bool operator ==(Object other) => @@ -902,7 +1037,9 @@ class TrashedLocalAssetEntityData extends i0.DataClass other.albumId == this.albumId && other.checksum == this.checksum && other.isFavorite == this.isFavorite && - other.orientation == this.orientation); + other.orientation == this.orientation && + other.source == this.source && + other.playbackStyle == this.playbackStyle); } class TrashedLocalAssetEntityCompanion @@ -919,6 +1056,8 @@ class TrashedLocalAssetEntityCompanion final i0.Value checksum; final i0.Value isFavorite; final i0.Value orientation; + final i0.Value source; + final i0.Value playbackStyle; const TrashedLocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -932,6 +1071,8 @@ class TrashedLocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + this.source = const i0.Value.absent(), + this.playbackStyle = const i0.Value.absent(), }); TrashedLocalAssetEntityCompanion.insert({ required String name, @@ -946,10 +1087,13 @@ class TrashedLocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + required i3.TrashOrigin source, + this.playbackStyle = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), id = i0.Value(id), - albumId = i0.Value(albumId); + albumId = i0.Value(albumId), + source = i0.Value(source); static i0.Insertable custom({ i0.Expression? name, i0.Expression? type, @@ -963,6 +1107,8 @@ class TrashedLocalAssetEntityCompanion i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? orientation, + i0.Expression? source, + i0.Expression? playbackStyle, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -977,6 +1123,8 @@ class TrashedLocalAssetEntityCompanion if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + if (playbackStyle != null) 'playback_style': playbackStyle, }); } @@ -993,6 +1141,8 @@ class TrashedLocalAssetEntityCompanion i0.Value? checksum, i0.Value? isFavorite, i0.Value? orientation, + i0.Value? source, + i0.Value? playbackStyle, }) { return i1.TrashedLocalAssetEntityCompanion( name: name ?? this.name, @@ -1007,6 +1157,8 @@ class TrashedLocalAssetEntityCompanion checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, ); } @@ -1051,6 +1203,18 @@ class TrashedLocalAssetEntityCompanion if (orientation.present) { map['orientation'] = i0.Variable(orientation.value); } + if (source.present) { + map['source'] = i0.Variable( + i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source.value), + ); + } + if (playbackStyle.present) { + map['playback_style'] = i0.Variable( + i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql( + playbackStyle.value, + ), + ); + } return map; } @@ -1068,7 +1232,9 @@ class TrashedLocalAssetEntityCompanion ..write('albumId: $albumId, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index d839b8bdf6..4df470277e 100644 --- a/mobile/lib/infrastructure/loaders/image_request.dart +++ b/mobile/lib/infrastructure/loaders/image_request.dart @@ -1,15 +1,12 @@ import 'dart:async'; import 'dart:ffi'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:ffi/ffi.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; -import 'package:logging/logging.dart'; part 'local_image_request.dart'; part 'thumbhash_image_request.dart'; @@ -27,6 +24,8 @@ abstract class ImageRequest { Future load(ImageDecoderCallback decode, {double scale = 1.0}); + Future loadCodec(); + void cancel() { if (_isCancelled) { return; @@ -37,27 +36,75 @@ abstract class ImageRequest { void _onCancelled(); - Future _fromPlatformImage(Map info) async { - final address = info['pointer']; - if (address == null) { - return null; - } - + Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async { final pointer = Pointer.fromAddress(address); if (_isCancelled) { malloc.free(pointer); return null; } - final int actualWidth; - final int actualHeight; - final int actualSize; final ui.ImmutableBuffer buffer; try { - actualWidth = info['width']!; - actualHeight = info['height']!; - actualSize = actualWidth * actualHeight * 4; - buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(actualSize)); + buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(length)); + } finally { + malloc.free(pointer); + } + + if (_isCancelled) { + buffer.dispose(); + return null; + } + + final descriptor = await ui.ImageDescriptor.encoded(buffer); + buffer.dispose(); + if (_isCancelled) { + descriptor.dispose(); + return null; + } + + final codec = await descriptor.instantiateCodec(); + if (_isCancelled) { + descriptor.dispose(); + codec.dispose(); + return null; + } + + return (codec, descriptor); + } + + Future _fromEncodedPlatformImage(int address, int length) async { + final result = await _codecFromEncodedPlatformImage(address, length); + if (result == null) return null; + + final (codec, descriptor) = result; + if (_isCancelled) { + descriptor.dispose(); + codec.dispose(); + return null; + } + + final frame = await codec.getNextFrame(); + descriptor.dispose(); + codec.dispose(); + if (_isCancelled) { + frame.image.dispose(); + return null; + } + + return frame; + } + + Future _fromDecodedPlatformImage(int address, int width, int height, int rowBytes) async { + final pointer = Pointer.fromAddress(address); + if (_isCancelled) { + malloc.free(pointer); + return null; + } + + final size = rowBytes * height; + final ui.ImmutableBuffer buffer; + try { + buffer = await ImmutableBuffer.fromUint8List(pointer.asTypedList(size)); } finally { malloc.free(pointer); } @@ -69,18 +116,28 @@ abstract class ImageRequest { final descriptor = ui.ImageDescriptor.raw( buffer, - width: actualWidth, - height: actualHeight, + width: width, + height: height, + rowBytes: rowBytes, pixelFormat: ui.PixelFormat.rgba8888, ); + buffer.dispose(); + final codec = await descriptor.instantiateCodec(); if (_isCancelled) { - buffer.dispose(); descriptor.dispose(); codec.dispose(); return null; } - return await codec.getNextFrame(); + final frame = await codec.getNextFrame(); + descriptor.dispose(); + codec.dispose(); + if (_isCancelled) { + frame.image.dispose(); + return null; + } + + return frame; } } diff --git a/mobile/lib/infrastructure/loaders/local_image_request.dart b/mobile/lib/infrastructure/loaders/local_image_request.dart index 7a1b3d8957..a6c9fa2989 100644 --- a/mobile/lib/infrastructure/loaders/local_image_request.dart +++ b/mobile/lib/infrastructure/loaders/local_image_request.dart @@ -16,20 +16,44 @@ class LocalImageRequest extends ImageRequest { return null; } - final Map info = await thumbnailApi.requestImage( + final info = await localImageApi.requestImage( localId, requestId: requestId, width: width, height: height, isVideo: assetType == AssetType.video, + preferEncoded: false, ); + if (info == null) { + return null; + } - final frame = await _fromPlatformImage(info); + final frame = await _fromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!); return frame == null ? null : ImageInfo(image: frame.image, scale: scale); } + @override + Future loadCodec() async { + if (_isCancelled) { + return null; + } + + final info = await localImageApi.requestImage( + localId, + requestId: requestId, + width: width, + height: height, + isVideo: assetType == AssetType.video, + preferEncoded: true, + ); + if (info == null) return null; + + final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null); + return codec; + } + @override Future _onCancelled() { - return thumbnailApi.cancelImageRequest(requestId); + return localImageApi.cancelRequest(requestId); } } diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index 03dcd6454a..40ed304bbe 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -1,14 +1,10 @@ part of 'image_request.dart'; class RemoteImageRequest extends ImageRequest { - static final log = Logger('RemoteImageRequest'); - static final client = HttpClient()..maxConnectionsPerHost = 16; - final RemoteCacheManager? cacheManager; final String uri; final Map headers; - HttpClientRequest? _request; - RemoteImageRequest({required this.uri, required this.headers, this.cacheManager}); + RemoteImageRequest({required this.uri, required this.headers}); @override Future load(ImageDecoderCallback decode, {double scale = 1.0}) async { @@ -16,164 +12,32 @@ class RemoteImageRequest extends ImageRequest { return null; } - // TODO: the cache manager makes everything sequential with its DB calls and its operations cannot be cancelled, - // so it ends up being a bottleneck. We only prefer fetching from it when it can skip the DB call. - final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: true); - if (cachedFileImage != null) { - return cachedFileImage; - } - - try { - final buffer = await _downloadImage(uri); - if (buffer == null) { - return null; - } - - return await _decodeBuffer(buffer, decode, scale); - } catch (e) { - if (_isCancelled) { - return null; - } - - final cachedFileImage = await _loadCachedFile(uri, decode, scale, inMemoryOnly: false); - if (cachedFileImage != null) { - return cachedFileImage; - } - - rethrow; - } finally { - _request = null; - } - } - - Future _downloadImage(String url) async { - if (_isCancelled) { - return null; - } - - final request = _request = await client.getUrl(Uri.parse(url)); - if (_isCancelled) { - request.abort(); - return _request = null; - } - - for (final entry in headers.entries) { - request.headers.set(entry.key, entry.value); - } - final response = await request.close(); - if (_isCancelled) { - return null; - } - - final cacheManager = this.cacheManager; - final streamController = StreamController>(sync: true); - final Stream> stream; - unawaited(cacheManager?.putStreamedFile(url, streamController.stream)); - stream = response.map((chunk) { - if (_isCancelled) { - throw StateError('Cancelled request'); - } - if (cacheManager != null) { - streamController.add(chunk); - } - return chunk; - }); - - try { - final Uint8List bytes = await _downloadBytes(stream, response.contentLength); - unawaited(streamController.close()); - return await ImmutableBuffer.fromUint8List(bytes); - } catch (e) { - streamController.addError(e); - unawaited(streamController.close()); - if (_isCancelled) { - return null; - } - rethrow; - } - } - - Future _downloadBytes(Stream> stream, int length) async { - final Uint8List bytes; - int offset = 0; - if (length > 0) { - // Known content length - use pre-allocated buffer - bytes = Uint8List(length); - await stream.listen((chunk) { - bytes.setAll(offset, chunk); - offset += chunk.length; - }, cancelOnError: true).asFuture(); - } else { - // Unknown content length - collect chunks dynamically - final chunks = >[]; - int totalLength = 0; - await stream.listen((chunk) { - chunks.add(chunk); - totalLength += chunk.length; - }, cancelOnError: true).asFuture(); - - bytes = Uint8List(totalLength); - for (final chunk in chunks) { - bytes.setAll(offset, chunk); - offset += chunk.length; - } - } - - return bytes; - } - - Future _loadCachedFile( - String url, - ImageDecoderCallback decode, - double scale, { - required bool inMemoryOnly, - }) async { - final cacheManager = this.cacheManager; - if (_isCancelled || cacheManager == null) { - return null; - } - - final file = await (inMemoryOnly ? cacheManager.getFileFromMemory(url) : cacheManager.getFileFromCache(url)); - if (_isCancelled || file == null) { - return null; - } - - try { - final buffer = await ImmutableBuffer.fromFilePath(file.file.path); - return await _decodeBuffer(buffer, decode, scale); - } catch (e) { - log.severe('Failed to decode cached image', e); - unawaited(_evictFile(url)); - return null; - } - } - - Future _evictFile(String url) async { - try { - await cacheManager?.removeFile(url); - } catch (e) { - log.severe('Failed to remove cached image', e); - } - } - - Future _decodeBuffer(ImmutableBuffer buffer, ImageDecoderCallback decode, scale) async { - if (_isCancelled) { - buffer.dispose(); - return null; - } - final codec = await decode(buffer); - if (_isCancelled) { - buffer.dispose(); - codec.dispose(); - return null; - } - final frame = await codec.getNextFrame(); - return ImageInfo(image: frame.image, scale: scale); + final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: false); + // Android always returns encoded data, so we need to check for both shapes of the response. + final frame = switch (info) { + {'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length), + {'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} => + await _fromDecodedPlatformImage(pointer, width, height, rowBytes), + _ => null, + }; + return frame == null ? null : ImageInfo(image: frame.image, scale: scale); } @override - void _onCancelled() { - _request?.abort(); - _request = null; + Future loadCodec() async { + if (_isCancelled) { + return null; + } + + final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId, preferEncoded: true); + if (info == null) return null; + + final (codec, _) = await _codecFromEncodedPlatformImage(info['pointer']!, info['length']!) ?? (null, null); + return codec; + } + + @override + Future _onCancelled() { + return remoteImageApi.cancelRequest(requestId); } } diff --git a/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart b/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart index a876020984..61e6a1b3ad 100644 --- a/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart +++ b/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart @@ -11,11 +11,14 @@ class ThumbhashImageRequest extends ImageRequest { return null; } - final Map info = await thumbnailApi.getThumbhash(thumbhash); - final frame = await _fromPlatformImage(info); + final Map info = await localImageApi.getThumbhash(thumbhash); + final frame = await _fromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!); return frame == null ? null : ImageInfo(image: frame.image, scale: scale); } + @override + Future loadCodec() => throw UnsupportedError('Thumbhash does not support codec loading'); + @override void _onCancelled() {} } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index b42aa31550..84fcc55cfd 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -18,6 +18,7 @@ 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/remote_asset_cloud_id.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; @@ -57,6 +58,7 @@ class IsarDatabaseRepository implements IDatabaseRepository { RemoteAlbumEntity, RemoteAlbumAssetEntity, RemoteAlbumUserEntity, + RemoteAssetCloudIdEntity, MemoryEntity, MemoryAssetEntity, StackEntity, @@ -95,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository { } @override - int get schemaVersion => 14; + int get schemaVersion => 21; @override MigrationStrategy get migration => MigrationStrategy( @@ -190,6 +192,48 @@ class Drift extends $Drift implements IDatabaseRepository { await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude); await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude); }, + from14To15: (m, v15) async { + await m.alterTable( + TableMigration( + v15.trashedLocalAssetEntity, + columnTransformer: {v15.trashedLocalAssetEntity.source: Constant(TrashOrigin.localSync.index)}, + newColumns: [v15.trashedLocalAssetEntity.source], + ), + ); + }, + from15To16: (m, v16) async { + // Add i_cloud_id to local and remote asset tables + await m.addColumn(v16.localAssetEntity, v16.localAssetEntity.iCloudId); + await m.createIndex(v16.idxLocalAssetCloudId); + await m.createTable(v16.remoteAssetCloudIdEntity); + }, + from16To17: (m, v17) async { + await m.addColumn(v17.remoteAssetEntity, v17.remoteAssetEntity.isEdited); + }, + from17To18: (m, v18) async { + await m.createIndex(v18.idxRemoteAssetCloudId); + }, + from18To19: (m, v19) async { + await m.createIndex(v19.idxAssetFacePersonId); + await m.createIndex(v19.idxAssetFaceAssetId); + await m.createIndex(v19.idxLocalAlbumAssetAlbumAsset); + await m.createIndex(v19.idxPartnerSharedWithId); + await m.createIndex(v19.idxPersonOwnerId); + await m.createIndex(v19.idxRemoteAlbumOwnerId); + await m.createIndex(v19.idxRemoteAlbumAssetAlbumAsset); + await m.createIndex(v19.idxRemoteAssetStackId); + await m.createIndex(v19.idxRemoteAssetLocalDateTimeDay); + await m.createIndex(v19.idxRemoteAssetLocalDateTimeMonth); + await m.createIndex(v19.idxStackPrimaryAssetId); + }, + from19To20: (m, v20) async { + await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible); + await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt); + }, + from20To21: (m, v21) async { + await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle); + await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle); + }, ), ); @@ -205,7 +249,9 @@ class Drift extends $Drift implements IDatabaseRepository { await customStatement('PRAGMA foreign_keys = ON'); await customStatement('PRAGMA synchronous = NORMAL'); await customStatement('PRAGMA journal_mode = WAL'); - await customStatement('PRAGMA busy_timeout = 30000'); + await customStatement('PRAGMA busy_timeout = 30000'); // 30s + await customStatement('PRAGMA cache_size = -32000'); // 32MB + await customStatement('PRAGMA temp_store = MEMORY'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index bd72da949c..ae805ad25e 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -27,21 +27,23 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. as i12; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' as i13; -import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart' as i14; -import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' as i15; -import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' as i16; -import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' as i17; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' as i18; -import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart' +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' as i19; -import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart' as i20; -import 'package:drift/internal/modular.dart' as i21; +import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' + as i21; +import 'package:drift/internal/modular.dart' as i22; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -72,18 +74,20 @@ abstract class $Drift extends i0.GeneratedDatabase { .$RemoteAlbumAssetEntityTable(this); late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13 .$RemoteAlbumUserEntityTable(this); - late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this); - late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15 + late final i14.$RemoteAssetCloudIdEntityTable remoteAssetCloudIdEntity = i14 + .$RemoteAssetCloudIdEntityTable(this); + late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this); + late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16 .$MemoryAssetEntityTable(this); - late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this); - late final i17.$AssetFaceEntityTable assetFaceEntity = i17 + late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this); + late final i18.$AssetFaceEntityTable assetFaceEntity = i18 .$AssetFaceEntityTable(this); - late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this); - late final i19.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i19 + late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this); + late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20 .$TrashedLocalAssetEntityTable(this); - i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer( + i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer( this, - ).accessor(i20.MergedAssetDrift.new); + ).accessor(i21.MergedAssetDrift.new); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -96,26 +100,40 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteAlbumEntity, localAlbumEntity, localAlbumAssetEntity, + i7.idxLocalAlbumAssetAlbumAsset, + i5.idxRemoteAlbumOwnerId, i4.idxLocalAssetChecksum, + i4.idxLocalAssetCloudId, + i3.idxStackPrimaryAssetId, i2.idxRemoteAssetOwnerChecksum, i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerLibraryChecksum, i2.idxRemoteAssetChecksum, + i2.idxRemoteAssetStackId, + i2.idxRemoteAssetLocalDateTimeDay, + i2.idxRemoteAssetLocalDateTimeMonth, authUserEntity, userMetadataEntity, partnerEntity, remoteExifEntity, remoteAlbumAssetEntity, remoteAlbumUserEntity, + remoteAssetCloudIdEntity, memoryEntity, memoryAssetEntity, personEntity, assetFaceEntity, storeEntity, trashedLocalAssetEntity, + i10.idxPartnerSharedWithId, i11.idxLatLng, - i19.idxTrashedLocalAssetChecksum, - i19.idxTrashedLocalAssetAlbum, + i12.idxRemoteAlbumAssetAlbumAsset, + i14.idxRemoteAssetCloudId, + i17.idxPersonOwnerId, + i18.idxAssetFacePersonId, + i18.idxAssetFaceAssetId, + i20.idxTrashedLocalAssetChecksum, + i20.idxTrashedLocalAssetAlbum, ]; @override i0.StreamQueryUpdateRules @@ -249,6 +267,18 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('remote_album_user_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName( + 'remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete, + ), + result: [ + i0.TableUpdate( + 'remote_asset_cloud_id_entity', + kind: i0.UpdateKind.delete, + ), + ], + ), i0.WritePropagation( on: i0.TableUpdateQuery.onTableName( 'user_entity', @@ -333,18 +363,24 @@ class $DriftManager { ); i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13 .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); - i14.$$MemoryEntityTableTableManager get memoryEntity => - i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); - i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => - i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); - i16.$$PersonEntityTableTableManager get personEntity => - i16.$$PersonEntityTableTableManager(_db, _db.personEntity); - i17.$$AssetFaceEntityTableTableManager get assetFaceEntity => - i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); - i18.$$StoreEntityTableTableManager get storeEntity => - i18.$$StoreEntityTableTableManager(_db, _db.storeEntity); - i19.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity => - i19.$$TrashedLocalAssetEntityTableTableManager( + i14.$$RemoteAssetCloudIdEntityTableTableManager + get remoteAssetCloudIdEntity => + i14.$$RemoteAssetCloudIdEntityTableTableManager( + _db, + _db.remoteAssetCloudIdEntity, + ); + i15.$$MemoryEntityTableTableManager get memoryEntity => + i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); + i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => + i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); + i17.$$PersonEntityTableTableManager get personEntity => + i17.$$PersonEntityTableTableManager(_db, _db.personEntity); + i18.$$AssetFaceEntityTableTableManager get assetFaceEntity => + i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); + i19.$$StoreEntityTableTableManager get storeEntity => + i19.$$StoreEntityTableTableManager(_db, _db.storeEntity); + i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity => + i20.$$TrashedLocalAssetEntityTableTableManager( _db, _db.trashedLocalAssetEntity, ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart index 21a3db5274..f83fd29cdc 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -5941,6 +5941,3554 @@ i1.GeneratedColumn _column_96(String aliasedName) => true, type: i1.DriftSqlType.dateTime, ); + +final class Schema15 extends i0.VersionedSchema { + Schema15({required super.database}) : super(version: 15); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape24 localAssetEntity = Shape24( + 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, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape25 extends i0.VersionedTable { + Shape25({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 albumId => + columnsByName['album_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 get source => + columnsByName['source']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_97(String aliasedName) => + i1.GeneratedColumn( + 'source', + aliasedName, + false, + type: i1.DriftSqlType.int, + ); + +final class Schema16 extends i0.VersionedSchema { + Schema16({required super.database}) : super(version: 16); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape17 remoteAssetEntity = Shape17( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape3 stackEntity = Shape3( + source: i0.VersionedTable( + entityName: 'stack_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_0, _column_9, _column_5, _column_15, _column_75], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape26 localAssetEntity = Shape26( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape26 extends i0.VersionedTable { + Shape26({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 get iCloudId => + columnsByName['i_cloud_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get adjustmentTime => + columnsByName['adjustment_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get latitude => + columnsByName['latitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get longitude => + columnsByName['longitude']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_98(String aliasedName) => + i1.GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + ); + +class Shape27 extends i0.VersionedTable { + Shape27({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get assetId => + columnsByName['asset_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get cloudId => + columnsByName['cloud_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get createdAt => + columnsByName['created_at']! as i1.GeneratedColumn; + i1.GeneratedColumn get adjustmentTime => + columnsByName['adjustment_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get latitude => + columnsByName['latitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get longitude => + columnsByName['longitude']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_99(String aliasedName) => + i1.GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: i1.DriftSqlType.string, + ); +i1.GeneratedColumn _column_100(String aliasedName) => + i1.GeneratedColumn( + 'created_at', + aliasedName, + true, + type: i1.DriftSqlType.dateTime, + ); + +final class Schema17 extends i0.VersionedSchema { + Schema17({required super.database}) : super(version: 17); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + 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 Shape26 localAssetEntity = Shape26( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape28 extends i0.VersionedTable { + Shape28({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 get libraryId => + columnsByName['library_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get isEdited => + columnsByName['is_edited']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_101(String aliasedName) => + i1.GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + +final class Schema18 extends i0.VersionedSchema { + Schema18({required super.database}) : super(version: 18); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxRemoteAssetCloudId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + 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 Shape26 localAssetEntity = Shape26( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +final class Schema19 extends i0.VersionedSchema { + Schema19({required super.database}) : super(version: 19); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + 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 Shape26 localAssetEntity = Shape26( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAlbumOwnerId = i1.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape11 memoryEntity = Shape11( + source: i0.VersionedTable( + entityName: 'memory_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_18, + _column_15, + _column_8, + _column_62, + _column_63, + _column_64, + _column_65, + _column_66, + _column_67, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape12 memoryAssetEntity = Shape12( + source: i0.VersionedTable( + entityName: 'memory_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'], + columns: [_column_36, _column_68], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape14 personEntity = Shape14( + source: i0.VersionedTable( + entityName: 'person_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_9, + _column_5, + _column_15, + _column_1, + _column_69, + _column_71, + _column_72, + _column_73, + _column_74, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape15 assetFaceEntity = Shape15( + source: i0.VersionedTable( + entityName: 'asset_face_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_36, + _column_76, + _column_77, + _column_78, + _column_79, + _column_80, + _column_81, + _column_82, + _column_83, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +final class Schema20 extends i0.VersionedSchema { + Schema20({required super.database}) : super(version: 20); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + 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 Shape26 localAssetEntity = Shape26( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAlbumOwnerId = i1.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + 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 Shape29 assetFaceEntity = Shape29( + 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, + _column_102, + _column_18, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape25 trashedLocalAssetEntity = Shape25( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape29 extends i0.VersionedTable { + Shape29({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 get isVisible => + columnsByName['is_visible']! as i1.GeneratedColumn; + i1.GeneratedColumn get deletedAt => + columnsByName['deleted_at']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_102(String aliasedName) => + i1.GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + +final class Schema21 extends i0.VersionedSchema { + Schema21({required super.database}) : super(version: 21); + @override + late final List entities = [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + late final Shape20 userEntity = Shape20( + source: i0.VersionedTable( + entityName: 'user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_84, + _column_85, + _column_91, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape28 remoteAssetEntity = Shape28( + source: i0.VersionedTable( + entityName: 'remote_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_86, + _column_101, + ], + 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 Shape30 localAssetEntity = Shape30( + 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, + _column_98, + _column_96, + _column_46, + _column_47, + _column_103, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape9 remoteAlbumEntity = Shape9( + source: i0.VersionedTable( + entityName: 'remote_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_56, + _column_9, + _column_5, + _column_15, + _column_57, + _column_58, + _column_59, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape19 localAlbumEntity = Shape19( + source: i0.VersionedTable( + entityName: 'local_album_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_5, + _column_31, + _column_32, + _column_90, + _column_33, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape22 localAlbumAssetEntity = Shape22( + source: i0.VersionedTable( + entityName: 'local_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_34, _column_35, _column_33], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAlbumOwnerId = i1.Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + final i1.Index idxLocalAssetChecksum = i1.Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + final i1.Index idxLocalAssetCloudId = i1.Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + final i1.Index idxStackPrimaryAssetId = i1.Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + final i1.Index idxRemoteAssetOwnerChecksum = i1.Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + final i1.Index idxRemoteAssetChecksum = i1.Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + final i1.Index idxRemoteAssetStackId = i1.Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final Shape21 authUserEntity = Shape21( + source: i0.VersionedTable( + entityName: 'auth_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [ + _column_0, + _column_1, + _column_3, + _column_2, + _column_84, + _column_85, + _column_92, + _column_93, + _column_7, + _column_94, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape4 userMetadataEntity = Shape4( + source: i0.VersionedTable( + entityName: 'user_metadata_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(user_id, "key")'], + columns: [_column_25, _column_26, _column_27], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape5 partnerEntity = Shape5( + source: i0.VersionedTable( + entityName: 'partner_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'], + columns: [_column_28, _column_29, _column_30], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape8 remoteExifEntity = Shape8( + source: i0.VersionedTable( + entityName: 'remote_exif_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_37, + _column_38, + _column_39, + _column_40, + _column_41, + _column_11, + _column_10, + _column_42, + _column_43, + _column_44, + _column_45, + _column_46, + _column_47, + _column_48, + _column_49, + _column_50, + _column_51, + _column_52, + _column_53, + _column_54, + _column_55, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape7 remoteAlbumAssetEntity = Shape7( + source: i0.VersionedTable( + entityName: 'remote_album_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id, album_id)'], + columns: [_column_36, _column_60], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape10 remoteAlbumUserEntity = Shape10( + source: i0.VersionedTable( + entityName: 'remote_album_user_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(album_id, user_id)'], + columns: [_column_60, _column_25, _column_61], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape27 remoteAssetCloudIdEntity = Shape27( + source: i0.VersionedTable( + entityName: 'remote_asset_cloud_id_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(asset_id)'], + columns: [ + _column_36, + _column_99, + _column_100, + _column_96, + _column_46, + _column_47, + ], + 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 Shape29 assetFaceEntity = Shape29( + 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, + _column_102, + _column_18, + ], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape18 storeEntity = Shape18( + source: i0.VersionedTable( + entityName: 'store_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id)'], + columns: [_column_87, _column_88, _column_89], + attachedDatabase: database, + ), + alias: null, + ); + late final Shape31 trashedLocalAssetEntity = Shape31( + source: i0.VersionedTable( + entityName: 'trashed_local_asset_entity', + withoutRowId: true, + isStrict: true, + tableConstraints: ['PRIMARY KEY(id, album_id)'], + columns: [ + _column_1, + _column_8, + _column_9, + _column_5, + _column_10, + _column_11, + _column_12, + _column_0, + _column_95, + _column_22, + _column_14, + _column_23, + _column_97, + _column_103, + ], + attachedDatabase: database, + ), + alias: null, + ); + final i1.Index idxPartnerSharedWithId = i1.Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + final i1.Index idxLatLng = i1.Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + final i1.Index idxRemoteAssetCloudId = i1.Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + final i1.Index idxPersonOwnerId = i1.Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + final i1.Index idxAssetFacePersonId = i1.Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + final i1.Index idxAssetFaceAssetId = i1.Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + final i1.Index idxTrashedLocalAssetChecksum = i1.Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + final i1.Index idxTrashedLocalAssetAlbum = i1.Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); +} + +class Shape30 extends i0.VersionedTable { + Shape30({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 get iCloudId => + columnsByName['i_cloud_id']! as i1.GeneratedColumn; + i1.GeneratedColumn get adjustmentTime => + columnsByName['adjustment_time']! as i1.GeneratedColumn; + i1.GeneratedColumn get latitude => + columnsByName['latitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get longitude => + columnsByName['longitude']! as i1.GeneratedColumn; + i1.GeneratedColumn get playbackStyle => + columnsByName['playback_style']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_103(String aliasedName) => + i1.GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: i1.DriftSqlType.int, + defaultValue: const CustomExpression('0'), + ); + +class Shape31 extends i0.VersionedTable { + Shape31({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 albumId => + columnsByName['album_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 get source => + columnsByName['source']! as i1.GeneratedColumn; + i1.GeneratedColumn get playbackStyle => + columnsByName['playback_style']! as i1.GeneratedColumn; +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -5955,6 +9503,13 @@ i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, required Future Function(i1.Migrator m, Schema14 schema) from13To14, + required Future Function(i1.Migrator m, Schema15 schema) from14To15, + required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, + required Future Function(i1.Migrator m, Schema18 schema) from17To18, + required Future Function(i1.Migrator m, Schema19 schema) from18To19, + required Future Function(i1.Migrator m, Schema20 schema) from19To20, + required Future Function(i1.Migrator m, Schema21 schema) from20To21, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -6023,6 +9578,41 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from13To14(migrator, schema); return 14; + case 14: + final schema = Schema15(database: database); + final migrator = i1.Migrator(database, schema); + await from14To15(migrator, schema); + return 15; + case 15: + final schema = Schema16(database: database); + final migrator = i1.Migrator(database, schema); + await from15To16(migrator, schema); + return 16; + case 16: + final schema = Schema17(database: database); + final migrator = i1.Migrator(database, schema); + await from16To17(migrator, schema); + return 17; + case 17: + final schema = Schema18(database: database); + final migrator = i1.Migrator(database, schema); + await from17To18(migrator, schema); + return 18; + case 18: + final schema = Schema19(database: database); + final migrator = i1.Migrator(database, schema); + await from18To19(migrator, schema); + return 19; + case 19: + final schema = Schema20(database: database); + final migrator = i1.Migrator(database, schema); + await from19To20(migrator, schema); + return 20; + case 20: + final schema = Schema21(database: database); + final migrator = i1.Migrator(database, schema); + await from20To21(migrator, schema); + return 21; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -6043,6 +9633,13 @@ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema12 schema) from11To12, required Future Function(i1.Migrator m, Schema13 schema) from12To13, required Future Function(i1.Migrator m, Schema14 schema) from13To14, + required Future Function(i1.Migrator m, Schema15 schema) from14To15, + required Future Function(i1.Migrator m, Schema16 schema) from15To16, + required Future Function(i1.Migrator m, Schema17 schema) from16To17, + required Future Function(i1.Migrator m, Schema18 schema) from17To18, + required Future Function(i1.Migrator m, Schema19 schema) from18To19, + required Future Function(i1.Migrator m, Schema20 schema) from19To20, + required Future Function(i1.Migrator m, Schema21 schema) from20To21, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -6058,5 +9655,12 @@ i1.OnUpgrade stepByStep({ from11To12: from11To12, from12To13: from12To13, from13To14: from13To14, + from14To15: from14To15, + from15To16: from15To16, + from16To17: from16To17, + from17To18: from17To18, + from18To19: from18To19, + from19To20: from19To20, + from20To21: from20To21, ), ); diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 9d4c9bc496..87a72f02be 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -246,6 +246,25 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); } + Future updateCloudMapping(Map cloudMapping) { + if (cloudMapping.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) { + for (final entry in cloudMapping.entries) { + final assetId = entry.key; + final cloudId = entry.value; + + batch.update( + _db.localAssetEntity, + LocalAssetEntityCompanion(iCloudId: Value(cloudId)), + where: (f) => f.id.equals(assetId), + ); + } + }); + } + Future Function(Iterable) get _upsertAssets => CurrentPlatform.isIOS ? _upsertAssetsDarwin : _upsertAssetsAndroid; @@ -282,6 +301,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { id: asset.id, orientation: Value(asset.orientation), isFavorite: Value(asset.isFavorite), + playbackStyle: Value(asset.playbackStyle), latitude: Value(asset.latitude), longitude: Value(asset.longitude), adjustmentTime: Value(asset.adjustmentTime), @@ -314,6 +334,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { checksum: const Value(null), orientation: Value(asset.orientation), isFavorite: Value(asset.isFavorite), + playbackStyle: Value(asset.playbackStyle), ); batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>( _db.localAssetEntity, diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 4d30e09716..6f6ef20aeb 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; @@ -8,6 +11,13 @@ 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 RemovalCandidatesResult { + final List assets; + final int totalBytes; + + const RemovalCandidatesResult({required this.assets, required this.totalBytes}); +} + class DriftLocalAssetRepository extends DriftDatabaseRepository { final Drift _db; @@ -126,4 +136,92 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { } return result; } + + Future getRemovalCandidates( + String userId, + DateTime cutoffDate, { + AssetKeepType keepMediaType = AssetKeepType.none, + bool keepFavorites = true, + Set keepAlbumIds = const {}, + }) async { + final iosSharedAlbumAssets = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]) + ..where(_db.localAlbumEntity.isIosSharedAlbum.equals(true)); + + final query = _db.localAssetEntity.select().join([ + innerJoin(_db.remoteAssetEntity, _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum)), + leftOuterJoin(_db.remoteExifEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteExifEntity.assetId)), + ]); + + Expression whereClause = + _db.localAssetEntity.createdAt.isSmallerOrEqualValue(cutoffDate) & + _db.remoteAssetEntity.ownerId.equals(userId) & + _db.remoteAssetEntity.deletedAt.isNull(); + + // Exclude assets that are in iOS shared albums + whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(iosSharedAlbumAssets); + + if (keepAlbumIds.isNotEmpty) { + final keepAlbumAssets = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where(_db.localAlbumAssetEntity.albumId.isIn(keepAlbumIds)); + whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(keepAlbumAssets); + } + + if (keepMediaType == AssetKeepType.photosOnly) { + // Keep photos = delete only videos + whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.video); + } else if (keepMediaType == AssetKeepType.videosOnly) { + // Keep videos = delete only photos + whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.image); + } + + if (keepFavorites) { + whereClause = + whereClause & _db.localAssetEntity.isFavorite.equals(false) & _db.remoteAssetEntity.isFavorite.equals(false); + } + + query.where(whereClause); + + final rows = await query.get(); + final assets = rows.map((row) => row.readTable(_db.localAssetEntity).toDto()).toList(); + final totalBytes = rows.fold(0, (sum, row) { + final fileSize = row.readTableOrNull(_db.remoteExifEntity)?.fileSize; + return sum + (fileSize ?? 0); + }); + + return RemovalCandidatesResult(assets: assets, totalBytes: totalBytes); + } + + Future> getEmptyCloudIdAssets() { + final query = _db.localAssetEntity.select()..where((row) => row.iCloudId.isNull()); + return query.map((row) => row.toDto()).get(); + } + + Future reconcileHashesFromCloudId() async { + await _db.customUpdate( + ''' + UPDATE local_asset_entity + SET checksum = remote_asset_entity.checksum + FROM remote_asset_cloud_id_entity + INNER JOIN remote_asset_entity + ON remote_asset_cloud_id_entity.asset_id = remote_asset_entity.id + WHERE local_asset_entity.i_cloud_id = remote_asset_cloud_id_entity.cloud_id + AND local_asset_entity.checksum IS NULL + AND remote_asset_cloud_id_entity.adjustment_time IS local_asset_entity.adjustment_time + AND remote_asset_cloud_id_entity.latitude IS local_asset_entity.latitude + AND remote_asset_cloud_id_entity.longitude IS local_asset_entity.longitude + AND remote_asset_cloud_id_entity.created_at IS local_asset_entity.created_at + ''', + updates: {_db.localAssetEntity}, + updateKind: UpdateKind.update, + ); + } } diff --git a/mobile/lib/infrastructure/repositories/logger_db.repository.dart b/mobile/lib/infrastructure/repositories/logger_db.repository.dart index 583fc42813..0037f4a1e3 100644 --- a/mobile/lib/infrastructure/repositories/logger_db.repository.dart +++ b/mobile/lib/infrastructure/repositories/logger_db.repository.dart @@ -22,6 +22,7 @@ class DriftLogger extends $DriftLogger implements IDatabaseRepository { await customStatement('PRAGMA synchronous = NORMAL'); await customStatement('PRAGMA journal_mode = WAL'); await customStatement('PRAGMA busy_timeout = 500'); + await customStatement('PRAGMA temp_store = MEMORY'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/map.repository.dart b/mobile/lib/infrastructure/repositories/map.repository.dart index 9b8cdcc19d..95e42337fc 100644 --- a/mobile/lib/infrastructure/repositories/map.repository.dart +++ b/mobile/lib/infrastructure/repositories/map.repository.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/services/map.service.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class DriftMapRepository extends DriftDatabaseRepository { @@ -12,9 +13,27 @@ class DriftMapRepository extends DriftDatabaseRepository { const DriftMapRepository(super._db) : _db = _db; - MapQuery remote(String ownerId) => _mapQueryBuilder( - assetFilter: (row) => - row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), + MapQuery remote(List ownerIds, TimelineMapOptions options) => _mapQueryBuilder( + assetFilter: (row) { + Expression condition = + row.deletedAt.isNull() & + row.ownerId.isIn(ownerIds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]); + + if (options.onlyFavorites) { + condition = condition & _db.remoteAssetEntity.isFavorite.equals(true); + } + + if (options.relativeDays != 0) { + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + condition = condition & _db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate); + } + + return condition; + }, ); MapQuery _mapQueryBuilder({Expression Function($RemoteAssetEntityTable row)? assetFilter}) { diff --git a/mobile/lib/infrastructure/repositories/network.repository.dart b/mobile/lib/infrastructure/repositories/network.repository.dart new file mode 100644 index 0000000000..a73322cb5c --- /dev/null +++ b/mobile/lib/infrastructure/repositories/network.repository.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:cronet_http/cronet_http.dart'; +import 'package:cupertino_http/cupertino_http.dart'; +import 'package:http/http.dart' as http; +import 'package:immich_mobile/utils/user_agent.dart'; +import 'package:path_provider/path_provider.dart'; + +class NetworkRepository { + static late Directory _cachePath; + static late String _userAgent; + static final _clients = {}; + + static Future init() { + return ( + getTemporaryDirectory().then((cachePath) => _cachePath = cachePath), + getUserAgentString().then((userAgent) => _userAgent = userAgent), + ).wait; + } + + static void reset() { + Future.microtask(init); + for (final client in _clients.values) { + client.close(); + } + _clients.clear(); + } + + const NetworkRepository(); + + /// Note: when disk caching is enabled, only one client may use a given directory at a time. + /// Different isolates or engines must use different directories. + http.Client getHttpClient( + String directoryName, { + CacheMode cacheMode = CacheMode.memory, + int diskCapacity = 0, + int maxConnections = 6, + int memoryCapacity = 10 << 20, + }) { + final cachedClient = _clients[directoryName]; + if (cachedClient != null) { + return cachedClient; + } + + final directory = Directory('${_cachePath.path}/$directoryName'); + directory.createSync(recursive: true); + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: cacheMode, + cacheMaxSize: diskCapacity, + storagePath: directory.path, + userAgent: _userAgent, + ); + return _clients[directoryName] = CronetClient.fromCronetEngine(engine, closeEngine: true); + } + + final config = URLSessionConfiguration.defaultSessionConfiguration() + ..httpMaximumConnectionsPerHost = maxConnections + ..cache = URLCache.withCapacity( + diskCapacity: diskCapacity, + memoryCapacity: memoryCapacity, + directory: directory.uri, + ) + ..httpAdditionalHeaders = {'User-Agent': _userAgent}; + return _clients[directoryName] = CupertinoClient.fromSessionConfiguration(config); + } +} diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index e2b8646dba..9e55d44867 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -7,10 +8,23 @@ class DriftPeopleRepository extends DriftDatabaseRepository { final Drift _db; const DriftPeopleRepository(this._db) : super(_db); + Future get(String personId) async { + final query = _db.select(_db.personEntity)..where((row) => row.id.equals(personId)); + + final result = await query.getSingleOrNull(); + return result?.toDto(); + } + Future> getAssetPeople(String assetId) async { - final query = _db.select(_db.assetFaceEntity).join([ - innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), - ])..where(_db.assetFaceEntity.assetId.equals(assetId) & _db.personEntity.isHidden.equals(false)); + final query = + _db.select(_db.assetFaceEntity).join([ + innerJoin(_db.personEntity, _db.personEntity.id.equalsExp(_db.assetFaceEntity.personId)), + ])..where( + _db.assetFaceEntity.assetId.equals(assetId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull() & + _db.personEntity.isHidden.equals(false), + ); return query.map((row) { final person = row.readTable(_db.personEntity); @@ -19,19 +33,30 @@ class DriftPeopleRepository extends DriftDatabaseRepository { } Future> getAllPeople() async { + final people = _db.personEntity; + final faces = _db.assetFaceEntity; + final assets = _db.remoteAssetEntity; + final query = - _db.select(_db.personEntity).join([ - leftOuterJoin(_db.assetFaceEntity, _db.assetFaceEntity.personId.equalsExp(_db.personEntity.id)), + _db.select(people).join([ + innerJoin(faces, faces.personId.equalsExp(people.id)), + innerJoin(assets, assets.id.equalsExp(faces.assetId)), ]) - ..where(_db.personEntity.isHidden.equals(false)) - ..groupBy([_db.personEntity.id], having: _db.assetFaceEntity.id.count().isBiggerOrEqualValue(3)) + ..where( + people.isHidden.equals(false) & + assets.deletedAt.isNull() & + assets.visibility.equalsValue(AssetVisibility.timeline) & + faces.isVisible.equals(true) & + faces.deletedAt.isNull(), + ) + ..groupBy([people.id], having: faces.id.count().isBiggerOrEqualValue(3) | people.name.equals('').not()) ..orderBy([ - OrderingTerm(expression: _db.personEntity.name.equals('').not(), mode: OrderingMode.desc), - OrderingTerm(expression: _db.assetFaceEntity.id.count(), mode: OrderingMode.desc), + OrderingTerm(expression: people.name.equals('').not(), mode: OrderingMode.desc), + OrderingTerm(expression: faces.id.count(), mode: OrderingMode.desc), ]); return query.map((row) { - final person = row.readTable(_db.personEntity); + final person = row.readTable(people); return person.toDto(); }).get(); } diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index d7d4a250ad..a594647f19 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:convert'; import 'package:drift/drift.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; @@ -321,26 +323,32 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { }).watchSingleOrNull(); } - Future getNewestAssetTimestamp(String albumId) { - final query = _db.remoteAlbumAssetEntity.selectOnly() - ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) - ..addColumns([_db.remoteAssetEntity.localDateTime.max()]) - ..join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), - ]); + Future> getSortedAlbumIds(List albumIds, {required AssetDateAggregation aggregation}) async { + if (albumIds.isEmpty) return []; - return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull(); - } + final jsonIds = jsonEncode(albumIds); + final sqlAgg = aggregation == AssetDateAggregation.start ? 'MIN' : 'MAX'; - Future getOldestAssetTimestamp(String albumId) { - final query = _db.remoteAlbumAssetEntity.selectOnly() - ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) - ..addColumns([_db.remoteAssetEntity.localDateTime.min()]) - ..join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), - ]); + final rows = await _db + .customSelect( + ''' + SELECT + raae.album_id, + $sqlAgg(rae.local_date_time) AS asset_date + FROM json_each(?) ids + INNER JOIN remote_album_asset_entity raae + ON raae.album_id = ids.value + INNER JOIN remote_asset_entity rae + ON rae.id = raae.asset_id + GROUP BY raae.album_id + ORDER BY asset_date ASC + ''', + variables: [Variable(jsonIds)], + readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity}, + ) + .get(); - return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull(); + return rows.map((row) => row.read('album_id')).toList(); } Future getCount() { diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index 96c204ea0e..df4172df99 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -255,6 +255,12 @@ class RemoteAssetRepository extends DriftDatabaseRepository { ); } + Future updateRating(String assetId, int rating) async { + await (_db.remoteExifEntity.update()..where((row) => row.assetId.equals(assetId))).write( + RemoteExifEntityCompanion(rating: Value(rating)), + ); + } + 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 index 34870dc1b3..bcfddfce6e 100644 --- a/mobile/lib/infrastructure/repositories/search_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -31,9 +31,11 @@ class SearchApiRepository extends ApiRepository { takenAfter: filter.date.takenAfter, takenBefore: filter.date.takenBefore, visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline, + rating: filter.rating.rating, isFavorite: filter.display.isFavorite ? true : null, isNotInAlbum: filter.display.isNotInAlbum ? true : null, personIds: filter.people.map((e) => e.id).toList(), + tagIds: filter.tagIds, type: type, page: page, size: 100, @@ -54,9 +56,11 @@ class SearchApiRepository extends ApiRepository { takenAfter: filter.date.takenAfter, takenBefore: filter.date.takenBefore, visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline, + rating: filter.rating.rating, isFavorite: filter.display.isFavorite ? true : null, isNotInAlbum: filter.display.isNotInAlbum ? true : null, personIds: filter.people.map((e) => e.id).toList(), + tagIds: filter.tagIds, type: type, page: page, size: 1000, diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 9532025d58..eaa6ce79f7 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -6,7 +6,9 @@ import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; class StorageRepository { - const StorageRepository(); + final log = Logger('StorageRepository'); + + StorageRepository(); Future getFileForAsset(String assetId) async { File? file; @@ -82,6 +84,51 @@ class StorageRepository { return entity; } + Future isAssetAvailableLocally(String assetId) async { + try { + final entity = await AssetEntity.fromId(assetId); + if (entity == null) { + log.warning("Cannot get AssetEntity for asset $assetId"); + return false; + } + + return await entity.isLocallyAvailable(isOrigin: true); + } catch (error, stackTrace) { + log.warning("Error checking if asset is locally available $assetId", error, stackTrace); + return false; + } + } + + Future loadFileFromCloud(String assetId, {PMProgressHandler? progressHandler}) async { + try { + final entity = await AssetEntity.fromId(assetId); + if (entity == null) { + log.warning("Cannot get AssetEntity for asset $assetId"); + return null; + } + + return await entity.loadFile(progressHandler: progressHandler); + } catch (error, stackTrace) { + log.warning("Error loading file from cloud for asset $assetId", error, stackTrace); + return null; + } + } + + Future loadMotionFileFromCloud(String assetId, {PMProgressHandler? progressHandler}) async { + try { + final entity = await AssetEntity.fromId(assetId); + if (entity == null) { + log.warning("Cannot get AssetEntity for asset $assetId"); + return null; + } + + return await entity.loadFile(withSubtype: true, progressHandler: progressHandler); + } catch (error, stackTrace) { + log.warning("Error loading motion file from cloud for asset $assetId", error, stackTrace); + return null; + } + } + Future clearCache() async { final log = Logger('StorageRepository'); diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 8bf2e80579..0e5c99edd7 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -19,8 +20,13 @@ class SyncApiRepository { return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data)); } + Future deleteSyncAck(List types) { + return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types)); + } + Future streamChanges( Future Function(List, Function() abort, Function() reset) onData, { + required SemVer serverVersion, Function()? onReset, int batchSize = kSyncEventBatchSize, http.Client? httpClient, @@ -45,6 +51,7 @@ class SyncApiRepository { SyncRequestType.usersV1, SyncRequestType.assetsV1, SyncRequestType.assetExifsV1, + SyncRequestType.assetMetadataV1, SyncRequestType.partnersV1, SyncRequestType.partnerAssetsV1, SyncRequestType.partnerAssetExifsV1, @@ -59,7 +66,8 @@ class SyncApiRepository { SyncRequestType.partnerStacksV1, SyncRequestType.userMetadataV1, SyncRequestType.peopleV1, - SyncRequestType.assetFacesV1, + if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1, + if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2, ], reset: shouldReset, ).toJson(), @@ -148,6 +156,8 @@ const _kResponseMap = { SyncEntityType.assetV1: SyncAssetV1.fromJson, SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson, + SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson, SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, @@ -183,6 +193,7 @@ const _kResponseMap = { SyncEntityType.personV1: SyncPersonV1.fromJson, SyncEntityType.personDeleteV1: SyncPersonDeleteV1.fromJson, SyncEntityType.assetFaceV1: SyncAssetFaceV1.fromJson, + SyncEntityType.assetFaceV2: SyncAssetFaceV2.fromJson, SyncEntityType.assetFaceDeleteV1: SyncAssetFaceDeleteV1.fromJson, SyncEntityType.syncCompleteV1: _SyncEmptyDto.fromJson, }; diff --git a/mobile/lib/infrastructure/repositories/sync_migration.repository.dart b/mobile/lib/infrastructure/repositories/sync_migration.repository.dart new file mode 100644 index 0000000000..814c8780ad --- /dev/null +++ b/mobile/lib/infrastructure/repositories/sync_migration.repository.dart @@ -0,0 +1,24 @@ +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class SyncMigrationRepository extends DriftDatabaseRepository { + final Drift _db; + + const SyncMigrationRepository(super.db) : _db = db; + + Future v20260128CopyExifWidthHeightToAsset() async { + await _db.customStatement(''' + UPDATE remote_asset_entity + SET width = CASE + WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.height + ELSE exif.width + END, + height = CASE + WHEN exif.orientation IN ('5', '6', '7', '8', '-90', '90') THEN exif.width + ELSE exif.height + END + FROM remote_exif_entity exif + WHERE exif.asset_id = remote_asset_entity.id + AND (exif.width IS NOT NULL OR exif.height IS NOT NULL); + '''); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 5ab1844571..8ff1c2d59c 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; +import 'package:immich_mobile/constants/constants.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'; @@ -18,10 +19,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift. 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/remote_asset_cloud_id.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:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey; import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey; @@ -54,6 +57,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { await _db.authUserEntity.deleteAll(); await _db.userEntity.deleteAll(); await _db.userMetadataEntity.deleteAll(); + await _db.remoteAssetCloudIdEntity.deleteAll(); }); await _db.customStatement('PRAGMA foreign_keys = ON'); }); @@ -194,6 +198,9 @@ class SyncStreamRepository extends DriftDatabaseRepository { livePhotoVideoId: Value(asset.livePhotoVideoId), stackId: Value(asset.stackId), libraryId: Value(asset.libraryId), + width: Value(asset.width), + height: Value(asset.height), + isEdited: Value(asset.isEdited), ); batch.insert( @@ -233,6 +240,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { rating: Value(exif.rating), projectionType: Value(exif.projectionType), lens: Value(exif.lensModel), + width: Value(exif.exifImageWidth), + height: Value(exif.exifImageHeight), ); batch.insert( @@ -245,10 +254,21 @@ class SyncStreamRepository extends DriftDatabaseRepository { await _db.batch((batch) { for (final exif in data) { + int? width; + int? height; + + if (ExifDtoConverter.isOrientationFlipped(exif.orientation)) { + width = exif.exifImageHeight; + height = exif.exifImageWidth; + } else { + width = exif.exifImageWidth; + height = exif.exifImageHeight; + } + batch.update( _db.remoteAssetEntity, - RemoteAssetEntityCompanion(width: Value(exif.exifImageWidth), height: Value(exif.exifImageHeight)), - where: (row) => row.id.equals(exif.assetId), + RemoteAssetEntityCompanion(width: Value(width), height: Value(height)), + where: (row) => row.id.equals(exif.assetId) & row.width.isNull() & row.height.isNull(), ); } }); @@ -258,6 +278,50 @@ class SyncStreamRepository extends DriftDatabaseRepository { } } + Future deleteAssetsMetadataV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final metadata in data) { + if (metadata.key == kMobileMetadataKey) { + batch.deleteWhere(_db.remoteAssetCloudIdEntity, (row) => row.assetId.equals(metadata.assetId)); + } + } + }); + } catch (error, stack) { + _logger.severe('Error: deleteAssetsMetadataV1', error, stack); + rethrow; + } + } + + Future updateAssetsMetadataV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final metadata in data) { + if (metadata.key == kMobileMetadataKey) { + final map = metadata.value as Map; + final companion = RemoteAssetCloudIdEntityCompanion( + cloudId: Value(map['iCloudId']?.toString()), + createdAt: Value(map['createdAt'] != null ? DateTime.parse(map['createdAt'] as String) : null), + adjustmentTime: Value( + map['adjustmentTime'] != null ? DateTime.parse(map['adjustmentTime'] as String) : null, + ), + latitude: Value(map['latitude'] != null ? (double.tryParse(map['latitude'] as String)) : null), + longitude: Value(map['longitude'] != null ? (double.tryParse(map['longitude'] as String)) : null), + ); + batch.insert( + _db.remoteAssetCloudIdEntity, + companion.copyWith(assetId: Value(metadata.assetId)), + onConflict: DoUpdate((_) => companion), + ); + } + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetsMetadataV1', error, stack); + rethrow; + } + } + Future deleteAlbumsV1(Iterable data) async { try { await _db.batch((batch) { @@ -588,6 +652,37 @@ class SyncStreamRepository extends DriftDatabaseRepository { } } + Future updateAssetFacesV2(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), + deletedAt: Value(assetFace.deletedAt), + isVisible: Value(assetFace.isVisible), + ); + + batch.insert( + _db.assetFaceEntity, + companion.copyWith(id: Value(assetFace.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateAssetFacesV2', error, stack); + rethrow; + } + } + Future deleteAssetFacesV1(Iterable data) async { try { await _db.batch((batch) { diff --git a/mobile/lib/infrastructure/repositories/tags_api.repository.dart b/mobile/lib/infrastructure/repositories/tags_api.repository.dart new file mode 100644 index 0000000000..e81b79c459 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/tags_api.repository.dart @@ -0,0 +1,17 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/api.repository.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:openapi/api.dart'; + +final tagsApiRepositoryProvider = Provider( + (ref) => TagsApiRepository(ref.read(apiServiceProvider).tagsApi), +); + +class TagsApiRepository extends ApiRepository { + final TagsApi _api; + const TagsApiRepository(this._api); + + Future?> getAllTags() async { + return await _api.getAllTags(); + } +} diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index d21e1e905b..4b4a13a4f9 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -15,6 +15,22 @@ import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:stream_transform/stream_transform.dart'; +class TimelineMapOptions { + final LatLngBounds bounds; + final bool onlyFavorites; + final bool includeArchived; + final bool withPartners; + final int relativeDays; + + const TimelineMapOptions({ + required this.bounds, + this.onlyFavorites = false, + this.includeArchived = false, + this.withPartners = false, + this.relativeDays = 0, + }); +} + class DriftTimelineRepository extends DriftDatabaseRepository { final Drift _db; @@ -70,6 +86,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { durationInSeconds: row.durationInSeconds, livePhotoVideoId: row.livePhotoVideoId, stackId: row.stackId, + isEdited: row.isEdited, ) : LocalAsset( id: row.localId!, @@ -84,6 +101,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository { isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, orientation: row.orientation, + playbackStyle: AssetPlaybackStyle.values[row.playbackStyle], + cloudId: row.iCloudId, + latitude: row.latitude, + longitude: row.longitude, + adjustmentTime: row.adjustmentTime, + isEdited: row.isEdited, ), ) .get(); @@ -104,7 +127,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.localAssetEntity.id.count(); - final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.localAssetEntity.createdAt.dateFmt(groupBy, toLocal: true); final query = _db.localAssetEntity.selectOnly().join([ @@ -181,7 +204,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { final album = albums.first; final isAscending = album.order == AlbumAssetOrder.asc; final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -253,6 +276,25 @@ class DriftTimelineRepository extends DriftDatabaseRepository { origin: origin, ); + TimelineQuery fromAssetsWithBuckets(List assets, TimelineOrigin origin) { + // Sort assets by date descending and group by day + final sorted = List.from(assets)..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + final Map bucketCounts = {}; + for (final asset in sorted) { + final localTime = asset.createdAt.toLocal(); + final date = DateTime(localTime.year, localTime.month, localTime.day); + bucketCounts[date] = (bucketCounts[date] ?? 0) + 1; + } + + final buckets = bucketCounts.entries.map((e) => TimeBucket(date: e.key, assetCount: e.value)).toList(); + + return ( + bucketSource: () => Stream.value(buckets), + assetSource: (offset, count) => Future.value(sorted.skip(offset).take(count).toList(growable: false)), + origin: origin, + ); + } + TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( filter: (row) => row.deletedAt.isNull() & row.visibility.equalsValue(AssetVisibility.timeline) & row.ownerId.equals(ownerId), @@ -282,6 +324,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { row.deletedAt.isNull() & row.ownerId.equals(userId) & row.visibility.equalsValue(AssetVisibility.archive), groupBy: groupBy, origin: TimelineOrigin.archive, + joinLocal: true, ); TimelineQuery locked(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder( @@ -320,7 +363,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -380,7 +423,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ); return query.map((row) { @@ -390,7 +435,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -405,7 +450,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); @@ -435,7 +482,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.ownerId.equals(userId) & _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & - _db.assetFaceEntity.personId.equals(personId), + _db.assetFaceEntity.personId.equals(personId) & + _db.assetFaceEntity.isVisible.equals(true) & + _db.assetFaceEntity.deletedAt.isNull(), ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); @@ -443,15 +492,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } - TimelineQuery map(String userId, LatLngBounds bounds, GroupAssetsBy groupBy) => ( - bucketSource: () => _watchMapBucket(userId, bounds, groupBy: groupBy), - assetSource: (offset, count) => _getMapBucketAssets(userId, bounds, offset: offset, count: count), + TimelineQuery map(List userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => ( + bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy), + assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count), origin: TimelineOrigin.map, ); Stream> _watchMapBucket( - String userId, - LatLngBounds bounds, { + List userId, + TimelineMapOptions options, { GroupAssetsBy groupBy = GroupAssetsBy.day, }) { if (groupBy == GroupAssetsBy.none) { @@ -460,7 +509,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -472,14 +521,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteExifEntity.inBounds(bounds) & - _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & + _db.remoteAssetEntity.ownerId.isIn(userId) & + _db.remoteExifEntity.inBounds(options.bounds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]) & _db.remoteAssetEntity.deletedAt.isNull(), ) ..groupBy([dateExp]) ..orderBy([OrderingTerm.desc(dateExp)]); + if (options.onlyFavorites) { + query.where(_db.remoteAssetEntity.isFavorite.equals(true)); + } + + if (options.relativeDays != 0) { + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); + } + return query.map((row) { final timeline = row.read(dateExp)!.truncateDate(groupBy); final assetCount = row.read(assetCountExp)!; @@ -488,8 +549,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } Future> _getMapBucketAssets( - String userId, - LatLngBounds bounds, { + List userId, + TimelineMapOptions options, { required int offset, required int count, }) { @@ -502,13 +563,26 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), ]) ..where( - _db.remoteAssetEntity.ownerId.equals(userId) & - _db.remoteExifEntity.inBounds(bounds) & - _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline) & + _db.remoteAssetEntity.ownerId.isIn(userId) & + _db.remoteExifEntity.inBounds(options.bounds) & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.timeline.index, + if (options.includeArchived) AssetVisibility.archive.index, + ]) & _db.remoteAssetEntity.deletedAt.isNull(), ) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..limit(count, offset: offset); + + if (options.onlyFavorites) { + query.where(_db.remoteAssetEntity.isFavorite.equals(true)); + } + + if (options.relativeDays != 0) { + final cutoffDate = DateTime.now().toUtc().subtract(Duration(days: options.relativeDays)); + query.where(_db.remoteAssetEntity.createdAt.isBiggerOrEqualValue(cutoffDate)); + } + return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); } @@ -537,7 +611,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } final assetCountExp = _db.remoteAssetEntity.id.count(); - final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy); final query = _db.remoteAssetEntity.selectOnly() ..addColumns([assetCountExp, dateExp]) @@ -599,10 +673,11 @@ List _generateBuckets(int count) { } extension on Expression { - Expression dateFmt(GroupAssetsBy groupBy) { + Expression dateFmt(GroupAssetsBy groupBy, {bool toLocal = false}) { // DateTimes are stored in UTC, so we need to convert them to local time inside the query before formatting - // to create the correct time bucket - final localTimeExp = modify(const DateTimeModifier.localTime()); + // to create the correct time bucket when toLocal is true + // toLocal is false for remote assets where localDateTime is already in the correct timezone + final localTimeExp = toLocal ? modify(const DateTimeModifier.localTime()) : this; return switch (groupBy) { GroupAssetsBy.day || GroupAssetsBy.auto => localTimeExp.date, GroupAssetsBy.month => localTimeExp.strftime("%Y-%m"), @@ -611,6 +686,11 @@ extension on Expression { } } +extension on $RemoteAssetEntityTable { + Expression effectiveCreatedAt(GroupAssetsBy groupBy) => + coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]); +} + extension on String { DateTime truncateDate(GroupAssetsBy groupBy) { final format = switch (groupBy) { diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 498e4227b7..1195256f5e 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -48,7 +48,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { _db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum), ), ])..where( - _db.trashedLocalAssetEntity.albumId.isInQuery(selectedAlbumIds) & + _db.trashedLocalAssetEntity.source.equalsValue(TrashOrigin.remoteSync) & + _db.trashedLocalAssetEntity.albumId.isInQuery(selectedAlbumIds) & _db.remoteAssetEntity.deletedAt.isNull(), )) .get(); @@ -84,6 +85,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { durationInSeconds: Value(item.asset.durationInSeconds), isFavorite: Value(item.asset.isFavorite), orientation: Value(item.asset.orientation), + playbackStyle: Value(item.asset.playbackStyle), + source: TrashOrigin.localSync, ); batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>( @@ -124,7 +127,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { Future trashLocalAsset(Map> assetsByAlbums) async { if (assetsByAlbums.isEmpty) { - return; + return Future.value(); } final companions = []; @@ -145,8 +148,10 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { durationInSeconds: Value(asset.durationInSeconds), isFavorite: Value(asset.isFavorite), orientation: Value(asset.orientation), + playbackStyle: Value(asset.playbackStyle), createdAt: Value(asset.createdAt), updatedAt: Value(asset.updatedAt), + source: const Value(TrashOrigin.remoteSync), ), ); } @@ -165,7 +170,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { Future applyRestoredAssets(List idList) async { if (idList.isEmpty) { - return; + return Future.value(); } final trashedAssets = []; @@ -192,6 +197,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { checksum: Value(e.checksum), isFavorite: Value(e.isFavorite), orientation: Value(e.orientation), + playbackStyle: Value(e.playbackStyle), ); }); @@ -205,6 +211,59 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { }); } + Future applyTrashedAssets(List idList) async { + if (idList.isEmpty) { + return Future.value(); + } + + final trashedAssets = <({LocalAssetEntityData asset, String albumId})>[]; + + for (final slice in idList.slices(kDriftMaxChunk)) { + final rows = await (_db.select(_db.localAlbumAssetEntity).join([ + innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), + ])..where(_db.localAlbumAssetEntity.assetId.isIn(slice))).get(); + + final assetsWithAlbum = rows.map( + (row) => + (albumId: row.readTable(_db.localAlbumAssetEntity).albumId, asset: row.readTable(_db.localAssetEntity)), + ); + + trashedAssets.addAll(assetsWithAlbum); + } + + if (trashedAssets.isEmpty) { + return; + } + + final companions = trashedAssets.map((e) { + return TrashedLocalAssetEntityCompanion.insert( + id: e.asset.id, + name: e.asset.name, + type: e.asset.type, + createdAt: Value(e.asset.createdAt), + updatedAt: Value(e.asset.updatedAt), + width: Value(e.asset.width), + height: Value(e.asset.height), + durationInSeconds: Value(e.asset.durationInSeconds), + checksum: Value(e.asset.checksum), + isFavorite: Value(e.asset.isFavorite), + orientation: Value(e.asset.orientation), + playbackStyle: Value(e.asset.playbackStyle), + source: TrashOrigin.localUser, + albumId: e.albumId, + ); + }); + + await _db.transaction(() async { + for (final companion in companions) { + await _db.into(_db.trashedLocalAssetEntity).insertOnConflictUpdate(companion); + } + for (final slice in idList.slices(kDriftMaxChunk)) { + await (_db.delete(_db.localAssetEntity)..where((t) => t.id.isIn(slice))).go(); + } + }); + } + Future>> getToTrash() async { final result = >{}; diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c3804d97f6..c35c27e141 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -18,7 +18,9 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; +import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; @@ -42,38 +44,43 @@ 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:immich_mobile/wm_executor.dart'; +import 'package:immich_ui/immich_ui.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:logging/logging.dart'; import 'package:timezone/data/latest.dart'; void main() async { - ImmichWidgetsBinding(); - unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); - final (isar, drift, logDb) = await Bootstrap.initDB(); - await Bootstrap.initDomain(isar, drift, logDb); - await initApp(); - // Warm-up isolate pool for worker manager - await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); - await migrateDatabaseIfNeeded(isar, drift); - HttpSSLOptions.apply(); + try { + ImmichWidgetsBinding(); + unawaited(BackgroundWorkerLockService(BackgroundWorkerLockApi()).lock()); + await EasyLocalization.ensureInitialized(); + final (isar, drift, logDb) = await Bootstrap.initDB(); + await Bootstrap.initDomain(isar, drift, logDb); + await initApp(); + // Warm-up isolate pool for worker manager + await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5)); + await migrateDatabaseIfNeeded(isar, drift); + HttpSSLOptions.apply(); - runApp( - ProviderScope( - overrides: [ - dbProvider.overrideWithValue(isar), - isarProvider.overrideWithValue(isar), - driftProvider.overrideWith(driftOverride(drift)), - ], - child: const MainWidget(), - ), - ); + runApp( + ProviderScope( + overrides: [ + dbProvider.overrideWithValue(isar), + isarProvider.overrideWithValue(isar), + driftProvider.overrideWith(driftOverride(drift)), + ], + child: const MainWidget(), + ), + ); + } catch (error, stack) { + runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString())); + } } Future initApp() async { - await EasyLocalization.ensureInitialized(); await initializeDateFormatting(); - if (kReleaseMode && Platform.isAndroid) { + if (Platform.isAndroid) { try { await FlutterDisplayMode.setHighRefreshRate(); dPrint(() => "Enabled high refresh mode"); @@ -112,7 +119,7 @@ Future initApp() async { await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); - await FileDownloader().trackTasks(); + unawaited(FileDownloader().trackTasks()); LicenseRegistry.addLicense(() async* { for (final license in nonPubLicenses.entries) { @@ -217,8 +224,8 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve ref .read(backgroundWorkerFgServiceProvider) .saveNotificationMessage( - IntlKeys.uploading_media.t(), - IntlKeys.backup_background_service_default_notification.t(), + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, ); } } else { @@ -236,6 +243,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve super.dispose(); } + @override + void reassemble() { + if (kDebugMode) { + NetworkRepository.reset(); + } + super.reassemble(); + } + @override Widget build(BuildContext context) { final router = ref.watch(appRouterProvider); @@ -252,6 +267,13 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve themeMode: ref.watch(immichThemeModeProvider), darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), + builder: (context, child) => ImmichTranslationProvider( + translations: ImmichTranslations( + submit: "submit".t(context: context), + password: "password".t(context: context), + ), + child: ImmichThemeProvider(colorScheme: context.colorScheme, child: child!), + ), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, navigatorObservers: () => [AppNavigationObserver(ref: ref)], diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 93322f5031..1b730e0c68 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -126,6 +126,41 @@ class SearchDateFilter { int get hashCode => takenBefore.hashCode ^ takenAfter.hashCode; } +class SearchRatingFilter { + int? rating; + SearchRatingFilter({this.rating}); + + SearchRatingFilter copyWith({int? rating}) { + return SearchRatingFilter(rating: rating ?? this.rating); + } + + Map toMap() { + return {'rating': rating}; + } + + factory SearchRatingFilter.fromMap(Map map) { + return SearchRatingFilter(rating: map['rating'] != null ? map['rating'] as int : null); + } + + String toJson() => json.encode(toMap()); + + factory SearchRatingFilter.fromJson(String source) => + SearchRatingFilter.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SearchRatingFilter(rating: $rating)'; + + @override + bool operator ==(covariant SearchRatingFilter other) { + if (identical(this, other)) return true; + + return other.rating == rating; + } + + @override + int get hashCode => rating.hashCode; +} + class SearchDisplayFilters { bool isNotInAlbum = false; bool isArchive = false; @@ -179,10 +214,12 @@ class SearchFilter { String? ocr; String? language; String? assetId; + List? tagIds; Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; + SearchRatingFilter rating; SearchDisplayFilters display; // Enum @@ -195,11 +232,13 @@ class SearchFilter { this.ocr, this.language, this.assetId, + this.tagIds, required this.people, required this.location, required this.camera, required this.date, required this.display, + required this.rating, required this.mediaType, }); @@ -209,6 +248,7 @@ class SearchFilter { (description == null || (description!.isEmpty)) && (assetId == null || (assetId!.isEmpty)) && (ocr == null || (ocr!.isEmpty)) && + (tagIds ?? []).isEmpty && people.isEmpty && location.country == null && location.state == null && @@ -220,6 +260,7 @@ class SearchFilter { display.isNotInAlbum == false && display.isArchive == false && display.isFavorite == false && + rating.rating == null && mediaType == AssetType.other; } @@ -231,10 +272,12 @@ class SearchFilter { String? ocr, String? assetId, Set? people, + List? tagIds, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, SearchDisplayFilters? display, + SearchRatingFilter? rating, AssetType? mediaType, }) { return SearchFilter( @@ -249,13 +292,15 @@ class SearchFilter { camera: camera ?? this.camera, date: date ?? this.date, display: display ?? this.display, + rating: rating ?? this.rating, mediaType: mediaType ?? this.mediaType, + tagIds: tagIds ?? this.tagIds, ); } @override String toString() { - return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, camera: $camera, date: $date, display: $display, mediaType: $mediaType, assetId: $assetId)'; + return 'SearchFilter(context: $context, filename: $filename, description: $description, language: $language, ocr: $ocr, people: $people, location: $location, tagIds: $tagIds, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)'; } @override @@ -269,10 +314,12 @@ class SearchFilter { other.ocr == ocr && other.assetId == assetId && other.people == people && + other.tagIds == tagIds && other.location == location && other.camera == camera && other.date == date && other.display == display && + other.rating == rating && other.mediaType == mediaType; } @@ -285,10 +332,12 @@ class SearchFilter { ocr.hashCode ^ assetId.hashCode ^ people.hashCode ^ + tagIds.hashCode ^ location.hashCode ^ camera.hashCode ^ date.hashCode ^ display.hashCode ^ + rating.hashCode ^ mediaType.hashCode; } } diff --git a/mobile/lib/models/server_info/server_features.model.dart b/mobile/lib/models/server_info/server_features.model.dart index 049628a8d2..78a80c9013 100644 --- a/mobile/lib/models/server_info/server_features.model.dart +++ b/mobile/lib/models/server_info/server_features.model.dart @@ -6,6 +6,7 @@ class ServerFeatures { final bool oauthEnabled; final bool passwordLogin; final bool ocr; + final bool smartSearch; const ServerFeatures({ required this.trash, @@ -13,21 +14,30 @@ class ServerFeatures { required this.oauthEnabled, required this.passwordLogin, this.ocr = false, + this.smartSearch = false, }); - ServerFeatures copyWith({bool? trash, bool? map, bool? oauthEnabled, bool? passwordLogin, bool? ocr}) { + ServerFeatures copyWith({ + bool? trash, + bool? map, + bool? oauthEnabled, + bool? passwordLogin, + bool? ocr, + bool? smartSearch, + }) { return ServerFeatures( trash: trash ?? this.trash, map: map ?? this.map, oauthEnabled: oauthEnabled ?? this.oauthEnabled, passwordLogin: passwordLogin ?? this.passwordLogin, ocr: ocr ?? this.ocr, + smartSearch: smartSearch ?? this.smartSearch, ); } @override String toString() { - return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr)'; + return 'ServerFeatures(trash: $trash, map: $map, oauthEnabled: $oauthEnabled, passwordLogin: $passwordLogin, ocr: $ocr, smartSearch: $smartSearch)'; } ServerFeatures.fromDto(ServerFeaturesDto dto) @@ -35,7 +45,8 @@ class ServerFeatures { map = dto.map, oauthEnabled = dto.oauth, passwordLogin = dto.passwordLogin, - ocr = dto.ocr; + ocr = dto.ocr, + smartSearch = dto.smartSearch; @override bool operator ==(covariant ServerFeatures other) { @@ -45,11 +56,17 @@ class ServerFeatures { other.map == map && other.oauthEnabled == oauthEnabled && other.passwordLogin == passwordLogin && - other.ocr == ocr; + other.ocr == ocr && + other.smartSearch == smartSearch; } @override int get hashCode { - return trash.hashCode ^ map.hashCode ^ oauthEnabled.hashCode ^ passwordLogin.hashCode ^ ocr.hashCode; + return trash.hashCode ^ + map.hashCode ^ + oauthEnabled.hashCode ^ + passwordLogin.hashCode ^ + ocr.hashCode ^ + smartSearch.hashCode; } } diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index a034960ddb..a039bb70eb 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -20,7 +20,7 @@ enum VersionStatus { class ServerInfo { final ServerVersion serverVersion; - final ServerVersion latestVersion; + final ServerVersion? latestVersion; final ServerFeatures serverFeatures; final ServerConfig serverConfig; final ServerDiskInfo serverDiskInfo; @@ -28,7 +28,7 @@ class ServerInfo { const ServerInfo({ required this.serverVersion, - required this.latestVersion, + this.latestVersion, required this.serverFeatures, required this.serverConfig, required this.serverDiskInfo, diff --git a/mobile/lib/models/server_info/server_version.model.dart b/mobile/lib/models/server_info/server_version.model.dart index 3aea98a80d..c8bf73db81 100644 --- a/mobile/lib/models/server_info/server_version.model.dart +++ b/mobile/lib/models/server_info/server_version.model.dart @@ -10,4 +10,8 @@ class ServerVersion extends SemVer { } ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_); + + bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) { + return this >= SemVer(major: major, minor: minor, patch: patch); + } } diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index 57a1f441eb..4315cf616a 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -14,6 +14,7 @@ class SharedLink { final String key; final bool showMetadata; final SharedLinkSource type; + final String? slug; const SharedLink({ required this.id, @@ -27,6 +28,7 @@ class SharedLink { required this.key, required this.showMetadata, required this.type, + required this.slug, }); SharedLink copyWith({ @@ -41,6 +43,7 @@ class SharedLink { String? key, bool? showMetadata, SharedLinkSource? type, + String? slug, }) { return SharedLink( id: id ?? this.id, @@ -54,6 +57,7 @@ class SharedLink { key: key ?? this.key, showMetadata: showMetadata ?? this.showMetadata, type: type ?? this.type, + slug: slug ?? this.slug, ); } @@ -66,6 +70,7 @@ class SharedLink { expiresAt = dto.expiresAt, key = dto.key, showMetadata = dto.showMetadata, + slug = dto.slug, type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual, title = dto.type == SharedLinkType.ALBUM ? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE" @@ -78,7 +83,7 @@ class SharedLink { @override String toString() => - 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)'; + 'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type, slug=$slug)'; @override bool operator ==(Object other) => @@ -94,7 +99,8 @@ class SharedLink { other.expiresAt == expiresAt && other.key == key && other.showMetadata == showMetadata && - other.type == type; + other.type == type && + other.slug == slug; @override int get hashCode => @@ -108,5 +114,6 @@ class SharedLink { expiresAt.hashCode ^ key.hashCode ^ showMetadata.hashCode ^ - type.hashCode; + type.hashCode ^ + slug.hashCode; } diff --git a/mobile/lib/models/upload/share_intent_attachment.model.dart b/mobile/lib/models/upload/share_intent_attachment.model.dart index ae05e4c492..e5388fce2c 100644 --- a/mobile/lib/models/upload/share_intent_attachment.model.dart +++ b/mobile/lib/models/upload/share_intent_attachment.model.dart @@ -7,7 +7,7 @@ import 'package:path/path.dart'; enum ShareIntentAttachmentType { image, video } -enum UploadStatus { enqueued, running, complete, notFound, failed, canceled, waitingToRetry, paused } +enum UploadStatus { enqueued, running, complete, failed } class ShareIntentAttachment { final String path; diff --git a/mobile/lib/pages/album/album_options.page.dart b/mobile/lib/pages/album/album_options.page.dart index b0f682ffed..ca65a92a79 100644 --- a/mobile/lib/pages/album/album_options.page.dart +++ b/mobile/lib/pages/album/album_options.page.dart @@ -134,7 +134,7 @@ class AlbumOptionsPage extends HookConsumerWidget { itemBuilder: (context, index) { final user = sharedUsers.value[index]; return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), diff --git a/mobile/lib/pages/album/album_shared_user_icons.dart b/mobile/lib/pages/album/album_shared_user_icons.dart index fe1823ec61..7cf6f387ae 100644 --- a/mobile/lib/pages/album/album_shared_user_icons.dart +++ b/mobile/lib/pages/album/album_shared_user_icons.dart @@ -41,7 +41,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget { itemBuilder: ((context, index) { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: UserCircleAvatar(user: sharedUsers.value[index], radius: 18, size: 36), + child: UserCircleAvatar(user: sharedUsers.value[index], size: 36), ); }), itemCount: sharedUsers.value.length, diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 47052ea436..cd6c2a62b0 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -93,11 +93,11 @@ class _DriftBackupPageState extends ConsumerState { Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); return; } - await backupNotifier.startBackup(currentUser.id); + await backupNotifier.startForegroundBackup(currentUser.id); } Future stopBackup() async { - await backupNotifier.cancel(); + await backupNotifier.stopForegroundBackup(); } return Scaffold( @@ -153,7 +153,7 @@ class _DriftBackupPageState extends ConsumerState { Icon(Icons.warning_rounded, color: context.colorScheme.error, fill: 1), const SizedBox(width: 8), Text( - IntlKeys.backup_error_sync_failed.t(), + context.t.backup_error_sync_failed, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), textAlign: TextAlign.center, ), diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 5fe1dfb6a1..93ab659032 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -113,10 +113,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState backgroundSync.hashAssets())); if (isBackupEnabled) { unawaited( - backupNotifier.cancel().whenComplete( + backupNotifier.stopForegroundBackup().whenComplete( () => backgroundSync.syncRemote().then((success) { if (success) { - return backupNotifier.startBackup(user.id); + return backupNotifier.startForegroundBackup(user.id); } else { Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup'); } diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 1e5c326478..f43c8b6a8e 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -60,10 +60,10 @@ class DriftBackupOptionsPage extends ConsumerWidget { final backupNotifier = ref.read(driftBackupProvider.notifier); final backgroundSync = ref.read(backgroundSyncProvider); unawaited( - backupNotifier.cancel().whenComplete( + backupNotifier.stopForegroundBackup().whenComplete( () => backgroundSync.syncRemote().then((success) { if (success) { - return backupNotifier.startBackup(currentUser.id); + return backupNotifier.startForegroundBackup(currentUser.id); } else { Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup'); } diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 612b6a8111..71249d1c4b 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -11,12 +11,70 @@ import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:path/path.dart' as path; @RoutePage() -class DriftUploadDetailPage extends ConsumerWidget { +class DriftUploadDetailPage extends ConsumerStatefulWidget { const DriftUploadDetailPage({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _DriftUploadDetailPageState(); +} + +class _DriftUploadDetailPageState extends ConsumerState { + final Set _seenTaskIds = {}; + final Set _failedTaskIds = {}; + + final Map _taskSlotAssignments = {}; + static const int _maxSlots = 3; + + /// Assigns uploading items to fixed slots to prevent jumping when items complete + List _assignItemsToSlots(List uploadingItems) { + final slots = List.filled(_maxSlots, null); + final currentTaskIds = uploadingItems.map((e) => e.taskId).toSet(); + + _taskSlotAssignments.removeWhere((taskId, _) => !currentTaskIds.contains(taskId)); + + for (final item in uploadingItems) { + final existingSlot = _taskSlotAssignments[item.taskId]; + if (existingSlot != null && existingSlot < _maxSlots) { + slots[existingSlot] = item; + } + } + + for (final item in uploadingItems) { + if (_taskSlotAssignments.containsKey(item.taskId)) continue; + + for (int i = 0; i < _maxSlots; i++) { + if (slots[i] == null) { + slots[i] = item; + _taskSlotAssignments[item.taskId] = i; + break; + } + } + } + + return slots; + } + + @override + Widget build(BuildContext context) { final uploadItems = ref.watch(driftBackupProvider.select((state) => state.uploadItems)); + final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); + + for (final item in uploadItems.values) { + if (item.isFailed == true) { + _failedTaskIds.add(item.taskId); + } + } + + for (final item in uploadItems.values) { + if (item.progress >= 1.0 && item.isFailed != true && !_failedTaskIds.contains(item.taskId)) { + if (!_seenTaskIds.contains(item.taskId)) { + _seenTaskIds.add(item.taskId); + } + } + } + + final uploadingItems = uploadItems.values.where((item) => item.progress < 1.0 && item.isFailed != true).toList(); + final failedItems = uploadItems.values.where((item) => item.isFailed == true).toList(); return Scaffold( appBar: AppBar( @@ -25,98 +83,326 @@ class DriftUploadDetailPage extends ConsumerWidget { elevation: 0, scrolledUnderElevation: 1, ), - body: uploadItems.isEmpty ? _buildEmptyState(context) : _buildUploadList(uploadItems), + body: _buildTwoSectionLayout(context, uploadingItems, failedItems, iCloudProgress), ); } - 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 _buildTwoSectionLayout( + BuildContext context, + List uploadingItems, + List failedItems, + Map iCloudProgress, + ) { + return CustomScrollView( + slivers: [ + // iCloud Downloads Section + if (iCloudProgress.isNotEmpty) ...[ + SliverToBoxAdapter( + child: _buildSectionHeader( + context, + title: "Downloading from iCloud", + count: iCloudProgress.length, + color: context.colorScheme.tertiary, + ), ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final entry = iCloudProgress.entries.elementAt(index); + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildICloudDownloadCard(context, entry.key, entry.value), + ); + }, childCount: iCloudProgress.length), + ), + ), + ], + + // Uploading Section + SliverToBoxAdapter( + child: _buildSectionHeader( + context, + title: "uploading".t(context: context), + count: uploadingItems.length, + color: context.colorScheme.primary, + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + // Use slot-based assignment to prevent items from jumping + final slots = _assignItemsToSlots(uploadingItems); + final item = slots[index]; + if (item != null) { + return _buildCurrentUploadCard(context, item); + } else { + return _buildPlaceholderCard(context); + } + }, childCount: 3), + ), + ), + + // Errors Section + if (failedItems.isNotEmpty) ...[ + SliverToBoxAdapter( + child: _buildSectionHeader( + context, + title: "errors_text".t(context: context), + count: failedItems.length, + color: context.colorScheme.error, + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = failedItems[index]; + return Padding(padding: const EdgeInsets.only(bottom: 8), child: _buildErrorCard(context, item)); + }, childCount: failedItems.length), + ), + ), + ], + + // Bottom padding + const SliverToBoxAdapter(child: SizedBox(height: 24)), + ], + ); + } + + Widget _buildSectionHeader(BuildContext context, {required String title, int? count, required Color color}) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600, color: color), + ), + const SizedBox(width: 8), + count != null + ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Text( + count.toString(), + style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.bold, color: color), + ), + ) + : const SizedBox.shrink(), ], ), ); } - 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); + Widget _buildICloudDownloadCard(BuildContext context, String assetId, double progress) { + final double progressPercentage = (progress * 100).clamp(0, 100); return Card( elevation: 0, - color: item.isFailed != null ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer, + color: context.colorScheme.tertiaryContainer.withValues(alpha: 0.5), shape: RoundedRectangleBorder( - borderRadius: const BorderRadius.all(Radius.circular(16)), - side: BorderSide(color: context.colorScheme.outline.withValues(alpha: 0.1), width: 1), + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: context.colorScheme.tertiary.withValues(alpha: 0.3), 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( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: context.colorScheme.tertiary.withValues(alpha: 0.2), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Icon(Icons.cloud_download_rounded, size: 24, color: context.colorScheme.tertiary), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - Text( - path.basename(item.filename), - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (item.error != null) - Text( - item.error!, - style: context.textTheme.bodySmall?.copyWith( - color: context.colorScheme.onErrorContainer.withValues(alpha: 0.6), - ), - ), - Text( - "backup_upload_details_page_more_details".t(context: context), - style: context.textTheme.bodySmall?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.6), - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + Text( + "downloading_from_icloud".t(context: context), + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - _buildProgressIndicator( - context, - item.progress, - progressPercentage, - isCompleted, - item.networkSpeedAsString, + const SizedBox(height: 4), + Text( + assetId, + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: LinearProgressIndicator( + value: progress, + backgroundColor: context.colorScheme.tertiary.withValues(alpha: 0.2), + valueColor: AlwaysStoppedAnimation(context.colorScheme.tertiary), + minHeight: 4, + ), ), ], ), + ), + const SizedBox(width: 12), + SizedBox( + width: 48, + child: Text( + "${progressPercentage.toStringAsFixed(0)}%", + textAlign: TextAlign.right, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: context.colorScheme.tertiary, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCurrentUploadCard(BuildContext context, DriftUploadStatus item) { + final double progressPercentage = (item.progress * 100).clamp(0, 100); + final isFailed = item.isFailed == true; + + return Card( + elevation: 0, + color: isFailed + ? context.colorScheme.errorContainer + : context.colorScheme.primaryContainer.withValues(alpha: 0.5), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide( + color: isFailed + ? context.colorScheme.error.withValues(alpha: 0.3) + : context.colorScheme.primary.withValues(alpha: 0.3), + width: 1, + ), + ), + child: InkWell( + onTap: () => _showFileDetailDialog(context, item), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: SizedBox( + height: 64, + child: Row( + children: [ + _CurrentUploadThumbnail(taskId: item.taskId), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + path.basename(item.filename), + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + isFailed + ? item.error ?? "unable_to_upload_file".t(context: context) + : "${formatHumanReadableBytes(item.fileSize, 1)} â€ĸ ${item.networkSpeedAsString}", + style: context.textTheme.labelLarge?.copyWith( + color: isFailed + ? context.colorScheme.error + : context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (!isFailed) ...[ + const SizedBox(height: 8), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: LinearProgressIndicator( + value: item.progress, + backgroundColor: context.colorScheme.primary.withValues(alpha: 0.2), + valueColor: AlwaysStoppedAnimation(context.colorScheme.primary), + minHeight: 4, + ), + ), + ], + ], + ), + ), + const SizedBox(width: 12), + SizedBox( + width: 48, + child: isFailed + ? Icon(Icons.error_rounded, color: context.colorScheme.error, size: 28) + : Text( + "${progressPercentage.toStringAsFixed(0)}%", + textAlign: TextAlign.right, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: context.colorScheme.primary, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildErrorCard(BuildContext context, DriftUploadStatus item) { + return Card( + elevation: 0, + color: context.colorScheme.errorContainer, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: context.colorScheme.error.withValues(alpha: 0.3), width: 1), + ), + child: InkWell( + onTap: () => _showFileDetailDialog(context, item), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + _CurrentUploadThumbnail(taskId: item.taskId), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + path.basename(item.filename), + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + item.error ?? "unable_to_upload_file".t(context: context), + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + Icon(Icons.error_rounded, color: context.colorScheme.error, size: 28), ], ), ), @@ -124,49 +410,84 @@ class DriftUploadDetailPage extends ConsumerWidget { ); } - 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, + Widget _buildPlaceholderCard(BuildContext context) { + return Card( + elevation: 0, + color: context.colorScheme.surfaceContainerLow.withValues(alpha: 0.5), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: context.colorScheme.outline.withValues(alpha: 0.1), width: 1, style: BorderStyle.solid), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: SizedBox( + height: 64, + child: Row( + children: [ + SizedBox( + width: 48, + height: 48, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.outline.withValues(alpha: 0.1), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Icon( + Icons.hourglass_empty_rounded, + size: 24, + color: context.colorScheme.outline.withValues(alpha: 0.3), + ), ), ), - ), - 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), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 14, + width: 120, + decoration: BoxDecoration( + color: context.colorScheme.outline.withValues(alpha: 0.1), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + ), + const SizedBox(height: 6), + Container( + height: 10, + width: 80, + decoration: BoxDecoration( + color: context.colorScheme.outline.withValues(alpha: 0.08), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + ), + const SizedBox(height: 8), + Container( + height: 4, + decoration: BoxDecoration( + color: context.colorScheme.outline.withValues(alpha: 0.1), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + ), + ], + ), ), - ], - ), - Text( - networkSpeedAsString, - style: context.textTheme.labelSmall?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.6), - fontSize: 10, + const SizedBox(width: 12), + SizedBox( + width: 48, + child: Text( + "0%", + textAlign: TextAlign.right, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: context.colorScheme.outline.withValues(alpha: 0.3), + ), + ), + ), + ], ), ), - ], + ), ); } @@ -178,9 +499,44 @@ class DriftUploadDetailPage extends ConsumerWidget { } } +class _CurrentUploadThumbnail extends ConsumerWidget { + final String taskId; + const _CurrentUploadThumbnail({required this.taskId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FutureBuilder( + future: _getAsset(ref), + builder: (context, snapshot) { + return SizedBox( + width: 48, + height: 48, + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.primary.withValues(alpha: 0.2), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + clipBehavior: Clip.antiAlias, + child: snapshot.data != null + ? Thumbnail.fromAsset(asset: snapshot.data!, size: const Size(48, 48), fit: BoxFit.cover) + : Icon(Icons.image, size: 24, color: context.colorScheme.primary), + ), + ); + }, + ); + } + + Future _getAsset(WidgetRef ref) async { + try { + return await ref.read(localAssetRepository).getById(taskId); + } catch (e) { + return null; + } + } +} + class FileDetailDialog extends ConsumerWidget { final DriftUploadStatus uploadStatus; - const FileDetailDialog({super.key, required this.uploadStatus}); @override @@ -212,14 +568,12 @@ class FileDetailDialog extends ConsumerWidget { 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)), @@ -237,7 +591,7 @@ class FileDetailDialog extends ConsumerWidget { ), ), const SizedBox(height: 24), - if (asset != null) ...[ + if (asset != null) _buildInfoSection(context, [ _buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)), _buildInfoRow(context, "local_id".t(context: context), asset.id), @@ -254,7 +608,6 @@ class FileDetailDialog extends ConsumerWidget { if (asset.checksum != null) _buildInfoRow(context, "checksum".t(context: context), asset.checksum!), ]), - ], ], ), ); @@ -282,7 +635,7 @@ class FileDetailDialog extends ConsumerWidget { 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]), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: children), ); } @@ -303,12 +656,7 @@ class FileDetailDialog extends ConsumerWidget { ), ), Expanded( - child: Text( - value, - style: context.textTheme.labelMedium?.copyWith(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), + child: Text(value, style: context.textTheme.labelMedium, maxLines: 3, overflow: TextOverflow.ellipsis), ), ], ), @@ -317,8 +665,7 @@ class FileDetailDialog extends ConsumerWidget { Future _getAssetDetails(WidgetRef ref, String localAssetId) async { try { - final repository = ref.read(localAssetRepository); - return await repository.getById(localAssetId); + return await ref.read(localAssetRepository).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 b533895cd7..a97a133b89 100644 --- a/mobile/lib/pages/backup/failed_backup_status.page.dart +++ b/mobile/lib/pages/backup/failed_backup_status.page.dart @@ -2,9 +2,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart'; import 'package:intl/intl.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; @RoutePage() class FailedBackupStatusPage extends HookConsumerWidget { @@ -58,7 +59,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { clipBehavior: Clip.hardEdge, child: Image( fit: BoxFit.cover, - image: ImmichLocalThumbnailProvider(asset: errorAsset.asset, height: 512, width: 512), + image: LocalThumbProvider(id: errorAsset.asset.localId!, assetType: base_asset.AssetType.video), ), ), ), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 37aec2f13c..336bf0b605 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -100,7 +100,7 @@ class AppLogPage extends HookConsumerWidget { minLeadingWidth: 10, title: Text( truncateLogMessage(logMessage.message, 4), - style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "Inconsolata"), + style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "GoogleSansCode"), ), subtitle: Text( "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}", diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index de9604b7ad..890e46888f 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -57,7 +57,7 @@ class AppLogDetailPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: SelectableText( text, - style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"), + style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"), ), ), ), @@ -88,7 +88,7 @@ class AppLogDetailPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: SelectableText( logger.toString(), - style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"), + style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"), ), ), ), diff --git a/mobile/lib/pages/common/gallery_stacked_children.dart b/mobile/lib/pages/common/gallery_stacked_children.dart index 7145bc2553..68123509ae 100644 --- a/mobile/lib/pages/common/gallery_stacked_children.dart +++ b/mobile/lib/pages/common/gallery_stacked_children.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; class GalleryStackedChildren extends HookConsumerWidget { final ValueNotifier stackIndex; @@ -70,7 +70,7 @@ class GalleryStackedChildren extends HookConsumerWidget { borderRadius: const BorderRadius.all(Radius.circular(4)), child: Image( fit: BoxFit.cover, - image: ImmichRemoteImageProvider(assetId: assetId), + image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: asset.thumbhash ?? ""), ), ), ), diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 9a7e78ddb8..0ef27f854b 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -221,8 +221,37 @@ class GalleryViewerPage extends HookConsumerWidget { onDragUpdate: (_, details, __) { handleSwipeUpDown(details); }, - onTapDown: (_, __, ___) { - ref.read(showControlsProvider.notifier).toggle(); + onTapDown: (ctx, tapDownDetails, _) { + final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); + if (!tapToNavigate) { + ref.read(showControlsProvider.notifier).toggle(); + return; + } + + double tapX = tapDownDetails.globalPosition.dx; + double screenWidth = ctx.width; + + // We want to change images if the user taps in the leftmost or + // rightmost quarter of the screen + bool tappedLeftSide = tapX < screenWidth / 4; + bool tappedRightSide = tapX > screenWidth * (3 / 4); + + int? currentPage = controller.page?.toInt(); + int maxPage = renderList.totalAssets - 1; + + if (tappedLeftSide && currentPage != null) { + // Nested if because we don't want to fallback to show/hide controls + if (currentPage != 0) { + controller.jumpToPage(currentPage - 1); + } + } else if (tappedRightSide && currentPage != null) { + // Nested if because we don't want to fallback to show/hide controls + if (currentPage != maxPage) { + controller.jumpToPage(currentPage + 1); + } + } else { + ref.read(showControlsProvider.notifier).toggle(); + } }, onLongPressStart: asset.isMotionPhoto ? (_, __, ___) { diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 1cfab355d6..c7c34b9cd2 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -7,7 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class SettingsHeader { String key = ""; @@ -61,7 +61,7 @@ class HeaderSettingsPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text(IntlKeys.headers_settings_tile_title).tr(), + title: Text(context.t.headers_settings_tile_title), centerTitle: false, actions: [ IconButton( diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 86c80253dc..e8f5eb2ee2 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewe import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; +import 'package:immich_mobile/widgets/settings/free_up_space_settings.dart'; 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'; @@ -22,6 +23,7 @@ enum SettingSection { advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), + freeUpSpace('free_up_space', Icons.cleaning_services_outlined, "free_up_space_settings_subtitle"), languages('language', Icons.language, "setting_languages_subtitle"), networking('networking_settings', Icons.wifi, "networking_subtitle"), notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), @@ -38,6 +40,7 @@ enum SettingSection { SettingSection.assetViewer => const AssetViewerSettings(), SettingSection.backup => Store.tryGet(StoreKey.betaTimeline) ?? false ? const DriftBackupSettings() : const BackupSettings(), + SettingSection.freeUpSpace => const FreeUpSpaceSettings(), SettingSection.languages => const LanguageSettings(), SettingSection.networking => const NetworkingSettings(), SettingSection.notifications => const NotificationSetting(), @@ -89,7 +92,7 @@ class _MobileLayout extends StatelessWidget { ], ) .toList(); - return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 16), children: [...settings]); + return ListView(padding: const EdgeInsets.only(top: 10.0, bottom: 60), children: [...settings]); } } diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 79db33104d..37c6b95806 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -1,10 +1,19 @@ import 'dart:async'; +import 'dart:io'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -13,7 +22,259 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/theme/color_scheme.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/widgets/common/immich_logo.dart'; +import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; +import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; + +class BootstrapErrorWidget extends StatelessWidget { + final String error; + final String stack; + + const BootstrapErrorWidget({super.key, required this.error, required this.stack}); + + @override + Widget build(BuildContext _) { + final immichTheme = defaultColorPreset.themeOfPreset; + + return EasyLocalization( + supportedLocales: locales.values.toList(), + path: translationsPath, + useFallbackTranslations: true, + fallbackLocale: locales.values.first, + assetLoader: const CodegenLoader(), + child: Builder( + builder: (lCtx) => MaterialApp( + title: 'Immich', + debugShowCheckedModeBanner: true, + localizationsDelegates: lCtx.localizationDelegates, + supportedLocales: lCtx.supportedLocales, + locale: lCtx.locale, + themeMode: ThemeMode.system, + darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: lCtx.locale), + theme: getThemeData(colorScheme: immichTheme.light, locale: lCtx.locale), + home: Builder( + builder: (ctx) => Scaffold( + body: Column( + children: [ + const SafeArea( + bottom: false, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ImmichLogo(size: 48), SizedBox(width: 12), ImmichTitleText(fontSize: 24)], + ), + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: _ErrorCard(error: error, stack: stack), + ), + ), + const Divider(height: 1), + const SafeArea( + top: false, + child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()), + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _BottomPanel extends StatefulWidget { + const _BottomPanel(); + + @override + State<_BottomPanel> createState() => _BottomPanelState(); +} + +class _BottomPanelState extends State<_BottomPanel> { + bool _cleared = false; + + Future _clearDatabase() async { + final confirmed = await showDialog( + context: context, + builder: (dialogCtx) => AlertDialog( + title: Text(context.t.reset_sqlite_clear_app_data), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.t.reset_sqlite_confirmation), + const SizedBox(height: 12), + Text( + context.t.reset_sqlite_confirmation_note, + style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.of(dialogCtx).pop(false), child: Text(context.t.cancel)), + TextButton( + onPressed: () => Navigator.of(dialogCtx).pop(true), + child: Text(context.t.confirm, style: TextStyle(color: Theme.of(context).colorScheme.error)), + ), + ], + ), + ); + + if (confirmed != true || !mounted) { + return; + } + + try { + final dir = await getApplicationDocumentsDirectory(); + for (final suffix in ['', '-wal', '-shm']) { + final file = File(path.join(dir.path, 'immich.sqlite$suffix')); + if (await file.exists()) { + await file.delete(); + } + } + } catch (_) { + return; + } + + if (mounted) { + setState(() => _cleared = true); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 8, + children: [ + Text( + _cleared ? context.t.reset_sqlite_done : context.t.scaffold_body_error_unrecoverable, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _ActionLink( + icon: Icons.chat_bubble_outline, + label: context.t.discord, + onTap: () => launchUrl(Uri.parse('https://discord.immich.app/'), mode: LaunchMode.externalApplication), + ), + _ActionLink( + icon: Icons.bug_report_outlined, + label: context.t.profile_drawer_github, + onTap: () => launchUrl( + Uri.parse('https://github.com/immich-app/immich/issues'), + mode: LaunchMode.externalApplication, + ), + ), + if (!_cleared) + _ActionLink( + icon: Icons.delete_outline, + label: context.t.reset_sqlite_clear_app_data, + onTap: _clearDatabase, + ), + ], + ), + ], + ); + } +} + +class _ActionLink extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _ActionLink({required this.icon, required this.label, required this.onTap}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 24), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12)), + ], + ), + ), + ); + } +} + +class _ErrorCard extends StatelessWidget { + final String error; + final String stack; + + const _ErrorCard({required this.error, required this.stack}); + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Card( + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ColoredBox( + color: scheme.error, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + child: Row( + children: [ + Expanded( + child: Text( + context.t.scaffold_body_error_occurred, + style: textTheme.titleSmall?.copyWith(color: scheme.onError), + ), + ), + IconButton( + tooltip: context.t.copy_error, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon(Icons.copy_outlined, size: 16, color: scheme.onError), + onPressed: () => Clipboard.setData(ClipboardData(text: '$error\n\n$stack')), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Text(error, style: textTheme.bodyMedium), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.t.stacktrace, style: textTheme.labelMedium), + const SizedBox(height: 4), + SelectableText(stack, style: textTheme.bodySmall?.copyWith(fontFamily: 'GoogleSansCode')), + ], + ), + ), + ], + ), + ); + } +} @RoutePage() class SplashScreenPage extends StatefulHookConsumerWidget { @@ -75,6 +336,8 @@ class SplashScreenPageState extends ConsumerState { _resumeBackup(backupProvider); }), _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), ]); } else { await backgroundManager.hashAssets(); @@ -107,9 +370,43 @@ class SplashScreenPageState extends ConsumerState { if (context.router.current.name == SplashScreenRoute.name) { final needBetaMigration = Store.get(StoreKey.needBetaMigration, false); if (needBetaMigration) { + bool migrate = + (await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("New Timeline Experience"), + content: const Text( + "The old timeline has been deprecated and will be removed in an upcoming release. Would you like to switch to the new timeline now?", + ), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), + ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), + ], + ), + )) ?? + false; + if (migrate != true) { + migrate = + (await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Are you sure?"), + content: const Text( + "If you choose to remain on the old timeline, you will be automatically migrated to the new timeline in an upcoming release. Would you like to switch now?", + ), + actions: [ + TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text("No")), + ElevatedButton(onPressed: () => Navigator.of(ctx).pop(true), child: const Text("Yes")), + ], + ), + )) ?? + false; + } await Store.put(StoreKey.needBetaMigration, false); - unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); - return; + if (migrate) { + unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)])); + return; + } } unawaited(context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute())); @@ -132,7 +429,7 @@ class SplashScreenPageState extends ConsumerState { if (isEnableBackup) { final currentUser = Store.tryGet(StoreKey.currentUser); if (currentUser != null) { - unawaited(notifier.handleBackupResume(currentUser.id)); + unawaited(notifier.startForegroundBackup(currentUser.id)); } } } diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index c9ab014456..2889785d0b 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -1,6 +1,4 @@ -import 'dart:async'; import 'dart:typed_data'; -import 'dart:ui'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -12,6 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/image_converter.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:path/path.dart' as p; @@ -30,27 +29,10 @@ class EditImagePage extends ConsumerWidget { final bool isEdited; const EditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - Future _imageToUint8List(Image image) async { - final Completer completer = Completer(); - image.image - .resolve(const ImageConfiguration()) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - info.image.toByteData(format: ImageByteFormat.png).then((byteData) { - if (byteData != null) { - completer.complete(byteData.buffer.asUint8List()); - } else { - completer.completeError('Failed to convert image to bytes'); - } - }); - }, onError: (exception, stackTrace) => completer.completeError(exception)), - ); - return completer.future; - } Future _saveEditedImage(BuildContext context, Asset asset, Image image, WidgetRef ref) async { try { - final Uint8List imageData = await _imageToUint8List(image); + final Uint8List imageData = await imageToUint8List(image); await ref .read(fileMediaRepositoryProvider) .saveImage(imageData, title: "${p.withoutExtension(asset.fileName)}_edited.jpg"); diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 2968bca18e..497d3e5151 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -234,7 +234,7 @@ class FolderPath extends StatelessWidget { Text( currentFolder.path, style: TextStyle( - fontFamily: 'Inconsolata', + fontFamily: 'GoogleSansCode', fontWeight: FontWeight.bold, fontSize: 14, color: context.colorScheme.onSurface.withAlpha(175), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 483427d2de..99a534e9cf 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -5,13 +5,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; @@ -41,13 +41,13 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const FavoritesRoute()), icon: Icons.favorite_outline_rounded, - label: IntlKeys.favorites.tr(), + label: context.t.favorites, ), const SizedBox(width: 8), ActionButton( onPressed: () => context.pushRoute(const ArchiveRoute()), icon: Icons.archive_outlined, - label: IntlKeys.archived.tr(), + label: context.t.archived, ), ], ), @@ -58,14 +58,14 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const SharedLinkRoute()), icon: Icons.link_outlined, - label: IntlKeys.shared_links.tr(), + label: context.t.shared_links, ), SizedBox(width: trashEnabled ? 8 : 0), trashEnabled ? ActionButton( onPressed: () => context.pushRoute(const TrashRoute()), icon: Icons.delete_outline_rounded, - label: IntlKeys.trash.tr(), + label: context.t.trash, ) : const SizedBox.shrink(), ], @@ -120,26 +120,20 @@ class QuickAccessButtons extends ConsumerWidget { ), ), leading: const Icon(Icons.folder_outlined, size: 26), - title: Text( - IntlKeys.folders.tr(), - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), + title: Text(context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), onTap: () => context.pushRoute(FolderRoute()), ), ListTile( leading: const Icon(Icons.lock_outline_rounded, size: 26), title: Text( - IntlKeys.locked_folder.tr(), + context.t.locked_folder, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const LockedRoute()), ), ListTile( leading: const Icon(Icons.group_outlined, size: 26), - title: Text( - IntlKeys.partners.tr(), - style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), - ), + title: Text(context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)), onTap: () => context.pushRoute(const PartnerRoute()), ), PartnerList(partners: partners), @@ -221,12 +215,7 @@ class PeopleCollectionCard extends ConsumerWidget { mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), children: people.take(4).map((person) { - return CircleAvatar( - backgroundImage: NetworkImage( - getFaceThumbnailUrl(person.id), - headers: ApiService.getRequestHeaders(), - ), - ); + return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); }).toList(), ); }, @@ -235,7 +224,7 @@ class PeopleCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.people.tr(), + context.t.people, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -295,7 +284,7 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.on_this_device.tr(), + context.t.on_this_device, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -346,7 +335,7 @@ class PlacesCollectionCard extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - IntlKeys.places.tr(), + context.t.places, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 375d4d2a96..bff52df6da 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; @@ -17,7 +17,6 @@ class PeopleCollectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final people = ref.watch(getAllPeopleProvider); - final headers = ApiService.getRequestHeaders(); final formFocus = useFocusNode(); final ValueNotifier search = useState(null); @@ -88,7 +87,7 @@ class PeopleCollectionPage extends HookConsumerWidget { elevation: 3, child: CircleAvatar( maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers), + backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), ), ), diff --git a/mobile/lib/pages/library/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index f376709316..a4a6f66915 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -10,9 +9,10 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.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'; @@ -113,6 +113,7 @@ class PlaceTile extends StatelessWidget { camera: SearchCameraFilter(), date: SearchDateFilter(), display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: SearchRatingFilter(), mediaType: AssetType.other, ), ), @@ -124,13 +125,10 @@ class PlaceTile extends StatelessWidget { title: Text(name, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500)), leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(20)), - child: CachedNetworkImage( + child: SizedBox( width: 80, height: 80, - fit: BoxFit.cover, - imageUrl: thumbnailUrl, - httpHeaders: ApiService.getRequestHeaders(), - errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), + child: Thumbnail(imageProvider: RemoteImageProvider(url: thumbnailUrl)), ), ), ); 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 1d7eaef080..47a3dd853d 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 @@ -29,6 +29,8 @@ class SharedLinkEditPage extends HookConsumerWidget { final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); final passwordController = useTextEditingController(text: existingLink?.password ?? ""); + final slugController = useTextEditingController(text: existingLink?.slug ?? ""); + final slugFocusNode = useFocusNode(); final showMetadata = useState(existingLink?.showMetadata ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true); final allowUpload = useState(existingLink?.allowUpload ?? false); @@ -108,6 +110,26 @@ class SharedLinkEditPage extends HookConsumerWidget { ); } + Widget buildSlugField() { + return TextField( + controller: slugController, + enabled: newShareLink.value.isEmpty, + focusNode: slugFocusNode, + textInputAction: TextInputAction.done, + autofocus: false, + decoration: InputDecoration( + labelText: 'custom_url'.tr(), + labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + border: const OutlineInputBorder(), + hintText: 'custom_url'.tr(), + hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), + disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))), + ), + onTapOutside: (_) => slugFocusNode.unfocus(), + ); + } + Widget buildShowMetaButton() { return SwitchListTile.adaptive( value: showMetadata.value, @@ -261,6 +283,7 @@ class SharedLinkEditPage extends HookConsumerWidget { allowUpload: allowUpload.value, description: descriptionController.text.isEmpty ? null : descriptionController.text, password: passwordController.text.isEmpty ? null : passwordController.text, + slug: slugController.text.isEmpty ? null : slugController.text, expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), ); ref.invalidate(sharedLinksStateProvider); @@ -274,7 +297,10 @@ class SharedLinkEditPage extends HookConsumerWidget { } if (newLink != null && serverUrl != null) { - newShareLink.value = "${serverUrl}share/${newLink.key}"; + final hasSlug = newLink.slug?.isNotEmpty == true; + final urlPath = hasSlug ? newLink.slug : newLink.key; + final basePath = hasSlug ? 's' : 'share'; + newShareLink.value = "$serverUrl$basePath/$urlPath"; copyLinkToClipboard(); } else if (newLink == null) { ImmichToast.show( @@ -292,6 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget { bool? meta; String? desc; String? password; + String? slug; DateTime? expiry; bool? changeExpiry; @@ -315,6 +342,12 @@ class SharedLinkEditPage extends HookConsumerWidget { password = passwordController.text; } + if (slugController.text != (existingLink!.slug ?? "")) { + slug = slugController.text.isEmpty ? null : slugController.text; + } else { + slug = existingLink!.slug; + } + if (editExpiry.value) { expiry = expiryAfter.value == 0 ? null : calculateExpiry(); changeExpiry = true; @@ -329,6 +362,7 @@ class SharedLinkEditPage extends HookConsumerWidget { allowUpload: upload, description: desc, password: password, + slug: slug, expiresAt: expiry, changeExpiry: changeExpiry, ); @@ -349,6 +383,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()), Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()), Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()), + Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()), Padding( padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding), child: buildShowMetaButton(), diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index e1d551900f..5f40b32baa 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -41,7 +41,7 @@ class LoginPage extends HookConsumerWidget { style: TextStyle( color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, - fontFamily: "Inconsolata", + fontFamily: "GoogleSansCode", ), ), const Text(' '), @@ -51,7 +51,7 @@ class LoginPage extends HookConsumerWidget { style: TextStyle( color: context.primaryColor, fontWeight: FontWeight.bold, - fontFamily: "Inconsolata", + fontFamily: "GoogleSansCode", ), ), onTap: () { diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index a93b826f03..993b91d8f7 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -118,7 +118,7 @@ class MapPage extends HookConsumerWidget { } // finds the nearest asset marker from the tap point and store it as the selectedMarker - Future onMarkerClicked(Point point, LatLng coords) async { + Future onMarkerClicked(Point point, LatLng _) async { // Guard map not created if (mapController.value == null) { return; @@ -370,6 +370,7 @@ class _MapWithMarker extends StatelessWidget { ? PositionedAssetMarkerIcon( point: value.point, assetRemoteId: value.marker.assetRemoteId, + assetThumbhash: '', durationInMilliseconds: value.shouldAnimate ? 100 : 0, onTap: onMarkerTapped, ) 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 a2c927c6bd..3dace15ced 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -28,7 +28,7 @@ class MapLocationPickerPage extends HookConsumerWidget { marker.value = await controller.value?.addMarkerAtLatLng(initialLatLng); } - Future onMapClick(Point point, LatLng centre) async { + Future onMapClick(Point _, LatLng centre) async { selectedLatLng.value = centre; await controller.value?.animateCamera(CameraUpdate.newLatLng(centre)); if (marker.value != null) { diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart index 7d2e612d25..8375eb14fd 100644 --- a/mobile/lib/pages/search/person_result.page.dart +++ b/mobile/lib/pages/search/person_result.page.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -88,10 +88,7 @@ class PersonResultPage extends HookConsumerWidget { padding: const EdgeInsets.only(left: 8.0, top: 24), child: Row( children: [ - CircleAvatar( - radius: 36, - backgroundImage: NetworkImage(getFaceThumbnailUrl(personId), headers: ApiService.getRequestHeaders()), - ), + CircleAvatar(radius: 36, backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(personId))), Expanded( child: Padding(padding: const EdgeInsets.only(left: 16.0, right: 16.0), child: buildTitleBlock()), ), diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 902110f6a8..dbd32ac94b 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -43,6 +43,7 @@ class SearchPage extends HookConsumerWidget { date: prefilter?.date ?? SearchDateFilter(), display: prefilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), mediaType: prefilter?.mediaType ?? AssetType.other, + rating: prefilter?.rating ?? SearchRatingFilter(), language: "${context.locale.languageCode}-${context.locale.countryCode}", ), ); diff --git a/mobile/lib/pages/settings/sync_status.page.dart b/mobile/lib/pages/settings/sync_status.page.dart index d54ba89e5d..58750e9e30 100644 --- a/mobile/lib/pages/settings/sync_status.page.dart +++ b/mobile/lib/pages/settings/sync_status.page.dart @@ -18,6 +18,7 @@ class SyncStatusPage extends StatelessWidget { splashRadius: 24, icon: const Icon(Icons.arrow_back_ios_rounded), ), + centerTitle: false, ), body: const SyncStatusAndActions(), ); diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 9d2dbe80c2..2be51fbfc9 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -1,7 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -12,7 +11,7 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/url_helper.dart'; @RoutePage() -class ShareIntentPage extends HookConsumerWidget { +class ShareIntentPage extends ConsumerWidget { const ShareIntentPage({super.key, required this.attachments}); final List attachments; @@ -21,12 +20,13 @@ class ShareIntentPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl() ?? '--'; final candidates = ref.watch(shareIntentUploadProvider); - final isUploaded = useState(false); - useOnAppLifecycleStateChange((previous, current) { - if (current == AppLifecycleState.resumed) { - isUploaded.value = false; - } - }); + + final isUploading = candidates.any((candidate) => candidate.status == UploadStatus.running); + final isUploaded = + candidates.isNotEmpty && + candidates.every( + (candidate) => candidate.status == UploadStatus.complete || candidate.status == UploadStatus.failed, + ); void removeAttachment(ShareIntentAttachment attachment) { ref.read(shareIntentUploadProvider.notifier).removeAttachment(attachment); @@ -37,11 +37,8 @@ class ShareIntentPage extends HookConsumerWidget { } void upload() async { - for (final attachment in candidates) { - await ref.read(shareIntentUploadProvider.notifier).upload(attachment.file); - } - - isUploaded.value = true; + final files = candidates.map((candidate) => candidate.file).toList(); + await ref.read(shareIntentUploadProvider.notifier).uploadAll(files); } bool isSelected(ShareIntentAttachment attachment) { @@ -84,7 +81,7 @@ class ShareIntentPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 16), child: LargeLeadingTile( onTap: () => toggleSelection(attachment), - disabled: isUploaded.value, + disabled: isUploading || isUploaded, selected: isSelected(attachment), leading: Stack( children: [ @@ -131,8 +128,8 @@ class ShareIntentPage extends HookConsumerWidget { child: SizedBox( height: 48, child: ElevatedButton( - onPressed: isUploaded.value ? null : upload, - child: isUploaded.value ? UploadingText(candidates: candidates) : const Text('upload').tr(), + onPressed: (isUploading || isUploaded) ? null : upload, + child: (isUploading || isUploaded) ? UploadingText(candidates: candidates) : const Text('upload').tr(), ), ), ), @@ -204,14 +201,7 @@ class UploadStatusIcon extends StatelessWidget { ], ), UploadStatus.complete => Icon(Icons.check_circle_rounded, color: Colors.green, semanticLabel: 'completed'.tr()), - UploadStatus.notFound || UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: 'failed'.tr()), - UploadStatus.canceled => Icon(Icons.cancel_rounded, color: Colors.red, semanticLabel: 'canceled'.tr()), - UploadStatus.waitingToRetry || UploadStatus.paused => Icon( - Icons.pause_circle_rounded, - color: context.primaryColor, - semanticLabel: 'paused'.tr(), - ), }; return statusIcon; diff --git a/mobile/lib/platform/local_image_api.g.dart b/mobile/lib/platform/local_image_api.g.dart new file mode 100644 index 0000000000..f23cb86ced --- /dev/null +++ b/mobile/lib/platform/local_image_api.g.dart @@ -0,0 +1,139 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +class LocalImageApi { + /// Constructor for [LocalImageApi]. 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. + LocalImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future?> requestImage( + String assetId, { + required int requestId, + required int width, + required int height, + required bool isVideo, + required bool preferEncoded, + }) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + assetId, + requestId, + width, + height, + isVideo, + preferEncoded, + ]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as Map?)?.cast(); + } + } + + Future cancelRequest(int requestId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future> getThumbhash(String thumbhash) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as Map?)!.cast(); + } + } +} diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index 1c3b4b083e..6681912c2f 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -29,6 +29,8 @@ bool _deepEquals(Object? a, Object? b) { return a == b; } +enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping } + class PlatformAsset { PlatformAsset({ required this.id, @@ -44,6 +46,7 @@ class PlatformAsset { this.adjustmentTime, this.latitude, this.longitude, + required this.playbackStyle, }); String id; @@ -72,6 +75,8 @@ class PlatformAsset { double? longitude; + PlatformAssetPlaybackStyle playbackStyle; + List _toList() { return [ id, @@ -87,6 +92,7 @@ class PlatformAsset { adjustmentTime, latitude, longitude, + playbackStyle, ]; } @@ -110,6 +116,7 @@ class PlatformAsset { adjustmentTime: result[10] as int?, latitude: result[11] as double?, longitude: result[12] as double?, + playbackStyle: result[13]! as PlatformAssetPlaybackStyle, ); } @@ -270,6 +277,45 @@ class HashResult { int get hashCode => Object.hashAll(_toList()); } +class CloudIdResult { + CloudIdResult({required this.assetId, this.error, this.cloudId}); + + String assetId; + + String? error; + + String? cloudId; + + List _toList() { + return [assetId, error, cloudId]; + } + + Object encode() { + return _toList(); + } + + static CloudIdResult decode(Object result) { + result as List; + return CloudIdResult(assetId: result[0]! as String, error: result[1] as String?, cloudId: result[2] as String?); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! CloudIdResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -277,18 +323,24 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformAsset) { + } else if (value is PlatformAssetPlaybackStyle) { buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else if (value is PlatformAlbum) { + writeValue(buffer, value.index); + } else if (value is PlatformAsset) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SyncDelta) { + } else if (value is PlatformAlbum) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is HashResult) { + } else if (value is SyncDelta) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is HashResult) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is CloudIdResult) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -298,13 +350,18 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - return PlatformAsset.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : PlatformAssetPlaybackStyle.values[value]; case 130: - return PlatformAlbum.decode(readValue(buffer)!); + return PlatformAsset.decode(readValue(buffer)!); case 131: - return SyncDelta.decode(readValue(buffer)!); + return PlatformAlbum.decode(readValue(buffer)!); case 132: + return SyncDelta.decode(readValue(buffer)!); + case 133: return HashResult.decode(readValue(buffer)!); + case 134: + return CloudIdResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -616,4 +673,32 @@ class NativeSyncApi { return (pigeonVar_replyList[0] as Map?)!.cast>(); } } + + Future> getCloudIdForAssetIds(List assetIds) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } } diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart new file mode 100644 index 0000000000..6ddb3cdb71 --- /dev/null +++ b/mobile/lib/platform/network_api.g.dart @@ -0,0 +1,232 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]), + ); + } + return a == b; +} + +class ClientCertData { + ClientCertData({required this.data, required this.password}); + + Uint8List data; + + String password; + + List _toList() { + return [data, password]; + } + + Object encode() { + return _toList(); + } + + static ClientCertData decode(Object result) { + result as List; + return ClientCertData(data: result[0]! as Uint8List, password: result[1]! as String); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ClientCertData || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class ClientCertPrompt { + ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm}); + + String title; + + String message; + + String cancel; + + String confirm; + + List _toList() { + return [title, message, cancel, confirm]; + } + + Object encode() { + return _toList(); + } + + static ClientCertPrompt decode(Object result) { + result as List; + return ClientCertPrompt( + title: result[0]! as String, + message: result[1]! as String, + cancel: result[2]! as String, + confirm: result[3]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! ClientCertPrompt || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is ClientCertData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is ClientCertPrompt) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return ClientCertData.decode(readValue(buffer)!); + case 130: + return ClientCertPrompt.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NetworkApi { + /// Constructor for [NetworkApi]. 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. + NetworkApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future addCertificate(ClientCertData clientData) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([clientData]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future selectCertificate(ClientCertPrompt promptText) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as ClientCertData?)!; + } + } + + Future removeCertificate() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} diff --git a/mobile/lib/platform/remote_image_api.g.dart b/mobile/lib/platform/remote_image_api.g.dart new file mode 100644 index 0000000000..24390293c9 --- /dev/null +++ b/mobile/lib/platform/remote_image_api.g.dart @@ -0,0 +1,135 @@ +// Autogenerated from Pigeon (v26.0.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + default: + return super.readValueOfType(type, buffer); + } + } +} + +class RemoteImageApi { + /// Constructor for [RemoteImageApi]. 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. + RemoteImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future?> requestImage( + String url, { + required Map headers, + required int requestId, + required bool preferEncoded, + }) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + url, + headers, + requestId, + preferEncoded, + ]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as Map?)?.cast(); + } + } + + Future cancelRequest(int requestId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future clearCache() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as int?)!; + } + } +} diff --git a/mobile/lib/presentation/pages/cleanup_preview.page.dart b/mobile/lib/presentation/pages/cleanup_preview.page.dart new file mode 100644 index 0000000000..556ed6412f --- /dev/null +++ b/mobile/lib/presentation/pages/cleanup_preview.page.dart @@ -0,0 +1,42 @@ +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/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.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'; + +@RoutePage() +class CleanupPreviewPage extends StatelessWidget { + final List assets; + + const CleanupPreviewPage({super.key, required this.assets}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('cleanup_preview_title'.t(context: context, args: {'count': assets.length.toString()})), + centerTitle: true, + elevation: 0, + scrolledUnderElevation: 0, + backgroundColor: context.colorScheme.surface, + ), + body: ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith((ref) { + final timelineService = ref + .watch(timelineFactoryProvider) + .fromAssetsWithBuckets(assets.cast(), TimelineOrigin.search); + ref.onDispose(timelineService.dispose); + return timelineService; + }), + ], + child: const Timeline(appBar: null, bottomSheet: null, groupBy: GroupAssetsBy.day, readOnly: true), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/ui_showcase.page.dart b/mobile/lib/presentation/pages/dev/ui_showcase.page.dart deleted file mode 100644 index 01fe928478..0000000000 --- a/mobile/lib/presentation/pages/dev/ui_showcase.page.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_ui/immich_ui.dart'; - -List _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) { - final children = []; - - final items = [ - (variant: ImmichVariant.filled, title: "Filled Variant"), - (variant: ImmichVariant.ghost, title: "Ghost Variant"), - ]; - - for (final (:variant, :title) in items) { - children.add(Text(title)); - children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)])); - } - - return children; -} - -@RoutePage() -class ImmichUIShowcasePage extends StatelessWidget { - const ImmichUIShowcasePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Immich UI Showcase')), - body: Padding( - padding: const EdgeInsets.all(20), - child: SingleChildScrollView( - child: Column( - spacing: 10, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("IconButton", style: context.textTheme.titleLarge), - ..._showcaseBuilder( - (variant, color) => - ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onTap: () {}), - ), - Text("CloseButton", style: context.textTheme.titleLarge), - ..._showcaseBuilder((variant, color) => ImmichCloseButton(color: color, variant: variant, onTap: () {})), - ], - ), - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index ac0cd7f309..fa5737443f 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -14,13 +14,15 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da @RoutePage() class DriftActivitiesPage extends HookConsumerWidget { final RemoteAlbum album; + final String? assetId; + final String? assetName; - const DriftActivitiesPage({super.key, required this.album}); + const DriftActivitiesPage({super.key, required this.album, this.assetId, this.assetName}); @override Widget build(BuildContext context, WidgetRef ref) { - final activityNotifier = ref.read(albumActivityProvider(album.id).notifier); - final activities = ref.watch(albumActivityProvider(album.id)); + final activityNotifier = ref.read(albumActivityProvider(album.id, assetId).notifier); + final activities = ref.watch(albumActivityProvider(album.id, assetId)); final listViewScrollController = useScrollController(); void scrollToBottom() { @@ -36,7 +38,13 @@ class DriftActivitiesPage extends HookConsumerWidget { overrides: [currentRemoteAlbumScopedProvider.overrideWithValue(album)], child: Scaffold( appBar: AppBar( - title: Text(album.name), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(album.name), + if (assetName != null) Text(assetName!, style: context.textTheme.bodySmall), + ], + ), actions: [const LikeActivityActionButton(iconOnly: true)], actionsPadding: const EdgeInsets.only(right: 8), ), @@ -47,7 +55,7 @@ class DriftActivitiesPage extends HookConsumerWidget { activityWidgets.add( Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: CommentBubble(activity: activity), + child: CommentBubble(activity: activity, isAssetActivity: assetId != null), ), ); } diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index a159c6c54a..c9fed636b4 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.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/routing/router.dart'; @@ -17,36 +18,63 @@ class DriftAlbumsPage extends ConsumerStatefulWidget { } class _DriftAlbumsPageState extends ConsumerState { + final ScrollController _scrollController = ScrollController(); + Future onRefresh() async { await ref.read(remoteAlbumProvider.notifier).refresh(); } + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final albumCount = ref.watch(remoteAlbumProvider.select((state) => state.albums.length)); + final showScrollbar = albumCount > 20; + + final scrollView = CustomScrollView( + controller: _scrollController, + slivers: [ + ImmichSliverAppBar( + snap: false, + floating: false, + pinned: true, + actions: [ + IconButton( + onPressed: () => context.pushRoute(const DriftCreateAlbumRoute()), + icon: const Icon(Icons.add_rounded), + ), + ], + showUploadButton: false, + ), + AlbumSelector( + onAlbumSelected: (album) { + context.router.push(RemoteAlbumRoute(album: album)); + }, + ), + ], + ); + 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) { - context.router.push(RemoteAlbumRoute(album: album)); - }, - ), - ], - ), + child: showScrollbar + ? RawScrollbar( + controller: _scrollController, + interactive: true, + thickness: 8, + radius: const Radius.circular(4), + thumbVisibility: false, + thumbColor: context.colorScheme.primary, + crossAxisMargin: 4, + mainAxisMargin: 60, + minThumbLength: 40, + child: scrollView, + ) + : scrollView, ); } } diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 9db6e98613..061edbaf26 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -149,7 +149,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), @@ -169,7 +169,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { itemBuilder: (context, index) { final user = sharedUsers[index]; return ListTile( - leading: UserCircleAvatar(user: user, radius: 22), + leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) : const SizedBox(), diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 2b7034770b..9da21c72ee 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -118,6 +118,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection ), _PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()), _PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId), + _PropertyItem(label: 'Is Edited', value: asset.isEdited.toString()), ]); } @@ -131,6 +132,7 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id); properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', '))); if (CurrentPlatform.isIOS) { + properties.add(_PropertyItem(label: 'Cloud ID', value: asset.cloudId)); properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString())); } properties.add( diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index d1d663e4f4..4708b5e615 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -12,8 +12,8 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/partner.provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_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'; @@ -179,12 +179,7 @@ class _PeopleCollectionCard extends ConsumerWidget { mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), children: people.take(4).map((person) { - return CircleAvatar( - backgroundImage: NetworkImage( - getFaceThumbnailUrl(person.id), - headers: ApiService.getRequestHeaders(), - ), - ); + return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); }).toList(), ); }, diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index de8dde7714..96384c97e5 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/map/map.widget.dart'; +import 'package:immich_mobile/presentation/widgets/map/map_settings_sheet.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() @@ -10,6 +11,16 @@ class DriftMapPage extends StatelessWidget { const DriftMapPage({super.key, this.initialLocation}); + void onSettingsPressed(BuildContext context) { + showModalBottomSheet( + elevation: 0.0, + showDragHandle: true, + isScrollControlled: true, + context: context, + builder: (_) => const DriftMapSettingsSheet(), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -18,8 +29,8 @@ class DriftMapPage extends StatelessWidget { children: [ DriftMap(initialLocation: initialLocation), Positioned( - left: 16, - top: 60, + left: 20, + top: 70, child: IconButton.filled( color: Colors.white, onPressed: () => context.pop(), @@ -32,6 +43,21 @@ class DriftMapPage extends StatelessWidget { ), ), ), + Positioned( + right: 20, + top: 70, + child: IconButton.filled( + color: Colors.white, + onPressed: () => onSettingsPressed(context), + icon: const Icon(Icons.more_vert_rounded), + style: IconButton.styleFrom( + padding: const EdgeInsets.all(8), + backgroundColor: Colors.indigo, + shadowColor: Colors.black26, + elevation: 4, + ), + ), + ), ], ), ); diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 9042f2f1f5..147165f2a3 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.wid 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/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart'; diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index ca4e20aad0..f73dac3af2 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -4,8 +4,8 @@ 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/infrastructure/people.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -31,7 +31,6 @@ class _DriftPeopleCollectionPageState extends ConsumerState { unawaited(context.pushRoute(DriftActivitiesRoute(album: _album))); } - Future showOptionSheet(BuildContext context) async { - final user = ref.watch(currentUserProvider); - final isOwner = user != null ? user.id == _album.ownerId : false; - final canAddPhotos = - await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor; - - unawaited( - 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: isOwner || canAddPhotos - ? () async { - await addAssets(context); - context.pop(); - } - : null, - onToggleAlbumOrder: isOwner - ? () async { - await toggleAlbumOrder(); - context.pop(); - } - : null, - onEditAlbum: isOwner - ? () async { - context.pop(); - await showEditTitleAndDescription(context); - } - : null, - onCreateSharedLink: isOwner - ? () async { - context.pop(); - unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))); - } - : null, - onShowOptions: () { - context.pop(); - context.pushRoute(DriftAlbumOptionsRoute(album: _album)); - }, - ); - }, - ), - ); - } - @override Widget build(BuildContext context) { final user = ref.watch(currentUserProvider); @@ -249,8 +188,16 @@ class _RemoteAlbumPageState extends ConsumerState { child: Timeline( appBar: RemoteAlbumSliverAppBar( icon: Icons.photo_album_outlined, - onShowOptions: () => showOptionSheet(context), - onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, + kebabMenu: _AlbumKebabMenu( + album: _album, + onDeleteAlbum: () => deleteAlbum(context), + onAddUsers: () => addUsers(context), + onAddPhotos: () => addAssets(context), + onToggleAlbumOrder: () => toggleAlbumOrder(), + onEditAlbum: () => showEditTitleAndDescription(context), + onCreateSharedLink: () => unawaited(context.pushRoute(SharedLinkEditRoute(albumId: _album.id))), + onShowOptions: () => context.pushRoute(DriftAlbumOptionsRoute(album: _album)), + ), onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, onActivity: () => showActivity(context), ), @@ -414,3 +361,77 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { ); } } + +class _AlbumKebabMenu extends ConsumerWidget { + final RemoteAlbum album; + final VoidCallback? onDeleteAlbum; + final VoidCallback? onAddUsers; + final VoidCallback? onAddPhotos; + final VoidCallback? onToggleAlbumOrder; + final VoidCallback? onEditAlbum; + final VoidCallback? onCreateSharedLink; + final VoidCallback? onShowOptions; + + const _AlbumKebabMenu({ + required this.album, + this.onDeleteAlbum, + this.onAddUsers, + this.onAddPhotos, + this.onToggleAlbumOrder, + this.onEditAlbum, + this.onCreateSharedLink, + this.onShowOptions, + }); + + 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, WidgetRef ref) { + final settings = context.dependOnInheritedWidgetOfExactType(); + final scrollProgress = _calculateScrollProgress(settings); + + final iconColor = Color.lerp(Colors.white, context.primaryColor, scrollProgress); + final iconShadows = [ + 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), + ]; + + final user = ref.watch(currentUserProvider); + final isOwner = user != null && user.id == album.ownerId; + + return FutureBuilder( + future: ref + .read(remoteAlbumServiceProvider) + .getUserRole(album.id, user?.id ?? '') + .then((role) => role == AlbumUserRole.editor), + builder: (context, snapshot) { + final canAddPhotos = snapshot.data ?? false; + + return DriftRemoteAlbumOption( + iconColor: iconColor, + iconShadows: iconShadows, + onDeleteAlbum: isOwner ? onDeleteAlbum : null, + onAddUsers: isOwner ? onAddUsers : null, + onAddPhotos: isOwner || canAddPhotos ? onAddPhotos : null, + onToggleAlbumOrder: isOwner ? onToggleAlbumOrder : null, + onEditAlbum: isOwner ? onEditAlbum : null, + onCreateSharedLink: isOwner ? onCreateSharedLink : null, + onShowOptions: onShowOptions, + ); + }, + ); + } +} diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index 8713166027..a85f69a75e 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -2,6 +2,7 @@ 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/generated/translations.g.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'; @@ -43,9 +44,7 @@ class DriftTrashPage extends StatelessWidget { return SliverPadding( padding: const EdgeInsets.all(16.0), - sliver: SliverToBoxAdapter( - child: const Text("trash_page_info").t(context: context, args: {"days": "$trashDays"}), - ), + sliver: SliverToBoxAdapter(child: Text(context.t.trash_page_info(days: trashDays))), ); }, ), diff --git a/mobile/lib/presentation/pages/editing/drift_crop.page.dart b/mobile/lib/presentation/pages/editing/drift_crop.page.dart index 1692140cd2..a213e4c640 100644 --- a/mobile/lib/presentation/pages/editing/drift_crop.page.dart +++ b/mobile/lib/presentation/pages/editing/drift_crop.page.dart @@ -37,7 +37,7 @@ class DriftCropImagePage extends HookWidget { icon: Icons.done_rounded, color: ImmichColor.primary, variant: ImmichVariant.ghost, - onTap: () async { + onPressed: () async { final croppedImage = await cropController.croppedImage(); unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); }, @@ -79,13 +79,13 @@ class DriftCropImagePage extends HookWidget { icon: Icons.rotate_left, variant: ImmichVariant.ghost, color: ImmichColor.secondary, - onTap: () => cropController.rotateLeft(), + onPressed: () => cropController.rotateLeft(), ), ImmichIconButton( icon: Icons.rotate_right, variant: ImmichVariant.ghost, color: ImmichColor.secondary, - onTap: () => cropController.rotateRight(), + onPressed: () => cropController.rotateRight(), ), ], ), diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart index f9903b6b94..a10202973d 100644 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/editing/drift_edit.page.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'dart:ui'; import 'package:auto_route/auto_route.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,7 +12,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/image_converter.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; @@ -32,23 +33,6 @@ class DriftEditImagePage extends ConsumerWidget { final bool isEdited; const DriftEditImagePage({super.key, required this.asset, required this.image, required this.isEdited}); - Future _imageToUint8List(Image image) async { - final Completer completer = Completer(); - image.image - .resolve(const ImageConfiguration()) - .addListener( - ImageStreamListener((ImageInfo info, bool _) { - info.image.toByteData(format: ImageByteFormat.png).then((byteData) { - if (byteData != null) { - completer.complete(byteData.buffer.asUint8List()); - } else { - completer.completeError('Failed to convert image to bytes'); - } - }); - }, onError: (exception, stackTrace) => completer.completeError(exception)), - ); - return completer.future; - } void _exitEditing(BuildContext context) { // this assumes that the only way to get to this page is from the AssetViewerRoute @@ -57,7 +41,7 @@ class DriftEditImagePage extends ConsumerWidget { Future _saveEditedImage(BuildContext context, BaseAsset asset, Image image, WidgetRef ref) async { try { - final Uint8List imageData = await _imageToUint8List(image); + final Uint8List imageData = await imageToUint8List(image); LocalAsset? localAsset; try { @@ -78,7 +62,7 @@ class DriftEditImagePage extends ConsumerWidget { return; } - await ref.read(uploadServiceProvider).manualBackup([localAsset]); + await ref.read(foregroundUploadServiceProvider).uploadManual([localAsset], CancellationToken()); } catch (e) { ImmichToast.show( durationInSecond: 6, diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart new file mode 100644 index 0000000000..f460633cbb --- /dev/null +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:crop_image/crop_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.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/images/image_provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/image_converter.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; + +@RoutePage() +class ProfilePictureCropPage extends ConsumerStatefulWidget { + final BaseAsset asset; + + const ProfilePictureCropPage({super.key, required this.asset}); + + @override + ConsumerState createState() => _ProfilePictureCropPageState(); +} + +class _ProfilePictureCropPageState extends ConsumerState { + late final CropController _cropController; + bool _isLoading = false; + bool _didInitCropController = false; + + @override + void initState() { + super.initState(); + _cropController = CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1)); + + // Lock aspect ratio to 1:1 for circular/square crop + // CropController depends on CropImage initializing its bitmap size. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _didInitCropController) { + return; + } + _didInitCropController = true; + + _cropController.crop = const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9); + _cropController.aspectRatio = 1.0; + }); + } + + @override + void dispose() { + _cropController.dispose(); + super.dispose(); + } + + Future _handleDone() async { + if (_isLoading) return; + + setState(() { + _isLoading = true; + }); + + try { + final croppedImage = await _cropController.croppedImage(); + final pngBytes = await imageToUint8List(croppedImage); + final xFile = XFile.fromData(pngBytes, mimeType: 'image/png'); + final success = await ref + .read(uploadProfileImageProvider.notifier) + .upload(xFile, fileName: 'profile-picture.png'); + + if (!context.mounted) return; + + if (success) { + final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath; + ref.read(authProvider.notifier).updateUserProfileImagePath(profileImagePath); + final user = ref.read(currentUserProvider); + if (user != null) { + unawaited(ref.read(currentUserProvider.notifier).refresh()); + } + unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + + ImmichToast.show( + context: context, + msg: 'profile_picture_set'.tr(), + gravity: ToastGravity.BOTTOM, + toastType: ToastType.success, + ); + + if (context.mounted) { + unawaited(context.maybePop()); + } + } else { + ImmichToast.show( + context: context, + msg: 'errors.unable_to_set_profile_picture'.tr(), + toastType: ToastType.error, + gravity: ToastGravity.BOTTOM, + ); + } + } catch (e) { + if (!context.mounted) return; + + ImmichToast.show( + context: context, + msg: 'errors.unable_to_set_profile_picture'.tr(), + toastType: ToastType.error, + gravity: ToastGravity.BOTTOM, + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + // Create Image widget from asset + final image = Image(image: getFullImageProvider(widget.asset)); + + return Scaffold( + appBar: AppBar( + backgroundColor: context.scaffoldBackgroundColor, + title: Text("set_profile_picture".tr()), + leading: _isLoading ? null : const ImmichCloseButton(), + actions: [ + if (_isLoading) + const Padding( + padding: EdgeInsets.all(16.0), + child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)), + ) + else + ImmichIconButton( + icon: Icons.done_rounded, + color: ImmichColor.primary, + variant: ImmichVariant.ghost, + onPressed: _handleDone, + ), + ], + ), + backgroundColor: context.scaffoldBackgroundColor, + body: SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(7)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: ClipRRect( + child: CropImage(controller: _cropController, image: image, gridColor: Colors.white), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 58ca892f5f..0ce3f20641 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -7,6 +7,7 @@ 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/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -18,10 +19,13 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_s import 'package:immich_mobile/presentation/widgets/search/quick_date_picker.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_metadata.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/feature_check.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:immich_mobile/widgets/common/tag_picker.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'; @@ -30,6 +34,7 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar 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'; +import 'package:immich_mobile/widgets/search/search_filter/star_rating_picker.dart'; @RoutePage() class DriftSearchPage extends HookConsumerWidget { @@ -37,8 +42,15 @@ class DriftSearchPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textSearchType = useState(TextSearchType.context); - final searchHintText = useState('sunrise_on_the_beach'.t(context: context)); + final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures)); + final textSearchType = useState( + serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, + ); + final searchHintText = useState( + serverFeatures.smartSearch + ? 'sunrise_on_the_beach'.t(context: context) + : 'file_name_or_extension'.t(context: context), + ); final textSearchController = useTextEditingController(); final preFilter = ref.watch(searchPreFilterProvider); final filter = useState( @@ -48,9 +60,11 @@ class DriftSearchPage extends HookConsumerWidget { camera: preFilter?.camera ?? SearchCameraFilter(), date: preFilter?.date ?? SearchDateFilter(), display: preFilter?.display ?? SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: preFilter?.rating ?? SearchRatingFilter(), mediaType: preFilter?.mediaType ?? AssetType.other, language: "${context.locale.languageCode}-${context.locale.countryCode}", assetId: preFilter?.assetId, + tagIds: preFilter?.tagIds ?? [], ), ); @@ -61,11 +75,15 @@ class DriftSearchPage extends HookConsumerWidget { final dateRangeCurrentFilterWidget = useState(null); final cameraCurrentFilterWidget = useState(null); final locationCurrentFilterWidget = useState(null); + final tagCurrentFilterWidget = useState(null); final mediaTypeCurrentFilterWidget = useState(null); + final ratingCurrentFilterWidget = useState(null); final displayOptionCurrentFilterWidget = useState(null); final isSearching = useState(false); + final userPreferences = ref.watch(userMetadataPreferencesProvider); + SnackBar searchInfoSnackBar(String message) { return SnackBar( content: Text(message, style: context.textTheme.labelLarge), @@ -132,10 +150,12 @@ class DriftSearchPage extends HookConsumerWidget { 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, - ); + final label = value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); + if (label.isNotEmpty) { + peopleCurrentFilterWidget.value = Text(label, style: context.textTheme.labelLarge); + } else { + peopleCurrentFilterWidget.value = null; + } } handleClear() { @@ -161,6 +181,42 @@ class DriftSearchPage extends HookConsumerWidget { ); } + showTagPicker() { + handleOnSelect(Iterable tags) { + filter.value = filter.value.copyWith(tagIds: tags.map((t) => t.id).toList()); + final label = tags.map((t) => t.value).join(', '); + if (label.isEmpty) { + tagCurrentFilterWidget.value = null; + } else { + tagCurrentFilterWidget.value = Text( + label.isEmpty ? 'tags'.t(context: context) : label, + style: context.textTheme.labelLarge, + ); + } + } + + handleClear() { + filter.value = filter.value.copyWith(tagIds: []); + tagCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_tags_title'.t(context: context), + expanded: true, + onSearch: search, + onClear: handleClear, + child: TagPicker(onSelect: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), + ), + ), + ); + } + showLocationPicker() { handleOnSelect(Map value) { filter.value = filter.value.copyWith( @@ -369,6 +425,35 @@ class DriftSearchPage extends HookConsumerWidget { ); } + // STAR RATING PICKER + showStarRatingPicker() { + handleOnSelected(SearchRatingFilter rating) { + filter.value = filter.value.copyWith(rating: rating); + + ratingCurrentFilterWidget.value = Text( + 'rating_count'.t(args: {'count': rating.rating!}), + style: context.textTheme.labelLarge, + ); + } + + handleClear() { + filter.value = filter.value.copyWith(rating: SearchRatingFilter(rating: null)); + ratingCurrentFilterWidget.value = null; + search(); + } + + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FilterBottomSheetScaffold( + title: 'rating'.t(context: context), + onSearch: search, + onClear: handleClear, + child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), + ), + ); + } + // DISPLAY OPTION showDisplayOptionPicker() { handleOnSelect(Map value) { @@ -481,23 +566,26 @@ class DriftSearchPage extends HookConsumerWidget { ); }, 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, + FeatureCheck( + feature: (features) => features.smartSearch, + child: 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, ), - selectedColor: context.colorScheme.primary, - selected: textSearchType.value == TextSearchType.context, + onPressed: () { + textSearchType.value = TextSearchType.context; + searchHintText.value = 'sunrise_on_the_beach'.t(context: context); + }, ), - onPressed: () { - textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.t(context: context); - }, ), MenuItemButton( child: ListTile( @@ -610,6 +698,13 @@ class DriftSearchPage extends HookConsumerWidget { label: 'search_filter_location'.t(context: context), currentFilter: locationCurrentFilterWidget.value, ), + if (userPreferences.valueOrNull?.tagsEnabled ?? false) + SearchFilterChip( + icon: Icons.sell_outlined, + onTap: showTagPicker, + label: 'tags'.t(context: context), + currentFilter: tagCurrentFilterWidget.value, + ), SearchFilterChip( icon: Icons.camera_alt_outlined, onTap: showCameraPicker, @@ -629,6 +724,13 @@ class DriftSearchPage extends HookConsumerWidget { label: 'search_filter_media_type'.t(context: context), currentFilter: mediaTypeCurrentFilterWidget.value, ), + if (userPreferences.valueOrNull?.ratingsEnabled ?? false) + SearchFilterChip( + icon: Icons.star_outline_rounded, + onTap: showStarRatingPicker, + label: 'search_filter_star_rating'.t(context: context), + currentFilter: ratingCurrentFilterWidget.value, + ), SearchFilterChip( icon: Icons.display_settings_outlined, onTap: showDisplayOptionPicker, diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index 23cd19f363..4162f43a24 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.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/routes.provider.dart'; 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 index 675b5bf219..1ca875e483 100644 --- a/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/base_action_button.widget.dart @@ -53,7 +53,7 @@ class BaseActionButton extends ConsumerWidget { style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)), leadingIcon: Icon(iconData, color: effectiveIconColor), onPressed: onPressed, - child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)), + child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16, color: iconColor)), ); } 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 index 2dd9a265ed..710ec506c2 100644 --- 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 @@ -8,6 +8,7 @@ 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/asset_grid/permanent_delete_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// This delete action has the following behavior: @@ -25,6 +26,15 @@ class DeletePermanentActionButton extends ConsumerWidget { return; } + final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length; + final confirm = + await showDialog( + context: context, + builder: (context) => PermanentDeleteDialog(count: count), + ) ?? + false; + if (!confirm) return; + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); 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 index cb0e7091c8..d19a188561 100644 --- 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 @@ -5,6 +5,7 @@ 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/asset_grid/permanent_delete_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; /// This delete action has the following behavior: @@ -22,6 +23,18 @@ class DeleteTrashActionButton extends ConsumerWidget { return; } + final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); + + final confirmDelete = + await showDialog( + context: context, + builder: (context) => PermanentDeleteDialog(count: selectCount), + ) ?? + false; + if (!confirmDelete) { + return; + } + final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart index 4c7b6ffbdc..440985a0bb 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart @@ -4,7 +4,7 @@ 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/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; class EditImageActionButton extends ConsumerWidget { diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index 8c326974a7..a44b0b5815 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart new file mode 100644 index 0000000000..1d704aafe8 --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart @@ -0,0 +1,56 @@ +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 SetAlbumCoverActionButton extends ConsumerWidget { + final String albumId; + final ActionSource source; + final bool iconOnly; + final bool menuItem; + + const SetAlbumCoverActionButton({ + super.key, + required this.albumId, + required this.source, + this.iconOnly = false, + this.menuItem = false, + }); + + void _onTap(BuildContext context, WidgetRef ref) async { + if (!context.mounted) { + return; + } + + final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); + ref.read(multiSelectProvider.notifier).reset(); + + final successMessage = 'album_cover_updated'.t(context: context); + + 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.image_outlined, + label: 'set_as_album_cover'.t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, + onPressed: () => _onTap(context, ref), + maxWidth: 100, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart new file mode 100644 index 0000000000..c8dbb7cb1f --- /dev/null +++ b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart @@ -0,0 +1,35 @@ +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/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SetProfilePictureActionButton extends ConsumerWidget { + final BaseAsset asset; + final bool iconOnly; + final bool menuItem; + + const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false}); + + void _onTap(BuildContext context) { + if (!context.mounted) { + return; + } + + context.pushRoute(ProfilePictureCropRoute(asset: asset)); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BaseActionButton( + iconData: Icons.account_circle_outlined, + label: "set_as_profile_picture".t(context: context), + iconOnly: iconOnly, + menuItem: menuItem, + onPressed: () => _onTap(context), + maxWidth: 100, + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 4f272cb990..6fbd6f7dfa 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -41,16 +42,20 @@ class ShareActionButton extends ConsumerWidget { return; } + final cancelCompleter = Completer(); + const preparingDialog = _SharePreparingDialog(); await showDialog( context: context, builder: (BuildContext buildContext) { - ref.read(actionProvider.notifier).shareAssets(source, context).then((ActionResult result) { - ref.read(multiSelectProvider.notifier).reset(); - - if (!context.mounted) { + ref.read(actionProvider.notifier).shareAssets(source, context, cancelCompleter: cancelCompleter).then(( + ActionResult result, + ) { + if (cancelCompleter.isCompleted || !context.mounted) { return; } + ref.read(multiSelectProvider.notifier).reset(); + if (!result.success) { ImmichToast.show( context: context, @@ -64,11 +69,15 @@ class ShareActionButton extends ConsumerWidget { }); // show a loading spinner with a "Preparing" message - return const _SharePreparingDialog(); + return preparingDialog; }, barrierDismissible: false, useRootNavigator: false, - ); + ).then((_) { + if (!cancelCompleter.isCompleted) { + cancelCompleter.complete(); + } + }); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index 65ba744ec3..294ddfd1f5 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -34,6 +34,7 @@ class SimilarPhotosActionButton extends ConsumerWidget { camera: SearchCameraFilter(), date: SearchDateFilter(), display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: SearchRatingFilter(), mediaType: AssetType.image, ), ); 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 index 98ef831f9c..d69c5bced3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart @@ -1,12 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; class UploadActionButton extends ConsumerWidget { final ActionSource source; @@ -20,19 +25,38 @@ class UploadActionButton extends ConsumerWidget { return; } - final result = await ref.read(actionProvider.notifier).upload(source); + final isTimeline = source == ActionSource.timeline; + List? assets; - final successMessage = 'upload_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + if (source == ActionSource.timeline) { + assets = ref.read(multiSelectProvider).selectedAssets.whereType().toList(); + if (assets.isEmpty) { + return; + } + ref.read(multiSelectProvider.notifier).reset(); + } else { + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) => const _UploadProgressDialog(), + ), + ); + } - if (context.mounted) { + final result = await ref.read(actionProvider.notifier).upload(source, assets: assets); + + if (!isTimeline && context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + + if (context.mounted && !result.success) { ImmichToast.show( context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + msg: 'scaffold_body_error_occurred'.t(context: context), gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, + toastType: ToastType.error, ); - - ref.read(multiSelectProvider.notifier).reset(); } } @@ -47,3 +71,42 @@ class UploadActionButton extends ConsumerWidget { ); } } + +class _UploadProgressDialog extends ConsumerWidget { + const _UploadProgressDialog(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progressMap = ref.watch(assetUploadProgressProvider); + + // Calculate overall progress from all assets + final values = progressMap.values.where((v) => v >= 0).toList(); + final progress = values.isEmpty ? 0.0 : values.reduce((a, b) => a + b) / values.length; + final hasError = progressMap.values.any((v) => v < 0); + final percentage = (progress * 100).toInt(); + + return AlertDialog( + title: Text('uploading'.t(context: context)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (hasError) + const Icon(Icons.error_outline, color: Colors.red, size: 48) + else + CircularProgressIndicator(value: progress > 0 ? progress : null), + const SizedBox(height: 16), + Text(hasError ? 'Error' : '$percentage%'), + ], + ), + actions: [ + ImmichTextButton( + onPressed: () { + ref.read(manualUploadCancelTokenProvider)?.cancel(); + Navigator.of(context).pop(); + }, + labelText: 'cancel'.t(context: context), + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index c42f49091f..15749fb9af 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -5,6 +5,7 @@ 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/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -14,14 +15,15 @@ import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.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/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/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -86,7 +88,7 @@ class _AlbumSelectorState extends ConsumerState { } void onSearch(String searchTerm, QuickFilterMode filterMode) { - final userId = ref.watch(currentUserProvider)?.id; + final userId = ref.read(currentUserProvider)?.id; filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode); filterAlbums(); @@ -137,6 +139,10 @@ class _AlbumSelectorState extends ConsumerState { .read(remoteAlbumProvider.notifier) .sortAlbums(ref.read(remoteAlbumProvider).albums, sort.mode, isReverse: sort.isReverse); + if (!mounted) { + return; + } + setState(() { sortedAlbums = sorted; }); @@ -148,6 +154,10 @@ class _AlbumSelectorState extends ConsumerState { Future filterAlbums() async { if (filter.query == null) { + if (!mounted) { + return; + } + setState(() { shownAlbums = sortedAlbums; }); @@ -159,6 +169,10 @@ class _AlbumSelectorState extends ConsumerState { .read(remoteAlbumProvider.notifier) .searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode); + if (!mounted) { + return; + } + setState(() { shownAlbums = filteredAlbums; }); @@ -173,7 +187,7 @@ class _AlbumSelectorState extends ConsumerState { @override Widget build(BuildContext context) { - final userId = ref.watch(currentUserProvider)?.id; + final userId = ref.watch(currentUserProvider.select((user) => user?.id)); // refilter and sort when albums change ref.listen(remoteAlbumProvider.select((state) => state.albums), (_, _) async { @@ -268,6 +282,8 @@ class _SortButtonState extends ConsumerState<_SortButton> { setState(() { albumSortOption = sortMode; isSorting = true; + // reset sort order to default state when switching option + albumSortIsReverse = false; }); } @@ -280,6 +296,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { @override Widget build(BuildContext context) { + final effectiveOrder = albumSortOption.effectiveOrder(albumSortIsReverse); return MenuAnchor( controller: widget.controller, style: MenuStyle( @@ -294,7 +311,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { .map( (sortMode) => MenuItemButton( leadingIcon: albumSortOption == sortMode - ? albumSortIsReverse + ? effectiveOrder == SortOrder.desc ? Icon( Icons.keyboard_arrow_down, color: albumSortOption == sortMode @@ -310,18 +327,17 @@ class _SortButtonState extends ConsumerState<_SortButton> { : const Icon(Icons.abc, color: Colors.transparent), onPressed: () => onMenuTapped(sortMode), style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(16, 16, 32, 16)), + padding: WidgetStateProperty.all(const EdgeInsets.fromLTRB(12, 12, 24, 12)), backgroundColor: WidgetStateProperty.all( albumSortOption == sortMode ? context.colorScheme.primary : Colors.transparent, ), shape: WidgetStateProperty.all( - const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), ), ), child: Text( sortMode.label.t(context: context), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, + style: context.textTheme.labelLarge?.copyWith( color: albumSortOption == sortMode ? context.colorScheme.onPrimary : context.colorScheme.onSurface.withAlpha(185), @@ -343,16 +359,13 @@ class _SortButtonState extends ConsumerState<_SortButton> { children: [ Padding( padding: const EdgeInsets.only(right: 5), - child: albumSortIsReverse - ? const Icon(Icons.keyboard_arrow_down) - : const Icon(Icons.keyboard_arrow_up_rounded), + child: effectiveOrder == SortOrder.desc + ? Icon(Icons.keyboard_arrow_down, color: context.colorScheme.onSurface) + : Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface), ), Text( albumSortOption.label.t(context: context), - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(225), - ), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)), ), isSorting ? SizedBox( @@ -542,7 +555,11 @@ class _QuickSortAndViewMode extends StatelessWidget { initialIsReverse: currentIsReverse, ), IconButton( - icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), + icon: Icon( + isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, + size: 24, + color: context.colorScheme.onSurface, + ), onPressed: onToggleViewMode, ), ], @@ -662,6 +679,8 @@ class _GridAlbumCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + return GestureDetector( onTap: () => onAlbumSelected(album), child: Card( @@ -680,12 +699,22 @@ class _GridAlbumCard extends ConsumerWidget { borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), child: SizedBox( width: double.infinity, - child: album.thumbnailAssetId != null - ? Thumbnail.remote(remoteId: album.thumbnailAssetId!) - : Container( - color: context.colorScheme.surfaceContainerHighest, - child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), - ), + child: FutureBuilder( + future: albumThumbnailAsset, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data != null) { + return Thumbnail.remote( + remoteId: album.thumbnailAssetId!, + thumbhash: snapshot.data!.thumbHash ?? "", + ); + } + + return Container( + color: context.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), + ); + }, + ), ), ), ), diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 561b018ef8..1aeadf61bc 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -1,12 +1,14 @@ 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/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/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -class AlbumTile extends StatelessWidget { +class AlbumTile extends ConsumerWidget { const AlbumTile({super.key, required this.album, required this.isOwner, this.onAlbumSelected}); final RemoteAlbum album; @@ -14,7 +16,9 @@ class AlbumTile extends StatelessWidget { final Function(RemoteAlbum)? onAlbumSelected; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final albumThumbnailAsset = ref.read(assetServiceProvider).getRemoteAsset(album.thumbnailAssetId ?? ""); + return LargeLeadingTile( title: Text( album.name, @@ -29,23 +33,35 @@ class AlbumTile extends StatelessWidget { ), onTap: () => onAlbumSelected?.call(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.remote(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), - ), - ), + leading: FutureBuilder( + future: albumThumbnailAsset, + builder: (context, snapshot) { + return snapshot.hasData && snapshot.data != null + ? ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: SizedBox( + width: 80, + height: 80, + child: Thumbnail.remote( + remoteId: album.thumbnailAssetId!, + thumbhash: snapshot.data!.thumbHash ?? "", + ), + ), + ) + : 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), + ), + ); + }, + ), ); } } diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart index fe5c763ec5..691b46f80d 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -88,7 +88,7 @@ class _DriftActivityTextFieldState extends ConsumerState prefixIcon: user != null ? Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30, radius: 15), + child: UserCircleAvatar(user: user, size: 30), ) : null, suffixIcon: IconButton( diff --git a/mobile/lib/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.dart deleted file mode 100644 index 3b46b69958..0000000000 --- a/mobile/lib/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.dart +++ /dev/null @@ -1,85 +0,0 @@ -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/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/activities/comment_bubble.dart'; -import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; - -class ActivitiesBottomSheet extends HookConsumerWidget { - final DraggableScrollableController controller; - final double initialChildSize; - final bool scrollToBottomInitially; - - const ActivitiesBottomSheet({ - required this.controller, - this.initialChildSize = 0.35, - this.scrollToBottomInitially = true, - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final album = ref.watch(currentRemoteAlbumProvider)!; - final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; - - final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier); - final activities = ref.watch(albumActivityProvider(album.id, asset?.id)); - - Future onAddComment(String comment) async { - await activityNotifier.addComment(comment); - } - - Widget buildActivitiesSliver() { - return activities.widgetWhen( - onLoading: () => const SliverToBoxAdapter(child: SizedBox.shrink()), - onData: (data) { - return SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - if (index == data.length) { - return const SizedBox.shrink(); - } - final activity = data[data.length - 1 - index]; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: CommentBubble(activity: activity, isAssetActivity: true), - ); - }, childCount: data.length + 1), - ); - }, - ); - } - - return BaseBottomSheet( - actions: [], - slivers: [buildActivitiesSliver()], - footer: Padding( - // TODO: avoid fixed padding, use context.padding.bottom - padding: const EdgeInsets.only(bottom: 32), - child: Column( - children: [ - const Divider(indent: 16, endIndent: 16), - DriftActivityTextField( - isEnabled: album.isActivityEnabled, - isBottomSheet: true, - // likeId: likedId, - onSubmit: onAddComment, - ), - ], - ), - ), - controller: controller, - initialChildSize: initialChildSize, - minChildSize: 0.1, - maxChildSize: 0.88, - expand: false, - shouldCloseOnMinExtent: false, - resizeOnScroll: false, - backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white, - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details.widget.dart new file mode 100644 index 0000000000..949a6917e9 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details.widget.dart @@ -0,0 +1,45 @@ +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/asset_viewer/asset_details/appears_in_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/drag_handle.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; + +class AssetDetails extends ConsumerWidget { + final double minHeight; + + const AssetDetails({required this.minHeight, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) { + return const SizedBox.shrink(); + } + return Container( + constraints: BoxConstraints(minHeight: minHeight), + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DragHandle(), + const DateTimeDetails(), + const PeopleDetails(), + const LocationDetails(), + const TechnicalDetails(), + const RatingDetails(), + const AppearsInDetails(), + SizedBox(height: context.padding.bottom + 48), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart new file mode 100644 index 0000000000..a3d6bdb8ab --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/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/presentation/widgets/album/album_tile.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class AppearsInDetails extends ConsumerWidget { + const AppearsInDetails({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null || !asset.hasRemote) return const SizedBox.shrink(); + + String? remoteAssetId; + if (asset is RemoteAsset) { + remoteAssetId = asset.id; + } else if (asset is LocalAsset) { + remoteAssetId = asset.remoteAssetId; + } + + if (remoteAssetId == null) return const SizedBox.shrink(); + + final userId = ref.watch(currentUserProvider)?.id; + final assetAlbums = ref.watch(albumsContainingAssetProvider(remoteAssetId)); + + return assetAlbums.when( + data: (albums) { + if (albums.isEmpty) return const SizedBox.shrink(); + + albums.sortBy((a) => a.name); + + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Column( + spacing: 12, + children: [ + SheetTile( + title: 'appears_in'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + spacing: 12, + children: albums.map((album) { + final isOwner = album.ownerId == userId; + return AlbumTile( + album: album, + isOwner: isOwner, + onAlbumSelected: (album) async { + ref.invalidate(assetViewerProvider); + unawaited(context.router.popAndPush(RemoteAlbumRoute(album: album))); + }, + ); + }).toList(), + ), + ), + ], + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart new file mode 100644 index 0000000000..4872bf9e75 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart @@ -0,0 +1,142 @@ +import 'dart:async'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/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/duration_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/timezone.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +const _kSeparator = ' â€ĸ '; + +class DateTimeDetails extends ConsumerWidget { + const DateTimeDetails({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) return const SizedBox.shrink(); + + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; + final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); + + return Column( + children: [ + SheetTile( + title: _getDateTime(context, asset, exifInfo), + titleStyle: context.textTheme.labelLarge, + trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, + onTap: asset.hasRemote && isOwner + ? () async => await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context) + : null, + ), + if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), + ], + ); + } + + static String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) { + DateTime dateTime = asset.createdAt.toLocal(); + Duration timeZoneOffset = dateTime.timeZoneOffset; + + if (exifInfo?.dateTimeOriginal != null) { + (dateTime, timeZoneOffset) = applyTimezoneOffset( + dateTime: exifInfo!.dateTimeOriginal!, + timeZone: exifInfo.timeZone, + ); + } + + final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); + final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); + final timezone = 'GMT${timeZoneOffset.formatAsOffset()}'; + return '$date$_kSeparator$time $timezone'; + } +} + +class _SheetAssetDescription extends ConsumerStatefulWidget { + final ExifInfo exif; + final bool isEditable; + + const _SheetAssetDescription({required this.exif, this.isEditable = true}); + + @override + ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState(); +} + +class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> { + late TextEditingController _controller; + final _descriptionFocus = FocusNode(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.exif.description ?? ''); + } + + Future saveDescription(String? previousDescription) async { + final newDescription = _controller.text.trim(); + + if (newDescription == previousDescription) { + _descriptionFocus.unfocus(); + return; + } + + final editAction = await ref.read(actionProvider.notifier).updateDescription(ActionSource.viewer, newDescription); + + if (!editAction.success) { + _controller.text = previousDescription ?? ''; + + ImmichToast.show( + context: context, + msg: 'exif_bottom_sheet_description_error'.t(context: context), + toastType: ToastType.error, + ); + } + + _descriptionFocus.unfocus(); + } + + @override + Widget build(BuildContext context) { + final currentExifInfo = ref.watch(currentAssetExifProvider).valueOrNull; + + final currentDescription = currentExifInfo?.description ?? ''; + final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t( + context: context, + ); + if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { + _controller.text = currentDescription; + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: IgnorePointer( + ignoring: !widget.isEditable, + child: TextField( + controller: _controller, + keyboardType: TextInputType.multiline, + maxLines: null, + focusNode: _descriptionFocus, + decoration: InputDecoration( + hintText: hintText, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + onTapOutside: (_) => saveDescription(currentExifInfo?.description), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/drag_handle.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/drag_handle.widget.dart new file mode 100644 index 0000000000..8c24c5004c --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/drag_handle.widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class DragHandle extends StatelessWidget { + const DragHandle({super.key}); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Center( + child: Container( + width: 32, + height: 4, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(2)), + color: context.colorScheme.onSurfaceVariant, + ), + ), + ), + ); +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart similarity index 79% rename from mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart rename to mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index 4edd6855a8..0665f4d46c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -4,21 +4,22 @@ 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/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/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}); +class LocationDetails extends ConsumerStatefulWidget { + const LocationDetails({super.key}); @override - ConsumerState createState() => _SheetLocationDetailsState(); + ConsumerState createState() => _LocationDetailsState(); } -class _SheetLocationDetailsState extends ConsumerState { +class _LocationDetailsState extends ConsumerState { MapLibreMapController? _mapController; String? _getLocationName(ExifInfo? exifInfo) { @@ -41,7 +42,6 @@ class _SheetLocationDetailsState extends ConsumerState { void _onExifChanged(AsyncValue? previous, AsyncValue current) { final currentExif = current.valueOrNull; - if (currentExif != null && currentExif.hasCoordinates) { _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!))); } @@ -64,11 +64,10 @@ class _SheetLocationDetailsState extends ConsumerState { final hasCoordinates = exifInfo?.hasCoordinates ?? false; // Guard local assets - if (asset != null && asset is LocalAsset && asset.hasRemote) { + if (asset is! RemoteAsset) { return const SizedBox.shrink(); } - final remoteId = asset is LocalAsset ? asset.remoteId : (asset as RemoteAsset).id; final locationName = _getLocationName(exifInfo); final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}"; @@ -78,11 +77,8 @@ class _SheetLocationDetailsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ SheetTile( - title: 'location'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + title: 'location'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null, onTap: editLocation, ), @@ -92,7 +88,12 @@ class _SheetLocationDetailsState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated), + ExifMap( + exifInfo: exifInfo!, + markerId: asset.id, + markerAssetThumbhash: asset.thumbHash, + onMapCreated: _onMapCreated, + ), const SizedBox(height: 16), if (locationName != null) Padding( @@ -101,9 +102,7 @@ class _SheetLocationDetailsState extends ConsumerState { ), Text( coordinates, - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart similarity index 86% rename from mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart rename to mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 64f22eca92..5074c63c9c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -4,24 +4,25 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/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/people/person_edit_name_modal.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/people.utils.dart'; -class SheetPeopleDetails extends ConsumerStatefulWidget { - const SheetPeopleDetails({super.key}); +class PeopleDetails extends ConsumerStatefulWidget { + const PeopleDetails({super.key}); @override - ConsumerState createState() => _SheetPeopleDetailsState(); + ConsumerState createState() => _PeopleDetailsState(); } -class _SheetPeopleDetailsState extends ConsumerState { +class _PeopleDetailsState extends ConsumerState { @override Widget build(BuildContext context) { final asset = ref.watch(currentAssetNotifier); @@ -53,11 +54,8 @@ class _SheetPeopleDetailsState extends ConsumerState { Padding( padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16), child: Text( - "people".t(context: context).toUpperCase(), - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), + "people".t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), SizedBox( @@ -67,7 +65,7 @@ class _SheetPeopleDetailsState extends ConsumerState { scrollDirection: Axis.horizontal, children: [ for (final person in people) - _PeopleAvatar( + _Avatar( person: person, assetFileCreatedAt: asset.createdAt, onTap: () { @@ -99,19 +97,17 @@ class _SheetPeopleDetailsState extends ConsumerState { } } -class _PeopleAvatar extends StatelessWidget { +class _Avatar extends StatelessWidget { final DriftPerson person; final DateTime assetFileCreatedAt; final VoidCallback? onTap; final VoidCallback? onNameTap; final double imageSize = 96; - const _PeopleAvatar({required this.person, required this.assetFileCreatedAt, this.onTap, this.onNameTap}); + const _Avatar({required this.person, required this.assetFileCreatedAt, this.onTap, this.onNameTap}); @override Widget build(BuildContext context) { - final headers = ApiService.getRequestHeaders(); - return ConstrainedBox( constraints: const BoxConstraints(maxWidth: 96), child: Padding( @@ -129,7 +125,7 @@ class _PeopleAvatar extends StatelessWidget { elevation: 3, child: CircleAvatar( maxRadius: imageSize / 2, - backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers), + backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart new file mode 100644 index 0000000000..982ea67583 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart @@ -0,0 +1,52 @@ +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/extensions/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; + +class RatingDetails extends ConsumerWidget { + const RatingDetails({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRatingEnabled = ref + .watch(userMetadataPreferencesProvider) + .maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false); + + if (!isRatingEnabled) return const SizedBox.shrink(); + + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; + + return Padding( + padding: const EdgeInsets.only(left: 16.0, top: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Text( + 'rating'.t(context: context), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + RatingBar( + initialRating: exifInfo?.rating?.toDouble() ?? 0, + filledColor: context.themeData.colorScheme.primary, + unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100), + itemSize: 40, + onRatingUpdate: (rating) async { + await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round()); + }, + onClearRating: () async { + await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, 0); + }, + ), + ], + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart new file mode 100644 index 0000000000..d79362b559 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart @@ -0,0 +1,129 @@ +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/theme_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; + +const _kSeparator = ' â€ĸ '; + +class TechnicalDetails extends ConsumerWidget { + const TechnicalDetails({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asset = ref.watch(currentAssetNotifier); + if (asset == null) return const SizedBox.shrink(); + + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; + final cameraTitle = _getCameraInfoTitle(exifInfo); + final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null; + + return Column( + children: [ + SheetTile( + title: 'details'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + _buildFileInfoTile(context, ref, asset, exifInfo), + if (cameraTitle != null) ...[ + const SizedBox(height: 16), + SheetTile( + title: cameraTitle, + titleStyle: context.textTheme.labelLarge, + leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color), + subtitle: _getCameraInfoSubtitle(exifInfo), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + if (lensTitle != null) ...[ + const SizedBox(height: 16), + SheetTile( + title: lensTitle, + titleStyle: context.textTheme.labelLarge, + leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color), + subtitle: _getLensInfoSubtitle(exifInfo), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + ], + ); + } + + Widget _buildFileInfoTile(BuildContext context, WidgetRef ref, BaseAsset asset, ExifInfo? exifInfo) { + final icon = Icon( + asset.isImage ? Icons.image_outlined : Icons.videocam_outlined, + size: 24, + color: context.textTheme.labelLarge?.color, + ); + final subtitle = _getFileInfo(asset, exifInfo); + final subtitleStyle = context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary); + + if (asset is LocalAsset) { + final assetMediaRepository = ref.watch(assetMediaRepositoryProvider); + return FutureBuilder( + future: assetMediaRepository.getOriginalFilename(asset.id), + builder: (context, snapshot) { + return SheetTile( + title: snapshot.data ?? asset.name, + titleStyle: context.textTheme.labelLarge, + leading: icon, + subtitle: subtitle, + subtitleStyle: subtitleStyle, + ); + }, + ); + } + + return SheetTile( + title: asset.name, + titleStyle: context.textTheme.labelLarge, + leading: icon, + subtitle: subtitle, + subtitleStyle: subtitleStyle, + ); + } + + static String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { + final height = asset.height; + final width = asset.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', + }; + } + + static 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', + }; + } + + static String? _getCameraInfoSubtitle(ExifInfo? exifInfo) { + if (exifInfo == null) return null; + final exposureTime = exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null; + final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null; + return [exposureTime, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); + } + + static String? _getLensInfoSubtitle(ExifInfo? exifInfo) { + if (exifInfo == null) return null; + final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null; + final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null; + return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart new file mode 100644 index 0000000000..18fd8846b4 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -0,0 +1,483 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/gestures.dart' show Drag, kTouchSlop; +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/events.model.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_details.widget.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/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/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/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; + +enum _DragIntent { none, scroll, dismiss } + +class AssetPage extends ConsumerStatefulWidget { + final int index; + final int heroOffset; + final void Function(int direction)? onTapNavigate; + + const AssetPage({super.key, required this.index, required this.heroOffset, this.onTapNavigate}); + + @override + ConsumerState createState() => _AssetPageState(); +} + +class _AssetPageState extends ConsumerState { + PhotoViewControllerBase? _viewController; + StreamSubscription? _scaleBoundarySub; + StreamSubscription? _eventSubscription; + + AssetViewerStateNotifier get _viewer => ref.read(assetViewerProvider.notifier); + + late PhotoViewControllerValue _initialPhotoViewState; + + bool _showingDetails = false; + bool _isZoomed = false; + + final _scrollController = ScrollController(); + late final _proxyScrollController = ProxyScrollController(scrollController: _scrollController); + final ValueNotifier _videoScaleStateNotifier = ValueNotifier(PhotoViewScaleState.initial); + + double _snapOffset = 0.0; + + DragStartDetails? _dragStart; + _DragIntent _dragIntent = _DragIntent.none; + Drag? _drag; + + @override + void initState() { + super.initState(); + _eventSubscription = EventStream.shared.listen(_onEvent); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || !_proxyScrollController.hasClients) return; + _proxyScrollController.snapPosition.snapOffset = _snapOffset; + if (_showingDetails && _snapOffset > 0) { + _proxyScrollController.jumpTo(_snapOffset); + } + }); + } + + @override + void dispose() { + _proxyScrollController.dispose(); + _scaleBoundarySub?.cancel(); + _eventSubscription?.cancel(); + _videoScaleStateNotifier.dispose(); + super.dispose(); + } + + void _onEvent(Event event) { + switch (event) { + case ViewerShowDetailsEvent(): + _showDetails(); + default: + } + } + + void _showDetails() { + if (!_proxyScrollController.hasClients || _snapOffset <= 0) return; + _viewer.setShowingDetails(true); + _proxyScrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); + } + + bool _willClose(double scrollVelocity) { + if (!_proxyScrollController.hasClients || _snapOffset <= 0) return false; + + final position = _proxyScrollController.position; + return _proxyScrollController.position.pixels < _snapOffset && + SnapScrollPhysics.target(position, scrollVelocity, _snapOffset) < SnapScrollPhysics.minSnapDistance; + } + + void _syncShowingDetails() { + final offset = _proxyScrollController.offset; + if (offset > SnapScrollPhysics.minSnapDistance) { + _viewer.setShowingDetails(true); + } else if (offset < SnapScrollPhysics.minSnapDistance - kTouchSlop) { + _viewer.setShowingDetails(false); + } + } + + void _beginDrag(DragStartDetails details) { + _dragStart = details; + + if (_viewController != null) { + _initialPhotoViewState = _viewController!.value; + } + + if (_showingDetails) { + _dragIntent = _DragIntent.scroll; + _startProxyDrag(); + } + } + + void _startProxyDrag() { + if (_proxyScrollController.hasClients && _dragStart != null) { + _drag = _proxyScrollController.position.drag(_dragStart!, () => _drag = null); + } + } + + void _updateDrag(DragUpdateDetails details) { + if (_dragStart == null) return; + + if (_dragIntent == _DragIntent.none) { + _dragIntent = switch ((details.globalPosition - _dragStart!.globalPosition).dy) { + < 0 => _DragIntent.scroll, + > 0 => _DragIntent.dismiss, + _ => _DragIntent.none, + }; + } + + switch (_dragIntent) { + case _DragIntent.none: + case _DragIntent.scroll: + if (_drag == null) _startProxyDrag(); + _drag?.update(details); + + _syncShowingDetails(); + case _DragIntent.dismiss: + _handleDragDown(context, details.localPosition - _dragStart!.localPosition); + } + } + + void _endDrag(DragEndDetails details) { + if (_dragStart == null) return; + + final start = _dragStart; + _dragStart = null; + + final intent = _dragIntent; + _dragIntent = _DragIntent.none; + + switch (intent) { + case _DragIntent.none: + case _DragIntent.scroll: + final scrollVelocity = -(details.primaryVelocity ?? 0.0); + _viewer.setShowingDetails(!_willClose(scrollVelocity)); + + _drag?.end(details); + _drag = null; + case _DragIntent.dismiss: + const popThreshold = 75.0; + if (details.localPosition.dy - start!.localPosition.dy > popThreshold) { + context.maybePop(); + return; + } + _viewController?.animateMultiple( + position: _initialPhotoViewState.position, + scale: _viewController?.initialScale ?? _initialPhotoViewState.scale, + rotation: _initialPhotoViewState.rotation, + ); + _viewer.setOpacity(1.0); + } + } + + void _onDragStart( + BuildContext context, + DragStartDetails details, + PhotoViewControllerBase controller, + PhotoViewScaleStateController scaleStateController, + ) { + if (!_showingDetails && _isZoomed) return; + _beginDrag(details); + } + + void _onDragUpdate(BuildContext context, DragUpdateDetails details, PhotoViewControllerValue _) => + _updateDrag(details); + + void _onDragEnd(BuildContext context, DragEndDetails details, PhotoViewControllerValue _) => _endDrag(details); + + void _onDragCancel() => _endDrag(DragEndDetails(primaryVelocity: 0.0)); + + void _handleDragDown(BuildContext context, Offset delta) { + const dragRatio = 0.2; + + final distance = delta.dy.abs(); + final maxScaleDistance = context.height * 0.5; + final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio); + final initialScale = _viewController?.initialScale ?? _initialPhotoViewState.scale; + final updatedScale = initialScale != null ? initialScale * (1.0 - scaleReduction) : null; + + final opacity = 1.0 - (scaleReduction / dragRatio); + + _viewController?.updateMultiple(position: _initialPhotoViewState.position + delta, scale: updatedScale); + _viewer.setOpacity(opacity); + } + + void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) { + if (_showingDetails || _dragStart != null) return; + + final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); + if (!tapToNavigate) { + _viewer.toggleControls(); + return; + } + + final tapX = details.globalPosition.dx; + final screenWidth = context.width; + + // Navigate if the user taps in the leftmost or rightmost quarter of the screen + final tappedLeftSide = tapX < screenWidth / 4; + final tappedRightSide = tapX > screenWidth * (3 / 4); + + if (tappedLeftSide) { + widget.onTapNavigate?.call(-1); + } else if (tappedRightSide) { + widget.onTapNavigate?.call(1); + } else { + _viewer.toggleControls(); + } + } + + void _onLongPress(BuildContext context, LongPressStartDetails details, PhotoViewControllerValue controllerValue) => + ref.read(isPlayingMotionVideoProvider.notifier).playing = true; + + void _onScaleStateChanged(PhotoViewScaleState scaleState) { + _isZoomed = + scaleState == PhotoViewScaleState.zoomedIn || + scaleState == PhotoViewScaleState.covering || + _videoScaleStateNotifier.value == PhotoViewScaleState.zoomedIn || + _videoScaleStateNotifier.value == PhotoViewScaleState.covering; + _viewer.setZoomed(_isZoomed); + + if (scaleState != PhotoViewScaleState.initial) { + if (_dragStart == null) _viewer.setControls(false); + + ref.read(videoPlayerControlsProvider.notifier).pause(); + return; + } + + if (!_showingDetails) _viewer.setControls(true); + } + + void _listenForScaleBoundaries(PhotoViewControllerBase? controller) { + _scaleBoundarySub?.cancel(); + _scaleBoundarySub = null; + if (controller == null || controller.scaleBoundaries != null) return; + _scaleBoundarySub = controller.outputStateStream.listen((_) { + if (controller.scaleBoundaries != null) { + _scaleBoundarySub?.cancel(); + _scaleBoundarySub = null; + if (mounted) setState(() {}); + } + }); + } + + double _getImageHeight(double maxWidth, double maxHeight, BaseAsset? asset) { + final sb = _viewController?.scaleBoundaries; + if (sb != null) return sb.childSize.height * sb.initialScale; + + if (asset == null || asset.width == null || asset.height == null) return maxHeight; + + final r = asset.width! / asset.height!; + return math.min(maxWidth / r, maxHeight); + } + + void _onPageBuild(PhotoViewControllerBase controller) { + _viewController = controller; + _listenForScaleBoundaries(controller); + } + + Widget _buildPhotoView( + BaseAsset displayAsset, + BaseAsset asset, { + required bool isCurrentPage, + required bool showingDetails, + required bool isPlayingMotionVideo, + required BoxDecoration backgroundDecoration, + }) { + final heroAttributes = isCurrentPage ? PhotoViewHeroAttributes(tag: '${asset.heroTag}_${widget.heroOffset}') : null; + + if (displayAsset.isImage && !isPlayingMotionVideo) { + final size = context.sizeData; + return PhotoView( + key: Key(displayAsset.heroTag), + index: widget.index, + imageProvider: getFullImageProvider(displayAsset, size: size), + heroAttributes: heroAttributes, + loadingBuilder: (context, progress, index) => const Center(child: ImmichLoadingIndicator()), + backgroundDecoration: backgroundDecoration, + gaplessPlayback: true, + filterQuality: FilterQuality.high, + tightMode: true, + enablePanAlways: true, + disableScaleGestures: showingDetails, + scaleStateChangedCallback: _onScaleStateChanged, + onPageBuild: _onPageBuild, + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onDragCancel: _onDragCancel, + onTapUp: _onTapUp, + onLongPressStart: displayAsset.isMotionPhoto ? _onLongPress : null, + errorBuilder: (_, __, ___) => SizedBox( + width: size.width, + height: size.height, + child: Thumbnail.fromAsset(asset: displayAsset, fit: BoxFit.contain), + ), + ); + } + + return PhotoView.customChild( + key: Key(displayAsset.heroTag), + onDragStart: _onDragStart, + onDragUpdate: _onDragUpdate, + onDragEnd: _onDragEnd, + onDragCancel: _onDragCancel, + heroAttributes: heroAttributes, + filterQuality: FilterQuality.high, + basePosition: Alignment.center, + disableScaleGestures: true, + minScale: PhotoViewComputedScale.contained, + initialScale: PhotoViewComputedScale.contained, + tightMode: true, + onPageBuild: _onPageBuild, + enablePanAlways: true, + backgroundDecoration: backgroundDecoration, + child: NativeVideoViewer( + key: _NativeVideoViewerKey(displayAsset.heroTag), + asset: displayAsset, + scaleStateNotifier: _videoScaleStateNotifier, + disableScaleGestures: showingDetails, + image: Image( + image: getFullImageProvider(displayAsset, size: context.sizeData), + height: context.height, + width: context.width, + fit: BoxFit.contain, + alignment: Alignment.center, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final currentHeroTag = ref.watch(assetViewerProvider.select((s) => s.currentAsset?.heroTag)); + _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); + final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); + + final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index); + if (asset == null) { + return const Center(child: ImmichLoadingIndicator()); + } + + BaseAsset displayAsset = asset; + final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull; + if (stackChildren != null && stackChildren.isNotEmpty) { + displayAsset = stackChildren.elementAt(stackIndex); + } + + final viewportWidth = MediaQuery.widthOf(context); + final viewportHeight = MediaQuery.heightOf(context); + final imageHeight = _getImageHeight(viewportWidth, viewportHeight, displayAsset); + + final detailsOffset = (viewportHeight + imageHeight - kMinInteractiveDimension) / 2; + final snapTarget = viewportHeight / 3; + + _snapOffset = detailsOffset - snapTarget; + + if (_proxyScrollController.hasClients) { + _proxyScrollController.snapPosition.snapOffset = _snapOffset; + } + + return ProviderScope( + overrides: [ + currentAssetNotifier.overrideWith(() => ScopedAssetNotifier(asset)), + currentAssetExifProvider.overrideWith((ref) { + final a = ref.watch(currentAssetNotifier); + if (a == null) return Future.value(null); + return ref.watch(assetServiceProvider).getExif(a); + }), + ], + child: Stack( + children: [ + Offstage( + child: SingleChildScrollView( + controller: _proxyScrollController, + physics: const SnapScrollPhysics(), + child: const SizedBox.shrink(), + ), + ), + SingleChildScrollView( + controller: _scrollController, + physics: const NeverScrollableScrollPhysics(), + child: Stack( + children: [ + SizedBox( + width: viewportWidth, + height: viewportHeight, + child: _buildPhotoView( + displayAsset, + asset, + isCurrentPage: currentHeroTag == asset.heroTag, + showingDetails: _showingDetails, + isPlayingMotionVideo: isPlayingMotionVideo, + backgroundDecoration: BoxDecoration(color: _showingDetails ? Colors.black : Colors.transparent), + ), + ), + IgnorePointer( + ignoring: !_showingDetails, + child: Column( + children: [ + SizedBox(height: detailsOffset), + GestureDetector( + onVerticalDragStart: _beginDrag, + onVerticalDragUpdate: _updateDrag, + onVerticalDragEnd: _endDrag, + onVerticalDragCancel: _onDragCancel, + child: AnimatedOpacity( + opacity: _showingDetails ? 1.0 : 0.0, + duration: Durations.short2, + child: AssetDetails(minHeight: viewportHeight - snapTarget), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// A global key is used for video viewers to prevent them from being +// unnecessarily recreated. They're quite expensive, and maintain internal +// state. This can cause videos to restart multiple times during normal usage, +// like a hero animation. +// +// A plain ValueKey is insufficient, as it does not allow widgets to reparent. A +// GlobalObjectKey is fragile, as it checks if the given objects are identical, +// rather than equal. Hero tags are created with string interpolation, which +// prevents Dart from interning them. As such, hero tags are not identical, even +// if they are equal. +class _NativeVideoViewerKey extends GlobalKey { + final String value; + + const _NativeVideoViewerKey(this.value) : super.constructor(); + + @override + bool operator ==(Object other) => other is _NativeVideoViewerKey && other.value == value; + + @override + int get hashCode => value.hashCode; +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart new file mode 100644 index 0000000000..ca7498a37f --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; + +class AssetPreloader { + static final _dummyListener = ImageStreamListener((image, _) => image.dispose()); + + final TimelineService timelineService; + final bool Function() mounted; + + Timer? _timer; + ImageStream? _prevStream; + ImageStream? _nextStream; + + AssetPreloader({required this.timelineService, required this.mounted}); + + void preload(int index, Size size) { + unawaited(timelineService.preloadAssets(index)); + _timer?.cancel(); + _timer = Timer(Durations.medium4, () async { + if (!mounted()) return; + final (prev, next) = await ( + timelineService.getAssetAsync(index - 1), + timelineService.getAssetAsync(index + 1), + ).wait; + if (!mounted()) return; + _prevStream?.removeListener(_dummyListener); + _nextStream?.removeListener(_dummyListener); + _prevStream = prev != null ? _resolveImage(prev, size) : null; + _nextStream = next != null ? _resolveImage(next, size) : null; + }); + } + + ImageStream _resolveImage(BaseAsset asset, Size size) { + return getFullImageProvider(asset, size: size).resolve(ImageConfiguration.empty)..addListener(_dummyListener); + } + + void dispose() { + _timer?.cancel(); + _prevStream?.removeListener(_dummyListener); + _nextStream?.removeListener(_dummyListener); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart index 0978b3c9af..2835342b85 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -4,7 +4,7 @@ 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/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; class AssetStackRow extends ConsumerWidget { const AssetStackRow({super.key}); @@ -21,17 +21,11 @@ class AssetStackRow extends ConsumerWidget { return const SizedBox.shrink(); } - final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - final opacity = showControls ? ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)) : 0; - - return IgnorePointer( - ignoring: opacity < 255, - child: AnimatedOpacity( - opacity: opacity / 255, - duration: Durations.short2, - child: _StackList(stack: stackChildren), - ), - ); + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + if (showingDetails) { + return const SizedBox.shrink(); + } + return _StackList(stack: stackChildren); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index d992d243ee..d59e867d66 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -14,27 +14,19 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.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/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.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/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; -import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; @RoutePage() class AssetViewerPage extends StatelessWidget { @@ -79,10 +71,6 @@ class AssetViewer extends ConsumerStatefulWidget { _setAsset(ref, asset); } - void changeAsset(WidgetRef ref, BaseAsset asset) { - _setAsset(ref, asset); - } - static void _setAsset(WidgetRef ref, BaseAsset asset) { // Always holds the current asset from the timeline ref.read(assetViewerProvider.notifier).setAsset(asset); @@ -93,133 +81,71 @@ class AssetViewer extends ConsumerStatefulWidget { ref.read(videoPlaybackValueProvider.notifier).reset(); ref.read(videoPlayerControlsProvider.notifier).pause(); } + // Hide controls by default for videos + if (asset.isVideo) ref.read(assetViewerProvider.notifier).setControls(false); } } -const double _kBottomSheetMinimumExtent = 0.4; -const double _kBottomSheetSnapExtent = 0.67; - class _AssetViewerState extends ConsumerState { - static final _dummyListener = ImageStreamListener((image, _) => image.dispose()); - late PageController pageController; - late DraggableScrollableController bottomSheetController; - PersistentBottomSheetController? sheetCloseController; - // PhotoViewGallery takes care of disposing it's controllers - PhotoViewControllerBase? viewController; - StreamSubscription? reloadSubscription; - - 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 = []; - - ImageStream? _prevPreCacheStream; - ImageStream? _nextPreCacheStream; + late final _heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + late final _pageController = PageController(initialPage: widget.initialIndex); + late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted); + StreamSubscription? _reloadSubscription; KeepAliveLink? _stackChildrenKeepAlive; + bool _assetReloadRequested = false; + + void _onTapNavigate(int direction) { + final page = _pageController.page?.toInt(); + if (page == null) return; + final target = page + direction; + final maxPage = ref.read(timelineServiceProvider).totalAssets - 1; + if (target >= 0 && target <= maxPage) { + _pageController.jumpToPage(target); + } + } + @override void initState() { super.initState(); - assert(ref.read(currentAssetNotifier) != null, "Current asset should not be null when opening the AssetViewer"); - pageController = PageController(initialPage: widget.initialIndex); - totalAssets = ref.read(timelineServiceProvider).totalAssets; - bottomSheetController = DraggableScrollableController(); - WidgetsBinding.instance.addPostFrameCallback(_onAssetInit); - reloadSubscription = EventStream.shared.listen(_onEvent); - heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + final asset = ref.read(currentAssetNotifier); - if (asset != null) { - _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); - } + assert(asset != null, "Current asset should not be null when opening the AssetViewer"); + if (asset != null) _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); + + _reloadSubscription = EventStream.shared.listen(_onEvent); + + WidgetsBinding.instance.addPostFrameCallback(_onAssetInit); + + final assetViewer = ref.read(assetViewerProvider); + _setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails); } @override void dispose() { - pageController.dispose(); - bottomSheetController.dispose(); - _cancelTimers(); - reloadSubscription?.cancel(); - _prevPreCacheStream?.removeListener(_dummyListener); - _nextPreCacheStream?.removeListener(_dummyListener); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + _pageController.dispose(); + _preloader.dispose(); + _reloadSubscription?.cancel(); _stackChildrenKeepAlive?.close(); + + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + 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(); - } - - double _getVerticalOffsetForBottomSheet(double extent) => - (context.height * extent) - (context.height * _kBottomSheetMinimumExtent); - - ImageStream _precacheImage(BaseAsset asset) { - final provider = getFullImageProvider(asset, size: context.sizeData); - return provider.resolve(ImageConfiguration.empty)..addListener(_dummyListener); - } - - void _precacheAssets(int index) { - final timelineService = ref.read(timelineServiceProvider); - unawaited(timelineService.preCacheAssets(index)); - _cancelTimers(); - // This will trigger the pre-caching of adjacent assets ensuring - // that they are ready when the user navigates to them. - final timer = Timer(Durations.medium4, () async { - // Check if widget is still mounted before proceeding - if (!mounted) return; - - final (prevAsset, nextAsset) = await ( - timelineService.getAssetAsync(index - 1), - timelineService.getAssetAsync(index + 1), - ).wait; - if (!mounted) return; - _prevPreCacheStream?.removeListener(_dummyListener); - _nextPreCacheStream?.removeListener(_dummyListener); - _prevPreCacheStream = prevAsset != null ? _precacheImage(prevAsset) : null; - _nextPreCacheStream = nextAsset != null ? _precacheImage(nextAsset) : null; - }); - _delayedOperations.add(timer); - } - - void _onAssetInit(Duration _) { - _precacheAssets(widget.initialIndex); + void _onAssetInit(Duration timeStamp) { + _preloader.preload(widget.initialIndex, context.sizeData); _handleCasting(); } void _onAssetChanged(int index) async { final timelineService = ref.read(timelineServiceProvider); final asset = await timelineService.getAssetAsync(index); - if (asset == null) { - return; - } + if (asset == null) return; - widget.changeAsset(ref, asset); - _precacheAssets(index); + AssetViewer._setAsset(ref, asset); + _preloader.preload(index, context.sizeData); _handleCasting(); _stackChildrenKeepAlive?.close(); _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); @@ -230,438 +156,110 @@ class _AssetViewerState extends ConsumerState { final asset = ref.read(currentAssetNotifier); if (asset == null) return; - // hide any casting snackbars if they exist - context.scaffoldMessenger.hideCurrentSnackBar(); - - // send image to casting if the server has it if (asset is RemoteAsset) { + context.scaffoldMessenger.hideCurrentSnackBar(); ref.read(castProvider.notifier).loadMedia(asset, 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 && bottomSheetController.isAttached) { - final verticalOffset = - (context.height * bottomSheetController.size) - (context.height * _kBottomSheetMinimumExtent); - controller.position = Offset(0, -verticalOffset); - // Apply the zoom effect when the bottom sheet is showing - initialScale = controller.scale; - controller.scale = (controller.scale ?? 1.0) + 0.01; - } - } - - void _onPageChanged(int index, PhotoViewControllerBase? controller) { - _onAssetChanged(index); - viewController = controller; - } - - 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, + context.scaffoldMessenger.clearSnackBars(); + 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), + ), + ), ); - 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 && !ref.read(readonlyModeProvider)) { - _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.67) { - if (dragInProgress) { - blockGestures = true; - } - // Jump to a lower position before starting close animation to prevent glitch - if (bottomSheetController.isAttached) { - bottomSheetController.jumpTo(0.67); - } - 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, activitiesMode: event.activitiesMode); - final offset = _getVerticalOffsetForBottomSheet(extent); - viewController?.position = Offset(0, -offset); - return; + switch (event) { + case TimelineReloadEvent(): + _onTimelineReloadEvent(); + case ViewerReloadAssetEvent(): + _assetReloadRequested = true; + default: } } void _onTimelineReloadEvent() { - totalAssets = ref.read(timelineServiceProvider).totalAssets; + final timelineService = ref.read(timelineServiceProvider); + final totalAssets = timelineService.totalAssets; + if (totalAssets == 0) { context.maybePop(); return; } - if (assetReloadRequested) { - assetReloadRequested = false; - _onAssetReloadEvent(); - return; + var index = _pageController.page?.round() ?? 0; + final currentAsset = ref.read(currentAssetNotifier); + if (currentAsset != null) { + final newIndex = timelineService.getIndex(currentAsset.heroTag); + if (newIndex != null && newIndex != index) { + index = newIndex; + _pageController.jumpToPage(index); + } + } + + if (index >= totalAssets) { + index = totalAssets - 1; + _pageController.jumpToPage(index); + } + + if (_assetReloadRequested) { + _assetReloadRequested = false; + _onAssetReloadEvent(index); } } - void _onAssetReloadEvent() async { - final index = pageController.page?.round() ?? 0; + void _onAssetReloadEvent(int index) async { final timelineService = ref.read(timelineServiceProvider); - final newAsset = await timelineService.getAssetAsync(index); - if (newAsset == null) { - return; - } + final newAsset = await timelineService.getAssetAsync(index); + if (newAsset == null) return; final currentAsset = ref.read(currentAssetNotifier); - // Do not reload / close the bottom sheet if the asset has not changed - if (newAsset.heroTag == currentAsset?.heroTag) { - return; - } - setState(() { - _onAssetChanged(pageController.page!.round()); - sheetCloseController?.close(); - }); + // Do not reload if the asset has not changed + if (newAsset.heroTag == currentAsset?.heroTag) return; + + _onAssetChanged(index); } - void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent, bool activitiesMode = false}) { - ref.read(assetViewerProvider.notifier).setBottomSheet(true); - initialScale = viewController?.scale; - // viewController?.updateMultiple(scale: (viewController?.scale ?? 1.0) + 0.01); - previousExtent = _kBottomSheetMinimumExtent; - sheetCloseController = showBottomSheet( - context: ctx, - sheetAnimationStyle: const AnimationStyle(duration: Durations.medium2, reverseDuration: Durations.medium2), - 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: activitiesMode - ? ActivitiesBottomSheet(controller: bottomSheetController, initialChildSize: extent) - : 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.isAttached || - 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) { - return const Center(child: ImmichLoadingIndicator()); - } - - 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; - final timelineService = ref.read(timelineServiceProvider); - final asset = timelineService.getAssetSafe(index); - - // If asset is not available in buffer, return a placeholder - if (asset == null) { - return PhotoViewGalleryPageOptions.customChild( - heroAttributes: PhotoViewHeroAttributes(tag: 'loading_$index'), - child: Container( - width: ctx.width, - height: ctx.height, - color: backgroundColor, - child: const Center(child: CircularProgressIndicator()), - ), - ); - } - - BaseAsset displayAsset = asset; - final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull; - if (stackChildren != null && stackChildren.isNotEmpty) { - displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex); - } - - final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider); - if (displayAsset.isImage && !isPlayingMotionVideo) { - return _imageBuilder(ctx, displayAsset); - } - - return _videoBuilder(ctx, displayAsset); - } - - PhotoViewGalleryPageOptions _imageBuilder(BuildContext ctx, BaseAsset asset) { - final size = ctx.sizeData; - return PhotoViewGalleryPageOptions( - key: ValueKey(asset.heroTag), - imageProvider: getFullImageProvider(asset, size: size), - heroAttributes: PhotoViewHeroAttributes(tag: '${asset.heroTag}_$heroOffset'), - filterQuality: FilterQuality.high, - tightMode: true, - disableScaleGestures: showingBottomSheet, - onDragStart: _onDragStart, - onDragUpdate: _onDragUpdate, - onDragEnd: _onDragEnd, - onTapDown: _onTapDown, - onLongPressStart: asset.isMotionPhoto ? _onLongPress : null, - errorBuilder: (_, __, ___) => Container( - width: size.width, - height: size.height, - color: backgroundColor, - child: Thumbnail.fromAsset(asset: asset, fit: BoxFit.contain), - ), - ); - } - - 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, - maxScale: 1.0, - 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: ctx.sizeData), - fit: BoxFit.contain, - height: ctx.height, - width: ctx.width, - alignment: Alignment.center, - ), - ), - ), - ); - } - - void _onPop(bool didPop, T? result) { - ref.read(currentAssetNotifier.notifier).dispose(); + void _setSystemUIMode(bool controls, bool details) { + final mode = !controls || (CurrentPlatform.isIOS && details) + ? SystemUiMode.immersiveSticky + : SystemUiMode.edgeToEdge; + unawaited(SystemChrome.setEnabledSystemUIMode(mode)); } @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); final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + final isZoomed = ref.watch(assetViewerProvider.select((s) => s.isZoomed)); + final backgroundColor = showingDetails + ? context.colorScheme.surface + : Colors.black.withValues(alpha: ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity))); // Listen for casting changes and send initial asset to the cast provider - ref.listen(castProvider.select((value) => value.isCasting), (_, isCasting) async { + ref.listen(castProvider.select((value) => value.isCasting), (_, isCasting) { if (!isCasting) return; - - final asset = ref.read(currentAssetNotifier); - if (asset == null) return; - WidgetsBinding.instance.addPostFrameCallback((_) { _handleCasting(); }); }); - // Listen for control visibility changes and change system UI mode accordingly - ref.listen(assetViewerProvider.select((value) => value.showingControls), (_, showingControls) async { - if (showingControls) { - unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); - } else { - unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)); - } + ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) { + final (controls, details) = state; + _setSystemUIMode(controls, details); }); - // 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, + onPopInvokedWithResult: (didPop, result) => ref.read(currentAssetNotifier.notifier).dispose(), child: Scaffold( backgroundColor: backgroundColor, appBar: const ViewerTopAppBar(), @@ -675,33 +273,30 @@ class _AssetViewerState extends ConsumerState { child: const DownloadStatusFloatingButton(), ), ), + bottomNavigationBar: const ViewerBottomAppBar(), body: Stack( children: [ - PhotoViewGallery.builder( - gaplessPlayback: true, - loadingBuilder: _placeholderBuilder, - pageController: pageController, - scrollPhysics: CurrentPlatform.isIOS - ? const FastScrollPhysics() // Use bouncing physics for iOS - : const FastClampingScrollPhysics(), // Use heavy physics for Android - itemCount: totalAssets, - onPageChanged: _onPageChanged, - onPageBuild: _onPageBuild, - scaleStateChangedCallback: _onScaleStateChanged, - builder: _assetBuilder, - backgroundDecoration: BoxDecoration(color: backgroundColor), - enablePanAlways: true, + PhotoViewGestureDetectorScope( + axis: Axis.horizontal, + child: PageView.builder( + controller: _pageController, + physics: isZoomed + ? const NeverScrollableScrollPhysics() + : CurrentPlatform.isIOS + ? const FastScrollPhysics() + : const FastClampingScrollPhysics(), + itemCount: ref.read(timelineServiceProvider).totalAssets, + onPageChanged: (index) => _onAssetChanged(index), + itemBuilder: (context, index) => + AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate), + ), ), - if (!showingBottomSheet) - const Positioned( - bottom: 0, - left: 0, - right: 0, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [AssetStackRow(), ViewerBottomBar()], + if (!CurrentPlatform.isIOS) + IgnorePointer( + child: AnimatedContainer( + duration: Durations.short2, + color: Colors.black.withValues(alpha: showingDetails ? 0.6 : 0.0), + height: context.padding.top, ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart index 36e5bf67d9..dc510d6017 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart @@ -3,31 +3,35 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi import 'package:riverpod_annotation/riverpod_annotation.dart'; class AssetViewerState { - final int backgroundOpacity; - final bool showingBottomSheet; + final double backgroundOpacity; + final bool showingDetails; final bool showingControls; + final bool isZoomed; final BaseAsset? currentAsset; final int stackIndex; const AssetViewerState({ - this.backgroundOpacity = 255, - this.showingBottomSheet = false, + this.backgroundOpacity = 1.0, + this.showingDetails = false, this.showingControls = true, + this.isZoomed = false, this.currentAsset, this.stackIndex = 0, }); AssetViewerState copyWith({ - int? backgroundOpacity, - bool? showingBottomSheet, + double? backgroundOpacity, + bool? showingDetails, bool? showingControls, + bool? isZoomed, BaseAsset? currentAsset, int? stackIndex, }) { return AssetViewerState( backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity, - showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet, + showingDetails: showingDetails ?? this.showingDetails, showingControls: showingControls ?? this.showingControls, + isZoomed: isZoomed ?? this.isZoomed, currentAsset: currentAsset ?? this.currentAsset, stackIndex: stackIndex ?? this.stackIndex, ); @@ -35,7 +39,7 @@ class AssetViewerState { @override String toString() { - return 'AssetViewerState(opacity: $backgroundOpacity, bottomSheet: $showingBottomSheet, controls: $showingControls)'; + return 'AssetViewerState(opacity: $backgroundOpacity, showingDetails: $showingDetails, controls: $showingControls, isZoomed: $isZoomed)'; } @override @@ -44,8 +48,9 @@ class AssetViewerState { if (other.runtimeType != runtimeType) return false; return other is AssetViewerState && other.backgroundOpacity == backgroundOpacity && - other.showingBottomSheet == showingBottomSheet && + other.showingDetails == showingDetails && other.showingControls == showingControls && + other.isZoomed == isZoomed && other.currentAsset == currentAsset && other.stackIndex == stackIndex; } @@ -53,8 +58,9 @@ class AssetViewerState { @override int get hashCode => backgroundOpacity.hashCode ^ - showingBottomSheet.hashCode ^ + showingDetails.hashCode ^ showingControls.hashCode ^ + isZoomed.hashCode ^ currentAsset.hashCode ^ stackIndex.hashCode; } @@ -76,18 +82,18 @@ class AssetViewerStateNotifier extends Notifier { state = state.copyWith(currentAsset: asset, stackIndex: 0); } - void setOpacity(int opacity) { + void setOpacity(double opacity) { if (opacity == state.backgroundOpacity) { return; } - state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity == 255 ? true : state.showingControls); + state = state.copyWith(backgroundOpacity: opacity, showingControls: opacity >= 1.0 ? true : state.showingControls); } - void setBottomSheet(bool showing) { - if (showing == state.showingBottomSheet) { + void setShowingDetails(bool showing) { + if (showing == state.showingDetails) { return; } - state = state.copyWith(showingBottomSheet: showing, showingControls: showing ? true : state.showingControls); + state = state.copyWith(showingDetails: showing, showingControls: showing ? true : state.showingControls); if (showing) { ref.read(videoPlayerControlsProvider.notifier).pause(); } @@ -104,6 +110,13 @@ class AssetViewerStateNotifier extends Notifier { state = state.copyWith(showingControls: !state.showingControls); } + void setZoomed(bool isZoomed) { + if (isZoomed == state.isZoomed) { + return; + } + state = state.copyWith(isZoomed: isZoomed); + } + void setStackIndex(int index) { if (index == state.stackIndex) { return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 537f2fc31d..93006ab978 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -29,15 +29,9 @@ class ViewerBottomBar extends ConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final user = ref.watch(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; - final isSheetOpen = ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet)); - int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); - final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); - if (!showControls) { - opacity = 0; - } - final originalTheme = context.themeData; final actions = [ @@ -56,37 +50,30 @@ class ViewerBottomBar extends ConsumerWidget { ], ]; - 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( - color: Colors.black.withAlpha(125), - padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (asset.isVideo) const VideoControls(), - if (!isReadonlyModeEnabled) - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), - ], - ), - ), + return AnimatedSwitcher( + duration: Durations.short4, + child: showingDetails + ? 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( + color: Colors.black.withAlpha(125), + padding: EdgeInsets.only(bottom: context.padding.bottom, top: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (asset.isVideo) const VideoControls(), + if (!isReadonlyModeEnabled) + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions), + ], + ), + ), + ), ); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart deleted file mode 100644 index ed3873b510..0000000000 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ /dev/null @@ -1,388 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/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/duration_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; -import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.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/repositories/asset_media.repository.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:immich_mobile/utils/timezone.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.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(); - } - - return BaseBottomSheet( - actions: [], - slivers: const [_AssetDetailBottomSheet()], - controller: controller, - initialChildSize: initialChildSize, - minChildSize: 0.1, - maxChildSize: 0.88, - expand: false, - shouldCloseOnMinExtent: false, - resizeOnScroll: false, - backgroundColor: context.isDarkTheme ? context.colorScheme.surface : Colors.white, - ); - } -} - -class _AssetDetailBottomSheet extends ConsumerWidget { - const _AssetDetailBottomSheet(); - - String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) { - DateTime dateTime = asset.createdAt.toLocal(); - Duration timeZoneOffset = dateTime.timeZoneOffset; - - // Use EXIF timezone information if available (matching web app behavior) - if (exifInfo?.dateTimeOriginal != null) { - (dateTime, timeZoneOffset) = applyTimezoneOffset( - dateTime: exifInfo!.dateTimeOriginal!, - timeZone: exifInfo.timeZone, - ); - } - - final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); - final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); - final timezone = 'GMT${timeZoneOffset.formatAsOffset()}'; - return '$date$_kSeparator$time $timezone'; - } - - String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { - final height = asset.height; - final width = asset.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 exposureTime = exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null; - final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null; - return [exposureTime, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); - } - - String? _getLensInfoSubtitle(ExifInfo? exifInfo) { - if (exifInfo == null) { - return null; - } - final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null; - final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null; - return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); - } - - Future _editDateTime(BuildContext context, WidgetRef ref) async { - await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context); - } - - Widget _buildAppearsInList(WidgetRef ref, BuildContext context) { - final asset = ref.watch(currentAssetNotifier); - if (asset == null) { - return const SizedBox.shrink(); - } - - if (!asset.hasRemote) { - return const SizedBox.shrink(); - } - - String? remoteAssetId; - if (asset is RemoteAsset) { - remoteAssetId = asset.id; - } else if (asset is LocalAsset) { - remoteAssetId = asset.remoteAssetId; - } - - if (remoteAssetId == null) { - return const SizedBox.shrink(); - } - - final userId = ref.watch(currentUserProvider)?.id; - final assetAlbums = ref.watch(albumsContainingAssetProvider(remoteAssetId)); - - return assetAlbums.when( - data: (albums) { - if (albums.isEmpty) { - return const SizedBox.shrink(); - } - - albums.sortBy((a) => a.name); - - return Column( - spacing: 12, - children: [ - if (albums.isNotEmpty) - SheetTile( - title: 'appears_in'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 24), - child: Column( - spacing: 12, - children: albums.map((album) { - final isOwner = album.ownerId == userId; - return AlbumTile( - album: album, - isOwner: isOwner, - onAlbumSelected: (album) async { - ref.invalidate(assetViewerProvider); - unawaited(context.router.popAndPush(RemoteAlbumRoute(album: album))); - }, - ); - }).toList(), - ), - ), - ], - ); - }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ); - } - - @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); - final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null; - final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); - - // Build file info tile based on asset type - Widget buildFileInfoTile() { - if (asset is LocalAsset) { - final assetMediaRepository = ref.watch(assetMediaRepositoryProvider); - return FutureBuilder( - future: assetMediaRepository.getOriginalFilename(asset.id), - builder: (context, snapshot) { - final displayName = snapshot.data ?? asset.name; - return SheetTile( - title: displayName, - 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.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), - ); - }, - ); - } else { - // For remote assets, use the name directly - return 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.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), - ); - } - } - - return SliverList.list( - children: [ - // Asset Date and Time - SheetTile( - title: _getDateTime(context, asset, exifInfo), - titleStyle: context.textTheme.labelLarge, - trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, - onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null, - ), - if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), - const SheetPeopleDetails(), - const SheetLocationDetails(), - // Details header - SheetTile( - title: 'details'.t(context: context).toUpperCase(), - titleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ), - // File info - buildFileInfoTile(), - // Camera info - if (cameraTitle != null) ...[ - const SizedBox(height: 16), - SheetTile( - title: cameraTitle, - titleStyle: context.textTheme.labelLarge, - leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color), - subtitle: _getCameraInfoSubtitle(exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), - ), - ], - // Lens info - if (lensTitle != null) ...[ - const SizedBox(height: 16), - SheetTile( - title: lensTitle, - titleStyle: context.textTheme.labelLarge, - leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color), - subtitle: _getLensInfoSubtitle(exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), - ), - ], - // Appears in (Albums) - Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)), - // padding at the bottom to avoid cut-off - const SizedBox(height: 30), - ], - ); - } -} - -class _SheetAssetDescription extends ConsumerStatefulWidget { - final ExifInfo exif; - final bool isEditable; - - const _SheetAssetDescription({required this.exif, this.isEditable = true}); - - @override - ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState(); -} - -class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> { - late TextEditingController _controller; - final _descriptionFocus = FocusNode(); - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.exif.description ?? ''); - } - - Future saveDescription(String? previousDescription) async { - final newDescription = _controller.text.trim(); - - if (newDescription == previousDescription) { - _descriptionFocus.unfocus(); - return; - } - - final editAction = await ref.read(actionProvider.notifier).updateDescription(ActionSource.viewer, newDescription); - - if (!editAction.success) { - _controller.text = previousDescription ?? ''; - - ImmichToast.show( - context: context, - msg: 'exif_bottom_sheet_description_error'.t(context: context), - toastType: ToastType.error, - ); - } - - _descriptionFocus.unfocus(); - } - - @override - Widget build(BuildContext context) { - // Watch the current asset EXIF provider to get updates - final currentExifInfo = ref.watch(currentAssetExifProvider).valueOrNull; - - // Update controller text when EXIF data changes - final currentDescription = currentExifInfo?.description ?? ''; - final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t( - context: context, - ); - if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { - _controller.text = currentDescription; - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: IgnorePointer( - ignoring: !widget.isEditable, - child: TextField( - controller: _controller, - keyboardType: TextInputType.multiline, - focusNode: _descriptionFocus, - maxLines: null, // makes it grow as text is added - decoration: InputDecoration( - hintText: hintText, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - ), - onTapOutside: (_) => saveDescription(currentExifInfo?.description), - ), - ), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart new file mode 100644 index 0000000000..64090dc5c2 --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; + +class RatingBar extends StatefulWidget { + final double initialRating; + final int itemCount; + final double itemSize; + final Color filledColor; + final Color unfilledColor; + final ValueChanged? onRatingUpdate; + final VoidCallback? onClearRating; + final Widget? itemBuilder; + final double starPadding; + + const RatingBar({ + super.key, + this.initialRating = 0.0, + this.itemCount = 5, + this.itemSize = 40.0, + this.filledColor = Colors.amber, + this.unfilledColor = Colors.grey, + this.onRatingUpdate, + this.onClearRating, + this.itemBuilder, + this.starPadding = 4.0, + }); + + @override + State createState() => _RatingBarState(); +} + +class _RatingBarState extends State { + late double _currentRating; + + @override + void initState() { + super.initState(); + _currentRating = widget.initialRating; + } + + void _updateRating(Offset localPosition, bool isRTL, {bool isTap = false}) { + final totalWidth = widget.itemCount * widget.itemSize + (widget.itemCount - 1) * widget.starPadding; + double dx = localPosition.dx; + + if (isRTL) dx = totalWidth - dx; + + double newRating; + + if (dx <= 0) { + newRating = 0; + } else if (dx >= totalWidth) { + newRating = widget.itemCount.toDouble(); + } else { + double starWithPadding = widget.itemSize + widget.starPadding; + int tappedIndex = (dx / starWithPadding).floor().clamp(0, widget.itemCount - 1); + newRating = tappedIndex + 1.0; + + if (isTap && newRating == _currentRating && _currentRating != 0) { + newRating = 0; + } + } + + if (_currentRating != newRating) { + setState(() { + _currentRating = newRating; + }); + widget.onRatingUpdate?.call(newRating.round()); + } + } + + @override + Widget build(BuildContext context) { + final isRTL = Directionality.of(context) == TextDirection.rtl; + final double visualAlignmentOffset = 5.0; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Transform.translate( + offset: Offset(isRTL ? visualAlignmentOffset : -visualAlignmentOffset, 0), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (details) => _updateRating(details.localPosition, isRTL, isTap: true), + onPanUpdate: (details) => _updateRating(details.localPosition, isRTL, isTap: false), + child: Row( + mainAxisSize: MainAxisSize.min, + textDirection: isRTL ? TextDirection.rtl : TextDirection.ltr, + children: List.generate(widget.itemCount * 2 - 1, (i) { + if (i.isOdd) { + return SizedBox(width: widget.starPadding); + } + int index = i ~/ 2; + bool filled = _currentRating > index; + return widget.itemBuilder ?? + Icon( + Icons.star_rounded, + size: widget.itemSize, + color: filled ? widget.filledColor : widget.unfilledColor, + ); + }), + ), + ), + ), + if (_currentRating > 0) + Padding( + padding: const EdgeInsets.only(top: 12.0), + child: GestureDetector( + onTap: () { + setState(() { + _currentRating = 0; + }); + widget.onClearRating?.call(); + }, + child: Text( + 'rating_clear'.t(context: context), + style: TextStyle(color: context.themeData.colorScheme.primary), + ), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 8727f40a1a..30889835f6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -19,12 +20,13 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/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:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -52,6 +54,8 @@ class NativeVideoViewer extends HookConsumerWidget { final bool showControls; final int playbackDelayFactor; final Widget image; + final ValueNotifier? scaleStateNotifier; + final bool disableScaleGestures; const NativeVideoViewer({ super.key, @@ -59,6 +63,8 @@ class NativeVideoViewer extends HookConsumerWidget { required this.image, this.showControls = true, this.playbackDelayFactor = 1, + this.scaleStateNotifier, + this.disableScaleGestures = false, }); @override @@ -96,7 +102,7 @@ class NativeVideoViewer extends HookConsumerWidget { try { if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) { final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!; - final file = await const StorageRepository().getFileForAsset(id); + final file = await StorageRepository().getFileForAsset(id); if (!context.mounted) { return null; } @@ -138,6 +144,7 @@ class NativeVideoViewer extends HookConsumerWidget { final videoSource = useMemoized>(() => createSource()); final aspectRatio = useState(null); + useMemoized(() async { if (!context.mounted || aspectRatio.value != null) { return null; @@ -205,7 +212,7 @@ class NativeVideoViewer extends HookConsumerWidget { final videoPlayback = VideoPlaybackValue.fromNativeController(videoController); ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; - if (ref.read(assetViewerProvider.select((s) => s.showingBottomSheet))) { + if (ref.read(assetViewerProvider.select((s) => s.showingDetails))) { return; } @@ -313,6 +320,20 @@ class NativeVideoViewer extends HookConsumerWidget { Timer(const Duration(milliseconds: 200), checkIfBuffering); } + Size? videoContextSize(double? videoAspectRatio, BuildContext? context) { + Size? videoContextSize; + if (videoAspectRatio == null || context == null) { + return null; + } + final contextAspectRatio = context.width / context.height; + if (videoAspectRatio > contextAspectRatio) { + videoContextSize = Size(context.width, context.width / aspectRatio.value!); + } else { + videoContextSize = Size(context.height * aspectRatio.value!, context.height); + } + return videoContextSize; + } + ref.listen(currentAssetNotifier, (_, value) { final playerController = controller.value; if (playerController != null && value != asset) { @@ -393,26 +414,29 @@ class NativeVideoViewer extends HookConsumerWidget { } }); - 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, + return SizedBox( + width: context.width, + height: context.height, + child: Stack( + children: [ + // Hide thumbnail once video is visible to avoid it showing in background when zooming out on video. + if (!isVisible.value || controller.value == null) Center(child: image), + if (aspectRatio.value != null && !isCasting && isCurrent) + Visibility.maintain( + visible: isVisible.value, + child: PhotoView.customChild( + enableRotation: false, + disableScaleGestures: disableScaleGestures, + // Transparent to avoid a black flash when viewer becomes visible but video isn't loaded yet. + backgroundDecoration: const BoxDecoration(color: Colors.transparent), + scaleStateChangedCallback: (state) => scaleStateNotifier?.value = state, + childSize: videoContextSize(aspectRatio.value, context), + child: NativeVideoPlayerView(onViewReady: initController), ), ), - ), - if (showControls) const Center(child: VideoViewerControls()), - ], + 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 index c1324b8ac0..28cfe5e73c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.sta 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/asset_viewer/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'; @@ -19,8 +19,8 @@ class VideoViewerControls extends HookConsumerWidget { 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) { + final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); + if (showingDetails) { showControls = false; } final VideoPlaybackState state = ref.watch(videoPlaybackValueProvider.select((value) => value.state)); @@ -81,27 +81,35 @@ class VideoViewerControls extends HookConsumerWidget { } } + void toggleControlsVisibility() { + if (showBuffering) { + return; + } + if (showControls) { + ref.read(assetViewerProvider.notifier).setControls(false); + } else { + showControlsAndStartHideTimer(); + } + } + return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: showControlsAndStartHideTimer, - child: AbsorbPointer( - absorbing: !showControls, + behavior: HitTestBehavior.translucent, + onTap: toggleControlsVisibility, + child: IgnorePointer( + ignoring: !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, - ), + 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/asset_viewer/viewer_bottom_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart new file mode 100644 index 0000000000..aa3b8bb93f --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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'; + +class ViewerBottomAppBar extends ConsumerWidget { + const ViewerBottomAppBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + double opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); + final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); + + if (!showControls) { + opacity = 0.0; + } + + return IgnorePointer( + ignoring: opacity < 1.0, + child: AnimatedOpacity( + opacity: opacity, + duration: Durations.short2, + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [AssetStackRow(), ViewerBottomBar()], + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart index 10f3595d01..fb25e9e1cb 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.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/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; diff --git a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart similarity index 80% rename from mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart rename to mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 193cf60220..4b748abc27 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -3,16 +3,15 @@ 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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; @@ -35,8 +34,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final isInLockedView = ref.watch(inLockedViewProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); - final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); - int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); + final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails)); + double opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); if (album != null && album.isActivityEnabled && album.isShared && asset is RemoteAsset) { @@ -44,7 +43,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { } if (!showControls) { - opacity = 0; + opacity = 0.0; } final originalTheme = context.themeData; @@ -55,7 +54,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { IconButton( icon: const Icon(Icons.chat_outlined), onPressed: () { - EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)); + context.router.push( + DriftActivitiesRoute( + album: album, + assetId: asset is RemoteAsset ? asset.id : null, + assetName: asset.name, + ), + ); }, ), @@ -70,17 +75,17 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { final lockedViewActions = [ViewerKebabMenu(originalTheme: originalTheme)]; return IgnorePointer( - ignoring: opacity < 255, + ignoring: opacity < 1.0, child: AnimatedOpacity( - opacity: opacity / 255, + opacity: opacity, duration: Durations.short2, child: AppBar( - backgroundColor: isShowingSheet ? Colors.transparent : Colors.black.withAlpha(125), + backgroundColor: showingDetails ? Colors.transparent : Colors.black.withValues(alpha: 0.5), leading: const _AppBarBackButton(), iconTheme: const IconThemeData(size: 22, color: Colors.white), actionsIconTheme: const IconThemeData(size: 22, color: Colors.white), shape: const Border(), - actions: isShowingSheet || isReadonlyModeEnabled + actions: showingDetails || isReadonlyModeEnabled ? null : isInLockedView ? lockedViewActions @@ -99,9 +104,9 @@ class _AppBarBackButton extends ConsumerWidget { @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; + final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails)); + final backgroundColor = showingDetails && !context.isDarkTheme ? Colors.white : Colors.black; + final foregroundColor = showingDetails && !context.isDarkTheme ? Colors.black : Colors.white; return Padding( padding: const EdgeInsets.only(left: 12.0), @@ -112,7 +117,7 @@ class _AppBarBackButton extends ConsumerWidget { iconSize: 22, iconColor: foregroundColor, padding: EdgeInsets.zero, - elevation: isShowingSheet ? 4 : 0, + elevation: showingDetails ? 4 : 0, ), onPressed: context.maybePop, child: const Icon(Icons.arrow_back_rounded), diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index ae4cfbd1c6..7c92dc01d8 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,7 +1,6 @@ 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'; @@ -57,17 +56,15 @@ class BackupToggleButtonState extends ConsumerState with Sin @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 isSyncing = ref.watch(driftBackupProvider.select((state) => state.isSyncing)); - final isProcessing = uploadTasks.isNotEmpty || isSyncing; + final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); + + final errorCount = ref.watch(driftBackupProvider.select((state) => state.errorCount)); + + final isProcessing = uploadTasks.isNotEmpty || isSyncing || iCloudProgress.isNotEmpty; return AnimatedBuilder( animation: _animationController, @@ -115,7 +112,7 @@ class BackupToggleButtonState extends ConsumerState with Sin borderRadius: const BorderRadius.all(Radius.circular(20.5)), child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(20.5)), - onTap: () => isCanceling ? null : _onToggle(!_isEnabled), + onTap: () => _onToggle(!_isEnabled), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( @@ -154,35 +151,18 @@ class BackupToggleButtonState extends ConsumerState with Sin ), ], ), - if (enqueueCount != enqueueTotalCount) - Text( - "queue_status".t( - context: context, - args: {'count': enqueueCount.toString(), 'total': enqueueTotalCount.toString()}, + if (errorCount > 0) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + "upload_error_with_count".t(context: context, args: {'count': '$errorCount'}), + style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.error), ), - 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)), + Switch.adaptive(value: _isEnabled, onChanged: (value) => _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 index 9481ec12f5..cdff393a3f 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -36,7 +36,7 @@ class ArchiveBottomSheet extends ConsumerWidget { const ShareLinkActionButton(source: ActionSource.timeline), const UnArchiveActionButton(source: ActionSource.timeline), const FavoriteActionButton(source: ActionSource.timeline), - const DownloadActionButton(source: ActionSource.timeline), + if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index fb6034b869..1dee0f6456 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -75,7 +75,7 @@ class FavoriteBottomSheet extends ConsumerWidget { const ShareLinkActionButton(source: ActionSource.timeline), const UnFavoriteActionButton(source: ActionSource.timeline), const ArchiveActionButton(source: ActionSource.timeline), - const DownloadActionButton(source: ActionSource.timeline), + if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 9436707c84..8753a9c14f 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -108,7 +108,7 @@ class _GeneralBottomSheetState extends ConsumerState { const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - const DownloadActionButton(source: ActionSource.timeline), + if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), @@ -119,10 +119,11 @@ class _GeneralBottomSheetState extends ConsumerState { const MoveToLockFolderActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), - const DeleteActionButton(source: ActionSource.timeline), + if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline), ], - if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), - if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline), + if (multiselect.onlyLocal || multiselect.hasMerged) + const DeleteLocalActionButton(source: ActionSource.timeline), + if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), ], slivers: multiselect.hasRemote ? [ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index ac3772a02b..d7ef604718 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -14,7 +14,7 @@ class MapBottomSheet extends StatelessWidget { Widget build(BuildContext context) { return BaseBottomSheet( initialChildSize: 0.25, - maxChildSize: 0.9, + maxChildSize: 0.75, shouldCloseOnMinExtent: false, resizeOnScroll: false, actions: [], @@ -38,8 +38,13 @@ class _ScopedMapTimeline extends StatelessWidget { throw Exception('User must be logged in to access archive'); } - final bounds = ref.watch(mapStateProvider).bounds; - final timelineService = ref.watch(timelineFactoryProvider).map(user.id, bounds); + final users = ref.watch(mapStateProvider).withPartners + ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] + : [user.id]; + + final timelineService = ref + .watch(timelineFactoryProvider) + .map(users, ref.watch(mapStateProvider).toOptions()); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 2f2a2e0a4e..6848a07bb8 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_ 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/set_album_cover.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'; @@ -113,6 +114,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), + if (ownsAlbum && multiselect.selectedAssets.length == 1) + SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), ], slivers: ownsAlbum ? [ diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e77803c206..259ac824bb 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:ui' as ui; import 'package:async/async.dart'; import 'package:flutter/widgets.dart'; @@ -50,20 +50,51 @@ mixin CancellableImageProviderMixin on CancellableImageProvide return null; } - Stream loadRequest(ImageRequest request, ImageDecoderCallback decode) async* { + Stream loadRequest(ImageRequest request, ImageDecoderCallback decode, {bool evictOnError = true}) async* { if (isCancelled) { this.request = null; - unawaited(evict()); + PaintingBinding.instance.imageCache.evict(this); return; } try { final image = await request.load(decode); - if (image == null || isCancelled) { - unawaited(evict()); + if ((image == null && evictOnError) || isCancelled) { + PaintingBinding.instance.imageCache.evict(this); + return; + } else if (image == null) { return; } yield image; + } catch (e, stack) { + if (evictOnError) { + PaintingBinding.instance.imageCache.evict(this); + rethrow; + } + _log.warning('Non-fatal image load error', e, stack); + } finally { + this.request = null; + } + } + + Future loadCodecRequest(ImageRequest request) async { + if (isCancelled) { + this.request = null; + PaintingBinding.instance.imageCache.evict(this); + return null; + } + + try { + final codec = await request.loadCodec(); + if (codec == null || isCancelled) { + codec?.dispose(); + PaintingBinding.instance.imageCache.evict(this); + return null; + } + return codec; + } catch (e) { + PaintingBinding.instance.imageCache.evict(this); + rethrow; } finally { this.request = null; } @@ -112,14 +143,17 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080 provider = LocalFullImageProvider(id: id, size: size, assetType: asset.type); } else { final String assetId; + final String thumbhash; if (asset is LocalAsset && asset.hasRemote) { assetId = asset.remoteId!; + thumbhash = ""; } else if (asset is RemoteAsset) { assetId = asset.id; + thumbhash = asset.thumbHash ?? ""; } else { throw ArgumentError("Unsupported asset type: ${asset.runtimeType}"); } - provider = RemoteFullImageProvider(assetId: assetId); + provider = RemoteFullImageProvider(assetId: assetId, thumbhash: thumbhash, assetType: asset.type); } return provider; @@ -132,8 +166,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai } final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; - return assetId != null ? RemoteThumbProvider(assetId: assetId) : null; + final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; + return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash) : null; } bool _shouldUseLocalAsset(BaseAsset asset) => - asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)); + asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited; diff --git a/mobile/lib/presentation/widgets/images/local_image_provider.dart b/mobile/lib/presentation/widgets/images/local_image_provider.dart index c5dca57f9c..1c7d102239 100644 --- a/mobile/lib/presentation/widgets/images/local_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/local_image_provider.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -32,7 +31,7 @@ class LocalThumbProvider extends CancellableImageProvider DiagnosticsProperty('Id', key.id), DiagnosticsProperty('Size', key.size), ], - onDispose: cancel, + onLastListenerRemoved: cancel, ); } @@ -77,7 +76,7 @@ class LocalFullImageProvider extends CancellableImageProvider('Id', key.id), DiagnosticsProperty('Size', key.size), ], - onDispose: cancel, + onLastListenerRemoved: cancel, ); } @@ -85,7 +84,7 @@ class LocalFullImageProvider extends CancellableImageProvider images, { ImageInfo? initialImage, InformationCollector? informationCollector, - void Function()? onDispose, + void Function()? onLastListenerRemoved, }) { if (initialImage != null) { + didProvideImage = true; setImage(initialImage); } - _onDispose = onDispose; + _onLastListenerRemoved = onLastListenerRemoved; images.listen( - setImage, + (image) { + didProvideImage = true; + setImage(image); + }, onError: (Object error, StackTrace stack) { reportError( context: ErrorDescription('resolving a single-frame image stream'), @@ -40,12 +47,24 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter { } @override - void onDisposed() { - final onDispose = _onDispose; - if (onDispose != null) { - _onDispose = null; - onDispose(); + void addListener(ImageStreamListener listener) { + super.addListener(listener); + _listenerCount = _listenerCount + 1; + } + + @override + void removeListener(ImageStreamListener listener) { + super.removeListener(listener); + _listenerCount = _listenerCount - 1; + + final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage; + final bool noListenersAfterImage = _listenerCount == 0 && didProvideImage; + + final onLastListenerRemoved = _onLastListenerRemoved; + + if (onLastListenerRemoved != null && (noListenersAfterImage || onlyCacheListenerLeft)) { + _onLastListenerRemoved = null; + onLastListenerRemoved(); } - super.onDisposed(); } } diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index 7a063a8672..e7e5deb6a6 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -1,69 +1,66 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/painting.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/loaders/image_request.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; -import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:openapi/api.dart'; -class RemoteThumbProvider extends CancellableImageProvider - with CancellableImageProviderMixin { - static final cacheManager = RemoteThumbnailCacheManager(); - final String assetId; +class RemoteImageProvider extends CancellableImageProvider + with CancellableImageProviderMixin { + final String url; - RemoteThumbProvider({required this.assetId}); + RemoteImageProvider({required this.url}); + + RemoteImageProvider.thumbnail({required String assetId, required String thumbhash}) + : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash); @override - Future obtainKey(ImageConfiguration configuration) { + Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } @override - ImageStreamCompleter loadImage(RemoteThumbProvider key, ImageDecoderCallback decode) { + ImageStreamCompleter loadImage(RemoteImageProvider key, ImageDecoderCallback decode) { return OneFramePlaceholderImageStreamCompleter( _codec(key, decode), informationCollector: () => [ DiagnosticsProperty('Image provider', this), - DiagnosticsProperty('Asset Id', key.assetId), + DiagnosticsProperty('URL', key.url), ], - onDispose: cancel, + onLastListenerRemoved: cancel, ); } - Stream _codec(RemoteThumbProvider key, ImageDecoderCallback decode) { - final request = this.request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId), - headers: ApiService.getRequestHeaders(), - cacheManager: cacheManager, - ); + Stream _codec(RemoteImageProvider key, ImageDecoderCallback decode) { + final request = this.request = RemoteImageRequest(uri: key.url, headers: ApiService.getRequestHeaders()); return loadRequest(request, decode); } @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is RemoteThumbProvider) { - return assetId == other.assetId; + if (other is RemoteImageProvider) { + return url == other.url; } - return false; } @override - int get hashCode => assetId.hashCode; + int get hashCode => url.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { - static final cacheManager = RemoteThumbnailCacheManager(); final String assetId; + final String thumbhash; + final AssetType assetType; - RemoteFullImageProvider({required this.assetId}); + RemoteFullImageProvider({required this.assetId, required this.thumbhash, required this.assetType}); @override Future obtainKey(ImageConfiguration configuration) { @@ -74,12 +71,12 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), ], - onDispose: cancel, + onLastListenerRemoved: cancel, ); } @@ -87,39 +84,41 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode; } diff --git a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart index fcd2fca72f..7076febe3b 100644 --- a/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart +++ b/mobile/lib/presentation/widgets/images/thumb_hash_provider.dart @@ -17,7 +17,7 @@ class ThumbHashProvider extends CancellableImageProvider @override ImageStreamCompleter loadImage(ThumbHashProvider key, ImageDecoderCallback decode) { - return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode), onDispose: cancel); + return OneFramePlaceholderImageStreamCompleter(_loadCodec(key, decode), onLastListenerRemoved: cancel); } Stream _loadCodec(ThumbHashProvider key, ImageDecoderCallback decode) { diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 92b1bb2544..70a9057e12 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -21,9 +21,14 @@ class Thumbnail extends StatefulWidget { const Thumbnail({this.imageProvider, this.fit = BoxFit.cover, this.thumbhashProvider, super.key}); - Thumbnail.remote({required String remoteId, this.fit = BoxFit.cover, Size size = kThumbnailResolution, super.key}) - : imageProvider = RemoteThumbProvider(assetId: remoteId), - thumbhashProvider = null; + Thumbnail.remote({ + required String remoteId, + required String thumbhash, + this.fit = BoxFit.cover, + Size size = kThumbnailResolution, + super.key, + }) : imageProvider = RemoteImageProvider.thumbnail(assetId: remoteId, thumbhash: thumbhash), + thumbhashProvider = null; Thumbnail.fromAsset({ required BaseAsset? asset, @@ -228,16 +233,6 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix @override void dispose() { - final imageProvider = widget.imageProvider; - if (imageProvider is CancellableImageProvider) { - imageProvider.cancel(); - } - - final thumbhashProvider = widget.thumbhashProvider; - if (thumbhashProvider is CancellableImageProvider) { - thumbhashProvider.cancel(); - } - _fadeController.removeStatusListener(_onAnimationStatusChanged); _fadeController.dispose(); _stopListeningToStream(); diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index c7628cb472..d6485ae7b6 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -6,12 +6,14 @@ import 'package:immich_mobile/domain/models/setting.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/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -class ThumbnailTile extends ConsumerWidget { +class ThumbnailTile extends ConsumerStatefulWidget { const ThumbnailTile( this.asset, { this.size = kThumbnailResolution, @@ -30,9 +32,23 @@ class ThumbnailTile extends ConsumerWidget { final int? heroOffset; @override - Widget build(BuildContext context, WidgetRef ref) { - final asset = this.asset; - final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + ConsumerState createState() => _ThumbnailTileState(); +} + +class _ThumbnailTileState extends ConsumerState { + bool _hideIndicators = false; + bool _showSelectionContainer = false; + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final asset = widget.asset; + final heroIndex = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0; + final isCurrentAsset = ref.watch(assetViewerProvider.select((current) => current.currentAsset == asset)); final assetContainerColor = context.isDarkTheme ? context.primaryColor.darken(amount: 0.4) @@ -43,17 +59,40 @@ class ThumbnailTile extends ConsumerWidget { ); final bool storageIndicator = - ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && showStorageIndicator; + ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator; + + if (!isCurrentAsset) { + _hideIndicators = false; + } + + if (isSelected) { + _showSelectionContainer = true; + } + + final uploadProgress = asset is LocalAsset + ? ref.watch(assetUploadProgressProvider.select((map) => map[asset.id])) + : null; return Stack( children: [ - Container(color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor), + Container( + color: widget.lockSelection + ? context.colorScheme.surfaceContainerHighest + : _showSelectionContainer + ? assetContainerColor + : Colors.transparent, + ), AnimatedContainer( duration: Durations.short4, curve: Curves.decelerate, - padding: EdgeInsets.all(isSelected || lockSelection ? 6 : 0), + onEnd: () { + if (!isSelected) { + _showSelectionContainer = false; + } + }, + padding: EdgeInsets.all(isSelected || widget.lockSelection ? 6 : 0), child: TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: (isSelected || lockSelection) ? 15.0 : 0.0), + tween: Tween(begin: 0.0, end: (isSelected || widget.lockSelection) ? 15.0 : 0.0), duration: Durations.short4, curve: Curves.decelerate, builder: (context, value, child) { @@ -63,65 +102,106 @@ class ThumbnailTile extends ConsumerWidget { children: [ Positioned.fill( child: Hero( - tag: '${asset?.heroTag ?? ''}_$heroIndex', - child: Thumbnail.fromAsset(asset: asset, size: size), + // This key resets the hero animation when the asset is changed in the asset viewer. + // It doesn't seem like the best solution, and only works to reset the hero, not prime the hero of the new active asset for animation, + // but other solutions have failed thus far. + key: ValueKey(isCurrentAsset), + tag: '${asset?.heroTag}_$heroIndex', + child: Thumbnail.fromAsset(asset: asset, size: widget.size), + // Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree. + placeholderBuilder: (context, heroSize, child) { + if (!_hideIndicators) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() => _hideIndicators = true); + }); + } + return const SizedBox(); + }, + flightShuttleBuilder: (context, animation, direction, from, to) { + void animationStatusListener(AnimationStatus status) { + final heroInFlight = status == AnimationStatus.forward || status == AnimationStatus.reverse; + if (_hideIndicators != heroInFlight) { + setState(() => _hideIndicators = heroInFlight); + } + if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) { + animation.removeStatusListener(animationStatusListener); + } + } + + animation.addStatusListener(animationStatusListener); + return to.widget; + }, ), ), if (asset != null) - Align( - alignment: Alignment.topRight, - child: _AssetTypeIcons(asset: asset), + AnimatedOpacity( + opacity: _hideIndicators ? 0.0 : 1.0, + duration: Durations.short4, + child: Align( + alignment: Alignment.topRight, + child: _AssetTypeIcons(asset: asset), + ), ), if (storageIndicator && asset != null) - 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), + AnimatedOpacity( + opacity: _hideIndicators ? 0.0 : 1.0, + duration: Durations.short4, + child: 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.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), + 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 != null && asset.isFavorite) - const Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: EdgeInsets.only(left: 10.0, bottom: 6.0), - child: _TileOverlayIcon(Icons.favorite_rounded), + AnimatedOpacity( + duration: Durations.short4, + opacity: _hideIndicators ? 0.0 : 1.0, + child: const Align( + alignment: Alignment.bottomLeft, + child: Padding( + padding: EdgeInsets.only(left: 10.0, bottom: 6.0), + child: _TileOverlayIcon(Icons.favorite_rounded), + ), ), ), + if (uploadProgress != null) _UploadProgressOverlay(progress: uploadProgress), ], ), ), ), TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: (isSelected || lockSelection) ? 1.0 : 0.0), + tween: Tween(begin: 0.0, end: (isSelected || widget.lockSelection) ? 1.0 : 0.0), duration: Durations.short4, curve: Curves.decelerate, builder: (context, value, child) { return Padding( - padding: EdgeInsets.all((isSelected || lockSelection) ? value * 3.0 : 3.0), + padding: EdgeInsets.all((isSelected || widget.lockSelection) ? value * 3.0 : 3.0), child: Align( alignment: Alignment.topLeft, child: Opacity( - opacity: (isSelected || lockSelection) ? 1 : value, + opacity: (isSelected || widget.lockSelection) ? 1 : value, child: _SelectionIndicator( - isLocked: lockSelection, - color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor, + isLocked: widget.lockSelection, + color: widget.lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor, ), ), ), @@ -229,3 +309,46 @@ class _AssetTypeIcons extends StatelessWidget { ); } } + +class _UploadProgressOverlay extends StatelessWidget { + final double progress; + + const _UploadProgressOverlay({required this.progress}); + + @override + Widget build(BuildContext context) { + final isError = progress < 0; + final percentage = isError ? 0 : (progress * 100).toInt(); + + return Positioned.fill( + child: Container( + color: isError ? Colors.red.withValues(alpha: 0.6) : Colors.black54, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isError) + const Icon(Icons.error_outline, color: Colors.white, size: 36) + else + SizedBox( + width: 36, + height: 36, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 3, + backgroundColor: Colors.white24, + valueColor: const AlwaysStoppedAnimation(Colors.white), + ), + ), + const SizedBox(height: 4), + Text( + isError ? 'Error' : '$percentage%', + style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index b849f954ae..bfd3011050 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -1,11 +1,30 @@ +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/events.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; +import 'package:immich_mobile/providers/map/map_state.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; class MapState { + final ThemeMode themeMode; final LatLngBounds bounds; + final bool onlyFavorites; + final bool includeArchived; + final bool withPartners; + final int relativeDays; - const MapState({required this.bounds}); + const MapState({ + this.themeMode = ThemeMode.system, + required this.bounds, + this.onlyFavorites = false, + this.includeArchived = false, + this.withPartners = false, + this.relativeDays = 0, + }); @override bool operator ==(covariant MapState other) { @@ -15,9 +34,31 @@ class MapState { @override int get hashCode => bounds.hashCode; - MapState copyWith({LatLngBounds? bounds}) { - return MapState(bounds: bounds ?? this.bounds); + MapState copyWith({ + LatLngBounds? bounds, + ThemeMode? themeMode, + bool? onlyFavorites, + bool? includeArchived, + bool? withPartners, + int? relativeDays, + }) { + return MapState( + bounds: bounds ?? this.bounds, + themeMode: themeMode ?? this.themeMode, + onlyFavorites: onlyFavorites ?? this.onlyFavorites, + includeArchived: includeArchived ?? this.includeArchived, + withPartners: withPartners ?? this.withPartners, + relativeDays: relativeDays ?? this.relativeDays, + ); } + + TimelineMapOptions toOptions() => TimelineMapOptions( + bounds: bounds, + onlyFavorites: onlyFavorites, + includeArchived: includeArchived, + withPartners: withPartners, + relativeDays: relativeDays, + ); } class MapStateNotifier extends Notifier { @@ -31,11 +72,50 @@ class MapStateNotifier extends Notifier { return true; } + void switchTheme(ThemeMode mode) { + // TODO: Remove this line when map theme provider is removed + // Until then, keep both in sync as MapThemeOverride uses map state provider + // ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index); + ref.read(mapStateNotifierProvider.notifier).switchTheme(mode); + state = state.copyWith(themeMode: mode); + } + + void switchFavoriteOnly(bool isFavoriteOnly) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly); + state = state.copyWith(onlyFavorites: isFavoriteOnly); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + + void switchIncludeArchived(bool isIncludeArchived) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived); + state = state.copyWith(includeArchived: isIncludeArchived); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + + void switchWithPartners(bool isWithPartners) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners); + state = state.copyWith(withPartners: isWithPartners); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + + void setRelativeTime(int relativeDays) { + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays); + state = state.copyWith(relativeDays: relativeDays); + EventStream.shared.emit(const MapMarkerReloadEvent()); + } + @override - MapState build() => MapState( - // TODO: set default bounds - bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), - ); + MapState build() { + final appSettingsService = ref.read(appSettingsServiceProvider); + return MapState( + themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)], + onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly), + includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived), + withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners), + relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate), + bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)), + ); + } } // This provider watches the markers from the map service and serves the markers. diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 17dcffdade..72f4e8bda6 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/events.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/extensions/translate_extensions.dart'; @@ -51,11 +53,19 @@ class _DriftMapState extends ConsumerState { final _reloadMutex = AsyncMutex(); final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2)); final ValueNotifier bottomSheetOffset = ValueNotifier(0.25); + StreamSubscription? _eventSubscription; + + @override + void initState() { + super.initState(); + _eventSubscription = EventStream.shared.listen(_onEvent); + } @override void dispose() { _debouncer.dispose(); bottomSheetOffset.dispose(); + _eventSubscription?.cancel(); super.dispose(); } @@ -63,6 +73,8 @@ class _DriftMapState extends ConsumerState { mapController = controller; } + void _onEvent(_) => _debouncer.run(() => setBounds(forceReload: true)); + Future onMapReady() async { final controller = mapController; if (controller == null) { @@ -98,7 +110,7 @@ class _DriftMapState extends ConsumerState { ); } - _debouncer.run(setBounds); + _debouncer.run(() => setBounds(forceReload: true)); controller.addListener(onMapMoved); } @@ -110,7 +122,7 @@ class _DriftMapState extends ConsumerState { _debouncer.run(setBounds); } - Future setBounds() async { + Future setBounds({bool forceReload = false}) async { final controller = mapController; if (controller == null || !mounted) { return; @@ -127,7 +139,7 @@ class _DriftMapState extends ConsumerState { final bounds = await controller.getVisibleRegion(); unawaited( _reloadMutex.run(() async { - if (mounted && ref.read(mapStateProvider.notifier).setBounds(bounds)) { + if (mounted && (ref.read(mapStateProvider.notifier).setBounds(bounds) || forceReload)) { final markers = await ref.read(mapMarkerProvider(bounds).future); await reloadMarkers(markers); } @@ -203,7 +215,7 @@ class _Map extends StatelessWidget { onMapCreated: onMapCreated, onStyleLoadedCallback: onMapReady, attributionButtonPosition: AttributionButtonPosition.topRight, - attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72), + attributionButtonMargins: const Point(8, kToolbarHeight), ), ), ); @@ -244,7 +256,7 @@ class _DynamicMyLocationButton extends StatelessWidget { valueListenable: bottomSheetOffset, builder: (context, offset, child) { return Positioned( - right: 16, + right: 20, bottom: context.height * (offset - 0.02) + context.padding.bottom, child: AnimatedOpacity( opacity: offset < 0.8 ? 1 : 0, diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart new file mode 100644 index 0000000000..c581dd6292 --- /dev/null +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -0,0 +1,61 @@ +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/map/map.state.dart'; +import 'package:immich_mobile/widgets/map/map_settings/map_settings_list_tile.dart'; +import 'package:immich_mobile/widgets/map/map_settings/map_settings_time_dropdown.dart'; +import 'package:immich_mobile/widgets/map/map_settings/map_theme_picker.dart'; + +class DriftMapSettingsSheet extends HookConsumerWidget { + const DriftMapSettingsSheet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mapState = ref.watch(mapStateProvider); + + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.6, + builder: (ctx, scrollController) => SingleChildScrollView( + controller: scrollController, + child: Card( + elevation: 0.0, + shadowColor: Colors.transparent, + color: Colors.transparent, + margin: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + MapThemePicker( + themeMode: mapState.themeMode, + onThemeChange: (mode) => ref.read(mapStateProvider.notifier).switchTheme(mode), + ), + const Divider(height: 30, thickness: 1), + MapSettingsListTile( + title: "map_settings_only_show_favorites".t(context: context), + selected: mapState.onlyFavorites, + onChanged: (favoriteOnly) => ref.read(mapStateProvider.notifier).switchFavoriteOnly(favoriteOnly), + ), + MapSettingsListTile( + title: "map_settings_include_show_archived".t(context: context), + selected: mapState.includeArchived, + onChanged: (includeArchive) => + ref.read(mapStateProvider.notifier).switchIncludeArchived(includeArchive), + ), + MapSettingsListTile( + title: "map_settings_include_show_partners".t(context: context), + selected: mapState.withPartners, + onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), + ), + MapTimeDropDown( + relativeTime: mapState.relativeDays, + onTimeChange: (time) => ref.read(mapStateProvider.notifier).setRelativeTime(time), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index e85a6c05f8..62889b10cb 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -60,7 +60,11 @@ class DriftMemoryCard extends ConsumerWidget { child: SizedBox( width: 205, height: 200, - child: Thumbnail.remote(remoteId: memory.assets[0].id, fit: BoxFit.cover), + child: Thumbnail.remote( + remoteId: memory.assets[0].id, + thumbhash: memory.assets[0].thumbHash ?? "", + fit: BoxFit.cover, + ), ), ), Positioned( diff --git a/mobile/lib/presentation/widgets/people/partner_user_avatar.widget.dart b/mobile/lib/presentation/widgets/people/partner_user_avatar.widget.dart index 8cdf1ed286..8b391d50c6 100644 --- a/mobile/lib/presentation/widgets/people/partner_user_avatar.widget.dart +++ b/mobile/lib/presentation/widgets/people/partner_user_avatar.widget.dart @@ -1,10 +1,9 @@ -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'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; class PartnerUserAvatar extends StatelessWidget { const PartnerUserAvatar({super.key, required this.partner}); @@ -18,11 +17,7 @@ class PartnerUserAvatar extends StatelessWidget { return CircleAvatar( radius: 16, backgroundColor: context.primaryColor.withAlpha(50), - foregroundImage: CachedNetworkImageProvider( - url, - headers: ApiService.getRequestHeaders(), - cacheKey: "user-${partner.id}-profile", - ), + foregroundImage: RemoteImageProvider(url: url), // 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/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index dd6390406b..7ed02af26b 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -25,7 +25,7 @@ class _DriftPersonNameEditFormState extends ConsumerState? iconShadows; @override Widget build(BuildContext context, WidgetRef ref) { - TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); + final theme = context.themeData; + final menuChildren = []; - 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 (onShowOptions != null) - ListTile( - leading: const Icon(Icons.settings), - title: Text('options'.t(context: context), style: textStyle), - onTap: onShowOptions, - ), - 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, - ), - ], - ], + if (onEditAlbum != null) { + menuChildren.add( + BaseActionButton( + label: 'edit_album'.t(context: context), + iconData: Icons.edit, + onPressed: onEditAlbum, + menuItem: true, ), + ); + } + + if (onAddPhotos != null) { + menuChildren.add( + BaseActionButton( + label: 'add_photos'.t(context: context), + iconData: Icons.add_a_photo, + onPressed: onAddPhotos, + menuItem: true, + ), + ); + } + + if (onAddUsers != null) { + menuChildren.add( + BaseActionButton( + label: 'album_viewer_page_share_add_users'.t(context: context), + iconData: Icons.group_add, + onPressed: onAddUsers, + menuItem: true, + ), + ); + } + + if (onLeaveAlbum != null) { + menuChildren.add( + BaseActionButton( + label: 'leave_album'.t(context: context), + iconData: Icons.person_remove_rounded, + onPressed: onLeaveAlbum, + menuItem: true, + ), + ); + } + + if (onToggleAlbumOrder != null) { + menuChildren.add( + BaseActionButton( + label: 'change_display_order'.t(context: context), + iconData: Icons.swap_vert_rounded, + onPressed: onToggleAlbumOrder, + menuItem: true, + ), + ); + } + + if (onCreateSharedLink != null) { + menuChildren.add( + BaseActionButton( + label: 'create_shared_link'.t(context: context), + iconData: Icons.link, + onPressed: onCreateSharedLink, + menuItem: true, + ), + ); + } + + if (onShowOptions != null) { + menuChildren.add( + BaseActionButton( + label: 'options'.t(context: context), + iconData: Icons.settings, + onPressed: onShowOptions, + menuItem: true, + ), + ); + } + + if (onDeleteAlbum != null) { + menuChildren.add(const Divider(height: 1)); + menuChildren.add( + BaseActionButton( + label: 'delete_album'.t(context: context), + iconData: Icons.delete, + iconColor: context.isDarkTheme ? Colors.red[400] : Colors.red[800], + onPressed: onDeleteAlbum, + menuItem: true, + ), + ); + } + + return MenuAnchor( + consumeOutsideTap: true, + style: MenuStyle( + backgroundColor: WidgetStatePropertyAll(theme.scaffoldBackgroundColor), + surfaceTintColor: const WidgetStatePropertyAll(Colors.grey), + elevation: const WidgetStatePropertyAll(4), + shape: const WidgetStatePropertyAll( + RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + ), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), ), + menuChildren: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 150), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: menuChildren, + ), + ), + ], + builder: (context, controller, child) { + return IconButton( + icon: Icon(Icons.more_vert_rounded, color: iconColor ?? Colors.white, shadows: iconShadows), + onPressed: () => controller.isOpen ? controller.close() : controller.open(), + ); + }, ); } } diff --git a/mobile/lib/presentation/widgets/timeline/constants.dart b/mobile/lib/presentation/widgets/timeline/constants.dart index cfe96b1c81..3b4269925c 100644 --- a/mobile/lib/presentation/widgets/timeline/constants.dart +++ b/mobile/lib/presentation/widgets/timeline/constants.dart @@ -2,9 +2,11 @@ import 'dart:ui'; const double kTimelineHeaderExtent = 80.0; const Size kTimelineFixedTileExtent = Size.square(256); -const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size const double kTimelineSpacing = 2.0; const int kTimelineColumnCount = 3; const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300); const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800); + +const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size +const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB diff --git a/mobile/lib/presentation/widgets/timeline/fixed/row.dart b/mobile/lib/presentation/widgets/timeline/fixed/row.dart index 3fe3cea3c9..97067add24 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/row.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/row.dart @@ -1,27 +1,45 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -class FixedTimelineRow extends MultiChildRenderObjectWidget { - final double dimension; +class TimelineRow extends MultiChildRenderObjectWidget { + final double height; + final List widths; final double spacing; final TextDirection textDirection; - const FixedTimelineRow({ + const TimelineRow({ super.key, - required this.dimension, + required this.height, + required this.widths, required this.spacing, required this.textDirection, required super.children, }); + factory TimelineRow.fixed({ + required double dimension, + required double spacing, + required TextDirection textDirection, + required List children, + }) => TimelineRow( + height: dimension, + widths: List.filled(children.length, dimension), + spacing: spacing, + textDirection: textDirection, + children: children, + ); + @override RenderObject createRenderObject(BuildContext context) { - return RenderFixedRow(dimension: dimension, spacing: spacing, textDirection: textDirection); + return RenderFixedRow(height: height, widths: widths, spacing: spacing, textDirection: textDirection); } @override void updateRenderObject(BuildContext context, RenderFixedRow renderObject) { - renderObject.dimension = dimension; + renderObject.height = height; + renderObject.widths = widths; renderObject.spacing = spacing; renderObject.textDirection = textDirection; } @@ -29,7 +47,8 @@ class FixedTimelineRow extends MultiChildRenderObjectWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DoubleProperty('dimension', dimension)); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty>('widths', widths)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('textDirection', textDirection)); } @@ -43,21 +62,32 @@ class RenderFixedRow extends RenderBox RenderBoxContainerDefaultsMixin { RenderFixedRow({ List? children, - required double dimension, + required double height, + required List widths, required double spacing, required TextDirection textDirection, - }) : _dimension = dimension, + }) : _height = height, + _widths = widths, _spacing = spacing, _textDirection = textDirection { addAll(children); } - double get dimension => _dimension; - double _dimension; + double get height => _height; + double _height; - set dimension(double value) { - if (_dimension == value) return; - _dimension = value; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + List get widths => _widths; + List _widths; + + set widths(List value) { + if (listEquals(_widths, value)) return; + _widths = value; markNeedsLayout(); } @@ -86,7 +116,7 @@ class RenderFixedRow extends RenderBox } } - double get intrinsicWidth => dimension * childCount + spacing * (childCount - 1); + double get intrinsicWidth => widths.sum + (spacing * (childCount - 1)); @override double computeMinIntrinsicWidth(double height) => intrinsicWidth; @@ -95,10 +125,10 @@ class RenderFixedRow extends RenderBox double computeMaxIntrinsicWidth(double height) => intrinsicWidth; @override - double computeMinIntrinsicHeight(double width) => dimension; + double computeMinIntrinsicHeight(double width) => height; @override - double computeMaxIntrinsicHeight(double width) => dimension; + double computeMaxIntrinsicHeight(double width) => height; @override double? computeDistanceToActualBaseline(TextBaseline baseline) { @@ -118,7 +148,8 @@ class RenderFixedRow extends RenderBox @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DoubleProperty('dimension', dimension)); + properties.add(DoubleProperty('height', height)); + properties.add(DiagnosticsProperty>('widths', widths)); properties.add(DoubleProperty('spacing', spacing)); properties.add(EnumProperty('textDirection', textDirection)); } @@ -131,19 +162,25 @@ class RenderFixedRow extends RenderBox return; } // Use the entire width of the parent for the row. - size = Size(constraints.maxWidth, dimension); - // Each tile is forced to be dimension x dimension. - final childConstraints = BoxConstraints.tight(Size(dimension, dimension)); + size = Size(constraints.maxWidth, height); + final flipMainAxis = textDirection == TextDirection.rtl; - Offset offset = Offset(flipMainAxis ? size.width - dimension : 0, 0); - final dx = (flipMainAxis ? -1 : 1) * (dimension + spacing); + int childIndex = 0; + double currentX = flipMainAxis ? size.width - (widths.firstOrNull ?? 0) : 0; // Layout each child horizontally. - while (child != null) { + while (child != null && childIndex < widths.length) { + final width = widths[childIndex]; + final childConstraints = BoxConstraints.tight(Size(width, height)); child.layout(childConstraints, parentUsesSize: false); final childParentData = child.parentData! as _RowParentData; - childParentData.offset = offset; - offset += Offset(dx, 0); + childParentData.offset = Offset(currentX, 0); child = childParentData.nextSibling; + childIndex++; + + if (child != null && childIndex < widths.length) { + final nextWidth = widths[childIndex]; + currentX += flipMainAxis ? -(spacing + nextWidth) : width + spacing; + } } } } diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index b879b33f68..aa2112b8dd 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart'; @@ -78,6 +80,7 @@ class FixedSegment extends Segment { assetCount: numberOfAssets, tileHeight: tileHeight, spacing: spacing, + columnCount: columnCount, ); } } @@ -87,24 +90,32 @@ class _FixedSegmentRow extends ConsumerWidget { final int assetCount; final double tileHeight; final double spacing; + final int columnCount; const _FixedSegmentRow({ required this.assetIndex, required this.assetCount, required this.tileHeight, required this.spacing, + required this.columnCount, }); @override Widget build(BuildContext context, WidgetRef ref) { final isScrubbing = ref.watch(timelineStateProvider.select((s) => s.isScrubbing)); final timelineService = ref.read(timelineServiceProvider); + final isDynamicLayout = columnCount <= (context.isMobile ? 2 : 3); if (isScrubbing) { return _buildPlaceholder(context); } if (timelineService.hasRange(assetIndex, assetCount)) { - return _buildAssetRow(context, timelineService.getAssets(assetIndex, assetCount), timelineService); + return _buildAssetRow( + context, + timelineService.getAssets(assetIndex, assetCount), + timelineService, + isDynamicLayout, + ); } return FutureBuilder>( @@ -113,7 +124,7 @@ class _FixedSegmentRow extends ConsumerWidget { if (snapshot.connectionState != ConnectionState.done) { return _buildPlaceholder(context); } - return _buildAssetRow(context, snapshot.requireData, timelineService); + return _buildAssetRow(context, snapshot.requireData, timelineService, isDynamicLayout); }, ); } @@ -122,23 +133,58 @@ class _FixedSegmentRow extends ConsumerWidget { return SegmentBuilder.buildPlaceholder(context, assetCount, size: Size.square(tileHeight), spacing: spacing); } - Widget _buildAssetRow(BuildContext context, List assets, TimelineService timelineService) { - return FixedTimelineRow( - dimension: tileHeight, - spacing: spacing, - textDirection: Directionality.of(context), - children: [ - for (int i = 0; i < assets.length; i++) - TimelineAssetIndexWrapper( + Widget _buildAssetRow( + BuildContext context, + List assets, + TimelineService timelineService, + bool isDynamicLayout, + ) { + final children = [ + for (int i = 0; i < assets.length; i++) + TimelineAssetIndexWrapper( + assetIndex: assetIndex + i, + segmentIndex: 0, // For simplicity, using 0 for now + child: _AssetTileWidget( + key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), + asset: assets[i], assetIndex: assetIndex + i, - segmentIndex: 0, // For simplicity, using 0 for now - child: _AssetTileWidget( - key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), - asset: assets[i], - assetIndex: assetIndex + i, - ), ), - ], + ), + ]; + + final widths = List.filled(assets.length, tileHeight); + + if (isDynamicLayout) { + final aspectRatios = assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList(); + final meanAspectRatio = aspectRatios.sum / assets.length; + + // 1: mean width + // 0.5: width < mean - threshold + // 1.5: width > mean + threshold + final arConfiguration = aspectRatios.map((e) { + if (e - meanAspectRatio > 0.3) return 1.5; + if (e - meanAspectRatio < -0.3) return 0.5; + return 1.0; + }); + + // Normalize to get width distribution + final sum = arConfiguration.sum; + + int index = 0; + for (final ratio in arConfiguration) { + // Distribute the available width proportionally based on aspect ratio configuration + widths[index++] = ((ratio * assets.length) / sum) * tileHeight; + } + } + + return TimelineDragRegion( + child: TimelineRow( + height: tileHeight, + widths: widths, + spacing: spacing, + textDirection: Directionality.of(context), + children: children, + ), ); } } diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index 58d7f933e9..d31048fbb5 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -450,7 +450,7 @@ class _SegmentWidget extends StatelessWidget { alignment: Alignment.center, child: Text( _segment.date.year.toString(), - style: context.textTheme.labelMedium?.copyWith(fontFamily: "OverpassMono", fontWeight: FontWeight.w600), + style: context.textTheme.labelMedium?.copyWith(fontFamily: "GoogleSansCode", fontWeight: FontWeight.w600), ), ), ), diff --git a/mobile/lib/presentation/widgets/timeline/segment_builder.dart b/mobile/lib/presentation/widgets/timeline/segment_builder.dart index 79ffb47e95..442d42d536 100644 --- a/mobile/lib/presentation/widgets/timeline/segment_builder.dart +++ b/mobile/lib/presentation/widgets/timeline/segment_builder.dart @@ -24,7 +24,7 @@ abstract class SegmentBuilder { Size size = kTimelineFixedTileExtent, double spacing = kTimelineSpacing, }) => RepaintBoundary( - child: FixedTimelineRow( + child: TimelineRow.fixed( dimension: size.height, spacing: spacing, textDirection: Directionality.of(context), diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index a04e26d653..4d72a9b0a5 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -42,6 +42,8 @@ class Timeline extends StatelessWidget { this.withScrubber = true, this.snapToMonth = true, this.initialScrollOffset, + this.readOnly = false, + this.persistentBottomBar = false, }); final Widget? topSliverWidget; @@ -54,6 +56,8 @@ class Timeline extends StatelessWidget { final bool withScrubber; final bool snapToMonth; final double? initialScrollOffset; + final bool readOnly; + final bool persistentBottomBar; @override Widget build(BuildContext context) { @@ -73,6 +77,7 @@ class Timeline extends StatelessWidget { groupBy: groupBy, ), ), + if (readOnly) readonlyModeProvider.overrideWith(() => _AlwaysReadOnlyNotifier()), ], child: _SliverTimeline( topSliverWidget: topSliverWidget, @@ -80,8 +85,10 @@ class Timeline extends StatelessWidget { appBar: appBar, bottomSheet: bottomSheet, withScrubber: withScrubber, + persistentBottomBar: persistentBottomBar, snapToMonth: snapToMonth, initialScrollOffset: initialScrollOffset, + maxWidth: constraints.maxWidth, ), ), ), @@ -89,6 +96,17 @@ class Timeline extends StatelessWidget { } } +class _AlwaysReadOnlyNotifier extends ReadOnlyModeNotifier { + @override + bool build() => true; + + @override + void setReadonlyMode(bool value) {} + + @override + void toggleReadonlyMode() {} +} + class _SliverTimeline extends ConsumerStatefulWidget { const _SliverTimeline({ this.topSliverWidget, @@ -96,8 +114,10 @@ class _SliverTimeline extends ConsumerStatefulWidget { this.appBar, this.bottomSheet, this.withScrubber = true, + this.persistentBottomBar = false, this.snapToMonth = true, this.initialScrollOffset, + this.maxWidth, }); final Widget? topSliverWidget; @@ -105,8 +125,10 @@ class _SliverTimeline extends ConsumerStatefulWidget { final Widget? appBar; final Widget? bottomSheet; final bool withScrubber; + final bool persistentBottomBar; final bool snapToMonth; final double? initialScrollOffset; + final double? maxWidth; @override ConsumerState createState() => _SliverTimelineState(); @@ -125,14 +147,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { int _perRow = 4; double _scaleFactor = 3.0; double _baseScaleFactor = 3.0; - int? _scaleRestoreAssetIndex; + int? _restoreAssetIndex; @override void initState() { super.initState(); _scrollController = ScrollController( initialScrollOffset: widget.initialScrollOffset ?? 0.0, - onAttach: _restoreScalePosition, + onAttach: _restoreAssetPosition, ); _eventSubscription = EventStream.shared.listen(_onEvent); @@ -144,6 +166,20 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled); } + @override + void didUpdateWidget(covariant _SliverTimeline oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.maxWidth != oldWidget.maxWidth) { + final asyncSegments = ref.read(timelineSegmentProvider); + asyncSegments.whenData((segments) { + final index = _getCurrentAssetIndex(segments); + // Refresh to wait for new segments to be generated with the updated width before restoring the scroll position + final _ = ref.refresh(timelineArgsProvider); + _restoreAssetIndex = index; + }); + } + } + void _onEvent(Event event) { switch (event) { case ScrollToTopEvent(): @@ -161,18 +197,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { } } - void _onMultiSelectionToggled(_, bool isEnabled) { - EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); - } - - void _restoreScalePosition(_) { - if (_scaleRestoreAssetIndex == null) return; + void _restoreAssetPosition(_) { + if (_restoreAssetIndex == null) return; final asyncSegments = ref.read(timelineSegmentProvider); asyncSegments.whenData((segments) { - final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _scaleRestoreAssetIndex!); + final targetSegment = segments.lastWhereOrNull((segment) => segment.firstAssetIndex <= _restoreAssetIndex!); if (targetSegment != null) { - final assetIndexInSegment = _scaleRestoreAssetIndex! - targetSegment.firstAssetIndex; + final assetIndexInSegment = _restoreAssetIndex! - targetSegment.firstAssetIndex; final newColumnCount = ref.read(timelineArgsProvider).columnCount; final rowIndexInSegment = (assetIndexInSegment / newColumnCount).floor(); final targetRowIndex = targetSegment.firstIndex + 1 + rowIndexInSegment; @@ -184,7 +216,29 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { }); } }); - _scaleRestoreAssetIndex = null; + _restoreAssetIndex = null; + } + + void _onMultiSelectionToggled(_, bool isEnabled) { + EventStream.shared.emit(MultiSelectToggleEvent(isEnabled)); + } + + int? _getCurrentAssetIndex(List segments) { + final currentOffset = _scrollController.offset.clamp(0.0, _scrollController.position.maxScrollExtent); + final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; + int? targetAssetIndex; + if (segment != null) { + final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); + if (rowIndex > segment.firstIndex) { + final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); + final assetsPerRow = ref.read(timelineArgsProvider).columnCount; + final assetIndexInSegment = rowIndexInSegment * assetsPerRow; + targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; + } else { + targetAssetIndex = segment.firstAssetIndex; + } + } + return targetAssetIndex; } @override @@ -307,6 +361,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable)); final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); + final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled; + final isBottomWidgetVisible = + widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar); return PopScope( canPop: !isMultiSelectEnabled, @@ -387,28 +444,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { final newPerRow = 7 - newScaleFactor.toInt(); if (newPerRow != _perRow) { - final currentOffset = _scrollController.offset.clamp( - 0.0, - _scrollController.position.maxScrollExtent, - ); - final segment = segments.findByOffset(currentOffset) ?? segments.lastOrNull; - int? targetAssetIndex; - if (segment != null) { - final rowIndex = segment.getMinChildIndexForScrollOffset(currentOffset); - if (rowIndex > segment.firstIndex) { - final rowIndexInSegment = rowIndex - (segment.firstIndex + 1); - final assetsPerRow = ref.read(timelineArgsProvider).columnCount; - final assetIndexInSegment = rowIndexInSegment * assetsPerRow; - targetAssetIndex = segment.firstAssetIndex + assetIndexInSegment; - } else { - targetAssetIndex = segment.firstAssetIndex; - } - } - + final targetAssetIndex = _getCurrentAssetIndex(segments); setState(() { _scaleFactor = newScaleFactor; _perRow = newPerRow; - _scaleRestoreAssetIndex = targetAssetIndex; + _restoreAssetIndex = targetAssetIndex; }); ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow); @@ -429,7 +469,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { child: Stack( children: [ timeline, - if (!isSelectionMode && isMultiSelectEnabled) ...[ + if (isBottomWidgetVisible) Positioned( top: MediaQuery.paddingOf(context).top, left: 25, @@ -438,8 +478,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> { child: Center(child: _MultiSelectStatusButton()), ), ), - if (widget.bottomSheet != null) widget.bottomSheet!, - ], + if (isBottomWidgetVisible) widget.bottomSheet!, ], ), ), 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 3dd09f1282..c969dbd37d 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -73,18 +74,21 @@ class _AlbumSortHandlers { // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", _AlbumSortHandlers.title), - assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount), - lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified), - created(0, "library_page_sort_created", _AlbumSortHandlers.created), - mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent), - mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest); + title(1, "library_page_sort_title", _AlbumSortHandlers.title, SortOrder.asc), + assetCount(4, "library_page_sort_asset_count", _AlbumSortHandlers.assetCount, SortOrder.desc), + lastModified(3, "library_page_sort_last_modified", _AlbumSortHandlers.lastModified, SortOrder.desc), + created(0, "library_page_sort_created", _AlbumSortHandlers.created, SortOrder.desc), + mostRecent(2, "sort_recent", _AlbumSortHandlers.mostRecent, SortOrder.desc), + mostOldest(5, "sort_oldest", _AlbumSortHandlers.mostOldest, SortOrder.asc); final int storeIndex; final String label; final AlbumSortFn sortFn; + final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.sortFn); + const AlbumSortMode(this.storeIndex, this.label, this.sortFn, this.defaultOrder); + + SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } @riverpod diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 4b1bf3e809..883c4f4835 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -160,6 +160,8 @@ class AppLifeCycleNotifier extends StateNotifier { _resumeBackup(); }), _resumeBackup(), + // TODO: Bring back when the soft freeze issue is addressed + // _safeRun(backgroundManager.syncCloudIds(), "syncCloudIds"), ]); } else { await _safeRun(backgroundManager.hashAssets(), "hashAssets"); @@ -180,7 +182,7 @@ class AppLifeCycleNotifier extends StateNotifier { final currentUser = Store.tryGet(StoreKey.currentUser); if (currentUser != null) { await _safeRun( - _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id), + _ref.read(driftBackupProvider.notifier).startForegroundBackup(currentUser.id), "handleBackupResume", ); } @@ -237,6 +239,8 @@ class AppLifeCycleNotifier extends StateNotifier { if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { _ref.read(backupProvider.notifier).cancelBackup(); } + } else { + await _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); } _ref.read(websocketProvider.notifier).disconnect(); 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 881fdc359f..66a8deb466 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -1,37 +1,28 @@ import 'dart:io'; -import 'package:background_downloader/background_downloader.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:immich_mobile/services/foreground_upload.service.dart'; import 'package:logging/logging.dart'; -import 'package:path/path.dart'; +import 'package:path/path.dart' as p; final shareIntentUploadProvider = StateNotifierProvider>( ((ref) => ShareIntentUploadStateNotifier( ref.watch(appRouterProvider), - ref.watch(uploadServiceProvider), - ref.watch(shareIntentServiceProvider), + ref.read(foregroundUploadServiceProvider), + ref.read(shareIntentServiceProvider), )), ); class ShareIntentUploadStateNotifier extends StateNotifier> { final AppRouter router; - final UploadService _uploadService; + final ForegroundUploadService _foregroundUploadService; final ShareIntentService _shareIntentService; final Logger _logger = Logger('ShareIntentUploadStateNotifier'); - ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) { - _uploadService.taskStatusStream.listen(_updateUploadStatus); - _uploadService.taskProgressStream.listen(_taskProgressCallback); - } + ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]); void init() { _shareIntentService.onSharedMedia = onSharedMedia; @@ -67,97 +58,44 @@ class ShareIntentUploadStateNotifier extends StateNotifier uploadAll(List files) async { + for (final file in files) { + final fileId = p.hash(file.path).toString(); + _updateStatus(fileId, UploadStatus.running); } - final taskId = task.task.taskId; - final uploadStatus = switch (task.status) { - TaskStatus.complete => UploadStatus.complete, - TaskStatus.failed => UploadStatus.failed, - TaskStatus.canceled => UploadStatus.canceled, - TaskStatus.enqueued => UploadStatus.enqueued, - TaskStatus.running => UploadStatus.running, - TaskStatus.paused => UploadStatus.paused, - TaskStatus.notFound => UploadStatus.notFound, - TaskStatus.waitingToRetry => UploadStatus.waitingToRetry, - }; - - state = [ - for (final attachment in state) - if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment, - ]; - - if (task.status == TaskStatus.failed) { - String? error; - final exception = task.exception; - if (exception != null && exception is TaskHttpException) { - final message = tryJsonDecode(exception.description)?['message'] as String?; - if (message != null) { - final responseCode = exception.httpResponseCode; - error = "${exception.exceptionType}, response code $responseCode: $message"; - } - } - error ??= task.exception?.toString(); - - _logger.warning("Upload failed for asset: ${task.task.filename}, error: $error"); - } - } - - void _taskProgressCallback(TaskProgressUpdate update) { - // Ignore if the task is canceled or completed - 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, - ]; - } - - Future upload(File file) async { - final task = await _buildUploadTask(hash(file.path).toString(), file); - - await _uploadService.enqueueTasks([task]); - } - - 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, + await _foregroundUploadService.uploadShareIntent( + files, + onProgress: (fileId, bytes, totalBytes) { + final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; + _updateProgress(fileId, progress); + }, + onSuccess: (fileId) { + _updateStatus(fileId, UploadStatus.complete, progress: 1.0); + }, + onError: (fileId, errorMessage) { + _logger.warning("Upload failed for file: $fileId, error: $errorMessage"); + _updateStatus(fileId, UploadStatus.failed); + }, ); } + + void _updateStatus(String fileId, UploadStatus status, {double? progress}) { + final id = int.parse(fileId); + state = [ + for (final attachment in state) + if (attachment.id == id) + attachment.copyWith(status: status, uploadProgress: progress ?? attachment.uploadProgress) + else + attachment, + ]; + } + + void _updateProgress(String fileId, double progress) { + final id = int.parse(fileId); + state = [ + for (final attachment in state) + if (attachment.id == id) attachment.copyWith(uploadProgress: progress) else attachment, + ]; + } } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 9a15598998..49dc10240b 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -11,22 +11,23 @@ import 'package:immich_mobile/providers/api.provider.dart'; 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/foreground_upload.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; -import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/widget.service.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; final authProvider = StateNotifierProvider((ref) { return AuthNotifier( ref.watch(authServiceProvider), ref.watch(apiServiceProvider), ref.watch(userServiceProvider), - ref.watch(uploadServiceProvider), ref.watch(secureStorageServiceProvider), ref.watch(widgetServiceProvider), + ref, ); }); @@ -34,9 +35,10 @@ class AuthNotifier extends StateNotifier { final AuthService _authService; final ApiService _apiService; final UserService _userService; - final UploadService _uploadService; + final SecureStorageService _secureStorageService; final WidgetService _widgetService; + final Ref _ref; final _log = Logger("AuthenticationNotifier"); static const Duration _timeoutDuration = Duration(seconds: 7); @@ -45,9 +47,10 @@ class AuthNotifier extends StateNotifier { this._authService, this._apiService, this._userService, - this._uploadService, + this._secureStorageService, this._widgetService, + this._ref, ) : super( const AuthState( deviceId: "", @@ -87,7 +90,8 @@ class AuthNotifier extends StateNotifier { await _widgetService.clearCredentials(); await _authService.logout(); - await _uploadService.cancelBackup(); + await _ref.read(backgroundUploadServiceProvider).cancel(); + _ref.read(foregroundUploadServiceProvider).cancel(); } finally { await _cleanUp(); } diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index a61cd93022..37b3145eb4 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -5,16 +5,21 @@ import 'package:immich_mobile/providers/sync_status.provider.dart'; final backgroundSyncProvider = Provider((ref) { final syncStatusNotifier = ref.read(syncStatusProvider.notifier); - final backupProvider = ref.read(driftBackupProvider.notifier); final manager = BackgroundSyncManager( onRemoteSyncStart: () { syncStatusNotifier.startRemoteSync(); - backupProvider.updateError(BackupError.none); + final backupProvider = ref.read(driftBackupProvider.notifier); + if (backupProvider.mounted) { + backupProvider.updateError(BackupError.none); + } }, onRemoteSyncComplete: (isSuccess) { syncStatusNotifier.completeRemoteSync(); - backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); + final backupProvider = ref.read(driftBackupProvider.notifier); + if (backupProvider.mounted) { + backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); + } }, onRemoteSyncError: syncStatusNotifier.errorRemoteSync, onLocalSyncStart: syncStatusNotifier.startLocalSync, @@ -23,6 +28,9 @@ final backgroundSyncProvider = Provider((ref) { onHashingStart: syncStatusNotifier.startHashJob, onHashingComplete: syncStatusNotifier.completeHashJob, onHashingError: syncStatusNotifier.errorHashJob, + onCloudIdSyncStart: syncStatusNotifier.startCloudIdSync, + onCloudIdSyncComplete: syncStatusNotifier.completeCloudIdSync, + onCloudIdSyncError: syncStatusNotifier.errorCloudIdSync, ); ref.onDispose(manager.cancel); return manager; diff --git a/mobile/lib/providers/backup/asset_upload_progress.provider.dart b/mobile/lib/providers/backup/asset_upload_progress.provider.dart new file mode 100644 index 0000000000..e8aba430da --- /dev/null +++ b/mobile/lib/providers/backup/asset_upload_progress.provider.dart @@ -0,0 +1,33 @@ +import 'package:cancellation_token_http/http.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Tracks per-asset upload progress. +/// Key: local asset ID, Value: upload progress 0.0 to 1.0, or -1.0 for error +class AssetUploadProgressNotifier extends Notifier> { + static const double errorValue = -1.0; + + @override + Map build() => {}; + + void setProgress(String localAssetId, double progress) { + state = {...state, localAssetId: progress}; + } + + void setError(String localAssetId) { + state = {...state, localAssetId: errorValue}; + } + + void remove(String localAssetId) { + state = Map.from(state)..remove(localAssetId); + } + + void clear() { + state = {}; + } +} + +final assetUploadProgressProvider = NotifierProvider>( + AssetUploadProgressNotifier.new, +); + +final manualUploadCancelTokenProvider = StateProvider((ref) => null); diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index f52fc654f2..624c21f158 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -1,19 +1,18 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:async'; -import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; + import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/upload.service.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:logging/logging.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; class EnqueueStatus { final int enqueueCount; @@ -106,26 +105,24 @@ class DriftBackupState { final int remainderCount; final int processingCount; - final int enqueueCount; - final int enqueueTotalCount; - final bool isSyncing; - final bool isCanceling; final BackupError error; final Map uploadItems; + final CancellationToken? cancelToken; + + final Map iCloudDownloadProgress; const DriftBackupState({ required this.totalCount, required this.backupCount, required this.remainderCount, required this.processingCount, - required this.enqueueCount, - required this.enqueueTotalCount, - required this.isCanceling, required this.isSyncing, - required this.uploadItems, this.error = BackupError.none, + required this.uploadItems, + this.cancelToken, + this.iCloudDownloadProgress = const {}, }); DriftBackupState copyWith({ @@ -133,30 +130,30 @@ class DriftBackupState { int? backupCount, int? remainderCount, int? processingCount, - int? enqueueCount, - int? enqueueTotalCount, - bool? isCanceling, bool? isSyncing, - Map? uploadItems, BackupError? error, + Map? uploadItems, + CancellationToken? cancelToken, + Map? iCloudDownloadProgress, }) { return DriftBackupState( totalCount: totalCount ?? this.totalCount, backupCount: backupCount ?? this.backupCount, remainderCount: remainderCount ?? this.remainderCount, processingCount: processingCount ?? this.processingCount, - enqueueCount: enqueueCount ?? this.enqueueCount, - enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount, - isCanceling: isCanceling ?? this.isCanceling, isSyncing: isSyncing ?? this.isSyncing, - uploadItems: uploadItems ?? this.uploadItems, error: error ?? this.error, + uploadItems: uploadItems ?? this.uploadItems, + cancelToken: cancelToken ?? this.cancelToken, + iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, ); } + int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; + @override String toString() { - return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, isSyncing: $isSyncing, uploadItems: $uploadItems, error: $error)'; + return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, cancelToken: $cancelToken, iCloudDownloadProgress: $iCloudDownloadProgress)'; } @override @@ -168,12 +165,11 @@ class DriftBackupState { other.backupCount == backupCount && other.remainderCount == remainderCount && other.processingCount == processingCount && - other.enqueueCount == enqueueCount && - other.enqueueTotalCount == enqueueTotalCount && - other.isCanceling == isCanceling && other.isSyncing == isSyncing && + other.error == error && + mapEquals(other.iCloudDownloadProgress, iCloudDownloadProgress) && mapEquals(other.uploadItems, uploadItems) && - other.error == error; + other.cancelToken == cancelToken; } @override @@ -182,48 +178,48 @@ class DriftBackupState { backupCount.hashCode ^ remainderCount.hashCode ^ processingCount.hashCode ^ - enqueueCount.hashCode ^ - enqueueTotalCount.hashCode ^ - isCanceling.hashCode ^ isSyncing.hashCode ^ + error.hashCode ^ uploadItems.hashCode ^ - error.hashCode; + cancelToken.hashCode ^ + iCloudDownloadProgress.hashCode; } } final driftBackupProvider = StateNotifierProvider((ref) { - return DriftBackupNotifier(ref.watch(uploadServiceProvider)); + return DriftBackupNotifier( + ref.watch(foregroundUploadServiceProvider), + ref.watch(backgroundUploadServiceProvider), + UploadSpeedManager(), + ); }); class DriftBackupNotifier extends StateNotifier { - DriftBackupNotifier(this._uploadService) + DriftBackupNotifier(this._foregroundUploadService, this._backgroundUploadService, this._uploadSpeedManager) : super( const DriftBackupState( totalCount: 0, backupCount: 0, remainderCount: 0, processingCount: 0, - enqueueCount: 0, - enqueueTotalCount: 0, - isCanceling: false, isSyncing: false, uploadItems: {}, error: BackupError.none, ), - ) { - { - _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate); - _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate); - } - } + ); + + final ForegroundUploadService _foregroundUploadService; + final BackgroundUploadService _backgroundUploadService; + final UploadSpeedManager _uploadSpeedManager; - final UploadService _uploadService; - StreamSubscription? _statusSubscription; - StreamSubscription? _progressSubscription; final _logger = Logger("DriftBackupNotifier"); /// Remove upload item from state void _removeUploadItem(String taskId) { + if (!mounted) { + _logger.warning("Skip _removeUploadItem: notifier disposed"); + return; + } if (state.uploadItems.containsKey(taskId)) { final updatedItems = Map.from(state.uploadItems); updatedItems.remove(taskId); @@ -231,108 +227,16 @@ class DriftBackupNotifier extends StateNotifier { } } - void _handleTaskStatusUpdate(TaskStatusUpdate update) { - final taskId = update.task.taskId; - - switch (update.status) { - case TaskStatus.complete: - if (update.task.group == kBackupGroup) { - if (update.responseStatusCode == 201) { - 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: - // Ignore retry errors to avoid confusing users - if (update.exception?.description == 'Delayed or retried enqueue failed') { - _removeUploadItem(taskId); - return; - } - - final currentItem = state.uploadItems[taskId]; - if (currentItem == null) { - return; - } - - String? error; - final exception = update.exception; - if (exception != null && exception is TaskHttpException) { - final message = tryJsonDecode(exception.description)?['message'] as String?; - if (message != null) { - final responseCode = exception.httpResponseCode; - error = "${exception.exceptionType}, response code $responseCode: $message"; - } - } - error ??= update.exception?.toString(); - - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - taskId: currentItem.copyWith(isFailed: true, error: error), - }, - ); - _logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}"); - break; - - case TaskStatus.canceled: - _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), - }, - ); - + Future getBackupStatus(String userId) async { + if (!mounted) { + _logger.warning("Skip getBackupStatus (pre-call): notifier disposed"); + return; + } + final counts = await _foregroundUploadService.getBackupCounts(userId); + if (!mounted) { + _logger.warning("Skip getBackupStatus (post-call): notifier disposed"); 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 counts = await _uploadService.getBackupCounts(userId); state = state.copyWith( totalCount: counts.total, @@ -343,6 +247,10 @@ class DriftBackupNotifier extends StateNotifier { } void updateError(BackupError error) async { + if (!mounted) { + _logger.warning("Skip updateError: notifier disposed"); + return; + } state = state.copyWith(error: error); } @@ -350,52 +258,144 @@ class DriftBackupNotifier extends StateNotifier { state = state.copyWith(isSyncing: isSyncing); } - Future startBackup(String userId) { + Future startForegroundBackup(String userId) async { + // Cancel any existing backup before starting a new one + if (state.cancelToken != null) { + await stopForegroundBackup(); + } + state = state.copyWith(error: BackupError.none); - return _uploadService.startBackup(userId, _updateEnqueueCount); + + final cancelToken = CancellationToken(); + state = state.copyWith(cancelToken: cancelToken); + + return _foregroundUploadService.uploadCandidates( + userId, + cancelToken, + callbacks: UploadCallbacks( + onProgress: _handleForegroundBackupProgress, + onSuccess: _handleForegroundBackupSuccess, + onError: _handleForegroundBackupError, + onICloudProgress: _handleICloudProgress, + ), + ); } - void _updateEnqueueCount(EnqueueStatus status) { - state = state.copyWith(enqueueCount: status.enqueueCount, enqueueTotalCount: status.totalCount); + Future stopForegroundBackup() async { + state.cancelToken?.cancel(); + _uploadSpeedManager.clear(); + state = state.copyWith(cancelToken: null, uploadItems: {}, iCloudDownloadProgress: {}); } - Future cancel() async { - dPrint(() => "Canceling backup tasks..."); - state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none); + void _handleICloudProgress(String localAssetId, double progress) { + state = state.copyWith(iCloudDownloadProgress: {...state.iCloudDownloadProgress, localAssetId: progress}); - final activeTaskCount = await _uploadService.cancelBackup(); - - if (activeTaskCount > 0) { - dPrint(() => "$activeTaskCount tasks left, continuing to cancel..."); - await cancel(); - } else { - dPrint(() => "All tasks canceled successfully."); - // Clear all upload items when cancellation is complete - state = state.copyWith(isCanceling: false, uploadItems: {}); + if (progress >= 1.0) { + Future.delayed(const Duration(milliseconds: 250), () { + final updatedProgress = Map.from(state.iCloudDownloadProgress); + updatedProgress.remove(localAssetId); + state = state.copyWith(iCloudDownloadProgress: updatedProgress); + }); } } - Future handleBackupResume(String userId) async { - _logger.info("Resuming backup tasks..."); + void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) { + if (state.cancelToken == null) { + return; + } + + final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; + final networkSpeedAsString = _uploadSpeedManager.updateProgress(localAssetId, bytes, totalBytes); + final currentItem = state.uploadItems[localAssetId]; + if (currentItem != null) { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: currentItem.copyWith( + filename: filename, + progress: progress, + fileSize: totalBytes, + networkSpeedAsString: networkSpeedAsString, + ), + }, + ); + } else { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: DriftUploadStatus( + taskId: localAssetId, + filename: filename, + progress: progress, + fileSize: totalBytes, + networkSpeedAsString: networkSpeedAsString, + ), + }, + ); + } + } + + void _handleForegroundBackupSuccess(String localAssetId, String remoteAssetId) { + state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1); + _uploadSpeedManager.removeTask(localAssetId); + + Future.delayed(const Duration(milliseconds: 1000), () { + _removeUploadItem(localAssetId); + }); + } + + void _handleForegroundBackupError(String localAssetId, String errorMessage) { + _logger.severe("Upload failed for $localAssetId: $errorMessage"); + + final currentItem = state.uploadItems[localAssetId]; + if (currentItem != null) { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: currentItem.copyWith(isFailed: true, error: errorMessage), + }, + ); + } else { + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: DriftUploadStatus( + taskId: localAssetId, + filename: 'Unknown', + progress: 0, + fileSize: 0, + networkSpeedAsString: '', + isFailed: true, + error: errorMessage, + ), + }, + ); + } + + _uploadSpeedManager.removeTask(localAssetId); + } + + Future startBackupWithURLSession(String userId) async { + if (!mounted) { + _logger.warning("Skip handleBackupResume (pre-call): notifier disposed"); + return; + } + _logger.info("Start background backup sequence"); state = state.copyWith(error: BackupError.none); - final tasks = await _uploadService.getActiveTasks(kBackupGroup); - _logger.info("Found ${tasks.length} tasks"); + final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup); + if (!mounted) { + _logger.warning("Skip handleBackupResume (post-call): notifier disposed"); + return; + } + _logger.info("Found ${tasks.length} pending tasks"); if (tasks.isEmpty) { - // Start a new backup queue - _logger.info("Start a new backup queue"); - return startBackup(userId); + _logger.info("No pending tasks, starting new upload"); + return _backgroundUploadService.uploadBackupCandidates(userId); } - _logger.info("Tasks to resume: ${tasks.length}"); - return _uploadService.resumeBackup(); - } - - @override - void dispose() { - _statusSubscription?.cancel(); - _progressSubscription?.cancel(); - super.dispose(); + _logger.info("Resuming upload ${tasks.length} assets"); + return _backgroundUploadService.resume(); } } @@ -405,7 +405,7 @@ final driftBackupCandidateProvider = FutureProvider.autoDispose return []; } - return ref.read(backupRepositoryProvider).getCandidates(user.id, onlyHashed: false); + return ref.read(foregroundUploadServiceProvider).getBackupCandidates(user.id, onlyHashed: false); }); final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family, String>(( diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 75a2a35fb6..1cd5ded487 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -69,6 +69,7 @@ class CastNotifier extends StateNotifier { : AssetType.other, createdAt: asset.fileCreatedAt, updatedAt: asset.updatedAt, + isEdited: false, ); _gCastService.loadMedia(remoteAsset, reload); diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart new file mode 100644 index 0000000000..4d0bdba301 --- /dev/null +++ b/mobile/lib/providers/cleanup.provider.dart @@ -0,0 +1,194 @@ +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/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; + +class CleanupState { + final DateTime? selectedDate; + final List assetsToDelete; + final int totalBytes; + final bool isScanning; + final bool isDeleting; + final AssetKeepType keepMediaType; + final bool keepFavorites; + final Set keepAlbumIds; + + const CleanupState({ + this.selectedDate, + this.assetsToDelete = const [], + this.totalBytes = 0, + this.isScanning = false, + this.isDeleting = false, + this.keepMediaType = AssetKeepType.none, + this.keepFavorites = true, + this.keepAlbumIds = const {}, + }); + + CleanupState copyWith({ + DateTime? selectedDate, + List? assetsToDelete, + int? totalBytes, + bool? isScanning, + bool? isDeleting, + AssetKeepType? keepMediaType, + bool? keepFavorites, + Set? keepAlbumIds, + }) { + return CleanupState( + selectedDate: selectedDate ?? this.selectedDate, + assetsToDelete: assetsToDelete ?? this.assetsToDelete, + totalBytes: totalBytes ?? this.totalBytes, + isScanning: isScanning ?? this.isScanning, + isDeleting: isDeleting ?? this.isDeleting, + keepMediaType: keepMediaType ?? this.keepMediaType, + keepFavorites: keepFavorites ?? this.keepFavorites, + keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds, + ); + } +} + +final cleanupProvider = StateNotifierProvider((ref) { + return CleanupNotifier( + ref.watch(cleanupServiceProvider), + ref.watch(currentUserProvider)?.id, + ref.watch(appSettingsServiceProvider), + ); +}); + +class CleanupNotifier extends StateNotifier { + final CleanupService _cleanupService; + final String? _userId; + final AppSettingsService _appSettingsService; + + CleanupNotifier(this._cleanupService, this._userId, this._appSettingsService) : super(const CleanupState()) { + _loadPersistedSettings(); + } + + void _loadPersistedSettings() { + final keepFavorites = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepFavorites); + final keepMediaTypeIndex = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepMediaType); + final keepAlbumIdsString = _appSettingsService.getSetting(AppSettingsEnum.cleanupKeepAlbumIds); + final cutoffDaysAgo = _appSettingsService.getSetting(AppSettingsEnum.cleanupCutoffDaysAgo); + + final keepMediaType = AssetKeepType.values[keepMediaTypeIndex.clamp(0, AssetKeepType.values.length - 1)]; + final keepAlbumIds = keepAlbumIdsString.isEmpty ? {} : keepAlbumIdsString.split(',').toSet(); + final selectedDate = cutoffDaysAgo >= 0 ? DateTime.now().subtract(Duration(days: cutoffDaysAgo)) : null; + + state = state.copyWith( + keepFavorites: keepFavorites, + keepMediaType: keepMediaType, + keepAlbumIds: keepAlbumIds, + selectedDate: selectedDate, + ); + } + + void setSelectedDate(DateTime? date) { + state = state.copyWith(selectedDate: date, assetsToDelete: []); + if (date != null) { + final daysAgo = DateTime.now().difference(date).inDays; + _appSettingsService.setSetting(AppSettingsEnum.cleanupCutoffDaysAgo, daysAgo); + } + } + + void setKeepMediaType(AssetKeepType keepMediaType) { + state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); + _appSettingsService.setSetting(AppSettingsEnum.cleanupKeepMediaType, keepMediaType.index); + } + + void setKeepFavorites(bool keepFavorites) { + state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); + _appSettingsService.setSetting(AppSettingsEnum.cleanupKeepFavorites, keepFavorites); + } + + void toggleKeepAlbum(String albumId) { + final newKeepAlbumIds = Set.from(state.keepAlbumIds); + if (newKeepAlbumIds.contains(albumId)) { + newKeepAlbumIds.remove(albumId); + } else { + newKeepAlbumIds.add(albumId); + } + state = state.copyWith(keepAlbumIds: newKeepAlbumIds, assetsToDelete: []); + _persistExcludedAlbumIds(newKeepAlbumIds); + } + + void setExcludedAlbumIds(Set albumIds) { + state = state.copyWith(keepAlbumIds: albumIds, assetsToDelete: []); + _persistExcludedAlbumIds(albumIds); + } + + void _persistExcludedAlbumIds(Set albumIds) { + _appSettingsService.setSetting(AppSettingsEnum.cleanupKeepAlbumIds, albumIds.join(',')); + } + + void cleanupStaleAlbumIds(Set existingAlbumIds) { + final staleIds = state.keepAlbumIds.difference(existingAlbumIds); + if (staleIds.isNotEmpty) { + final cleanedIds = state.keepAlbumIds.intersection(existingAlbumIds); + state = state.copyWith(keepAlbumIds: cleanedIds); + _persistExcludedAlbumIds(cleanedIds); + } + } + + void applyDefaultAlbumSelections(List<(String id, String name)> albums) { + final isInitialized = _appSettingsService.getSetting(AppSettingsEnum.cleanupDefaultsInitialized); + if (isInitialized) return; + + final toKeep = _cleanupService.getDefaultKeepAlbumIds(albums); + + if (toKeep.isNotEmpty) { + final keepAlbumIds = {...state.keepAlbumIds, ...toKeep}; + state = state.copyWith(keepAlbumIds: keepAlbumIds); + _persistExcludedAlbumIds(keepAlbumIds); + } + + _appSettingsService.setSetting(AppSettingsEnum.cleanupDefaultsInitialized, true); + } + + Future scanAssets() async { + if (_userId == null || state.selectedDate == null) { + return; + } + + state = state.copyWith(isScanning: true); + try { + final result = await _cleanupService.getRemovalCandidates( + _userId, + state.selectedDate!, + keepMediaType: state.keepMediaType, + keepFavorites: state.keepFavorites, + keepAlbumIds: state.keepAlbumIds, + ); + + state = state.copyWith(assetsToDelete: result.assets, totalBytes: result.totalBytes, isScanning: false); + } catch (e) { + state = state.copyWith(isScanning: false); + rethrow; + } + } + + Future deleteAssets() async { + if (state.assetsToDelete.isEmpty) { + return 0; + } + + state = state.copyWith(isDeleting: true); + try { + final deletedCount = await _cleanupService.deleteLocalAssets(state.assetsToDelete.map((a) => a.id).toList()); + + state = state.copyWith(assetsToDelete: [], isDeleting: false); + + return deletedCount; + } catch (e) { + state = state.copyWith(isDeleting: false); + rethrow; + } + } + + void reset() { + // Only reset transient state, keep the persisted filter settings + state = state.copyWith(selectedDate: null, assetsToDelete: [], isScanning: false, isDeleting: false); + } +} diff --git a/mobile/lib/providers/image/cache/remote_image_cache_manager.dart b/mobile/lib/providers/image/cache/remote_image_cache_manager.dart index 41c541ccdb..d3de4b80c9 100644 --- a/mobile/lib/providers/image/cache/remote_image_cache_manager.dart +++ b/mobile/lib/providers/image/cache/remote_image_cache_manager.dart @@ -1,148 +1,25 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -// ignore: implementation_imports -import 'package:flutter_cache_manager/src/cache_store.dart'; -import 'package:logging/logging.dart'; -import 'package:uuid/uuid.dart'; -abstract class RemoteCacheManager extends CacheManager { - static final _log = Logger('RemoteCacheManager'); - - RemoteCacheManager.custom(super.config, CacheStore store) - // Unfortunately, CacheStore is not a public API - // ignore: invalid_use_of_visible_for_testing_member - : super.custom(cacheStore: store); - - Future putStreamedFile( - String url, - Stream> source, { - String? key, - String? eTag, - Duration maxAge = const Duration(days: 30), - String fileExtension = 'file', - }); - - // Unlike `putFileStream`, this method handles request cancellation, - // does not make a (slow) DB call checking if the file is already cached, - // does not synchronously check if a file exists, - // and deletes the file on cancellation without making these checks again. - Future putStreamedFileToStore( - CacheStore store, - String url, - Stream> source, { - String? key, - String? eTag, - Duration maxAge = const Duration(days: 30), - String fileExtension = 'file', - }) async { - final path = '${const Uuid().v1()}.$fileExtension'; - final file = await store.fileSystem.createFile(path); - final sink = file.openWrite(); - try { - await source.listen(sink.add, cancelOnError: true).asFuture(); - } catch (e) { - try { - await sink.close(); - await file.delete(); - } catch (e) { - _log.severe('Failed to delete incomplete cache file: $e'); - } - return; - } - - try { - await sink.flush(); - await sink.close(); - } catch (e) { - try { - await file.delete(); - } catch (e) { - _log.severe('Failed to delete incomplete cache file: $e'); - } - return; - } - - final cacheObject = CacheObject( - url, - key: key, - relativePath: path, - validTill: DateTime.now().add(maxAge), - eTag: eTag, - ); - try { - await store.putFile(cacheObject); - } catch (e) { - try { - await file.delete(); - } catch (e) { - _log.severe('Failed to delete untracked cache file: $e'); - } - } - } -} - -class RemoteImageCacheManager extends RemoteCacheManager { +class RemoteImageCacheManager extends CacheManager { static const key = 'remoteImageCacheKey'; static final RemoteImageCacheManager _instance = RemoteImageCacheManager._(); static final _config = Config(key, maxNrOfCacheObjects: 500, stalePeriod: const Duration(days: 30)); - static final _store = CacheStore(_config); factory RemoteImageCacheManager() { return _instance; } - RemoteImageCacheManager._() : super.custom(_config, _store); - - @override - Future putStreamedFile( - String url, - Stream> source, { - String? key, - String? eTag, - Duration maxAge = const Duration(days: 30), - String fileExtension = 'file', - }) { - return putStreamedFileToStore( - _store, - url, - source, - key: key, - eTag: eTag, - maxAge: maxAge, - fileExtension: fileExtension, - ); - } + RemoteImageCacheManager._() : super(_config); } -/// The cache manager for full size images [ImmichRemoteImageProvider] -class RemoteThumbnailCacheManager extends RemoteCacheManager { +class RemoteThumbnailCacheManager extends CacheManager { static const key = 'remoteThumbnailCacheKey'; static final RemoteThumbnailCacheManager _instance = RemoteThumbnailCacheManager._(); static final _config = Config(key, maxNrOfCacheObjects: 5000, stalePeriod: const Duration(days: 30)); - static final _store = CacheStore(_config); factory RemoteThumbnailCacheManager() { return _instance; } - RemoteThumbnailCacheManager._() : super.custom(_config, _store); - - @override - Future putStreamedFile( - String url, - Stream> source, { - String? key, - String? eTag, - Duration maxAge = const Duration(days: 30), - String fileExtension = 'file', - }) { - return putStreamedFileToStore( - _store, - url, - source, - key: key, - eTag: eTag, - maxAge: maxAge, - fileExtension: fileExtension, - ); - } + RemoteThumbnailCacheManager._() : super(_config); } diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart deleted file mode 100644 index b9e09eb357..0000000000 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:logging/logging.dart'; -import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; - -/// The local image provider for an asset -class ImmichLocalImageProvider extends ImageProvider { - final Asset asset; - // only used for videos - final double width; - final double height; - final Logger log = Logger('ImmichLocalImageProvider'); - - ImmichLocalImageProvider({required this.asset, required this.width, required this.height}) - : assert(asset.local != null, 'Only usable when asset.local is set'); - - /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key - /// that describes the precise image to load. - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage(ImmichLocalImageProvider key, ImageDecoderCallback decode) { - final chunkEvents = StreamController(); - return MultiImageStreamCompleter( - codec: _codec(key.asset, decode, chunkEvents), - scale: 1.0, - chunkEvents: chunkEvents.stream, - informationCollector: () sync* { - yield ErrorDescription(asset.fileName); - }, - ); - } - - // Streams in each stage of the image as we ask for it - Stream _codec( - Asset asset, - ImageDecoderCallback decode, - StreamController chunkEvents, - ) async* { - try { - final local = asset.local; - if (local == null) { - throw StateError('Asset ${asset.fileName} has no local data'); - } - - switch (asset.type) { - case AssetType.image: - final File? file = await local.originFile; - if (file == null) { - throw StateError("Opening file for asset ${asset.fileName} failed"); - } - final buffer = await ui.ImmutableBuffer.fromFilePath(file.path); - yield await decode(buffer); - break; - case AssetType.video: - final size = ThumbnailSize(width.ceil(), height.ceil()); - final thumbBytes = await local.thumbnailDataWithSize(size); - if (thumbBytes == null) { - throw StateError("Failed to load preview for ${asset.fileName}"); - } - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); - yield await decode(buffer); - break; - default: - throw StateError('Unsupported asset type ${asset.type}'); - } - } catch (error, stack) { - log.severe('Error loading local image ${asset.fileName}', error, stack); - } finally { - unawaited(chunkEvents.close()); - } - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is ImmichLocalImageProvider) { - return asset.id == other.asset.id && asset.localId == other.asset.localId; - } - return false; - } - - @override - int get hashCode => Object.hash(asset.id, asset.localId); -} diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart deleted file mode 100644 index 5edb0fc79e..0000000000 --- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:photo_manager/photo_manager.dart' show ThumbnailSize; -import 'package:logging/logging.dart'; - -/// The local image provider for an asset -/// Only viable -class ImmichLocalThumbnailProvider extends ImageProvider { - final Asset asset; - final int height; - final int width; - final CacheManager? cacheManager; - final Logger log = Logger("ImmichLocalThumbnailProvider"); - final String? userId; - - ImmichLocalThumbnailProvider({ - required this.asset, - this.height = 256, - this.width = 256, - this.cacheManager, - this.userId, - }) : assert(asset.local != null, 'Only usable when asset.local is set'); - - /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key - /// that describes the precise image to load. - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage(ImmichLocalThumbnailProvider key, ImageDecoderCallback decode) { - final cache = cacheManager ?? ThumbnailImageCacheManager(); - return MultiImageStreamCompleter( - codec: _codec(key.asset, cache, decode), - scale: 1.0, - informationCollector: () sync* { - yield ErrorDescription(key.asset.fileName); - }, - ); - } - - // Streams in each stage of the image as we ask for it - Stream _codec(Asset assetData, CacheManager cache, ImageDecoderCallback decode) async* { - 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 codec = await decode(buffer); - yield codec; - return; - } catch (error) { - log.severe('Found thumbnail in cache, but loading it failed', error); - } - } - - final thumbnailBytes = await assetData.local?.thumbnailDataWithSize(ThumbnailSize(width, height), quality: 80); - if (thumbnailBytes == null) { - throw StateError("Loading thumb for local photo ${assetData.fileName} failed"); - } - - final buffer = await ui.ImmutableBuffer.fromUint8List(thumbnailBytes); - final codec = await decode(buffer); - yield codec; - await cache.putFile(cacheKey, thumbnailBytes); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is ImmichLocalThumbnailProvider) { - return asset.id == other.asset.id && asset.localId == other.asset.localId; - } - return false; - } - - @override - int get hashCode => Object.hash(asset.id, asset.localId); -} diff --git a/mobile/lib/providers/image/immich_remote_image_provider.dart b/mobile/lib/providers/image/immich_remote_image_provider.dart deleted file mode 100644 index 16d5312e4c..0000000000 --- a/mobile/lib/providers/image/immich_remote_image_provider.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.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:openapi/api.dart' as api; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -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 { - /// The [Asset.remoteId] of the asset to fetch - final String assetId; - - /// The image cache manager - final CacheManager? cacheManager; - - const ImmichRemoteImageProvider({required this.assetId, this.cacheManager}); - - /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key - /// that describes the precise image to load. - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage(ImmichRemoteImageProvider key, ImageDecoderCallback decode) { - final cache = cacheManager ?? RemoteImageCacheManager(); - final chunkEvents = StreamController(); - return MultiImageStreamCompleter( - codec: _codec(key, cache, decode, chunkEvents), - scale: 1.0, - chunkEvents: chunkEvents.stream, - ); - } - - /// Whether to show the original file or load a compressed version - bool get _useOriginal => Store.get(AppSettingsEnum.loadOriginal.storeKey, AppSettingsEnum.loadOriginal.defaultValue); - - // Streams in each stage of the image as we ask for it - Stream _codec( - ImmichRemoteImageProvider key, - CacheManager cache, - ImageDecoderCallback decode, - StreamController chunkEvents, - ) async* { - // Load the higher resolution version of the image - final url = getThumbnailUrlForRemoteId(key.assetId, type: api.AssetMediaSize.preview); - final codec = await ImageLoader.loadImageFromCache(url, cache: cache, decode: decode, chunkEvents: chunkEvents); - yield codec; - - // Load the final remote image - if (_useOriginal) { - // Load the original image - final url = getOriginalUrlForRemoteId(key.assetId); - final codec = await ImageLoader.loadImageFromCache(url, cache: cache, decode: decode, chunkEvents: chunkEvents); - yield codec; - } - await chunkEvents.close(); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is ImmichRemoteImageProvider) { - return assetId == other.assetId; - } - - return false; - } - - @override - int get hashCode => assetId.hashCode; -} diff --git a/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart b/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart deleted file mode 100644 index 08ee4325e8..0000000000 --- a/mobile/lib/providers/image/immich_remote_thumbnail_provider.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.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:openapi/api.dart' as api; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; -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 { - /// The [Asset.remoteId] of the asset to fetch - final String assetId; - - final int? height; - final int? width; - - /// The image cache manager - final CacheManager? cacheManager; - - const ImmichRemoteThumbnailProvider({required this.assetId, this.height, this.width, this.cacheManager}); - - /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key - /// that describes the precise image to load. - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter loadImage(ImmichRemoteThumbnailProvider key, ImageDecoderCallback decode) { - final cache = cacheManager ?? ThumbnailImageCacheManager(); - return MultiImageStreamCompleter(codec: _codec(key, cache, decode), scale: 1.0); - } - - // Streams in each stage of the image as we ask for it - Stream _codec(ImmichRemoteThumbnailProvider key, CacheManager cache, ImageDecoderCallback decode) async* { - // Load a preview to the chunk events - final preview = getThumbnailUrlForRemoteId(key.assetId, type: api.AssetMediaSize.thumbnail); - - yield await ImageLoader.loadImageFromCache(preview, cache: cache, decode: decode); - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is ImmichRemoteThumbnailProvider) { - return assetId == other.assetId; - } - - return false; - } - - @override - int get hashCode => assetId.hashCode; -} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d4d850d8c1..c06bcabf26 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:background_downloader/background_downloader.dart'; +import 'package:cancellation_token_http/http.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -9,14 +10,15 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/timeline.service.dart'; -import 'package:immich_mobile/services/upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -40,7 +42,7 @@ class ActionResult { class ActionNotifier extends Notifier { final Logger _logger = Logger('ActionNotifier'); late ActionService _service; - late UploadService _uploadService; + late ForegroundUploadService _foregroundUploadService; late DownloadService _downloadService; late AssetService _assetService; @@ -48,7 +50,7 @@ class ActionNotifier extends Notifier { @override void build() { - _uploadService = ref.watch(uploadServiceProvider); + _foregroundUploadService = ref.watch(foregroundUploadServiceProvider); _service = ref.watch(actionServiceProvider); _assetService = ref.watch(assetServiceProvider); _downloadService = ref.watch(downloadServiceProvider); @@ -341,6 +343,22 @@ class ActionNotifier extends Notifier { } } + Future setAlbumCover(ActionSource source, String albumId) async { + final assets = _getAssets(source); + final asset = assets.first; + if (asset is! RemoteAsset) { + return const ActionResult(count: 1, success: false, error: 'Asset must be remote'); + } + + try { + await _service.setAlbumCover(albumId, asset.id); + return const ActionResult(count: 1, success: true); + } catch (error, stack) { + _logger.severe('Failed to set album cover', error, stack); + return ActionResult(count: 1, success: false, error: error.toString()); + } + } + Future updateDescription(ActionSource source, String description) async { final ids = _getRemoteIdsForSource(source); if (ids.length != 1) { @@ -357,6 +375,22 @@ class ActionNotifier extends Notifier { } } + Future updateRating(ActionSource source, int rating) async { + final ids = _getRemoteIdsForSource(source); + if (ids.length != 1) { + _logger.warning('updateRating called with multiple assets, expected single asset'); + return ActionResult(count: ids.length, success: false, error: 'Expected single asset for rating update'); + } + + try { + final isUpdated = await _service.updateRating(ids.first, rating); + return ActionResult(count: 1, success: isUpdated); + } catch (error, stack) { + _logger.severe('Failed to update rating for asset', error, stack); + return ActionResult(count: 1, success: false, error: error.toString()); + } + } + Future stack(String userId, ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { @@ -387,11 +421,15 @@ class ActionNotifier extends Notifier { } } - Future shareAssets(ActionSource source, BuildContext context) async { + Future shareAssets( + ActionSource source, + BuildContext context, { + Completer? cancelCompleter, + }) async { final ids = _getAssets(source).toList(growable: false); try { - await _service.shareAssets(ids, context); + await _service.shareAssets(ids, context, cancelCompleter: cancelCompleter); return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to share assets', error, stack); @@ -411,14 +449,44 @@ class ActionNotifier extends Notifier { } } - Future upload(ActionSource source) async { - final assets = _getAssets(source).whereType().toList(); + Future upload(ActionSource source, {List? assets}) async { + final assetsToUpload = assets ?? _getAssets(source).whereType().toList(); + + final progressNotifier = ref.read(assetUploadProgressProvider.notifier); + final cancelToken = CancellationToken(); + ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken; + + // Initialize progress for all assets + for (final asset in assetsToUpload) { + progressNotifier.setProgress(asset.id, 0.0); + } + try { - await _uploadService.manualBackup(assets); - return ActionResult(count: assets.length, success: true); + await _foregroundUploadService.uploadManual( + assetsToUpload, + cancelToken, + callbacks: UploadCallbacks( + onProgress: (localAssetId, filename, bytes, totalBytes) { + final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; + progressNotifier.setProgress(localAssetId, progress); + }, + onSuccess: (localAssetId, remoteAssetId) { + progressNotifier.remove(localAssetId); + }, + onError: (localAssetId, errorMessage) { + progressNotifier.setError(localAssetId); + }, + ), + ); + return ActionResult(count: assetsToUpload.length, success: true); } catch (error, stack) { _logger.severe('Failed manually upload assets', error, stack); - return ActionResult(count: assets.length, success: false, error: error.toString()); + return ActionResult(count: assetsToUpload.length, success: false, error: error.toString()); + } finally { + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + Future.delayed(const Duration(seconds: 2), () { + progressNotifier.clear(); + }); } } } diff --git a/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart b/mobile/lib/providers/infrastructure/asset_viewer/asset.provider.dart similarity index 85% rename from mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart rename to mobile/lib/providers/infrastructure/asset_viewer/asset.provider.dart index 1956170c1e..5718333759 100644 --- a/mobile/lib/providers/infrastructure/asset_viewer/current_asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset_viewer/asset.provider.dart @@ -31,6 +31,18 @@ class CurrentAssetNotifier extends AutoDisposeNotifier { } } +class ScopedAssetNotifier extends CurrentAssetNotifier { + final BaseAsset _asset; + + ScopedAssetNotifier(this._asset); + + @override + BaseAsset? build() { + setAsset(_asset); + return _asset; + } +} + final currentAssetExifProvider = FutureProvider.autoDispose((ref) { final currentAsset = ref.watch(currentAssetNotifier); if (currentAsset == null) { diff --git a/mobile/lib/providers/infrastructure/map.provider.dart b/mobile/lib/providers/infrastructure/map.provider.dart index e774cec756..d9d261521e 100644 --- a/mobile/lib/providers/infrastructure/map.provider.dart +++ b/mobile/lib/providers/infrastructure/map.provider.dart @@ -1,7 +1,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/domain/services/map.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/map.repository.dart'; +import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; final mapRepositoryProvider = Provider((ref) => DriftMapRepository(ref.watch(driftProvider))); @@ -13,7 +15,11 @@ final mapServiceProvider = Provider( throw Exception('User must be logged in to access map'); } - final mapService = ref.watch(mapFactoryProvider).remote(user.id); + final users = ref.watch(mapStateProvider).withPartners + ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] + : [user.id]; + + final mapService = ref.watch(mapFactoryProvider).remote(users, ref.watch(mapStateProvider).toOptions()); return mapService; }, // Empty dependencies to inform the framework that this provider diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart index 11c5280c02..01d0f61d1c 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -4,7 +4,9 @@ import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; -import 'package:immich_mobile/platform/thumbnail_api.g.dart'; +import 'package:immich_mobile/platform/local_image_api.g.dart'; +import 'package:immich_mobile/platform/network_api.g.dart'; +import 'package:immich_mobile/platform/remote_image_api.g.dart'; final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); @@ -16,4 +18,8 @@ final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); final connectivityApiProvider = Provider((_) => ConnectivityApi()); -final thumbnailApi = ThumbnailApi(); +final localImageApi = LocalImageApi(); + +final remoteImageApi = RemoteImageApi(); + +final networkApi = NetworkApi(); diff --git a/mobile/lib/providers/infrastructure/storage.provider.dart b/mobile/lib/providers/infrastructure/storage.provider.dart index ccca964027..82d1209c97 100644 --- a/mobile/lib/providers/infrastructure/storage.provider.dart +++ b/mobile/lib/providers/infrastructure/storage.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -final storageRepositoryProvider = Provider((ref) => const StorageRepository()); +final storageRepositoryProvider = Provider((ref) => StorageRepository()); diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index 6ba9c4bb78..5b9f29225e 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:immich_mobile/domain/services/local_sync.service.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_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -13,6 +14,8 @@ import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; +final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider))); + final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( syncApiRepository: ref.watch(syncApiRepositoryProvider), @@ -21,6 +24,8 @@ final syncStreamServiceProvider = Provider( trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), localFilesManager: ref.watch(localFilesManagerRepositoryProvider), storageRepository: ref.watch(storageRepositoryProvider), + syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider), + api: ref.watch(apiServiceProvider), cancelChecker: ref.watch(cancellationProvider), ), ); @@ -32,6 +37,7 @@ final syncStreamRepositoryProvider = Provider((ref) => SyncStreamRepository(ref. final localSyncServiceProvider = Provider( (ref) => LocalSyncService( localAlbumRepository: ref.watch(localAlbumRepository), + localAssetRepository: ref.watch(localAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), localFilesManager: ref.watch(localFilesManagerRepositoryProvider), storageRepository: ref.watch(storageRepositoryProvider), diff --git a/mobile/lib/providers/infrastructure/tag.provider.dart b/mobile/lib/providers/infrastructure/tag.provider.dart new file mode 100644 index 0000000000..23d4d86861 --- /dev/null +++ b/mobile/lib/providers/infrastructure/tag.provider.dart @@ -0,0 +1,17 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/tag.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/tags_api.repository.dart'; + +class TagNotifier extends AsyncNotifier> { + @override + Future> build() async { + final repo = ref.read(tagsApiRepositoryProvider); + final allTags = await repo.getAllTags(); + if (allTags == null) { + return {}; + } + return allTags.map((t) => Tag.fromDto(t)).toSet(); + } +} + +final tagProvider = AsyncNotifierProvider>(TagNotifier.new); diff --git a/mobile/lib/providers/infrastructure/user_metadata.provider.dart b/mobile/lib/providers/infrastructure/user_metadata.provider.dart index 2e2ae7555b..9a463463f5 100644 --- a/mobile/lib/providers/infrastructure/user_metadata.provider.dart +++ b/mobile/lib/providers/infrastructure/user_metadata.provider.dart @@ -1,7 +1,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user_metadata.model.dart'; import 'package:immich_mobile/infrastructure/repositories/user_metadata.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; final userMetadataRepository = Provider( (ref) => DriftUserMetadataRepository(ref.watch(driftProvider)), ); + +final userMetadataProvider = FutureProvider>((ref) async { + final repository = ref.watch(userMetadataRepository); + final user = ref.watch(currentUserProvider); + if (user == null) return []; + return repository.getUserMetadata(user.id); +}); + +final userMetadataPreferencesProvider = FutureProvider((ref) async { + final metadataList = await ref.watch(userMetadataProvider.future); + final metadataWithPrefs = metadataList.firstWhere((meta) => meta.preferences != null); + return metadataWithPrefs.preferences; +}); diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 9619ba86a1..98300894f9 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -15,7 +15,6 @@ class ServerInfoNotifier extends StateNotifier { : super( const ServerInfo( serverVersion: ServerVersion(major: 0, minor: 0, patch: 0), - latestVersion: ServerVersion(major: 0, minor: 0, patch: 0), serverFeatures: ServerFeatures(map: true, trash: true, oauthEnabled: false, passwordLogin: true), serverConfig: ServerConfig( trashDays: 30, @@ -32,17 +31,18 @@ class ServerInfoNotifier extends StateNotifier { final ServerInfoService _serverInfoService; final _log = Logger("ServerInfoNotifier"); - Future getServerInfo() async { + Future getServerInfo() async { await getServerVersion(); await getServerFeatures(); await getServerConfig(); + return state; } Future getServerVersion() async { try { final serverVersion = await _serverInfoService.getServerVersion(); - // using isClientOutOfDate since that will show to users reguardless of if they are an admin + // using isClientOutOfDate since that will show to users regardless of if they are an admin if (serverVersion == null) { state = state.copyWith(versionStatus: VersionStatus.error); return; @@ -75,7 +75,7 @@ class ServerInfoNotifier extends StateNotifier { state = state.copyWith(versionStatus: VersionStatus.upToDate); } - handleReleaseInfo(ServerVersion serverVersion, ServerVersion latestVersion) { + handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { // Update local server version _checkServerVersionMismatch(serverVersion, latestVersion: latestVersion); } @@ -103,7 +103,9 @@ final serverInfoProvider = StateNotifierProvider final versionWarningPresentProvider = Provider.family((ref, user) { final serverInfo = ref.watch(serverInfoProvider); - return serverInfo.versionStatus == VersionStatus.clientOutOfDate || - serverInfo.versionStatus == VersionStatus.error || - ((user?.isAdmin ?? false) && serverInfo.versionStatus == VersionStatus.serverOutOfDate); + return switch (serverInfo.versionStatus) { + VersionStatus.clientOutOfDate || VersionStatus.error => true, + VersionStatus.serverOutOfDate => serverInfo.latestVersion != null && (user?.isAdmin ?? false), + VersionStatus.upToDate => false, + }; }); diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart index 8e24bbf4d0..203184fc87 100644 --- a/mobile/lib/providers/sync_status.provider.dart +++ b/mobile/lib/providers/sync_status.provider.dart @@ -21,6 +21,7 @@ class SyncStatusState { final SyncStatus remoteSyncStatus; final SyncStatus localSyncStatus; final SyncStatus hashJobStatus; + final SyncStatus cloudIdSyncStatus; final String? errorMessage; @@ -28,6 +29,7 @@ class SyncStatusState { this.remoteSyncStatus = SyncStatus.idle, this.localSyncStatus = SyncStatus.idle, this.hashJobStatus = SyncStatus.idle, + this.cloudIdSyncStatus = SyncStatus.idle, this.errorMessage, }); @@ -35,12 +37,14 @@ class SyncStatusState { SyncStatus? remoteSyncStatus, SyncStatus? localSyncStatus, SyncStatus? hashJobStatus, + SyncStatus? cloudIdSyncStatus, String? errorMessage, }) { return SyncStatusState( remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus, localSyncStatus: localSyncStatus ?? this.localSyncStatus, hashJobStatus: hashJobStatus ?? this.hashJobStatus, + cloudIdSyncStatus: cloudIdSyncStatus ?? this.cloudIdSyncStatus, errorMessage: errorMessage ?? this.errorMessage, ); } @@ -48,6 +52,7 @@ class SyncStatusState { bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing; bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing; bool get isHashing => hashJobStatus == SyncStatus.syncing; + bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing; @override bool operator ==(Object other) { @@ -56,11 +61,12 @@ class SyncStatusState { other.remoteSyncStatus == remoteSyncStatus && other.localSyncStatus == localSyncStatus && other.hashJobStatus == hashJobStatus && + other.cloudIdSyncStatus == cloudIdSyncStatus && other.errorMessage == errorMessage; } @override - int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, errorMessage); + int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, cloudIdSyncStatus, errorMessage); } class SyncStatusNotifier extends Notifier { @@ -71,6 +77,7 @@ class SyncStatusNotifier extends Notifier { remoteSyncStatus: SyncStatus.idle, localSyncStatus: SyncStatus.idle, hashJobStatus: SyncStatus.idle, + cloudIdSyncStatus: SyncStatus.idle, ); } @@ -109,6 +116,18 @@ class SyncStatusNotifier extends Notifier { void startHashJob() => setHashJobStatus(SyncStatus.syncing); void completeHashJob() => setHashJobStatus(SyncStatus.success); void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error); + + /// + /// Cloud ID Sync Job + /// + + void setCloudIdSyncStatus(SyncStatus status, [String? errorMessage]) { + state = state.copyWith(cloudIdSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null); + } + + void startCloudIdSync() => setCloudIdSyncStatus(SyncStatus.syncing); + void completeCloudIdSync() => setCloudIdSyncStatus(SyncStatus.success); + void errorCloudIdSync(String error) => setCloudIdSyncStatus(SyncStatus.error, error); } final syncStatusProvider = NotifierProvider(SyncStatusNotifier.new); diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 0b3f7e610b..6e375f3852 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -24,10 +24,12 @@ class MultiSelectState { bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null); - bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); - bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged); + bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); + + bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote); + MultiSelectState copyWith({ Set? selectedAssets, Set? lockedSelectionAssets, diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 5aa924ed1c..a2b7a23f05 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -61,10 +61,10 @@ class UploadProfileImageNotifier extends StateNotifier final UserService _userService; - Future upload(XFile file) async { + Future upload(XFile file, {String? fileName}) async { state = state.copyWith(status: UploadProfileStatus.loading); - var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes()); + var profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes()); if (profileImagePath != null) { dPrint(() => "Successfully upload profile image"); diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 6a1083bfcc..f9473ce440 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -144,6 +144,7 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_asset_hidden', _handleOnAssetHidden); } else { socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + socket.on('AssetEditReadyV1', _handleSyncAssetEditReady); } socket.on('on_config_update', _handleOnConfigUpdate); @@ -192,10 +193,12 @@ class WebsocketNotifier extends StateNotifier { void stopListeningToBetaEvents() { state.socket?.off('AssetUploadReadyV1'); + state.socket?.off('AssetEditReadyV1'); } void startListeningToBetaEvents() { state.socket?.on('AssetUploadReadyV1', _handleSyncAssetUploadReady); + state.socket?.on('AssetEditReadyV1', _handleSyncAssetEditReady); } void listenUploadEvent() { @@ -315,6 +318,10 @@ class WebsocketNotifier extends StateNotifier { _batchDebouncer.run(_processBatchedAssetUploadReady); } + void _handleSyncAssetEditReady(dynamic data) { + unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditBatch([data])); + } + void _processBatchedAssetUploadReady() { if (_batchedAssetUploadReady.isEmpty) { return; diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 07639fbb3a..011b1edc94 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -80,8 +80,8 @@ class AssetApiRepository extends ApiRepository { return _stacksApi.deleteStacks(BulkIdsDto(ids: ids)); } - Future downloadAsset(String id) { - return _api.downloadAssetWithHttpInfo(id); + Future downloadAsset(String id, {required bool edited}) { + return _api.downloadAssetWithHttpInfo(id, edited: edited); } _mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) { @@ -101,6 +101,10 @@ class AssetApiRepository extends ApiRepository { Future updateDescription(String assetId, String description) { return _api.updateAsset(assetId, UpdateAssetDto(description: description)); } + + Future updateRating(String assetId, int rating) { + return _api.updateAsset(assetId, UpdateAssetDto(rating: rating)); + } } extension on StackResponseDto { diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 2e4bdfd32c..fecfe6df4d 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -23,7 +23,6 @@ final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref. class AssetMediaRepository { final AssetApiRepository _assetApiRepository; - static final Logger _log = Logger("AssetMediaRepository"); const AssetMediaRepository(this._assetApiRepository); @@ -58,6 +57,7 @@ class AssetMediaRepository { static asset_entity.Asset? toAsset(AssetEntity? local) { if (local == null) return null; + final asset_entity.Asset asset = asset_entity.Asset( checksum: "", localId: local.id, @@ -72,19 +72,21 @@ class AssetMediaRepository { height: local.height, isFavorite: local.isFavorite, ); + if (asset.fileCreatedAt.year == 1970) { asset.fileCreatedAt = asset.fileModifiedAt; } + if (local.latitude != null) { asset.exifInfo = ExifInfo(latitude: local.latitude, longitude: local.longitude); } + asset.local = local; return asset; } Future getOriginalFilename(String id) async { final entity = await AssetEntity.fromId(id); - if (entity == null) { return null; } @@ -101,28 +103,53 @@ class AssetMediaRepository { } } + /// Deletes temporary files in parallel + Future _cleanupTempFiles(List tempFiles) async { + await Future.wait( + tempFiles.map((file) async { + try { + await file.delete(); + } catch (e) { + _log.warning("Failed to delete temporary file: ${file.path}", e); + } + }), + ); + } + // TODO: make this more efficient - Future shareAssets(List assets, BuildContext context) async { + Future shareAssets(List assets, BuildContext context, {Completer? cancelCompleter}) async { final downloadedXFiles = []; final tempFiles = []; for (var asset in assets) { + if (cancelCompleter != null && cancelCompleter.isCompleted) { + // if cancelled, delete any temp files created so far + await _cleanupTempFiles(tempFiles); + return 0; + } + final localId = (asset is LocalAsset) ? asset.id : asset is RemoteAsset ? asset.localId : null; - if (localId != null) { + if (localId != null && !asset.isEdited) { File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile; downloadedXFiles.add(XFile(f!.path)); if (CurrentPlatform.isIOS) { tempFiles.add(f); } - } else if (asset is RemoteAsset) { + } else { + final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId; + if (remoteId == null) { + _log.warning("Asset has no remote ID for sharing: $asset"); + continue; + } + final tempDir = await getTemporaryDirectory(); final name = asset.name; final tempFile = await File('${tempDir.path}/$name').create(); - final res = await _assetApiRepository.downloadAsset(asset.id); + final res = await _assetApiRepository.downloadAsset(remoteId, edited: true); if (res.statusCode != 200) { _log.severe("Download for $name failed", res.toLoggerString()); @@ -132,9 +159,6 @@ class AssetMediaRepository { await tempFile.writeAsBytes(res.bodyBytes); downloadedXFiles.add(XFile(tempFile.path)); tempFiles.add(tempFile); - } else { - _log.warning("Asset type not supported for sharing: $asset"); - continue; } } @@ -143,6 +167,11 @@ class AssetMediaRepository { return 0; } + if (cancelCompleter != null && cancelCompleter.isCompleted) { + await _cleanupTempFiles(tempFiles); + return 0; + } + // we dont want to await the share result since the // "preparing" dialog will not disappear until final size = context.sizeData; @@ -151,13 +180,7 @@ class AssetMediaRepository { downloadedXFiles, sharePositionOrigin: Rect.fromPoints(Offset.zero, Offset(size.width / 3, size.height)), ).then((result) async { - for (var file in tempFiles) { - try { - await file.delete(); - } catch (e) { - _log.warning("Failed to delete temporary file: ${file.path}", e); - } - } + await _cleanupTempFiles(tempFiles); }), ); diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 654be78fb4..f5cdb6d5c0 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -25,6 +25,8 @@ class FileMediaRepository { type: AssetType.image, createdAt: entity.createDateTime, updatedAt: entity.modifiedDateTime, + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, ); } diff --git a/mobile/lib/repositories/local_files_manager.repository.dart b/mobile/lib/repositories/local_files_manager.repository.dart index 765c9a6f0e..6a6200b2e1 100644 --- a/mobile/lib/repositories/local_files_manager.repository.dart +++ b/mobile/lib/repositories/local_files_manager.repository.dart @@ -10,7 +10,7 @@ final localFilesManagerRepositoryProvider = Provider( class LocalFilesManagerRepository { LocalFilesManagerRepository(this._service); - final Logger _logger = Logger('SyncStreamService'); + final Logger _logger = Logger('LocalFilesManagerRepo'); final LocalFilesManagerService _service; Future moveToTrash(List mediaUrls) async { @@ -38,8 +38,10 @@ class LocalFilesManagerRepository { for (final asset in assets) { _logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}"); try { - await _service.restoreFromTrashById(asset.id, asset.type.index); - restoredIds.add(asset.id); + final result = await _service.restoreFromTrashById(asset.id, asset.type.index); + if (result) { + restoredIds.add(asset.id); + } } catch (e) { _logger.warning("Restoring failure: $e"); } diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 38f2c22cf2..aff84683c3 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -20,6 +21,7 @@ class UploadTaskWithFile { final uploadRepositoryProvider = Provider((ref) => UploadRepository()); class UploadRepository { + final Logger logger = Logger('UploadRepository'); void Function(TaskStatusUpdate)? onUploadStatus; void Function(TaskProgressUpdate)? onTaskProgress; @@ -92,52 +94,114 @@ class UploadRepository { ); } - Future backupWithDartClient(Iterable tasks, CancellationToken cancelToken) async { - final httpClient = Client(); + Future uploadFile({ + required File file, + required String originalFileName, + required Map headers, + required Map fields, + required Client httpClient, + required CancellationToken cancelToken, + required void Function(int bytes, int totalBytes) onProgress, + required String logContext, + }) async { final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - Logger logger = Logger('UploadRepository'); - for (final candidate in tasks) { - if (cancelToken.isCancelled) { - logger.warning("Backup was cancelled by the user"); - break; + try { + final fileStream = file.openRead(); + final assetRawUploadData = MultipartFile("assetData", fileStream, file.lengthSync(), filename: originalFileName); + + final baseRequest = _CustomMultipartRequest('POST', Uri.parse('$savedEndpoint/assets'), onProgress: onProgress); + + baseRequest.headers.addAll(headers); + baseRequest.fields.addAll(fields); + baseRequest.files.add(assetRawUploadData); + + final response = await httpClient.send(baseRequest, cancellationToken: cancelToken); + final responseBodyString = await response.stream.bytesToString(); + + if (![200, 201].contains(response.statusCode)) { + String? errorMessage; + + if (response.statusCode == 413) { + errorMessage = 'Error(413) File is too large to upload'; + return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage); + } + + try { + final error = jsonDecode(responseBodyString); + errorMessage = error['message'] ?? error['error']; + } catch (_) { + errorMessage = responseBodyString.isNotEmpty + ? responseBodyString + : 'Upload failed with status ${response.statusCode}'; + } + + return UploadResult.error(statusCode: response.statusCode, errorMessage: errorMessage); } try { - final fileStream = candidate.file.openRead(); - final assetRawUploadData = MultipartFile( - "assetData", - fileStream, - candidate.file.lengthSync(), - filename: candidate.task.filename, - ); - - final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets')); - - baseRequest.headers.addAll(candidate.task.headers); - baseRequest.fields.addAll(candidate.task.fields); - baseRequest.files.add(assetRawUploadData); - - final response = await httpClient.send(baseRequest, cancellationToken: cancelToken); - - final responseBody = jsonDecode(await response.stream.bytesToString()); - - if (![200, 201].contains(response.statusCode)) { - final error = responseBody; - - logger.warning( - "Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}", - ); - - continue; - } - } on CancelledException { - logger.warning("Backup was cancelled by the user"); - break; - } catch (error, stackTrace) { - logger.warning("Error backup asset: ${error.toString()}: $stackTrace"); - continue; + final responseBody = jsonDecode(responseBodyString); + return UploadResult.success(remoteAssetId: responseBody['id'] as String); + } catch (e) { + return UploadResult.error(errorMessage: 'Failed to parse server response'); } + } on CancelledException { + logger.warning("Upload $logContext was cancelled"); + return UploadResult.cancelled(); + } catch (error, stackTrace) { + logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace"); + return UploadResult.error(errorMessage: error.toString()); } } } + +class UploadResult { + final bool isSuccess; + final bool isCancelled; + final String? remoteAssetId; + final String? errorMessage; + final int? statusCode; + + const UploadResult({ + required this.isSuccess, + required this.isCancelled, + this.remoteAssetId, + this.errorMessage, + this.statusCode, + }); + + factory UploadResult.success({required String remoteAssetId}) { + return UploadResult(isSuccess: true, isCancelled: false, remoteAssetId: remoteAssetId); + } + + factory UploadResult.error({String? errorMessage, int? statusCode}) { + return UploadResult(isSuccess: false, isCancelled: false, errorMessage: errorMessage, statusCode: statusCode); + } + + factory UploadResult.cancelled() { + return const UploadResult(isSuccess: false, isCancelled: true); + } +} + +class _CustomMultipartRequest extends MultipartRequest { + _CustomMultipartRequest(super.method, super.url, {required this.onProgress}); + + final void Function(int bytes, int totalBytes) onProgress; + + @override + ByteStream finalize() { + final byteStream = super.finalize(); + final total = contentLength; + var bytes = 0; + + final t = StreamTransformer.fromHandlers( + handleData: (List data, EventSink> sink) { + bytes += data.length; + onProgress.call(bytes, total); + sink.add(data); + }, + ); + final stream = byteStream.transform(t); + return ByteStream(stream); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 9c4a193381..b385bcbf71 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -78,9 +78,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; 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/dev/ui_showcase.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; @@ -106,6 +106,7 @@ import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/editing/drift_crop.page.dart'; +import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/editing/drift_edit.page.dart'; import 'package:immich_mobile/presentation/pages/editing/drift_filter.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; @@ -164,7 +165,7 @@ class AppRouter extends RootStackRouter { late final List routes = [ AutoRoute(page: SplashScreenRoute.page, initial: true), AutoRoute(page: PermissionOnboardingRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]), + AutoRoute(page: LoginRoute.page), AutoRoute(page: ChangePasswordRoute.page), AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), AutoRoute( @@ -198,6 +199,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: EditImageRoute.page), AutoRoute(page: CropImageRoute.page), AutoRoute(page: FilterImageRoute.page), + AutoRoute(page: ProfilePictureCropRoute.page), CustomRoute( page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard], @@ -337,7 +339,7 @@ class AppRouter extends RootStackRouter { AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: ImmichUIShowcaseRoute.page, guards: [_authGuard, _duplicateGuard]), + AutoRoute(page: CleanupPreviewRoute.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 939bf73369..2d57c16573 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -611,6 +611,43 @@ class ChangePasswordRoute extends PageRouteInfo { ); } +/// generated route for +/// [CleanupPreviewPage] +class CleanupPreviewRoute extends PageRouteInfo { + CleanupPreviewRoute({ + Key? key, + required List assets, + List? children, + }) : super( + CleanupPreviewRoute.name, + args: CleanupPreviewRouteArgs(key: key, assets: assets), + initialChildren: children, + ); + + static const String name = 'CleanupPreviewRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return CleanupPreviewPage(key: args.key, assets: args.assets); + }, + ); +} + +class CleanupPreviewRouteArgs { + const CleanupPreviewRouteArgs({this.key, required this.assets}); + + final Key? key; + + final List assets; + + @override + String toString() { + return 'CleanupPreviewRouteArgs{key: $key, assets: $assets}'; + } +} + /// generated route for /// [CreateAlbumPage] class CreateAlbumRoute extends PageRouteInfo { @@ -716,10 +753,17 @@ class DriftActivitiesRoute extends PageRouteInfo { DriftActivitiesRoute({ Key? key, required RemoteAlbum album, + String? assetId, + String? assetName, List? children, }) : super( DriftActivitiesRoute.name, - args: DriftActivitiesRouteArgs(key: key, album: album), + args: DriftActivitiesRouteArgs( + key: key, + album: album, + assetId: assetId, + assetName: assetName, + ), initialChildren: children, ); @@ -729,21 +773,35 @@ class DriftActivitiesRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return DriftActivitiesPage(key: args.key, album: args.album); + return DriftActivitiesPage( + key: args.key, + album: args.album, + assetId: args.assetId, + assetName: args.assetName, + ); }, ); } class DriftActivitiesRouteArgs { - const DriftActivitiesRouteArgs({this.key, required this.album}); + const DriftActivitiesRouteArgs({ + this.key, + required this.album, + this.assetId, + this.assetName, + }); final Key? key; final RemoteAlbum album; + final String? assetId; + + final String? assetName; + @override String toString() { - return 'DriftActivitiesRouteArgs{key: $key, album: $album}'; + return 'DriftActivitiesRouteArgs{key: $key, album: $album, assetId: $assetId, assetName: $assetName}'; } } @@ -1815,22 +1873,6 @@ class HeaderSettingsRoute extends PageRouteInfo { ); } -/// generated route for -/// [ImmichUIShowcasePage] -class ImmichUIShowcaseRoute extends PageRouteInfo { - const ImmichUIShowcaseRoute({List? children}) - : super(ImmichUIShowcaseRoute.name, initialChildren: children); - - static const String name = 'ImmichUIShowcaseRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ImmichUIShowcasePage(); - }, - ); -} - /// generated route for /// [LibraryPage] class LibraryRoute extends PageRouteInfo { @@ -2401,6 +2443,44 @@ class PlacesCollectionRouteArgs { } } +/// generated route for +/// [ProfilePictureCropPage] +class ProfilePictureCropRoute + extends PageRouteInfo { + ProfilePictureCropRoute({ + Key? key, + required BaseAsset asset, + List? children, + }) : super( + ProfilePictureCropRoute.name, + args: ProfilePictureCropRouteArgs(key: key, asset: asset), + initialChildren: children, + ); + + static const String name = 'ProfilePictureCropRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return ProfilePictureCropPage(key: args.key, asset: args.asset); + }, + ); +} + +class ProfilePictureCropRouteArgs { + const ProfilePictureCropRouteArgs({this.key, required this.asset}); + + final Key? key; + + final BaseAsset asset; + + @override + String toString() { + return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}'; + } +} + /// generated route for /// [RecentlyTakenPage] class RecentlyTakenRoute extends PageRouteInfo { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 4261613a19..c435bf9d79 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,9 +5,13 @@ 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/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/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/infrastructure/repositories/trashed_local_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'; @@ -28,6 +32,7 @@ final actionServiceProvider = Provider( ref.watch(localAssetRepository), ref.watch(driftAlbumApiRepositoryProvider), ref.watch(remoteAlbumRepository), + ref.watch(trashedLocalAssetRepository), ref.watch(assetMediaRepositoryProvider), ref.watch(downloadRepositoryProvider), ), @@ -39,6 +44,7 @@ class ActionService { final DriftLocalAssetRepository _localAssetRepository; final DriftAlbumApiRepository _albumApiRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository; + final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final AssetMediaRepository _assetMediaRepository; final DownloadRepository _downloadRepository; @@ -48,6 +54,7 @@ class ActionService { this._localAssetRepository, this._albumApiRepository, this._remoteAlbumRepository, + this._trashedLocalAssetRepository, this._assetMediaRepository, this._downloadRepository, ); @@ -82,11 +89,7 @@ class ActionService { // 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); - } + await _deleteLocalAssets(localIds); } } @@ -110,11 +113,7 @@ class ActionService { await _remoteAssetRepository.trash(remoteIds); if (localIds.isNotEmpty) { - final deletedIds = await _assetMediaRepository.deleteAll(localIds); - - if (deletedIds.isNotEmpty) { - await _localAssetRepository.delete(deletedIds); - } + await _deleteLocalAssets(localIds); } } @@ -123,22 +122,12 @@ class ActionService { await _remoteAssetRepository.delete(remoteIds); if (localIds.isNotEmpty) { - final deletedIds = await _assetMediaRepository.deleteAll(localIds); - - if (deletedIds.isNotEmpty) { - await _localAssetRepository.delete(deletedIds); - } + await _deleteLocalAssets(localIds); } } Future deleteLocal(List localIds) async { - final deletedIds = await _assetMediaRepository.deleteAll(localIds); - if (deletedIds.isNotEmpty) { - await _localAssetRepository.delete(deletedIds); - return deletedIds.length; - } - - return 0; + return await _deleteLocalAssets(localIds); } Future editLocation(List remoteIds, BuildContext context) async { @@ -225,6 +214,14 @@ class ActionService { return true; } + Future updateRating(String assetId, int rating) async { + // update remote first, then local to ensure consistency + await _assetApiRepository.updateRating(assetId, rating); + await _remoteAssetRepository.updateRating(assetId, rating); + + return true; + } + Future stack(String userId, List remoteIds) async { final stack = await _assetApiRepository.stack(remoteIds); await _remoteAssetRepository.stack(userId, stack); @@ -235,11 +232,30 @@ class ActionService { await _assetApiRepository.unStack(stackIds); } - Future shareAssets(List assets, BuildContext context) { - return _assetMediaRepository.shareAssets(assets, context); + Future shareAssets(List assets, BuildContext context, {Completer? cancelCompleter}) { + return _assetMediaRepository.shareAssets(assets, context, cancelCompleter: cancelCompleter); } Future> downloadAll(List assets) { return _downloadRepository.downloadAllAssets(assets); } + + Future setAlbumCover(String albumId, String assetId) async { + final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, thumbnailAssetId: assetId); + await _remoteAlbumRepository.update(updatedAlbum); + return true; + } + + Future _deleteLocalAssets(List localIds) async { + final deletedIds = await _assetMediaRepository.deleteAll(localIds); + if (deletedIds.isEmpty) { + return 0; + } + if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds); + } else { + await _localAssetRepository.delete(deletedIds); + } + return deletedIds.length; + } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 1a714b6f40..bafe780647 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -35,6 +35,7 @@ class ApiService implements Authentication { late ViewsApi viewApi; late MemoriesApi memoriesApi; late SessionsApi sessionsApi; + late TagsApi tagsApi; ApiService() { // The below line ensures that the api clients are initialized when the service is instantiated @@ -74,6 +75,7 @@ class ApiService implements Authentication { viewApi = ViewsApi(_apiClient); memoriesApi = MemoriesApi(_apiClient); sessionsApi = SessionsApi(_apiClient); + tagsApi = TagsApi(_apiClient); } Future _setUserAgentHeader() async { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index aa247682a7..db4fc9965a 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -35,6 +35,7 @@ enum AppSettingsEnum { loopVideo(StoreKey.loopVideo, "loopVideo", true), loadOriginalVideo(StoreKey.loadOriginalVideo, "loadOriginalVideo", false), autoPlayVideo(StoreKey.autoPlayVideo, "autoPlayVideo", true), + tapToNavigate(StoreKey.tapToNavigate, "tapToNavigate", false), mapThemeMode(StoreKey.mapThemeMode, null, 0), mapShowFavoriteOnly(StoreKey.mapShowFavoriteOnly, null, false), mapIncludeArchived(StoreKey.mapIncludeArchived, null, false), @@ -54,7 +55,12 @@ enum AppSettingsEnum { readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), albumGridView(StoreKey.albumGridView, "albumGridView", false), backupRequireCharging(StoreKey.backupRequireCharging, null, false), - backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30); + backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30), + cleanupKeepFavorites(StoreKey.cleanupKeepFavorites, null, true), + cleanupKeepMediaType(StoreKey.cleanupKeepMediaType, null, 0), + cleanupKeepAlbumIds(StoreKey.cleanupKeepAlbumIds, null, ""), + cleanupCutoffDaysAgo(StoreKey.cleanupCutoffDaysAgo, null, -1), + cleanupDefaultsInitialized(StoreKey.cleanupDefaultsInitialized, null, false); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/background_upload.service.dart similarity index 75% rename from mobile/lib/services/upload.service.dart rename to mobile/lib/services/background_upload.service.dart index 1ce0cf0322..d54a677c24 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; -import 'package:cancellation_token_http/http.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/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -15,7 +15,6 @@ 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/app_settings.provider.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/asset_media.repository.dart'; @@ -26,12 +25,12 @@ import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; -final uploadServiceProvider = Provider((ref) { - final service = UploadService( +final backgroundUploadServiceProvider = Provider((ref) { + final service = BackgroundUploadService( ref.watch(uploadRepositoryProvider), - ref.watch(backupRepositoryProvider), ref.watch(storageRepositoryProvider), ref.watch(localAssetRepository), + ref.watch(backupRepositoryProvider), ref.watch(appSettingsServiceProvider), ref.watch(assetMediaRepositoryProvider), ); @@ -40,12 +39,70 @@ final uploadServiceProvider = Provider((ref) { return service; }); -class UploadService { - UploadService( +/// Metadata for upload tasks to track live photo handling +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; +} + +/// Service for handling background uploads using iOS URLSession (background_downloader) +/// +/// This service handles asynchronous background uploads that can continue +/// even when the app is suspended. Primarily used for iOS background backup. +class BackgroundUploadService { + BackgroundUploadService( this._uploadRepository, - this._backupRepository, this._storageRepository, this._localAssetRepository, + this._backupRepository, this._appSettingsService, this._assetMediaRepository, ) { @@ -54,12 +111,12 @@ class UploadService { } final UploadRepository _uploadRepository; - final DriftBackupRepository _backupRepository; final StorageRepository _storageRepository; final DriftLocalAssetRepository _localAssetRepository; + final DriftBackupRepository _backupRepository; final AppSettingsService _appSettingsService; final AssetMediaRepository _assetMediaRepository; - final Logger _logger = Logger('UploadService'); + final Logger _logger = Logger('BackgroundUploadService'); final StreamController _taskStatusController = StreamController.broadcast(); final StreamController _taskProgressController = StreamController.broadcast(); @@ -87,116 +144,53 @@ class UploadService { _taskProgressController.close(); } + /// Enqueue tasks to the background upload queue Future> enqueueTasks(List tasks) { return _uploadRepository.enqueueBackgroundAll(tasks); } + /// Get a list of tasks that are ENQUEUED or RUNNING Future> getActiveTasks(String group) { return _uploadRepository.getActiveTasks(group); } - Future<({int total, int remainder, int processing})> getBackupCounts(String userId) { - return _backupRepository.getAllCounts(userId); - } - - Future manualBackup(List localAssets) async { + /// Start background upload using iOS URLSession + /// + /// Finds backup candidates, builds upload tasks, and enqueues them + /// for background processing. + Future uploadBackupCandidates(String userId) async { await _storageRepository.clearCache(); + shouldAbortQueuingTasks = false; + + final candidates = await _backupRepository.getCandidates(userId); + if (candidates.isEmpty) { + _logger.info("No new backup candidates found, finishing background upload"); + return; + } + + _logger.info("Found ${candidates.length} backup candidates for background tasks"); + + const batchSize = 100; + final batch = candidates.take(batchSize).toList(); List tasks = []; - for (final asset in localAssets) { - final task = await getUploadTask( - asset, - group: kManualUploadGroup, - priority: 1, // High priority after upload motion photo part - ); + + for (final asset in batch) { + final task = await getUploadTask(asset); if (task != null) { tasks.add(task); } } - if (tasks.isNotEmpty) { + if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { + _logger.info("Enqueuing ${tasks.length} background upload tasks"); await enqueueTasks(tasks); } } - /// Find backup candidates - /// Build the upload tasks - /// Enqueue the tasks - Future startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async { - await _storageRepository.clearCache(); - - 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; - await enqueueTasks(tasks); - - onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length)); - } - } - } - - Future startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async { - await _storageRepository.clearCache(); - - shouldAbortQueuingTasks = false; - - final candidates = await _backupRepository.getCandidates(userId); - if (candidates.isEmpty) { - return; - } - - const batchSize = 100; - for (int i = 0; i < candidates.length; i += batchSize) { - if (shouldAbortQueuingTasks || token.isCancelled) { - break; - } - - final batch = candidates.skip(i).take(batchSize).toList(); - List tasks = []; - for (final asset in batch) { - final requireWifi = _shouldRequireWiFi(asset); - if (requireWifi && !hasWifi) { - _logger.warning('Skipping upload for ${asset.id} because it requires WiFi'); - continue; - } - - final task = await _getUploadTaskWithFile(asset); - if (task != null) { - tasks.add(task); - } - } - - if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { - await _uploadRepository.backupWithDartClient(tasks, token); - } - } - } - - /// Cancel all ongoing uploads and reset the upload queue + /// Cancel all ongoing background uploads and reset the upload queue /// - /// Return the number of left over tasks in the queue - Future cancelBackup() async { + /// Returns the number of tasks left in the queue + Future cancel() async { shouldAbortQueuingTasks = true; await _storageRepository.clearCache(); @@ -207,7 +201,8 @@ class UploadService { return activeTasks.length; } - Future resumeBackup() { + /// Resume background backup processing + Future resume() { return _uploadRepository.start(); } @@ -265,46 +260,11 @@ class UploadService { } } - Future _getUploadTaskWithFile(LocalAsset asset) async { - final entity = await _storageRepository.getAssetEntityForAsset(asset); - if (entity == null) { - return null; - } - - final file = await _storageRepository.getFileForAsset(asset.id); - if (file == null) { - return null; - } - - final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name; - - String metadata = UploadTaskMetadata( - localAssetId: asset.id, - isLivePhotos: entity.isLivePhoto, - livePhotoVideoId: '', - ).toJson(); - - return UploadTaskWithFile( - file: file, - task: await buildUploadTask( - file, - createdAt: asset.createdAt, - modifiedAt: asset.updatedAt, - originalFileName: originalFileName, - deviceAssetId: asset.id, - metadata: metadata, - group: "group", - priority: 0, - isFavorite: asset.isFavorite, - requiresWiFi: false, - ), - ); - } - @visibleForTesting Future getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { + _logger.warning("Asset entity not found for ${asset.id} - ${asset.name}"); return null; } @@ -327,10 +287,16 @@ class UploadService { } if (file == null) { + _logger.warning("Failed to get file for asset ${asset.id} - ${asset.name}"); return null; } - final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + final hasExtension = p.extension(fileName).isNotEmpty; + if (!hasExtension) { + fileName = p.setExtension(fileName, p.extension(asset.name)); + } + final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName; String metadata = UploadTaskMetadata( @@ -352,6 +318,10 @@ class UploadService { priority: priority, isFavorite: asset.isFavorite, requiresWiFi: requiresWiFi, + cloudId: entity.isLivePhoto ? null : asset.cloudId, + adjustmentTime: entity.isLivePhoto ? null : asset.adjustmentTime?.toIso8601String(), + latitude: entity.isLivePhoto ? null : asset.latitude?.toString(), + longitude: entity.isLivePhoto ? null : asset.longitude?.toString(), ); } @@ -383,6 +353,10 @@ class UploadService { priority: 0, // Highest priority to get upload immediately isFavorite: asset.isFavorite, requiresWiFi: requiresWiFi, + cloudId: asset.cloudId, + adjustmentTime: asset.adjustmentTime?.toIso8601String(), + latitude: asset.latitude?.toString(), + longitude: asset.longitude?.toString(), ); } @@ -410,6 +384,10 @@ class UploadService { int? priority, bool? isFavorite, bool requiresWiFi = true, + String? cloudId, + String? adjustmentTime, + String? latitude, + String? longitude, }) async { final serverEndpoint = Store.get(StoreKey.serverEndpoint); final url = Uri.parse('$serverEndpoint/assets').toString(); @@ -425,6 +403,19 @@ class UploadService { 'isFavorite': isFavorite?.toString() ?? 'false', 'duration': '0', if (fields != null) ...fields, + if (CurrentPlatform.isIOS && cloudId != null) + 'metadata': jsonEncode([ + RemoteAssetMetadataItem( + key: RemoteAssetMetadataKey.mobileApp, + value: RemoteAssetMobileAppMetadata( + cloudId: cloudId, + createdAt: createdAt.toIso8601String(), + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ), + ), + ]), }; return UploadTask( @@ -447,56 +438,3 @@ class UploadService { ); } } - -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/cleanup.service.dart b/mobile/lib/services/cleanup.service.dart new file mode 100644 index 0000000000..fca5584859 --- /dev/null +++ b/mobile/lib/services/cleanup.service.dart @@ -0,0 +1,70 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; + +final cleanupServiceProvider = Provider((ref) { + return CleanupService(ref.watch(localAssetRepository), ref.watch(assetMediaRepositoryProvider)); +}); + +class CleanupService { + static final int _deleteBatchSize = CurrentPlatform.isAndroid ? 2000 : 10000; + + final DriftLocalAssetRepository _localAssetRepository; + final AssetMediaRepository _assetMediaRepository; + + const CleanupService(this._localAssetRepository, this._assetMediaRepository); + + Future getRemovalCandidates( + String userId, + DateTime cutoffDate, { + AssetKeepType keepMediaType = AssetKeepType.none, + bool keepFavorites = true, + Set keepAlbumIds = const {}, + }) { + return _localAssetRepository.getRemovalCandidates( + userId, + cutoffDate, + keepMediaType: keepMediaType, + keepFavorites: keepFavorites, + keepAlbumIds: keepAlbumIds, + ); + } + + Future deleteLocalAssets(List localIds) async { + if (localIds.isEmpty) { + return 0; + } + + int deletedCount = 0; + + for (int index = 0; index < localIds.length; index += _deleteBatchSize) { + final end = index + _deleteBatchSize < localIds.length ? index + _deleteBatchSize : localIds.length; + final batch = localIds.sublist(index, end); + + final deletedIds = await _assetMediaRepository.deleteAll(batch); + if (deletedIds.isNotEmpty) { + await _localAssetRepository.delete(deletedIds); + deletedCount += deletedIds.length; + } + } + + return deletedCount; + } + + /// Returns album IDs that should be kept by default (e.g., messaging app albums) + Set getDefaultKeepAlbumIds(List<(String id, String name)> albums) { + const messagingApps = ['whatsapp', 'telegram', 'signal', 'messenger', 'viber', 'wechat', 'line']; + + final toKeep = {}; + for (final (id, name) in albums) { + final albumName = name.toLowerCase(); + if (messagingApps.any((app) => albumName.contains(app))) { + toKeep.add(id); + } + } + return toKeep; + } +} diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 6ede7f6830..9d2bdbe4a0 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -1,7 +1,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/asset.service.dart' as beta_asset_service; import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/domain/services/people.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -11,7 +14,9 @@ import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart' as beta_asset_provider; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/asset.service.dart'; @@ -30,6 +35,8 @@ final deepLinkServiceProvider = Provider( ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), ref.watch(driftMemoryServiceProvider), + ref.watch(driftPeopleServiceProvider), + ref.watch(currentUserProvider), ), ); @@ -45,7 +52,10 @@ class DeepLinkService { final TimelineFactory _betaTimelineFactory; final beta_asset_service.AssetService _betaAssetService; final RemoteAlbumService _betaRemoteAlbumService; - final DriftMemoryService _betaMemoryServiceProvider; + final DriftMemoryService _betaMemoryService; + final DriftPeopleService _betaPeopleService; + + final UserDto? _currentUser; const DeepLinkService( this._memoryService, @@ -56,7 +66,9 @@ class DeepLinkService { this._betaTimelineFactory, this._betaAssetService, this._betaRemoteAlbumService, - this._betaMemoryServiceProvider, + this._betaMemoryService, + this._betaPeopleService, + this._currentUser, ); DeepLink _handleColdStart(PageRouteInfo route, bool isColdStart) { @@ -77,6 +89,7 @@ class DeepLinkService { "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), "asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref), "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), + "people" => await _buildPeopleDeepLink(queryParams['id'] ?? ''), "activity" => await _buildActivityDeepLink(queryParams['albumId'] ?? ''), _ => null, }; @@ -99,6 +112,7 @@ class DeepLinkService { 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)'); + final peopleRegex = RegExp('/people/($uuidRegex)'); PageRouteInfo? deepLinkRoute; if (assetRegex.hasMatch(path)) { @@ -107,6 +121,11 @@ class DeepLinkService { } else if (albumRegex.hasMatch(path)) { final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAlbumDeepLink(albumId); + } else if (peopleRegex.hasMatch(path)) { + final peopleId = peopleRegex.firstMatch(path)?.group(1) ?? ''; + deepLinkRoute = await _buildPeopleDeepLink(peopleId); + } else if (path == "/memory") { + deepLinkRoute = await _buildMemoryDeepLink(null); } // Deep link resolution failed, safely handle it based on the app state @@ -118,17 +137,33 @@ class DeepLinkService { return _handleColdStart(deepLinkRoute, isColdStart); } - Future _buildMemoryDeepLink(String memoryId) async { + Future _buildMemoryDeepLink(String? memoryId) async { if (Store.isBetaTimelineEnabled) { - final memory = await _betaMemoryServiceProvider.get(memoryId); + List memories = []; - if (memory == null) { + if (memoryId == null) { + if (_currentUser == null) { + return null; + } + + memories = await _betaMemoryService.getMemoryLane(_currentUser.id); + } else { + final memory = await _betaMemoryService.get(memoryId); + if (memory != null) { + memories = [memory]; + } + } + + if (memories.isEmpty) { return null; } - return DriftMemoryRoute(memories: [memory], memoryIndex: 0); + return DriftMemoryRoute(memories: memories, memoryIndex: 0); } else { // TODO: Remove this when beta is default + if (memoryId == null) { + return null; + } final memory = await _memoryService.getMemoryById(memoryId); if (memory == null) { @@ -200,4 +235,18 @@ class DeepLinkService { return DriftActivitiesRoute(album: album); } + + Future _buildPeopleDeepLink(String personId) async { + if (Store.isBetaTimelineEnabled == false) { + return null; + } + + final person = await _betaPeopleService.get(personId); + + if (person == null) { + return null; + } + + return DriftPersonRoute(person: person); + } } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart new file mode 100644 index 0000000000..cd28942bd2 --- /dev/null +++ b/mobile/lib/services/foreground_upload.service.dart @@ -0,0 +1,493 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cancellation_token_http/http.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/extensions/network_capability_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/platform/connectivity_api.g.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; +import 'package:immich_mobile/repositories/upload.repository.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:logging/logging.dart'; +import 'package:path/path.dart' as p; +import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; + +/// Callbacks for upload progress and status updates +class UploadCallbacks { + final void Function(String id, String filename, int bytes, int totalBytes)? onProgress; + final void Function(String localId, String remoteId)? onSuccess; + final void Function(String id, String errorMessage)? onError; + final void Function(String id, double progress)? onICloudProgress; + + const UploadCallbacks({this.onProgress, this.onSuccess, this.onError, this.onICloudProgress}); +} + +final foregroundUploadServiceProvider = Provider((ref) { + return ForegroundUploadService( + ref.watch(uploadRepositoryProvider), + ref.watch(storageRepositoryProvider), + ref.watch(backupRepositoryProvider), + ref.watch(connectivityApiProvider), + ref.watch(appSettingsServiceProvider), + ref.watch(assetMediaRepositoryProvider), + ); +}); + +/// Service for handling foreground HTTP uploads +/// +/// This service handles synchronous uploads using HTTP client with +/// concurrent worker pools. Used for manual backups, auto backups +/// (foreground mode), and share intent uploads. +class ForegroundUploadService { + ForegroundUploadService( + this._uploadRepository, + this._storageRepository, + this._backupRepository, + this._connectivityApi, + this._appSettingsService, + this._assetMediaRepository, + ); + + final UploadRepository _uploadRepository; + final StorageRepository _storageRepository; + final DriftBackupRepository _backupRepository; + final ConnectivityApi _connectivityApi; + final AppSettingsService _appSettingsService; + final AssetMediaRepository _assetMediaRepository; + final Logger _logger = Logger('ForegroundUploadService'); + + bool shouldAbortUpload = false; + + Future<({int total, int remainder, int processing})> getBackupCounts(String userId) { + return _backupRepository.getAllCounts(userId); + } + + Future> getBackupCandidates(String userId, {bool onlyHashed = true}) { + return _backupRepository.getCandidates(userId, onlyHashed: onlyHashed); + } + + /// Bulk upload of backup candidates from selected albums + Future uploadCandidates( + String userId, + CancellationToken cancelToken, { + UploadCallbacks callbacks = const UploadCallbacks(), + bool useSequentialUpload = false, + }) async { + final candidates = await _backupRepository.getCandidates(userId); + if (candidates.isEmpty) { + return; + } + + final networkCapabilities = await _connectivityApi.getCapabilities(); + final hasWifi = networkCapabilities.isUnmetered; + _logger.info('Network capabilities: $networkCapabilities, hasWifi/isUnmetered: $hasWifi'); + + if (useSequentialUpload) { + await _uploadSequentially(items: candidates, cancelToken: cancelToken, hasWifi: hasWifi, callbacks: callbacks); + } else { + await _executeWithWorkerPool( + items: candidates, + cancelToken: cancelToken, + shouldSkip: (asset) { + final requireWifi = _shouldRequireWiFi(asset); + return requireWifi && !hasWifi; + }, + processItem: (asset, httpClient) => _uploadSingleAsset(asset, httpClient, cancelToken, callbacks: callbacks), + ); + } + } + + /// Sequential upload - used for background isolate where concurrent HTTP clients may cause issues + Future _uploadSequentially({ + required List items, + required CancellationToken cancelToken, + required bool hasWifi, + required UploadCallbacks callbacks, + }) async { + final httpClient = Client(); + await _storageRepository.clearCache(); + shouldAbortUpload = false; + + try { + for (final asset in items) { + if (shouldAbortUpload || cancelToken.isCancelled) { + break; + } + + final requireWifi = _shouldRequireWiFi(asset); + if (requireWifi && !hasWifi) { + _logger.warning('Skipping upload for ${asset.id} because it requires WiFi'); + continue; + } + + await _uploadSingleAsset(asset, httpClient, cancelToken, callbacks: callbacks); + } + } finally { + httpClient.close(); + } + } + + /// Manually upload picked local assets + Future uploadManual( + List localAssets, + CancellationToken cancelToken, { + UploadCallbacks callbacks = const UploadCallbacks(), + }) async { + if (localAssets.isEmpty) { + return; + } + + await _executeWithWorkerPool( + items: localAssets, + cancelToken: cancelToken, + processItem: (asset, httpClient) => _uploadSingleAsset(asset, httpClient, cancelToken, callbacks: callbacks), + ); + } + + /// Upload files from shared intent + Future uploadShareIntent( + List files, { + CancellationToken? cancelToken, + void Function(String fileId, int bytes, int totalBytes)? onProgress, + void Function(String fileId)? onSuccess, + void Function(String fileId, String errorMessage)? onError, + }) async { + if (files.isEmpty) { + return; + } + + final effectiveCancelToken = cancelToken ?? CancellationToken(); + + await _executeWithWorkerPool( + items: files, + cancelToken: effectiveCancelToken, + processItem: (file, httpClient) async { + final fileId = p.hash(file.path).toString(); + + final result = await _uploadSingleFile( + file, + deviceAssetId: fileId, + httpClient: httpClient, + cancelToken: effectiveCancelToken, + onProgress: (bytes, totalBytes) => onProgress?.call(fileId, bytes, totalBytes), + ); + + if (result.isSuccess) { + onSuccess?.call(fileId); + } else if (!result.isCancelled && result.errorMessage != null) { + onError?.call(fileId, result.errorMessage!); + } + }, + ); + } + + void cancel() { + shouldAbortUpload = true; + } + + /// Generic worker pool for concurrent uploads + /// + /// [items] - List of items to process + /// [cancelToken] - Token to cancel the operation + /// [processItem] - Function to process each item with an HTTP client + /// [shouldSkip] - Optional function to skip items (e.g., WiFi requirement check) + /// [concurrentWorkers] - Number of concurrent workers (default: 3) + Future _executeWithWorkerPool({ + required List items, + required CancellationToken cancelToken, + required Future Function(T item, Client httpClient) processItem, + bool Function(T item)? shouldSkip, + int concurrentWorkers = 3, + }) async { + final httpClients = List.generate(concurrentWorkers, (_) => Client()); + + await _storageRepository.clearCache(); + shouldAbortUpload = false; + + try { + int currentIndex = 0; + + Future worker(Client httpClient) async { + while (true) { + if (shouldAbortUpload || cancelToken.isCancelled) { + break; + } + + final index = currentIndex; + if (index >= items.length) { + break; + } + currentIndex++; + + final item = items[index]; + + if (shouldSkip?.call(item) ?? false) { + continue; + } + + await processItem(item, httpClient); + } + } + + final workerFutures = >[]; + for (int i = 0; i < concurrentWorkers; i++) { + workerFutures.add(worker(httpClients[i])); + } + + await Future.wait(workerFutures); + } finally { + for (final client in httpClients) { + client.close(); + } + } + } + + Future _uploadSingleAsset( + LocalAsset asset, + Client httpClient, + CancellationToken cancelToken, { + required UploadCallbacks callbacks, + }) async { + File? file; + File? livePhotoFile; + + try { + final entity = await _storageRepository.getAssetEntityForAsset(asset); + if (entity == null) { + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); + return; + } + + final isAvailableLocally = await _storageRepository.isAssetAvailableLocally(asset.id); + + if (!isAvailableLocally && CurrentPlatform.isIOS) { + _logger.info("Loading iCloud asset ${asset.id} - ${asset.name}"); + + // Create progress handler for iCloud download + PMProgressHandler? progressHandler; + StreamSubscription? progressSubscription; + + progressHandler = PMProgressHandler(); + progressSubscription = progressHandler.stream.listen((event) { + callbacks.onICloudProgress?.call(asset.localId!, event.progress); + }); + + try { + file = await _storageRepository.loadFileFromCloud(asset.id, progressHandler: progressHandler); + if (entity.isLivePhoto) { + livePhotoFile = await _storageRepository.loadMotionFileFromCloud( + asset.id, + progressHandler: progressHandler, + ); + } + } finally { + await progressSubscription.cancel(); + } + } else { + // Get files locally + file = await _storageRepository.getFileForAsset(asset.id); + if (file == null) { + _logger.warning("Failed to get file ${asset.id} - ${asset.name}"); + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); + return; + } + + // For live photos, get the motion video file + if (entity.isLivePhoto) { + livePhotoFile = await _storageRepository.getMotionFileForAsset(asset); + if (livePhotoFile == null) { + _logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}"); + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); + } + } + } + + if (file == null) { + _logger.warning("Failed to obtain file from iCloud for asset ${asset.id} - ${asset.name}"); + callbacks.onError?.call(asset.localId!, "asset_not_found_on_icloud".t()); + return; + } + + String fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name; + + /// Handle special file name from DJI or Fusion app + /// If the file name has no extension, likely due to special renaming template by specific apps + /// we append the original extension from the asset name + final hasExtension = p.extension(fileName).isNotEmpty; + if (!hasExtension) { + fileName = p.setExtension(fileName, p.extension(asset.name)); + } + + final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName; + final deviceId = Store.get(StoreKey.deviceId); + + final headers = ApiService.getRequestHeaders(); + final fields = { + 'deviceAssetId': asset.localId!, + 'deviceId': deviceId, + 'fileCreatedAt': asset.createdAt.toUtc().toIso8601String(), + 'fileModifiedAt': asset.updatedAt.toUtc().toIso8601String(), + 'isFavorite': asset.isFavorite.toString(), + 'duration': asset.duration.toString(), + }; + + // Upload live photo video first if available + String? livePhotoVideoId; + if (entity.isLivePhoto && livePhotoFile != null) { + final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoFile.path)); + + final livePhotoResult = await _uploadRepository.uploadFile( + file: livePhotoFile, + originalFileName: livePhotoTitle, + headers: headers, + fields: fields, + httpClient: httpClient, + cancelToken: cancelToken, + onProgress: (bytes, totalBytes) => + callbacks.onProgress?.call(asset.localId!, livePhotoTitle, bytes, totalBytes), + logContext: 'livePhotoVideo[${asset.localId}]', + ); + + if (livePhotoResult.isSuccess && livePhotoResult.remoteAssetId != null) { + livePhotoVideoId = livePhotoResult.remoteAssetId; + } + } + + if (livePhotoVideoId != null) { + fields['livePhotoVideoId'] = livePhotoVideoId; + } + + // Add cloudId metadata only to the still image, not the motion video, becasue when the sync id happens, the motion video can get associated with the wrong still image. + if (CurrentPlatform.isIOS && asset.cloudId != null) { + fields['metadata'] = jsonEncode([ + RemoteAssetMetadataItem( + key: RemoteAssetMetadataKey.mobileApp, + value: RemoteAssetMobileAppMetadata( + cloudId: asset.cloudId, + createdAt: asset.createdAt.toIso8601String(), + adjustmentTime: asset.adjustmentTime?.toIso8601String(), + latitude: asset.latitude?.toString(), + longitude: asset.longitude?.toString(), + ), + ), + ]); + } + + final result = await _uploadRepository.uploadFile( + file: file, + originalFileName: originalFileName, + headers: headers, + fields: fields, + httpClient: httpClient, + cancelToken: cancelToken, + onProgress: (bytes, totalBytes) => + callbacks.onProgress?.call(asset.localId!, originalFileName, bytes, totalBytes), + logContext: 'asset[${asset.localId}]', + ); + + if (result.isSuccess && result.remoteAssetId != null) { + callbacks.onSuccess?.call(asset.localId!, result.remoteAssetId!); + } else if (result.isCancelled) { + _logger.warning(() => "Backup was cancelled by the user"); + shouldAbortUpload = true; + } else if (result.errorMessage != null) { + _logger.severe( + () => + "Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt} | ${result.errorMessage}", + ); + + callbacks.onError?.call(asset.localId!, result.errorMessage!); + + if (result.errorMessage == "Quota has been exceeded!") { + shouldAbortUpload = true; + } + } + } catch (error, stackTrace) { + _logger.severe(() => "Error backup asset: ${error.toString()}", stackTrace); + callbacks.onError?.call(asset.localId!, error.toString()); + } finally { + if (Platform.isIOS) { + try { + await file?.delete(); + await livePhotoFile?.delete(); + } catch (error, stackTrace) { + _logger.severe(() => "ERROR deleting file: ${error.toString()}", stackTrace); + } + } + } + } + + Future _uploadSingleFile( + File file, { + required String deviceAssetId, + required Client httpClient, + required CancellationToken cancelToken, + void Function(int bytes, int totalBytes)? onProgress, + }) async { + try { + final stats = await file.stat(); + final fileCreatedAt = stats.changed; + final fileModifiedAt = stats.modified; + final filename = p.basename(file.path); + + final headers = ApiService.getRequestHeaders(); + final deviceId = Store.get(StoreKey.deviceId); + + final fields = { + 'deviceAssetId': deviceAssetId, + 'deviceId': deviceId, + 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), + 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), + 'isFavorite': 'false', + 'duration': '0', + }; + + return await _uploadRepository.uploadFile( + file: file, + originalFileName: filename, + headers: headers, + fields: fields, + httpClient: httpClient, + cancelToken: cancelToken, + onProgress: onProgress ?? (_, __) {}, + logContext: 'shareIntent[$deviceAssetId]', + ); + } catch (e) { + return UploadResult.error(errorMessage: e.toString()); + } + } + + bool _shouldRequireWiFi(LocalAsset asset) { + bool requiresWiFi = true; + + if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { + requiresWiFi = false; + } else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) { + requiresWiFi = false; + } + + return requiresWiFi; + } +} diff --git a/mobile/lib/services/shared_link.service.dart b/mobile/lib/services/shared_link.service.dart index 25151c234f..46e83f0fc4 100644 --- a/mobile/lib/services/shared_link.service.dart +++ b/mobile/lib/services/shared_link.service.dart @@ -37,6 +37,7 @@ class SharedLinkService { required bool allowUpload, String? description, String? password, + String? slug, String? albumId, List? assetIds, DateTime? expiresAt, @@ -54,6 +55,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, ); } else if (assetIds != null) { dto = SharedLinkCreateDto( @@ -64,6 +66,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, assetIds: assetIds, ); } @@ -88,6 +91,7 @@ class SharedLinkService { bool? changeExpiry = false, String? description, String? password, + String? slug, DateTime? expiresAt, }) async { try { @@ -100,6 +104,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, changeExpiryTime: changeExpiry, ), ); diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 8e3773839c..3837d6337c 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -40,7 +40,7 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale fontWeight: FontWeight.w600, fontSize: 18, ), - backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, + backgroundColor: colorScheme.surface, foregroundColor: colorScheme.primary, elevation: 0, scrolledUnderElevation: 0, @@ -61,14 +61,21 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale ), ), chipTheme: const ChipThemeData(side: BorderSide.none), - sliderTheme: const SliderThemeData(thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), trackHeight: 2.0), + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, + // ignore: deprecated_member_use + year2023: false, + ), bottomNavigationBarTheme: const BottomNavigationBarThemeData(type: BottomNavigationBarType.fixed), popupMenuTheme: const PopupMenuThemeData( shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), ), navigationBarTheme: NavigationBarThemeData( backgroundColor: isDark ? colorScheme.surfaceContainer : colorScheme.surface, - labelTextStyle: const WidgetStatePropertyAll(TextStyle(fontSize: 14, fontWeight: FontWeight.w500)), + labelTextStyle: const WidgetStatePropertyAll( + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, overflow: TextOverflow.ellipsis), + ), ), inputDecorationTheme: InputDecorationTheme( focusedBorder: OutlineInputBorder( @@ -147,9 +154,9 @@ ImmichTheme decolorizeSurfaces({required ImmichTheme theme}) { } String? _getFontFamilyFromLocale(Locale locale) { - if (localesNotSupportedByOverpass.contains(locale)) { + if (localesNotSupportedByAppFont.contains(locale)) { // Let Flutter use the default font return null; } - return 'Overpass'; + return 'GoogleSans'; } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 1a2883bee7..2e26d8e80d 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -20,9 +20,11 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_ 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/remove_from_lock_folder_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.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/similar_photos_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; @@ -42,6 +44,7 @@ class ActionButtonContext { final bool isCasting; final TimelineOrigin timelineOrigin; final ThemeData? originalTheme; + final int selectedCount; const ActionButtonContext({ required this.asset, @@ -56,6 +59,7 @@ class ActionButtonContext { this.isCasting = false, this.timelineOrigin = TimelineOrigin.main, this.originalTheme, + this.selectedCount = 1, }); } @@ -65,7 +69,9 @@ enum ActionButtonType { share, shareLink, cast, + setAlbumCover, similarPhotos, + setProfilePicture, viewInTimeline, download, upload, @@ -134,6 +140,11 @@ enum ActionButtonType { context.isOwner && // !context.isInLockedView && // context.currentAlbum != null, + ActionButtonType.setAlbumCover => + context.isOwner && // + !context.isInLockedView && // + context.currentAlbum != null && // + context.selectedCount == 1, ActionButtonType.unstack => context.isOwner && // !context.isInLockedView && // @@ -146,6 +157,10 @@ enum ActionButtonType { ActionButtonType.similarPhotos => !context.isInLockedView && // context.asset is RemoteAsset, + ActionButtonType.setProfilePicture => + !context.isInLockedView && // + context.asset is RemoteAsset && // + context.isOwner, ActionButtonType.openInfo => true, ActionButtonType.viewInTimeline => context.timelineOrigin != TimelineOrigin.main && @@ -213,6 +228,12 @@ enum ActionButtonType { iconOnly: iconOnly, menuItem: menuItem, ), + ActionButtonType.setAlbumCover => SetAlbumCoverActionButton( + albumId: context.currentAlbum!.id, + source: context.source, + iconOnly: iconOnly, + menuItem: menuItem, + ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.similarPhotos => SimilarPhotosActionButton( @@ -220,12 +241,17 @@ enum ActionButtonType { iconOnly: iconOnly, menuItem: menuItem, ), + ActionButtonType.setProfilePicture => SetProfilePictureActionButton( + asset: context.asset, + iconOnly: iconOnly, + menuItem: menuItem, + ), ActionButtonType.openInfo => BaseActionButton( label: 'info'.tr(), iconData: Icons.info_outline, iconColor: context.originalTheme?.iconTheme.color, menuItem: true, - onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()), + onPressed: () => EventStream.shared.emit(const ViewerShowDetailsEvent()), ), ActionButtonType.viewInTimeline => BaseActionButton( label: 'view_in_timeline'.tr(), @@ -251,7 +277,7 @@ enum ActionButtonType { int get kebabMenuGroup => switch (this) { // 0: info ActionButtonType.openInfo => 0, - // 10: move,remove, and delete + // 10: move, remove, and delete ActionButtonType.trash => 10, ActionButtonType.deletePermanent => 10, ActionButtonType.removeFromLockFolder => 10, diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index f5c7513d1b..d63a92ba37 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -21,6 +21,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; @@ -28,15 +29,15 @@ import 'package:path_provider/path_provider.dart'; void configureFileDownloaderNotifications() { FileDownloader().configureNotificationForGroup( kDownloadGroupImage, - running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'), + running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), + complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), progressBar: true, ); FileDownloader().configureNotificationForGroup( kDownloadGroupVideo, - running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'), + running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), + complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), progressBar: true, ); @@ -106,5 +107,7 @@ abstract final class Bootstrap { storeRepository: storeRepo, shouldBuffer: shouldBufferLogs, ); + + await NetworkRepository.init(); } } diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart index 3a73e5b320..66de6493ab 100644 --- a/mobile/lib/utils/bytes_units.dart +++ b/mobile/lib/utils/bytes_units.dart @@ -19,7 +19,7 @@ String formatBytes(int bytes) { String formatHumanReadableBytes(int bytes, int decimals) { if (bytes <= 0) return "0 B"; - const suffixes = ["B", "KB", "MB", "GB", "TB"]; + const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"]; var i = (log(bytes) / log(1024)).floor(); return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}'; } diff --git a/mobile/lib/utils/cache/custom_image_cache.dart b/mobile/lib/utils/cache/custom_image_cache.dart index a3905baf9b..99ce0db57c 100644 --- a/mobile/lib/utils/cache/custom_image_cache.dart +++ b/mobile/lib/utils/cache/custom_image_cache.dart @@ -2,10 +2,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/presentation/widgets/images/thumb_hash_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'; -import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; /// [ImageCache] that uses two caches for small and large images /// so that a single large image does not evict all small images @@ -39,14 +35,9 @@ 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) { return switch (key) { - ImmichLocalImageProvider() || - ImmichRemoteImageProvider() || - LocalFullImageProvider() || - RemoteFullImageProvider() => _large, + LocalFullImageProvider() || RemoteFullImageProvider() => _large, ThumbHashProvider() => _thumbhash, _ => _small, }; diff --git a/mobile/lib/utils/http_ssl_options.dart b/mobile/lib/utils/http_ssl_options.dart index c4e2ad69f7..a93387c9db 100644 --- a/mobile/lib/utils/http_ssl_options.dart +++ b/mobile/lib/utils/http_ssl_options.dart @@ -1,26 +1,20 @@ import 'dart:io'; -import 'package:flutter/services.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; -import 'package:logging/logging.dart'; class HttpSSLOptions { - static const MethodChannel _channel = MethodChannel('immich/httpSSLOptions'); - - static void apply({bool applyNative = true}) { + static void apply() { AppSettingsEnum setting = AppSettingsEnum.allowSelfSignedSSLCert; bool allowSelfSignedSSLCert = Store.get(setting.storeKey as StoreKey, setting.defaultValue); - _apply(allowSelfSignedSSLCert, applyNative: applyNative); + return _apply(allowSelfSignedSSLCert); } - static void applyFromSettings(bool newValue) { - _apply(newValue); - } + static void applyFromSettings(bool newValue) => _apply(newValue); - static void _apply(bool allowSelfSignedSSLCert, {bool applyNative = true}) { + static void _apply(bool allowSelfSignedSSLCert) { String? serverHost; if (allowSelfSignedSSLCert && Store.tryGet(StoreKey.currentUser) != null) { serverHost = Uri.parse(Store.tryGet(StoreKey.serverEndpoint) ?? "").host; @@ -29,14 +23,5 @@ class HttpSSLOptions { SSLClientCertStoreVal? clientCert = SSLClientCertStoreVal.load(); HttpOverrides.global = HttpSSLCertOverride(allowSelfSignedSSLCert, serverHost, clientCert); - - if (applyNative && Platform.isAndroid) { - _channel - .invokeMethod("apply", [allowSelfSignedSSLCert, serverHost, clientCert?.data, clientCert?.password]) - .onError((e, _) { - final log = Logger("HttpSSLOptions"); - log.severe('Failed to set SSL options', e.message); - }); - } } } diff --git a/mobile/lib/utils/image_converter.dart b/mobile/lib/utils/image_converter.dart new file mode 100644 index 0000000000..6711e2bd56 --- /dev/null +++ b/mobile/lib/utils/image_converter.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +/// Converts a Flutter [Image] widget to a [Uint8List] in PNG format. +/// +/// This function resolves the image stream and converts it to byte data. +/// Returns a [Future] that completes with the image bytes or completes with an error +/// if the conversion fails. +Future imageToUint8List(Image image) async { + final Completer completer = Completer(); + image.image + .resolve(const ImageConfiguration()) + .addListener( + ImageStreamListener((ImageInfo info, bool _) { + info.image.toByteData(format: ImageByteFormat.png).then((byteData) { + if (byteData != null) { + completer.complete(byteData.buffer.asUint8List()); + } else { + completer.completeError('Failed to convert image to bytes'); + } + }); + }, onError: (exception, stackTrace) => completer.completeError(exception)), + ); + return completer.future; +} diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 21722cb901..079f0e51fa 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,4 +1,3 @@ -import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -10,14 +9,18 @@ String getThumbnailUrl(final Asset asset, {AssetMediaSize type = AssetMediaSize. } String getThumbnailCacheKey(final Asset asset, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - return getThumbnailCacheKeyForRemoteId(asset.remoteId!, type: type); + return getThumbnailCacheKeyForRemoteId(asset.remoteId!, asset.thumbhash!, type: type); } -String getThumbnailCacheKeyForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) { +String getThumbnailCacheKeyForRemoteId( + final String id, + final String thumbhash, { + AssetMediaSize type = AssetMediaSize.thumbnail, +}) { if (type == AssetMediaSize.thumbnail) { - return 'thumbnail-image-$id'; + return 'thumbnail-image-$id-$thumbhash'; } else { - return '${id}_previewStage'; + return '${id}_${thumbhash}_previewStage'; } } @@ -32,26 +35,27 @@ String getAlbumThumbNailCacheKey(final Album album, {AssetMediaSize type = Asset if (album.thumbnail.value?.remoteId == null) { return ''; } - return getThumbnailCacheKeyForRemoteId(album.thumbnail.value!.remoteId!, type: type); + return getThumbnailCacheKeyForRemoteId( + album.thumbnail.value!.remoteId!, + album.thumbnail.value!.thumbhash!, + type: type, + ); } -String getOriginalUrlForRemoteId(final String id) { - return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original'; +String getOriginalUrlForRemoteId(final String id, {bool edited = true}) { + return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original?edited=$edited'; } -String getImageCacheKey(final Asset asset) { - // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == noDbId; - return '${isFromDto ? asset.remoteId : asset.id}_fullStage'; +String getThumbnailUrlForRemoteId( + final String id, { + AssetMediaSize type = AssetMediaSize.thumbnail, + bool edited = true, + String? thumbhash, +}) { + final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}&edited=$edited'; + return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url; } -String getThumbnailUrlForRemoteId(final String id, {AssetMediaSize type = AssetMediaSize.thumbnail}) { - 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/isolate.dart b/mobile/lib/utils/isolate.dart index 491e1bf107..7ac120acb4 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -54,7 +54,7 @@ Cancelable runInIsolateGentle({ Logger log = Logger("IsolateLogger"); try { - HttpSSLOptions.apply(applyNative: false); + HttpSSLOptions.apply(); result = await computation(ref); } on CanceledError { log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 35cdc7addf..5a212e25ad 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; @@ -17,12 +18,15 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/platform/network_api.g.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -31,7 +35,7 @@ import 'package:isar/isar.dart'; // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; -const int targetVersion = 19; +const int targetVersion = 23; Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { final hasVersion = Store.tryGet(StoreKey.version) != null; @@ -86,6 +90,25 @@ Future migrateDatabaseIfNeeded(Isar db, Drift drift) async { } } + if (version < 20 && Store.isBetaTimelineEnabled) { + await _syncLocalAlbumIsIosSharedAlbum(drift); + } + + if (version < 21) { + final certData = SSLClientCertStoreVal.load(); + if (certData != null) { + await networkApi.addCertificate(ClientCertData(data: certData.data, password: certData.password ?? "")); + } + } + + if (version < 23 && Store.isBetaTimelineEnabled) { + await _populateLocalAssetPlaybackStyle(drift); + } + + if (version < 22 && !Store.isBetaTimelineEnabled) { + await Store.put(StoreKey.needBetaMigration, true); + } + if (targetVersion >= 12) { await Store.put(StoreKey.version, targetVersion); return; @@ -258,6 +281,25 @@ Future _populateLocalAssetTime(Drift db) async { } } +Future _syncLocalAlbumIsIosSharedAlbum(Drift db) async { + try { + final nativeApi = NativeSyncApi(); + final albums = await nativeApi.getAlbums(); + await db.batch((batch) { + for (final album in albums) { + batch.update( + db.localAlbumEntity, + LocalAlbumEntityCompanion(isIosSharedAlbum: Value(album.isCloud)), + where: (t) => t.id.equals(album.id), + ); + } + }); + dPrint(() => "[MIGRATION] Successfully updated isIosSharedAlbum for ${albums.length} albums"); + } catch (error) { + dPrint(() => "[MIGRATION] Error while syncing local album isIosSharedAlbum: $error"); + } +} + Future migrateDeviceAssetToSqlite(Isar db, Drift drift) async { try { final isarDeviceAssets = await db.deviceAssetEntitys.where().findAll(); @@ -356,6 +398,52 @@ Future migrateStoreToIsar(Isar db, Drift drift) async { } } +Future _populateLocalAssetPlaybackStyle(Drift db) async { + try { + final nativeApi = NativeSyncApi(); + + final albums = await nativeApi.getAlbums(); + for (final album in albums) { + final assets = await nativeApi.getAssetsForAlbum(album.id); + await db.batch((batch) { + for (final asset in assets) { + batch.update( + db.localAssetEntity, + LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), + where: (t) => t.id.equals(asset.id), + ); + } + }); + } + + final trashedAssetMap = await nativeApi.getTrashedAssets(); + for (final assets in trashedAssetMap.values) { + await db.batch((batch) { + for (final asset in assets) { + batch.update( + db.trashedLocalAssetEntity, + TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))), + where: (t) => t.id.equals(asset.id), + ); + } + }); + } + + dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets"); + } catch (error) { + dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error"); + } +} + +AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) { + PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown, + PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image, + PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video, + PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated, + PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto, + PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping, +}; + class _DeviceAsset { final String assetId; final List? hash; diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 0c1f03086f..090889ff32 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) { if (value is Map) { addDefault(value, 'visibility', 'timeline'); addDefault(value, 'createdAt', DateTime.now().toIso8601String()); + addDefault(value, 'isEdited', false); } break; case 'UserAdminResponseDto': @@ -46,6 +47,10 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); addDefault(value, 'hasProfileImage', false); } + case 'SyncAssetV1': + if (value is Map) { + addDefault(value, 'isEdited', false); + } case 'ServerFeaturesDto': if (value is Map) { addDefault(value, 'ocr', false); diff --git a/mobile/lib/utils/option.dart b/mobile/lib/utils/option.dart new file mode 100644 index 0000000000..3470e8489e --- /dev/null +++ b/mobile/lib/utils/option.dart @@ -0,0 +1,58 @@ +sealed class Option { + const Option(); + + const factory Option.some(T value) = Some; + + const factory Option.none() = None; + + factory Option.fromNullable(T? value) => value != null ? Some(value) : None(); + + @pragma('vm:prefer-inline') + bool get isSome => this is Some; + + @pragma('vm:prefer-inline') + bool get isNone => this is None; + + @pragma('vm:prefer-inline') + T? get unwrapOrNull => switch (this) { + Some(:final value) => value, + None() => null, + }; + + U fold(U Function(T value) onSome, U Function() onNone) => switch (this) { + Some(:final value) => onSome(value), + None() => onNone(), + }; + + @override + String toString() => switch (this) { + Some(:final value) => 'Some($value)', + None() => 'None', + }; +} + +final class Some extends Option { + final T value; + + const Some(this.value); + + @override + bool operator ==(Object other) => other is Some && other.value == value; + + @override + int get hashCode => value.hashCode; +} + +final class None extends Option { + const None(); + + @override + bool operator ==(Object other) => other is None; + + @override + int get hashCode => 0; +} + +extension ObjectOptionExtension on T? { + Option toOption() => Option.fromNullable(this); +} diff --git a/mobile/lib/utils/upload_speed_calculator.dart b/mobile/lib/utils/upload_speed_calculator.dart new file mode 100644 index 0000000000..a2153e6e3d --- /dev/null +++ b/mobile/lib/utils/upload_speed_calculator.dart @@ -0,0 +1,182 @@ +/// A class to calculate upload speed based on progress updates. +/// +/// Tracks bytes transferred over time and calculates average speed +/// using a sliding window approach to smooth out fluctuations. +class UploadSpeedCalculator { + /// Creates an UploadSpeedCalculator with the given window size. + /// + /// [windowSize] determines how many recent samples to use for + /// calculating the average speed. Default is 5 samples. + UploadSpeedCalculator({this.windowSize = 5}); + + /// The number of samples to keep in the sliding window. + final int windowSize; + + /// List of recent speed samples (bytes per second). + final List _speedSamples = []; + + /// The timestamp of the last progress update. + DateTime? _lastUpdateTime; + + /// The bytes transferred at the last progress update. + int _lastBytes = 0; + + /// The total file size being uploaded. + int _totalBytes = 0; + + /// Resets the calculator for a new upload. + void reset() { + _speedSamples.clear(); + _lastUpdateTime = null; + _lastBytes = 0; + _totalBytes = 0; + } + + /// Updates the calculator with the current progress. + /// + /// [currentBytes] is the number of bytes transferred so far. + /// [totalBytes] is the total size of the file being uploaded. + /// + /// Returns the calculated speed in MB/s, or -1 if not enough data. + double update(int currentBytes, int totalBytes) { + final now = DateTime.now(); + _totalBytes = totalBytes; + + if (_lastUpdateTime == null) { + _lastUpdateTime = now; + _lastBytes = currentBytes; + return -1; + } + + final elapsed = now.difference(_lastUpdateTime!); + + // Only calculate if at least 100ms has passed to avoid division by very small numbers + if (elapsed.inMilliseconds < 100) { + return _currentSpeed; + } + + final bytesTransferred = currentBytes - _lastBytes; + final elapsedSeconds = elapsed.inMilliseconds / 1000.0; + + // Calculate bytes per second, then convert to MB/s + final bytesPerSecond = bytesTransferred / elapsedSeconds; + final mbPerSecond = bytesPerSecond / (1024 * 1024); + + // Add to sliding window + _speedSamples.add(mbPerSecond); + if (_speedSamples.length > windowSize) { + _speedSamples.removeAt(0); + } + + _lastUpdateTime = now; + _lastBytes = currentBytes; + + return _currentSpeed; + } + + /// Returns the current calculated speed in MB/s. + /// + /// Returns -1 if no valid speed has been calculated yet. + double get _currentSpeed { + if (_speedSamples.isEmpty) { + return -1; + } + // Calculate average of all samples in the window + final sum = _speedSamples.fold(0.0, (prev, speed) => prev + speed); + return sum / _speedSamples.length; + } + + /// Returns the current speed in MB/s, or -1 if not available. + double get speed => _currentSpeed; + + /// Returns a human-readable string representation of the current speed. + /// + /// Returns '-- MB/s' if N/A, otherwise in MB/s or kB/s format. + String get speedAsString { + final s = _currentSpeed; + return switch (s) { + <= 0 => '-- MB/s', + >= 1 => '${s.round()} MB/s', + _ => '${(s * 1000).round()} kB/s', + }; + } + + /// Returns the estimated time remaining as a Duration. + /// + /// Returns Duration with negative seconds if not calculable. + Duration get timeRemaining { + final s = _currentSpeed; + if (s <= 0 || _totalBytes <= 0 || _lastBytes >= _totalBytes) { + return const Duration(seconds: -1); + } + + final remainingBytes = _totalBytes - _lastBytes; + final bytesPerSecond = s * 1024 * 1024; + final secondsRemaining = remainingBytes / bytesPerSecond; + + return Duration(seconds: secondsRemaining.round()); + } + + /// Returns a human-readable string representation of time remaining. + /// + /// Returns '--:--' if N/A, otherwise HH:MM:SS or MM:SS format. + String get timeRemainingAsString { + final remaining = timeRemaining; + return switch (remaining.inSeconds) { + <= 0 => '--:--', + < 3600 => + '${remaining.inMinutes.toString().padLeft(2, "0")}' + ':${remaining.inSeconds.remainder(60).toString().padLeft(2, "0")}', + _ => + '${remaining.inHours}' + ':${remaining.inMinutes.remainder(60).toString().padLeft(2, "0")}' + ':${remaining.inSeconds.remainder(60).toString().padLeft(2, "0")}', + }; + } +} + +/// Manager for tracking upload speeds for multiple concurrent uploads. +/// +/// Each upload is identified by a unique task ID. +class UploadSpeedManager { + /// Map of task IDs to their speed calculators. + final Map _calculators = {}; + + /// Gets or creates a speed calculator for the given task ID. + UploadSpeedCalculator getCalculator(String taskId) { + return _calculators.putIfAbsent(taskId, () => UploadSpeedCalculator()); + } + + /// Updates progress for a specific task and returns the speed string. + /// + /// [taskId] is the unique identifier for the upload task. + /// [currentBytes] is the number of bytes transferred so far. + /// [totalBytes] is the total size of the file being uploaded. + /// + /// Returns the human-readable speed string. + String updateProgress(String taskId, int currentBytes, int totalBytes) { + final calculator = getCalculator(taskId); + calculator.update(currentBytes, totalBytes); + return calculator.speedAsString; + } + + /// Gets the current speed string for a specific task. + String getSpeedAsString(String taskId) { + return _calculators[taskId]?.speedAsString ?? '-- MB/s'; + } + + /// Gets the time remaining string for a specific task. + String getTimeRemainingAsString(String taskId) { + return _calculators[taskId]?.timeRemainingAsString ?? '--:--'; + } + + /// Removes a task from tracking. + void removeTask(String taskId) { + _calculators.remove(taskId); + } + + /// Clears all tracked tasks. + void clear() { + _calculators.clear(); + } +} diff --git a/mobile/lib/widgets/activities/activity_text_field.dart b/mobile/lib/widgets/activities/activity_text_field.dart index a61a284844..d21cdfbc94 100644 --- a/mobile/lib/widgets/activities/activity_text_field.dart +++ b/mobile/lib/widgets/activities/activity_text_field.dart @@ -63,7 +63,7 @@ class ActivityTextField extends HookConsumerWidget { prefixIcon: user != null ? Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: UserCircleAvatar(user: user, size: 30, radius: 15), + child: UserCircleAvatar(user: user, size: 30), ) : null, suffixIcon: Padding( diff --git a/mobile/lib/widgets/activities/activity_tile.dart b/mobile/lib/widgets/activities/activity_tile.dart index 76c0b7bf2a..ac3b6c95a4 100644 --- a/mobile/lib/widgets/activities/activity_tile.dart +++ b/mobile/lib/widgets/activities/activity_tile.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -40,7 +40,7 @@ class ActivityTile extends HookConsumerWidget { child: Icon(Icons.thumb_up, color: context.primaryColor), ) : isBottomSheet - ? UserCircleAvatar(user: activity.user, size: 30, radius: 15) + ? UserCircleAvatar(user: activity.user, size: 30) : UserCircleAvatar(user: activity.user), title: _ActivityTitle( userName: activity.user.name, @@ -102,7 +102,7 @@ class _ActivityAssetThumbnail extends StatelessWidget { decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)), image: DecorationImage( - image: ImmichRemoteThumbnailProvider(assetId: assetId), + image: RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: ""), fit: BoxFit.cover, ), ), diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 3dd46cd92a..401e4b8e99 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -4,9 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; @@ -41,7 +41,7 @@ class CommentBubble extends ConsumerWidget { // avatar (hidden for own messages) Widget avatar = const SizedBox.shrink(); if (!isOwn) { - avatar = UserCircleAvatar(user: activity.user, size: 28, radius: 14); + avatar = UserCircleAvatar(user: activity.user, size: 28); } // Thumbnail with tappable behavior and optional heart overlay @@ -56,7 +56,7 @@ class CommentBubble extends ConsumerWidget { child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(10)), child: Image( - image: ImmichRemoteThumbnailProvider(assetId: activity.assetId!), + image: RemoteImageProvider.thumbnail(assetId: activity.assetId!, thumbhash: ""), fit: BoxFit.cover, ), ), diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index 423410eedf..386084b034 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -1,12 +1,12 @@ import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; @@ -32,15 +32,12 @@ class AlbumThumbnailListTile extends StatelessWidget { } buildAlbumThumbnail() { - return CachedNetworkImage( + return SizedBox( width: cardSize, height: cardSize, - fit: BoxFit.cover, - fadeInDuration: const Duration(milliseconds: 200), - imageUrl: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail), - httpHeaders: ApiService.getRequestHeaders(), - cacheKey: getAlbumThumbNailCacheKey(album, type: AssetMediaSize.thumbnail), - errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), + child: Thumbnail( + imageProvider: RemoteImageProvider(url: getAlbumThumbnailUrl(album, type: AssetMediaSize.thumbnail)), + ), ); } diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index 8913e94136..2025fa7583 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -33,7 +33,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { itemBuilder: ((context, index) { return Padding( padding: const EdgeInsets.only(right: 4.0), - child: UserCircleAvatar(user: sharedUsers[index], radius: 18, size: 36, hasBorder: true), + child: UserCircleAvatar(user: sharedUsers[index], size: 36, hasBorder: true), ); }), itemCount: sharedUsers.length, diff --git a/mobile/lib/widgets/asset_grid/permanent_delete_dialog.dart b/mobile/lib/widgets/asset_grid/permanent_delete_dialog.dart new file mode 100644 index 0000000000..18265b8d46 --- /dev/null +++ b/mobile/lib/widgets/asset_grid/permanent_delete_dialog.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class PermanentDeleteDialog extends StatelessWidget { + const PermanentDeleteDialog({super.key, required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), + title: Text(context.t.permanently_delete), + content: ImmichFormattedText(context.t.permanently_delete_assets_prompt(count: count)), + actions: [ + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: () => context.pop(false), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.surfaceDim, + foregroundColor: context.primaryColor, + ), + child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + + child: FilledButton( + onPressed: () => context.pop(true), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.errorContainer, + foregroundColor: context.colorScheme.onErrorContainer, + ), + child: Text(context.t.delete, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart index faa058ced4..1a3ef3eac3 100644 --- a/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart +++ b/mobile/lib/widgets/asset_viewer/advanced_bottom_sheet.dart @@ -58,7 +58,7 @@ class AdvancedBottomSheet extends HookConsumerWidget { style: const TextStyle( fontSize: 12.0, fontWeight: FontWeight.bold, - fontFamily: "Inconsolata", + fontFamily: "GoogleSansCode", ), showCursor: true, ), diff --git a/mobile/lib/widgets/asset_viewer/center_play_button.dart b/mobile/lib/widgets/asset_viewer/center_play_button.dart index 26d0a41129..55d8be8095 100644 --- a/mobile/lib/widgets/asset_viewer/center_play_button.dart +++ b/mobile/lib/widgets/asset_viewer/center_play_button.dart @@ -21,23 +21,20 @@ class CenterPlayButton extends StatelessWidget { @override Widget build(BuildContext context) { - return ColoredBox( - color: Colors.transparent, - child: Center( - child: UnconstrainedBox( - child: AnimatedOpacity( - opacity: show ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: DecoratedBox( - decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), - child: IconButton( - iconSize: 32, - padding: const EdgeInsets.all(12.0), - icon: isFinished - ? Icon(Icons.replay, color: iconColor) - : AnimatedPlayPause(color: iconColor, playing: isPlaying), - onPressed: onPressed, - ), + return Center( + child: UnconstrainedBox( + child: AnimatedOpacity( + opacity: show ? 1.0 : 0.0, + duration: const Duration(milliseconds: 100), + child: DecoratedBox( + decoration: BoxDecoration(color: backgroundColor, shape: BoxShape.circle), + child: IconButton( + iconSize: 32, + padding: const EdgeInsets.all(12.0), + icon: isFinished + ? Icon(Icons.replay, color: iconColor) + : AnimatedPlayPause(color: iconColor, playing: isPlaying), + onPressed: onPressed, ), ), ), diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart index 7ad290c152..6edf226e8b 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart @@ -74,7 +74,7 @@ class AssetLocation extends HookConsumerWidget { ], ), asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), - ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId), + ExifMap(exifInfo: exifInfo!, markerId: asset.remoteId, markerAssetThumbhash: asset.thumbhash), const SizedBox(height: 16), getLocationName(), Text( 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 893e534084..f48ee06fdd 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -10,10 +10,20 @@ import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; + // TODO: Pass in a BaseAsset instead of the ID and thumbhash when removing old timeline + // This is currently structured this way because of the old timeline implementation + // reusing this component final String? markerId; + final String? markerAssetThumbhash; final MapCreatedCallback? onMapCreated; - const ExifMap({super.key, required this.exifInfo, this.markerId = 'marker', this.onMapCreated}); + const ExifMap({ + super.key, + required this.exifInfo, + this.markerAssetThumbhash, + this.markerId = 'marker', + this.onMapCreated, + }); @override Widget build(BuildContext context) { @@ -61,6 +71,7 @@ class ExifMap extends StatelessWidget { width: constraints.maxWidth, zoom: 12.0, assetMarkerRemoteId: markerId, + assetThumbhash: markerAssetThumbhash, onTap: (tapPosition, latLong) async { Uri? uri = await createCoordinatesUri(); diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 2ef7e24cd7..7911679577 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -53,6 +53,7 @@ class BackupInfoCard extends StatelessWidget { info, style: context.textTheme.titleLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255), + fontFeatures: const [FontFeature.tabularFigures()], ), ), if (isLoading) diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index 596e46d934..84128ddde2 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -4,6 +4,7 @@ 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/extensions/theme_extensions.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'; @@ -41,6 +42,13 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { return Icon(Icons.circle, color: context.colorScheme.surfaceContainerHighest); } + Widget buildSubtitle() { + return Text( + album.isIosSharedAlbum ? '${album.assetCount} (iCloud Shared Album)' : album.assetCount.toString(), + style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ); + } + return GestureDetector( onDoubleTap: () { ref.watch(hapticFeedbackProvider.notifier).selectionClick(); @@ -73,8 +81,8 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { } }, leading: buildIcon(), - title: Text(album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - subtitle: Text(album.assetCount.toString()), + title: Text(album.name, style: context.textTheme.titleSmall), + subtitle: buildSubtitle(), trailing: IconButton( onPressed: () { context.pushRoute(LocalTimelineRoute(album: album)); diff --git a/mobile/lib/widgets/backup/upload_progress_bar.dart b/mobile/lib/widgets/backup/upload_progress_bar.dart index 65ff6c758a..641ed14878 100644 --- a/mobile/lib/widgets/backup/upload_progress_bar.dart +++ b/mobile/lib/widgets/backup/upload_progress_bar.dart @@ -36,7 +36,7 @@ class BackupUploadProgressBar extends ConsumerWidget { ), Text( " ${uploadProgress.toStringAsFixed(0)}%", - style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"), + style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"), ), ], ), diff --git a/mobile/lib/widgets/backup/upload_stats.dart b/mobile/lib/widgets/backup/upload_stats.dart index c9b626c51c..38f99e53fc 100644 --- a/mobile/lib/widgets/backup/upload_stats.dart +++ b/mobile/lib/widgets/backup/upload_stats.dart @@ -26,10 +26,10 @@ class BackupUploadStats extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono")), + Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")), Text( _formatUploadFileSpeed(uploadFileSpeed), - style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"), + style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"), ), ], ), 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 53fc32ddb3..c330fb4649 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 @@ -16,6 +16,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart'; @@ -51,7 +52,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: Stack( alignment: Alignment.centerLeft, children: [ - IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close, size: 20)), + IconButton( + onPressed: () => context.pop(), + icon: Icon(Icons.close, size: 20, color: context.colorScheme.onSurfaceVariant), + ), Align( alignment: Alignment.center, child: Padding( @@ -87,6 +91,14 @@ class ImmichAppBarDialog extends HookConsumerWidget { return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); } + buildFreeUpSpaceButton() { + return buildActionButton( + Icons.cleaning_services_outlined, + "free_up_space", + () => context.pushRoute(SettingsSubRoute(section: SettingSection.freeUpSpace)), + ); + } + buildAppLogButton() { return buildActionButton( Icons.assignment_outlined, @@ -144,42 +156,23 @@ class ImmichAppBarDialog extends HookConsumerWidget { percentage = user.quotaUsageInBytes / user.quotaSizeInBytes; } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 4), - decoration: BoxDecoration(color: context.colorScheme.surface), - child: ListTile( - minLeadingWidth: 50, - leading: Icon(Icons.storage_rounded, color: theme.primaryColor), - title: Text( - "backup_controller_page_server_storage", - style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w500), - ).tr(), - isThreeLine: true, - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: LinearProgressIndicator( - minHeight: 10.0, - value: percentage, - 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(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), - ), - ], - ), + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + Text("backup_controller_page_server_storage".tr(), style: context.textTheme.labelLarge), + LinearProgressIndicator( + minHeight: 10.0, + value: percentage, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), - ), + Text( + 'backup_controller_page_storage_format', + style: context.textTheme.bodySmall, + ).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), + ], ), ); } @@ -266,11 +259,25 @@ class ImmichAppBarDialog extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Container(padding: const EdgeInsets.symmetric(horizontal: 8), child: buildTopRow()), - const AppBarProfileInfoBox(), - buildStorageInformation(), - const AppBarServerInfo(), + Container( + decoration: BoxDecoration( + color: context.colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 8), + child: Column( + children: [ + const AppBarProfileInfoBox(), + Divider(thickness: 4, color: context.colorScheme.surfaceContainer), + buildStorageInformation(), + Divider(thickness: 4, color: context.colorScheme.surfaceContainer), + const AppBarServerInfo(), + ], + ), + ), if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), + buildFreeUpSpaceButton(), buildSettingButton(), buildSignOutButton(), buildFooter(), 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 bc1d608b10..a9fdb9a43f 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 @@ -34,7 +34,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - final userImage = UserCircleAvatar(radius: 22, size: 44, user: user); + final userImage = UserCircleAvatar(size: 44, user: user, hasBorder: true); if (uploadProfileImageStatus == UploadProfileStatus.loading) { return const SizedBox(height: 40, width: 40, child: ImmichLoadingIndicator(borderRadius: 20)); @@ -80,50 +80,40 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.colorScheme.surface, - borderRadius: const BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)), - ), - child: ListTile( - minLeadingWidth: 50, - leading: GestureDetector( - onTap: pickUserProfileImage, - onLongPress: toggleReadonlyMode, - child: Stack( - clipBehavior: Clip.none, - children: [ - AbsorbPointer(child: buildUserProfileImage()), - if (!isReadonlyModeEnabled) - Positioned( - bottom: -5, - right: -8, - child: Material( - color: context.colorScheme.surfaceContainerHighest, - elevation: 3, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), - child: Padding( - padding: const EdgeInsets.all(5.0), - child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), - ), - ), + return ListTile( + minLeadingWidth: 50, + leading: GestureDetector( + onTap: pickUserProfileImage, + onLongPress: toggleReadonlyMode, + child: Stack( + clipBehavior: Clip.none, + children: [ + AbsorbPointer(child: buildUserProfileImage()), + if (!isReadonlyModeEnabled) + Positioned( + bottom: -5, + right: -8, + child: Material( + color: context.colorScheme.surfaceContainerHighest, + elevation: 3, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(50.0))), + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Icon(Icons.camera_alt_outlined, color: context.primaryColor, size: 14), ), - ], - ), - ), - title: Text( - authState.name, - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500), - ), - subtitle: Text( - authState.userEmail, - style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ), + ), + ), + ], ), ), + title: Text( + authState.name, + style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor, fontWeight: FontWeight.w500), + ), + subtitle: Text( + authState.userEmail, + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), ); } } 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 a83a3beee3..2809505c58 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 @@ -23,8 +23,6 @@ class AppBarServerInfo extends HookConsumerWidget { final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user)); final appInfo = useState({}); - const titleFontSize = 12.0; - const contentFontSize = 11.0; getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -37,187 +35,103 @@ class AppBarServerInfo extends HookConsumerWidget { return null; }, []); + const divider = Divider(thickness: 1); + return Padding( - padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.surface, - borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (showVersionWarning) ...[ - const Padding(padding: EdgeInsets.symmetric(horizontal: 8.0), child: ServerUpdateNotification()), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - ], - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_info_box_app_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - serverInfoState.serverVersion.major > 0 - ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" - : "--", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Text( - "server_info_box_server_url".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - Expanded( - flex: 0, - child: Container( - width: 200, - padding: const EdgeInsets.only(right: 10.0), - child: Tooltip( - verticalOffset: 0, - decoration: BoxDecoration( - color: context.primaryColor.withValues(alpha: 0.9), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - textStyle: TextStyle( - color: context.isDarkTheme ? Colors.black : Colors.white, - fontWeight: FontWeight.bold, - ), - message: getServerUrl() ?? '--', - preferBelow: false, - triggerMode: TooltipTriggerMode.tap, - child: Text( - getServerUrl() ?? '--', - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - textAlign: TextAlign.end, - ), - ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.symmetric(horizontal: 10), child: Divider(thickness: 1)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10.0), - child: Row( - children: [ - if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) - const Padding( - padding: EdgeInsets.only(right: 5.0), - child: Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12), - ), - Text( - "latest_version".tr(), - style: TextStyle( - fontSize: titleFontSize, - color: context.textTheme.labelSmall?.color, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text( - serverInfoState.latestVersion.major > 0 - ? "${serverInfoState.latestVersion.major}.${serverInfoState.latestVersion.minor}.${serverInfoState.latestVersion.patch}" - : "--", - style: TextStyle( - fontSize: contentFontSize, - color: context.colorScheme.onSurfaceSecondary, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (showVersionWarning) ...[const ServerUpdateNotification(), divider], + _ServerInfoItem( + label: "server_info_box_app_version".tr(), + text: "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", ), - ), + divider, + _ServerInfoItem( + label: "server_version".tr(), + text: serverInfoState.serverVersion.major > 0 + ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" + : "--", + ), + divider, + _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), + if (serverInfoState.latestVersion != null) ...[ + divider, + _ServerInfoItem( + label: "latest_version".tr(), + text: serverInfoState.latestVersion!.major > 0 + ? "${serverInfoState.latestVersion!.major}.${serverInfoState.latestVersion!.minor}.${serverInfoState.latestVersion!.patch}" + : "--", + tooltip: true, + icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate + ? const Icon(Icons.info, color: Color.fromARGB(255, 243, 188, 106), size: 12) + : null, + ), + ], + ], ), ); } } + +class _ServerInfoItem extends StatelessWidget { + final String label; + final String text; + final bool tooltip; + final Icon? icon; + + static const titleFontSize = 12.0; + static const contentFontSize = 11.0; + + const _ServerInfoItem({required this.label, required this.text, this.tooltip = false, this.icon}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (icon != null) ...[icon as Widget, const SizedBox(width: 8)], + Text( + label, + style: TextStyle( + fontSize: titleFontSize, + color: context.textTheme.labelSmall?.color, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _maybeTooltip( + context, + Text( + text, + style: TextStyle( + fontSize: contentFontSize, + color: context.colorScheme.onSurfaceSecondary, + fontWeight: FontWeight.w500, + overflow: TextOverflow.ellipsis, + ), + textAlign: TextAlign.end, + ), + ), + ), + ], + ); + } + + Widget _maybeTooltip(BuildContext context, Widget child) => tooltip + ? Tooltip( + verticalOffset: 0, + decoration: BoxDecoration( + color: context.primaryColor.withValues(alpha: 0.9), + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + textStyle: TextStyle(color: context.colorScheme.onPrimary, fontWeight: FontWeight.bold), + message: text, + preferBelow: false, + triggerMode: TooltipTriggerMode.tap, + child: child, + ) + : child; +} diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index b3dc04236c..56b7e91eec 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -1,6 +1,5 @@ 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'; @@ -51,7 +50,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: UserCircleAvatar(radius: 17, size: 31, user: user), + child: UserCircleAvatar(size: 32, user: user), ), ), ); @@ -153,11 +152,6 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { actions: [ if (actions != null) ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), - if (kDebugMode || kProfileMode) - IconButton( - icon: const Icon(Icons.palette_rounded), - onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), - ), if (isCasting) Padding( padding: const EdgeInsets.only(right: 12), diff --git a/mobile/lib/widgets/common/immich_image.dart b/mobile/lib/widgets/common/immich_image.dart index c8bc9c1f6a..141a2ac7d4 100644 --- a/mobile/lib/widgets/common/immich_image.dart +++ b/mobile/lib/widgets/common/immich_image.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/image/immich_local_image_provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:octo_image/octo_image.dart'; @@ -34,13 +35,21 @@ class ImmichImage extends StatelessWidget { } if (asset == null) { - return ImmichRemoteImageProvider(assetId: assetId!); + return RemoteFullImageProvider(assetId: assetId!, thumbhash: '', assetType: base_asset.AssetType.video); } if (useLocal(asset)) { - return ImmichLocalImageProvider(asset: asset, width: width, height: height); + return LocalFullImageProvider( + id: asset.localId!, + assetType: base_asset.AssetType.video, + size: Size(width, height), + ); } else { - return ImmichRemoteImageProvider(assetId: asset.remoteId!); + return RemoteFullImageProvider( + assetId: asset.remoteId!, + thumbhash: asset.thumbhash ?? '', + assetType: base_asset.AssetType.video, + ); } } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index dd985ebfe2..cb429c9f48 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; @@ -46,41 +48,37 @@ class ImmichSliverAppBar extends ConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); 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 && !isReadonlyModeEnabled) - Padding( - padding: const EdgeInsets.only(right: 12), - child: IconButton( - onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); - }, + return SliverIgnorePointer( + ignoring: isMultiSelectEnabled, + sliver: SliverAnimatedOpacity( + duration: Durations.medium1, + opacity: isMultiSelectEnabled ? 0 : 1, + sliver: SliverAppBar( + backgroundColor: context.colorScheme.surface, + surfaceTintColor: context.colorScheme.surfaceTint, + elevation: 0, + scrolledUnderElevation: 1.0, + floating: floating, + pinned: pinned, + snap: snap, + expandedHeight: expandedHeight, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(5))), + automaticallyImplyLeading: false, + centerTitle: false, + title: title ?? const _ImmichLogoWithText(), + actions: [ + const _SyncStatusIndicator(), + if (isCasting && !isReadonlyModeEnabled) + 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) && !isReadonlyModeEnabled) - IconButton( - icon: const Icon(Icons.palette_rounded), - onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()), - ), - if (showUploadButton && !isReadonlyModeEnabled) - const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), - const Padding(padding: EdgeInsets.only(right: 20), child: _ProfileIndicator()), - ], + if (actions != null) ...actions!, + if (showUploadButton && !isReadonlyModeEnabled) const _BackupIndicator(), + const _ProfileIndicator(), + const SizedBox(width: 8), + ], + ), ), ); } @@ -90,27 +88,14 @@ class _ImmichLogoWithText extends StatelessWidget { const _ImmichLogoWithText(); @override - Widget build(BuildContext context) { - return Builder( - builder: (BuildContext context) { - return Row( - children: [ - Builder( - builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 3.0), - child: SvgPicture.asset( - context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', - height: 40, - ), - ); - }, - ), - ], - ); - }, - ); - } + Widget build(BuildContext context) => AnimatedOpacity( + opacity: IconTheme.of(context).opacity ?? 1, + duration: kThemeChangeDuration, + child: SvgPicture.asset( + context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg', + height: 40, + ), + ); } class _ProfileIndicator extends ConsumerWidget { @@ -122,7 +107,10 @@ class _ProfileIndicator extends ConsumerWidget { final bool versionWarningPresent = ref.watch(versionWarningPresentProvider(user)); final serverInfoState = ref.watch(serverInfoProvider); - const widgetSize = 30.0; + const widgetSize = 32.0; + + // TODO: remove this when update Flutter version newer than 3.35.7 + final isIpad = defaultTargetPlatform == TargetPlatform.iOS && !context.isMobile; void toggleReadonlyMode() { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); @@ -139,22 +127,23 @@ class _ProfileIndicator extends ConsumerWidget { ); } - return InkWell( - onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), + return IconButton( + onPressed: () => showDialog( + context: context, + useRootNavigator: false, + barrierDismissible: !isIpad, + builder: (ctx) => const ImmichAppBarDialog(), + ), onLongPress: () => toggleReadonlyMode(), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( - label: Container( - decoration: BoxDecoration( - color: context.isDarkTheme ? Colors.black : Colors.white, - borderRadius: BorderRadius.circular(widgetSize / 2), - ), - child: Icon( + icon: Badge( + label: _BadgeLabel( + Icon( Icons.info, color: serverInfoState.versionStatus == VersionStatus.error ? context.colorScheme.error : context.primaryColor, size: widgetSize / 2, + semanticLabel: 'new_version_available'.tr(), ), ), backgroundColor: Colors.transparent, @@ -165,7 +154,16 @@ class _ProfileIndicator extends ConsumerWidget { ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( label: "logged_in_as".tr(namedArgs: {"user": user.name}), - child: AbsorbPointer(child: UserCircleAvatar(radius: 17, size: 31, user: user)), + child: AbsorbPointer( + child: Builder( + builder: (context) => UserCircleAvatar( + size: 34, + user: user, + opacity: IconTheme.of(context).opacity ?? 1, + hasBorder: true, + ), + ), + ), ), ), ); @@ -181,10 +179,9 @@ class _BackupIndicator extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final indicatorIcon = _getBackupBadgeIcon(context, ref); - return InkWell( - onTap: () => context.pushRoute(const DriftBackupRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Badge( + return IconButton( + onPressed: () => context.pushRoute(const DriftBackupRoute()), + icon: Badge( label: indicatorIcon, backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, @@ -266,12 +263,14 @@ class _BadgeLabel extends StatelessWidget { @override Widget build(BuildContext context) { + final opacity = IconTheme.of(context).opacity ?? 1; + return Container( width: _kBadgeWidgetSize / 2, height: _kBadgeWidgetSize / 2, decoration: BoxDecoration( - color: backgroundColor ?? context.colorScheme.surfaceContainer, - border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)), + color: (backgroundColor ?? context.colorScheme.surfaceContainer).withValues(alpha: opacity), + border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3 * opacity)), borderRadius: BorderRadius.circular(_kBadgeWidgetSize / 2), ), child: indicator, @@ -334,23 +333,30 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with 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), - ), - ), - ), - ); - }, + return Padding( + padding: const EdgeInsets.all(8), + child: TweenAnimationBuilder( + tween: Tween(end: IconTheme.of(context).opacity ?? 1), + duration: kThemeChangeDuration, + builder: (context, opacity, child) { + return AnimatedBuilder( + animation: Listenable.merge([_rotationAnimation, _dismissalAnimation]), + builder: (context, child) { + final dismissalValue = isSyncing ? 1.0 : _dismissalAnimation.value; + return IconTheme( + data: IconTheme.of(context).copyWith(opacity: opacity * dismissalValue), + child: Transform( + alignment: Alignment.center, + transform: Matrix4.identity() + ..scaleByDouble(dismissalValue, dismissalValue, dismissalValue, 1.0) + ..rotateZ(-_rotationAnimation.value * 2 * math.pi), + child: const Icon(Icons.sync), + ), + ); + }, + ); + }, + ), ); } } diff --git a/mobile/lib/widgets/common/immich_thumbnail.dart b/mobile/lib/widgets/common/immich_thumbnail.dart index 612a6a4bd0..f17353c3aa 100644 --- a/mobile/lib/widgets/common/immich_thumbnail.dart +++ b/mobile/lib/widgets/common/immich_thumbnail.dart @@ -2,15 +2,15 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart'; -import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/utils/hooks/blurhash_hook.dart'; import 'package:immich_mobile/utils/thumbnail_utils.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart'; import 'package:octo_image/octo_image.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as base_asset; class ImmichThumbnail extends HookConsumerWidget { const ImmichThumbnail({this.asset, this.width = 250, this.height = 250, this.fit = BoxFit.cover, super.key}); @@ -24,26 +24,29 @@ class ImmichThumbnail extends HookConsumerWidget { /// either by using the asset ID or the asset itself /// [asset] is the Asset to request, or else use [assetId] to get a remote /// image provider - static ImageProvider imageProvider({Asset? asset, String? assetId, String? userId, int thumbnailSize = 256}) { + static ImageProvider imageProvider({Asset? asset, String? assetId, int thumbnailSize = 256}) { if (asset == null && assetId == null) { throw Exception('Must supply either asset or assetId'); } if (asset == null) { - return ImmichRemoteThumbnailProvider(assetId: assetId!); + return RemoteImageProvider.thumbnail(assetId: assetId!, thumbhash: ""); } if (ImmichImage.useLocal(asset)) { - return ImmichLocalThumbnailProvider(asset: asset, height: thumbnailSize, width: thumbnailSize, userId: userId); + return LocalThumbProvider( + id: asset.localId!, + assetType: base_asset.AssetType.video, + size: Size(thumbnailSize.toDouble(), thumbnailSize.toDouble()), + ); } else { - return ImmichRemoteThumbnailProvider(assetId: asset.remoteId!, height: thumbnailSize, width: thumbnailSize); + return RemoteImageProvider.thumbnail(assetId: asset.remoteId!, thumbhash: asset.thumbhash ?? ""); } } @override Widget build(BuildContext context, WidgetRef ref) { Uint8List? blurhash = useBlurHashRef(asset).value; - final userId = ref.watch(currentUserProvider)?.id; if (asset == null) { return Container( @@ -56,7 +59,7 @@ class ImmichThumbnail extends HookConsumerWidget { final assetAltText = getAltText(asset!.exifInfo, asset!.fileCreatedAt, asset!.type, []); - final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset, userId: userId); + final thumbnailProviderInstance = ImmichThumbnail.imageProvider(asset: asset); customErrorBuilder(BuildContext ctx, Object error, StackTrace? stackTrace) { thumbnailProviderInstance.evict(); diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index d5a7ea7cd9..a2a9d1bdbd 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -14,8 +14,8 @@ 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/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -230,10 +230,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S elevation: 3, child: CircleAvatar( maxRadius: 84 / 2, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(widget.person.id), - headers: ApiService.getRequestHeaders(), - ), + backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(widget.person.id)), ), ), ), diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index c486d473b0..50746f5cbd 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -24,15 +24,13 @@ class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget { const RemoteAlbumSliverAppBar({ super.key, this.icon = Icons.camera, - this.onShowOptions, - this.onToggleAlbumOrder, + required this.kebabMenu, this.onEditTitle, this.onActivity, }); final IconData icon; - final void Function()? onShowOptions; - final void Function()? onToggleAlbumOrder; + final Widget kebabMenu; final void Function()? onEditTitle; final void Function()? onActivity; @@ -91,21 +89,12 @@ class _MesmerizingSliverAppBarState extends ConsumerState context.maybePop(), ), actions: [ - if (widget.onToggleAlbumOrder != null) - IconButton( - icon: Icon(Icons.swap_vert_rounded, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onToggleAlbumOrder, - ), if (currentAlbum.isActivityEnabled && currentAlbum.isShared) IconButton( icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows), onPressed: widget.onActivity, ), - if (widget.onShowOptions != null) - IconButton( - icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows), - onPressed: widget.onShowOptions, - ), + widget.kebabMenu, ], title: Builder( builder: (context) { @@ -265,22 +254,9 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ), 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)], - ), - ), - ), + child: LayoutBuilder( + builder: (context, constraints) => + _DynamicText(text: currentAlbum.name, maxWidth: constraints.maxWidth), ), ), if (currentAlbum.description.isNotEmpty) @@ -560,3 +536,46 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic ); } } + +class _DynamicText extends StatelessWidget { + final String text; + final double maxWidth; + + const _DynamicText({required this.text, required this.maxWidth}); + + static const _baseTextStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + shadows: [Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black54)], + overflow: TextOverflow.ellipsis, + ); + + int _lineCount(double fontSize) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: _baseTextStyle.copyWith(fontSize: fontSize), + ), + maxLines: 3, + textDirection: TextDirection.ltr, + )..layout(maxWidth: maxWidth); + return textPainter.computeLineMetrics().length; + } + + double _fontSize() { + final fontSizes = [44.0, 36.0]; + for (final fontSize in fontSizes) { + final lineCount = _lineCount(fontSize); + if (lineCount == 1) { + return fontSize; + } + } + return 28; + } + + @override + Widget build(BuildContext context) { + return Text(text, style: _baseTextStyle.copyWith(fontSize: _fontSize()), maxLines: 3); + } +} diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart new file mode 100644 index 0000000000..0ab25d14cb --- /dev/null +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/tag.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/tag.provider.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; + +class TagPicker extends HookConsumerWidget { + const TagPicker({super.key, required this.onSelect, required this.filter}); + + final Function(Iterable) onSelect; + final Set filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formFocus = useFocusNode(); + final searchQuery = useState(''); + final tags = ref.watch(tagProvider); + final selectedTagIds = useState>(filter); + final borderRadius = const BorderRadius.all(Radius.circular(10)); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SearchField( + focusNode: formFocus, + onChanged: (value) => searchQuery.value = value, + onTapOutside: (_) => formFocus.unfocus(), + filled: true, + hintText: 'filter_tags'.tr(), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), + child: Divider(color: context.colorScheme.surfaceContainerHighest, thickness: 1), + ), + Expanded( + child: tags.widgetWhen( + onData: (tags) { + final queryResult = tags + .where((t) => t.value.toLowerCase().contains(searchQuery.value.toLowerCase())) + .toList(); + return ListView.builder( + itemCount: queryResult.length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final tag = queryResult[index]; + final isSelected = selectedTagIds.value.any((id) => id == tag.id); + + return Padding( + padding: const EdgeInsets.only(bottom: 2.0), + child: Container( + decoration: BoxDecoration( + color: isSelected ? context.primaryColor : context.primaryColor.withAlpha(25), + borderRadius: borderRadius, + ), + child: ListTile( + title: Text( + tag.value, + style: context.textTheme.bodyLarge?.copyWith( + color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, + ), + ), + onTap: () { + final newSelected = {...selectedTagIds.value}; + if (isSelected) { + newSelected.removeWhere((id) => id == tag.id); + } else { + newSelected.add(tag.id); + } + selectedTagIds.value = newSelected; + onSelect(tags.where((t) => newSelected.contains(t.id))); + }, + ), + ), + ); + }, + ); + }, + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/common/user_avatar.dart b/mobile/lib/widgets/common/user_avatar.dart index ff0e39f371..911d6a9f10 100644 --- a/mobile/lib/widgets/common/user_avatar.dart +++ b/mobile/lib/widgets/common/user_avatar.dart @@ -1,10 +1,9 @@ -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'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; Widget userAvatar(BuildContext context, UserDto u, {double? radius}) { final url = "${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image"; @@ -12,11 +11,7 @@ Widget userAvatar(BuildContext context, UserDto u, {double? radius}) { return CircleAvatar( radius: radius, backgroundColor: context.primaryColor.withAlpha(50), - foregroundImage: CachedNetworkImageProvider( - url, - headers: ApiService.getRequestHeaders(), - cacheKey: "user-${u.id}-profile", - ), + foregroundImage: RemoteImageProvider(url: url), // silence errors if user has no profile image, use initials as fallback onForegroundImageError: (exception, stackTrace) {}, child: Text(nameFirstLetter.toUpperCase()), diff --git a/mobile/lib/widgets/common/user_circle_avatar.dart b/mobile/lib/widgets/common/user_circle_avatar.dart index b46f560122..c6e4f4719e 100644 --- a/mobile/lib/widgets/common/user_circle_avatar.dart +++ b/mobile/lib/widgets/common/user_circle_avatar.dart @@ -1,64 +1,59 @@ -import 'dart:math'; - -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/widgets/common/transparent_image.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; // ignore: must_be_immutable class UserCircleAvatar extends ConsumerWidget { final UserDto user; - double radius; double size; bool hasBorder; + double opacity; - UserCircleAvatar({super.key, this.radius = 22, this.size = 44, this.hasBorder = false, required this.user}); + UserCircleAvatar({super.key, this.size = 44, this.hasBorder = false, this.opacity = 1, required this.user}); @override Widget build(BuildContext context, WidgetRef ref) { - final userAvatarColor = user.avatarColor.toColor(); + final userAvatarColor = user.avatarColor.toColor().withValues(alpha: opacity); final profileImageUrl = - '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}'; + '${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${user.profileChangedAt.millisecondsSinceEpoch}'; + + final textColor = (user.avatarColor.toColor().computeLuminance() > 0.5 ? Colors.black : Colors.white).withValues( + alpha: opacity, + ); final textIcon = DefaultTextStyle( - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: userAvatarColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, - ), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: textColor), child: Text(user.name[0].toUpperCase()), ); 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: UnconstrainedBox( + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: userAvatarColor, + shape: BoxShape.circle, + border: hasBorder ? Border.all(color: userAvatarColor.withValues(alpha: opacity), width: 1.5) : null, + ), child: user.hasProfileImage ? ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(50)), - child: CachedNetworkImage( + borderRadius: BorderRadius.all(Radius.circular(size / 2)), + child: Image( fit: BoxFit.cover, - cacheKey: '${user.id}-${user.profileChangedAt.toIso8601String()}', width: size, height: size, - placeholder: (_, __) => Image.memory(kTransparentImage), - imageUrl: profileImageUrl, - httpHeaders: ApiService.getRequestHeaders(), - fadeInDuration: const Duration(milliseconds: 300), - errorWidget: (context, error, stackTrace) => textIcon, + image: RemoteImageProvider(url: profileImageUrl), + errorBuilder: (context, error, stackTrace) => textIcon, + color: Colors.white.withValues(alpha: opacity), + colorBlendMode: BlendMode.modulate, ), ) - : textIcon, + : Center(child: textIcon), ), ), ); diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index f810973298..2aa770f104 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -14,6 +14,7 @@ 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/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -29,12 +30,7 @@ import 'package:immich_mobile/utils/version_compatibility.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/forms/login/email_input.dart'; -import 'package:immich_mobile/widgets/forms/login/loading_icon.dart'; -import 'package:immich_mobile/widgets/forms/login/login_button.dart'; -import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart'; -import 'package:immich_mobile/widgets/forms/login/password_input.dart'; -import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart'; +import 'package:immich_ui/immich_ui.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -45,16 +41,33 @@ class LoginForm extends HookConsumerWidget { final log = Logger('LoginForm'); + String? _validateUrl(String? url) { + if (url == null || url.isEmpty) return null; + + final parsedUrl = Uri.tryParse(url); + if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) { + return 'login_form_err_invalid_url'.tr(); + } + + return null; + } + + String? _validateEmail(String? email) { + if (email == null || email == '') return null; + if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr(); + if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr(); + if (email.contains(' ') || !email.contains('@')) { + return 'login_form_err_invalid_email'.tr(); + } + return null; + } + @override Widget build(BuildContext context, WidgetRef ref) { 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(); - final isLoading = useState(false); - final isLoadingServer = useState(false); final isOauthEnable = useState(false); final isPasswordLoginEnable = useState(false); final oAuthButtonLabel = useState('OAuth'); @@ -96,7 +109,6 @@ class LoginForm extends HookConsumerWidget { } try { - isLoadingServer.value = true; final endpoint = await ref.read(authProvider.notifier).validateServerUrl(serverUrl); // Fetch and load server config and features @@ -120,7 +132,6 @@ class LoginForm extends HookConsumerWidget { ); isOauthEnable.value = false; isPasswordLoginEnable.value = true; - isLoadingServer.value = false; } on HandshakeException { ImmichToast.show( context: context, @@ -130,7 +141,6 @@ class LoginForm extends HookConsumerWidget { ); isOauthEnable.value = false; isPasswordLoginEnable.value = true; - isLoadingServer.value = false; } catch (e) { ImmichToast.show( context: context, @@ -140,10 +150,7 @@ class LoginForm extends HookConsumerWidget { ); isOauthEnable.value = false; isPasswordLoginEnable.value = true; - isLoadingServer.value = false; } - - isLoadingServer.value = false; } useEffect(() { @@ -230,8 +237,6 @@ class LoginForm extends HookConsumerWidget { login() async { TextInput.finishAutofillContext(); - isLoading.value = true; - // Invalidate all api repository provider instance to take into account new access token invalidateAllApiRepositoryProviders(ref); @@ -261,8 +266,6 @@ class LoginForm extends HookConsumerWidget { toastType: ToastType.error, gravity: ToastGravity.TOP, ); - } finally { - isLoading.value = false; } } @@ -306,8 +309,6 @@ class LoginForm extends HookConsumerWidget { codeChallenge, ); - isLoading.value = true; - // Invalidate all api repository provider instance to take into account new access token invalidateAllApiRepositoryProviders(ref); } catch (error, stack) { @@ -319,7 +320,6 @@ class LoginForm extends HookConsumerWidget { toastType: ToastType.error, gravity: ToastGravity.TOP, ); - isLoading.value = false; return; } @@ -338,7 +338,6 @@ class LoginForm extends HookConsumerWidget { .saveAuthInfo(accessToken: loginResponseDto.accessToken); if (isSuccess) { - isLoading.value = false; final permission = ref.watch(galleryPermissionNotifier); final isBeta = Store.isBetaTimelineEnabled; if (!isBeta && (permission.isGranted || permission.isLimited)) { @@ -364,9 +363,7 @@ class LoginForm extends HookConsumerWidget { toastType: ToastType.error, gravity: ToastGravity.TOP, ); - } finally { - isLoading.value = false; - } + } finally {} } else { ImmichToast.show( context: context, @@ -374,66 +371,10 @@ class LoginForm extends HookConsumerWidget { toastType: ToastType.info, gravity: ToastGravity.TOP, ); - isLoading.value = false; return; } } - buildSelectServer() { - const buttonRadius = 25.0; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ServerEndpointInput( - controller: serverEndpointController, - focusNode: serverEndpointFocusNode, - onSubmit: getServerAuthSettings, - ), - const SizedBox(height: 18), - Row( - children: [ - Expanded( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(buttonRadius), - bottomLeft: Radius.circular(buttonRadius), - ), - ), - ), - onPressed: () => context.pushRoute(const SettingsRoute()), - icon: const Icon(Icons.settings_rounded), - label: const Text(""), - ), - ), - const SizedBox(width: 1), - Expanded( - flex: 3, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(buttonRadius), - bottomRight: Radius.circular(buttonRadius), - ), - ), - ), - onPressed: isLoadingServer.value ? null : getServerAuthSettings, - icon: const Icon(Icons.arrow_forward_rounded), - label: const Text('next', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), - ), - ), - ], - ), - const SizedBox(height: 18), - if (isLoadingServer.value) const LoadingIcon(), - ], - ); - } - buildVersionCompatWarning() { checkVersionMismatch(); @@ -455,66 +396,103 @@ class LoginForm extends HookConsumerWidget { ); } - buildLogin() { - return AutofillGroup( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildVersionCompatWarning(), - Text( - sanitizeUrl(serverEndpointController.text), - style: context.textTheme.displaySmall, - textAlign: TextAlign.center, + final serverSelectionOrLogin = serverEndpoint.value == null + ? Padding( + padding: const EdgeInsets.only(top: ImmichSpacing.md), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + ImmichForm( + submitText: 'next'.t(context: context), + submitIcon: Icons.arrow_forward_rounded, + onSubmit: getServerAuthSettings, + child: ImmichTextInput( + controller: serverEndpointController, + label: 'login_form_endpoint_url'.t(context: context), + hintText: 'login_form_endpoint_hint'.t(context: context), + validator: _validateUrl, + keyboardAction: TextInputAction.next, + keyboardType: TextInputType.url, + autofillHints: const [AutofillHints.url], + autoCorrect: false, + onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), + ), + ), + ImmichTextButton( + labelText: 'settings'.t(context: context), + icon: Icons.settings, + variant: ImmichVariant.ghost, + onPressed: () => context.pushRoute(const SettingsRoute()), + ), + ], ), - if (isPasswordLoginEnable.value) ...[ - const SizedBox(height: 18), - EmailInput( - controller: emailController, - focusNode: emailFocusNode, - onSubmit: passwordFocusNode.requestFocus, - ), - const SizedBox(height: 8), - PasswordInput(controller: passwordController, focusNode: passwordFocusNode, onSubmit: login), - ], - - // Note: This used to have an AnimatedSwitcher, but was removed - // because of https://github.com/flutter/flutter/issues/120874 - isLoading.value - ? const LoadingIcon() - : Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 18), - if (isPasswordLoginEnable.value) LoginButton(onPressed: login), - if (isOauthEnable.value) ...[ - if (isPasswordLoginEnable.value) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black), - ), - OAuthLoginButton( - serverEndpointController: serverEndpointController, - buttonLabel: oAuthButtonLabel.value, - isLoading: isLoading, - onPressed: oAuthLogin, + ) + : AutofillGroup( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + children: [ + buildVersionCompatWarning(), + Padding( + padding: const EdgeInsets.only(bottom: ImmichSpacing.md), + child: Text( + sanitizeUrl(serverEndpointController.text), + style: context.textTheme.displaySmall, + textAlign: TextAlign.center, + ), + ), + if (isPasswordLoginEnable.value) + ImmichForm( + submitText: 'login'.t(context: context), + submitIcon: Icons.login_rounded, + onSubmit: login, + child: Column( + spacing: ImmichSpacing.md, + children: [ + ImmichTextInput( + controller: emailController, + label: 'email'.t(context: context), + hintText: 'login_form_email_hint'.t(context: context), + validator: _validateEmail, + keyboardAction: TextInputAction.next, + keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.email], + onSubmit: (_, _) => passwordFocusNode.requestFocus(), + ), + ImmichPasswordInput( + controller: passwordController, + focusNode: passwordFocusNode, + label: 'password'.t(context: context), + hintText: 'login_form_password_hint'.t(context: context), + keyboardAction: TextInputAction.go, + onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), ), ], - ], + ), ), - if (!isOauthEnable.value && !isPasswordLoginEnable.value) Center(child: const Text('login_disabled').tr()), - const SizedBox(height: 12), - TextButton.icon( - icon: const Icon(Icons.arrow_back), - onPressed: () => serverEndpoint.value = null, - label: const Text('back').tr(), + if (isOauthEnable.value) + ImmichForm( + submitText: oAuthButtonLabel.value, + submitIcon: Icons.pin_outlined, + onSubmit: oAuthLogin, + child: isPasswordLoginEnable.value + ? Padding( + padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0), + child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5), + ) + : const SizedBox.shrink(), + ), + if (!isOauthEnable.value && !isPasswordLoginEnable.value) + Center(child: const Text('login_disabled').tr()), + ImmichTextButton( + labelText: 'back'.t(context: context), + icon: Icons.arrow_back, + variant: ImmichVariant.ghost, + onPressed: () => serverEndpoint.value = null, + ), + ], ), - ], - ), - ); - } - - final serverSelectionOrLogin = serverEndpoint.value == null ? buildSelectServer() : buildLogin(); + ); return LayoutBuilder( builder: (context, constraints) { diff --git a/mobile/lib/widgets/forms/login/o_auth_login_button.dart b/mobile/lib/widgets/forms/login/o_auth_login_button.dart deleted file mode 100644 index 2d9b603b3c..0000000000 --- a/mobile/lib/widgets/forms/login/o_auth_login_button.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class OAuthLoginButton extends ConsumerWidget { - final TextEditingController serverEndpointController; - final ValueNotifier isLoading; - final String buttonLabel; - final Function() onPressed; - - const OAuthLoginButton({ - super.key, - required this.serverEndpointController, - required this.isLoading, - required this.buttonLabel, - required this.onPressed, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: context.primaryColor.withAlpha(230), - padding: const EdgeInsets.symmetric(vertical: 12), - ), - onPressed: onPressed, - icon: const Icon(Icons.pin_rounded), - label: Text(buttonLabel, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - ); - } -} diff --git a/mobile/lib/widgets/forms/login/password_input.dart b/mobile/lib/widgets/forms/login/password_input.dart deleted file mode 100644 index 5cdfcc9567..0000000000 --- a/mobile/lib/widgets/forms/login/password_input.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -class PasswordInput extends HookConsumerWidget { - final TextEditingController controller; - final FocusNode? focusNode; - final Function()? onSubmit; - - const PasswordInput({super.key, required this.controller, this.focusNode, this.onSubmit}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isPasswordVisible = useState(false); - - return TextFormField( - obscureText: !isPasswordVisible.value, - controller: controller, - decoration: InputDecoration( - labelText: 'password'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_password_hint'.tr(), - hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - suffixIcon: IconButton( - onPressed: () => isPasswordVisible.value = !isPasswordVisible.value, - icon: Icon(isPasswordVisible.value ? Icons.visibility_off_sharp : Icons.visibility_sharp), - ), - ), - autofillHints: const [AutofillHints.password], - keyboardType: TextInputType.text, - onFieldSubmitted: (_) => onSubmit?.call(), - focusNode: focusNode, - textInputAction: TextInputAction.go, - ); - } -} diff --git a/mobile/lib/widgets/forms/login/server_endpoint_input.dart b/mobile/lib/widgets/forms/login/server_endpoint_input.dart deleted file mode 100644 index f9bc1690af..0000000000 --- a/mobile/lib/widgets/forms/login/server_endpoint_input.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/utils/url_helper.dart'; - -class ServerEndpointInput extends StatelessWidget { - final TextEditingController controller; - final FocusNode focusNode; - final Function()? onSubmit; - - const ServerEndpointInput({super.key, required this.controller, required this.focusNode, this.onSubmit}); - - String? _validateInput(String? url) { - 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) { - return 'login_form_err_invalid_url'.tr(); - } - - return null; - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 16.0), - child: TextFormField( - controller: controller, - decoration: InputDecoration( - labelText: 'login_form_endpoint_url'.tr(), - border: const OutlineInputBorder(), - hintText: 'login_form_endpoint_hint'.tr(), - errorMaxLines: 4, - ), - validator: _validateInput, - autovalidateMode: AutovalidateMode.always, - focusNode: focusNode, - autofillHints: const [AutofillHints.url], - keyboardType: TextInputType.url, - autocorrect: false, - onFieldSubmitted: (_) => onSubmit?.call(), - textInputAction: TextInputAction.go, - ), - ); - } -} diff --git a/mobile/lib/widgets/forms/pin_input.dart b/mobile/lib/widgets/forms/pin_input.dart index 88e27f005e..c4f0d8f3b7 100644 --- a/mobile/lib/widgets/forms/pin_input.dart +++ b/mobile/lib/widgets/forms/pin_input.dart @@ -43,7 +43,7 @@ class PinInput extends StatelessWidget { final defaultPinTheme = PinTheme( width: getPinSize().width, height: getPinSize().height, - textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'Overpass Mono'), + textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'GoogleSansCode'), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(19)), border: Border.all(color: context.colorScheme.surfaceBright), diff --git a/mobile/lib/widgets/map/asset_marker_icon.dart b/mobile/lib/widgets/map/asset_marker_icon.dart new file mode 100644 index 0000000000..ff6058161b --- /dev/null +++ b/mobile/lib/widgets/map/asset_marker_icon.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; + +class AssetMarkerIcon extends StatelessWidget { + const AssetMarkerIcon({required this.id, required this.thumbhash, super.key}); + + final String id; + final String thumbhash; + + @override + Widget build(BuildContext context) { + final imageUrl = getThumbnailUrlForRemoteId(id); + return LayoutBuilder( + builder: (context, constraints) { + final pinHeight = constraints.maxHeight * 0.14; + final pinWidth = constraints.maxWidth * 0.14; + return SizedOverflowBox( + size: Size(pinWidth, pinHeight), + child: Stack( + // alignment: AlignmentGeometry.center, + children: [ + Positioned( + bottom: 0, + left: constraints.maxWidth * 0.5, + child: CustomPaint( + painter: _PinPainter( + primaryColor: context.colorScheme.onSurface, + secondaryColor: context.colorScheme.surface, + primaryRadius: constraints.maxHeight * 0.06, + secondaryRadius: constraints.maxHeight * 0.038, + ), + child: SizedBox(height: pinHeight, width: pinWidth), + ), + ), + Positioned( + top: constraints.maxHeight * 0.07, + left: constraints.maxWidth * 0.17, + child: CircleAvatar( + radius: constraints.maxHeight * 0.40, + backgroundColor: context.colorScheme.onSurface, + child: CircleAvatar( + radius: constraints.maxHeight * 0.37, + backgroundImage: RemoteImageProvider(url: imageUrl), + ), + ), + ), + ], + ), + ); + }, + ); + } +} + +class _PinPainter extends CustomPainter { + final Color primaryColor; + final Color secondaryColor; + final double primaryRadius; + final double secondaryRadius; + + const _PinPainter({ + required this.primaryColor, + required this.secondaryColor, + required this.primaryRadius, + required this.secondaryRadius, + }); + + @override + void paint(Canvas canvas, Size size) { + Paint primaryBrush = Paint() + ..color = primaryColor + ..style = PaintingStyle.fill; + + Paint secondaryBrush = Paint() + ..color = secondaryColor + ..style = PaintingStyle.fill; + + Paint lineBrush = Paint() + ..color = primaryColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + canvas.drawCircle(Offset(size.width / 2, size.height), primaryRadius, primaryBrush); + canvas.drawCircle(Offset(size.width / 2, size.height), secondaryRadius, secondaryBrush); + canvas.drawPath(getTrianglePath(size.width, size.height), primaryBrush); + // The line is to make the above triangluar path more prominent since it has a slight curve + canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2, size.height), lineBrush); + } + + Path getTrianglePath(double x, double y) { + final firstEndPoint = Offset(x / 2, y); + final controlPoint = Offset(x / 2, y * 0.3); + final secondEndPoint = Offset(x, 0); + + return Path() + ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, firstEndPoint.dx, firstEndPoint.dy) + ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, secondEndPoint.dx, secondEndPoint.dy) + ..lineTo(0, 0); + } + + @override + bool shouldRepaint(_PinPainter old) { + return old.primaryColor != primaryColor || old.secondaryColor != secondaryColor; + } +} 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 e97875fd90..762c402def 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 @@ -1,4 +1,3 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -14,7 +13,7 @@ class MapSettingsListTile extends StatelessWidget { Widget build(BuildContext context) { return SwitchListTile.adaptive( activeThumbColor: context.primaryColor, - title: Text(title, style: context.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold)).tr(), + title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)), 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 b601887e1e..2a4dacaff7 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 @@ -1,5 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; class MapTimeDropDown extends StatelessWidget { final int relativeTime; @@ -11,41 +13,47 @@ class MapTimeDropDown extends StatelessWidget { Widget build(BuildContext context) { final now = DateTime.now(); - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text("date_range".tr(), style: const TextStyle(fontWeight: FontWeight.bold)), - ), - LayoutBuilder( - builder: (_, constraints) => DropdownMenu( - width: constraints.maxWidth * 0.9, - enableSearch: false, - enableFilter: false, - initialSelection: relativeTime, - onSelected: (value) => onTimeChange(value!), - dropdownMenuEntries: [ - DropdownMenuEntry(value: 0, label: "all".tr()), - DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".tr()), - DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), - DropdownMenuEntry(value: 30, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"})), - DropdownMenuEntry( - value: now - .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) - .inDays, - label: "map_settings_date_range_option_year".tr(), - ), - DropdownMenuEntry( - value: now - .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) - .inDays, - label: "map_settings_date_range_option_years".tr(namedArgs: {'years': "3"}), - ), - ], + return Padding( + padding: const EdgeInsets.only(left: 16, right: 28.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "date_range".t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), - ), - ], + Flexible( + child: DropdownMenu( + enableSearch: false, + enableFilter: false, + initialSelection: relativeTime, + onSelected: (value) => onTimeChange(value!), + dropdownMenuEntries: [ + DropdownMenuEntry(value: 0, label: "all".t(context: context)), + DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".t(context: context)), + DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), + DropdownMenuEntry( + value: 30, + label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"}), + ), + DropdownMenuEntry( + value: now + .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) + .inDays, + label: "map_settings_date_range_option_year".t(context: context), + ), + DropdownMenuEntry( + value: now + .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) + .inDays, + label: "map_settings_date_range_option_years".t(args: {'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 63f35ebe4c..7866c0ecdc 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -18,9 +18,9 @@ class MapThemePicker extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Center( child: Text( - "map_settings_theme_settings", - style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - ).tr(), + "map_settings_theme_settings".t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), ), ), Row( diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index 55f5ff77c6..7defb52264 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -7,7 +7,7 @@ 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'; +import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; /// A non-interactive thumbnail of a map in the given coordinates with optional markers @@ -19,6 +19,7 @@ class MapThumbnail extends HookConsumerWidget { final Function(Point, LatLng)? onTap; final LatLng centre; final String? assetMarkerRemoteId; + final String? assetThumbhash; final bool showMarkerPin; final double zoom; final double height; @@ -35,6 +36,7 @@ class MapThumbnail extends HookConsumerWidget { this.onTap, this.zoom = 8, this.assetMarkerRemoteId, + this.assetThumbhash, this.showMarkerPin = false, this.themeMode, this.showAttribution = true, @@ -43,21 +45,12 @@ class MapThumbnail extends HookConsumerWidget { @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), - ); - } onCreated?.call(mapController); } @@ -88,11 +81,11 @@ class MapThumbnail extends HookConsumerWidget { child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), child: Stack( - alignment: Alignment.center, + alignment: AlignmentGeometry.topCenter, children: [ style.widgetWhen( onData: (style) => MapLibreMap( - initialCameraPosition: CameraPosition(target: offsettedCentre, zoom: zoom), + initialCameraPosition: CameraPosition(target: centre, zoom: zoom), styleString: style, onMapCreated: onMapCreated, onStyleLoadedCallback: onStyleLoaded, @@ -107,12 +100,16 @@ class MapThumbnail extends HookConsumerWidget { attributionButtonMargins: showAttribution == false ? const Point(-100, 0) : null, ), ), - ValueListenableBuilder( - valueListenable: position, - builder: (_, value, __) => value != null && assetMarkerRemoteId != null - ? PositionedAssetMarkerIcon(size: height / 2, point: value, assetRemoteId: assetMarkerRemoteId!) - : const SizedBox.shrink(), - ), + if (assetMarkerRemoteId != null && assetThumbhash != null) + Container( + width: width, + height: height / 2, + alignment: Alignment.bottomCenter, + child: SizedBox.square( + dimension: height / 2.5, + child: AssetMarkerIcon(id: assetMarkerRemoteId!, thumbhash: assetThumbhash!), + ), + ), ], ), ), diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart index 0944f7ce3e..b6d7241cf4 100644 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart @@ -1,15 +1,14 @@ import 'dart:io'; import 'dart:math'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; class PositionedAssetMarkerIcon extends StatelessWidget { final Point point; final String assetRemoteId; + final String assetThumbhash; final double size; final int durationInMilliseconds; @@ -18,6 +17,7 @@ class PositionedAssetMarkerIcon extends StatelessWidget { const PositionedAssetMarkerIcon({ required this.point, required this.assetRemoteId, + required this.assetThumbhash, this.size = 100, this.durationInMilliseconds = 100, this.onTap, @@ -35,111 +35,9 @@ class PositionedAssetMarkerIcon extends StatelessWidget { onTap: () => onTap?.call(), child: SizedBox.square( dimension: size, - child: _AssetMarkerIcon(id: assetRemoteId, key: Key(assetRemoteId)), + child: AssetMarkerIcon(id: assetRemoteId, thumbhash: assetThumbhash, key: Key(assetRemoteId)), ), ), ); } } - -class _AssetMarkerIcon extends StatelessWidget { - const _AssetMarkerIcon({required this.id, super.key}); - - final String id; - - @override - Widget build(BuildContext context) { - final imageUrl = getThumbnailUrlForRemoteId(id); - final cacheKey = getThumbnailCacheKeyForRemoteId(id); - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - Positioned( - bottom: 0, - left: constraints.maxWidth * 0.5, - child: CustomPaint( - painter: _PinPainter( - primaryColor: context.colorScheme.onSurface, - secondaryColor: context.colorScheme.surface, - primaryRadius: constraints.maxHeight * 0.06, - secondaryRadius: constraints.maxHeight * 0.038, - ), - child: SizedBox(height: constraints.maxHeight * 0.14, width: constraints.maxWidth * 0.14), - ), - ), - Positioned( - top: constraints.maxHeight * 0.07, - left: constraints.maxWidth * 0.17, - child: CircleAvatar( - radius: constraints.maxHeight * 0.40, - backgroundColor: context.colorScheme.onSurface, - child: CircleAvatar( - radius: constraints.maxHeight * 0.37, - backgroundImage: CachedNetworkImageProvider( - imageUrl, - cacheKey: cacheKey, - headers: ApiService.getRequestHeaders(), - errorListener: (_) => const Icon(Icons.image_not_supported_outlined), - ), - ), - ), - ), - ], - ); - }, - ); - } -} - -class _PinPainter extends CustomPainter { - final Color primaryColor; - final Color secondaryColor; - final double primaryRadius; - final double secondaryRadius; - - const _PinPainter({ - required this.primaryColor, - required this.secondaryColor, - required this.primaryRadius, - required this.secondaryRadius, - }); - - @override - void paint(Canvas canvas, Size size) { - Paint primaryBrush = Paint() - ..color = primaryColor - ..style = PaintingStyle.fill; - - Paint secondaryBrush = Paint() - ..color = secondaryColor - ..style = PaintingStyle.fill; - - Paint lineBrush = Paint() - ..color = primaryColor - ..style = PaintingStyle.stroke - ..strokeWidth = 2; - - canvas.drawCircle(Offset(size.width / 2, size.height), primaryRadius, primaryBrush); - canvas.drawCircle(Offset(size.width / 2, size.height), secondaryRadius, secondaryBrush); - canvas.drawPath(getTrianglePath(size.width, size.height), primaryBrush); - // The line is to make the above triangluar path more prominent since it has a slight curve - canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2, size.height), lineBrush); - } - - Path getTrianglePath(double x, double y) { - final firstEndPoint = Offset(x / 2, y); - final controlPoint = Offset(x / 2, y * 0.3); - final secondEndPoint = Offset(x, 0); - - return Path() - ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, firstEndPoint.dx, firstEndPoint.dy) - ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, secondEndPoint.dx, secondEndPoint.dy) - ..lineTo(0, 0); - } - - @override - bool shouldRepaint(_PinPainter old) { - return old.primaryColor != primaryColor || old.secondaryColor != secondaryColor; - } -} diff --git a/mobile/lib/widgets/photo_view/photo_view.dart b/mobile/lib/widgets/photo_view/photo_view.dart index 69be96ed53..f9d3c66767 100644 --- a/mobile/lib/widgets/photo_view/photo_view.dart +++ b/mobile/lib/widgets/photo_view/photo_view.dart @@ -257,6 +257,7 @@ class PhotoView extends StatefulWidget { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, this.customSize, @@ -299,6 +300,7 @@ class PhotoView extends StatefulWidget { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, this.customSize, @@ -417,6 +419,9 @@ class PhotoView extends StatefulWidget { /// location. final PhotoViewImageDragUpdateCallback? onDragUpdate; + /// A callback when a drag gesture is canceled by the system. + final VoidCallback? onDragCancel; + /// A pointer that will trigger a scale has stopped contacting the screen at a /// particular location. final PhotoViewImageScaleEndCallback? onScaleEnd; @@ -543,7 +548,7 @@ class _PhotoViewState extends State with AutomaticKeepAliveClientMixi return LayoutBuilder( builder: (BuildContext context, 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.transparent); return widget._isCustomChild ? CustomChildWrapper( @@ -564,6 +569,7 @@ class _PhotoViewState extends State with AutomaticKeepAliveClientMixi onDragStart: widget.onDragStart, onDragEnd: widget.onDragEnd, onDragUpdate: widget.onDragUpdate, + onDragCancel: widget.onDragCancel, onScaleEnd: widget.onScaleEnd, onLongPressStart: widget.onLongPressStart, outerSize: computedOuterSize, @@ -596,6 +602,7 @@ class _PhotoViewState extends State with AutomaticKeepAliveClientMixi onDragStart: widget.onDragStart, onDragEnd: widget.onDragEnd, onDragUpdate: widget.onDragUpdate, + onDragCancel: widget.onDragCancel, onScaleEnd: widget.onScaleEnd, onLongPressStart: widget.onLongPressStart, outerSize: computedOuterSize, diff --git a/mobile/lib/widgets/photo_view/photo_view_gallery.dart b/mobile/lib/widgets/photo_view/photo_view_gallery.dart index af5b9a7ce7..aa33d18403 100644 --- a/mobile/lib/widgets/photo_view/photo_view_gallery.dart +++ b/mobile/lib/widgets/photo_view/photo_view_gallery.dart @@ -284,6 +284,7 @@ class _PhotoViewGalleryState extends State { onDragStart: pageOption.onDragStart, onDragEnd: pageOption.onDragEnd, onDragUpdate: pageOption.onDragUpdate, + onDragCancel: pageOption.onDragCancel, onScaleEnd: pageOption.onScaleEnd, onLongPressStart: pageOption.onLongPressStart, gestureDetectorBehavior: pageOption.gestureDetectorBehavior, @@ -321,6 +322,7 @@ class _PhotoViewGalleryState extends State { onDragStart: pageOption.onDragStart, onDragEnd: pageOption.onDragEnd, onDragUpdate: pageOption.onDragUpdate, + onDragCancel: pageOption.onDragCancel, onScaleEnd: pageOption.onScaleEnd, onLongPressStart: pageOption.onLongPressStart, gestureDetectorBehavior: pageOption.gestureDetectorBehavior, @@ -367,6 +369,7 @@ class PhotoViewGalleryPageOptions { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, this.gestureDetectorBehavior, @@ -397,6 +400,7 @@ class PhotoViewGalleryPageOptions { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, this.gestureDetectorBehavior, @@ -454,9 +458,12 @@ class PhotoViewGalleryPageOptions { /// Mirror to [PhotoView.onDragDown] final PhotoViewImageDragEndCallback? onDragEnd; - /// Mirror to [PhotoView.onDraUpdate] + /// Mirror to [PhotoView.onDragUpdate] final PhotoViewImageDragUpdateCallback? onDragUpdate; + /// Mirror to [PhotoView.onDragCancel] + final VoidCallback? onDragCancel; + /// Mirror to [PhotoView.onTapDown] final PhotoViewImageTapDownCallback? onTapDown; 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 2c8b406385..b9475a9ee2 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 @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/widgets/photo_view/src/utils/ignorable_change_notifier.dart'; +import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_utils.dart'; /// The interface in which controllers will be implemented. /// @@ -62,6 +63,9 @@ abstract class PhotoViewControllerBase { /// The scale factor to transform the child (image or a customChild). late double? scale; + double? get initialScale; + ScaleBoundaries? scaleBoundaries; + /// Nevermind this method :D, look away void setScaleInvisibly(double? scale); @@ -141,6 +145,9 @@ class PhotoViewController implements PhotoViewControllerBase _outputCtrl; + @override + ScaleBoundaries? scaleBoundaries; + late void Function(Offset)? _animatePosition; late void Function(double)? _animateScale; late void Function(double)? _animateRotation; @@ -311,4 +318,7 @@ class PhotoViewController implements PhotoViewControllerBase scaleBoundaries?.initialScale ?? initial.scale; } 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 d21b49f020..72c4766c45 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 @@ -36,6 +36,7 @@ class PhotoViewCore extends StatefulWidget { required this.onDragStart, required this.onDragEnd, required this.onDragUpdate, + required this.onDragCancel, required this.onScaleEnd, required this.onLongPressStart, required this.gestureDetectorBehavior, @@ -62,6 +63,7 @@ class PhotoViewCore extends StatefulWidget { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, this.gestureDetectorBehavior, @@ -100,6 +102,7 @@ class PhotoViewCore extends StatefulWidget { final PhotoViewImageDragStartCallback? onDragStart; final PhotoViewImageDragEndCallback? onDragEnd; final PhotoViewImageDragUpdateCallback? onDragUpdate; + final VoidCallback? onDragCancel; final PhotoViewImageLongPressStartCallback? onLongPressStart; @@ -386,6 +389,7 @@ class PhotoViewCoreState extends State onDragUpdate: widget.onDragUpdate != null ? (details) => widget.onDragUpdate!(context, details, widget.controller.value) : null, + onDragCancel: widget.onDragCancel, hitDetector: this, onTapUp: widget.onTapUp != null ? (details) => widget.onTapUp!(context, details, value) : null, onTapDown: widget.onTapDown != null ? (details) => widget.onTapDown!(context, details, value) : null, 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 7a5406c675..6cbcec8d82 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 @@ -16,6 +16,7 @@ class PhotoViewGestureDetector extends StatelessWidget { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onLongPressStart, this.child, this.onTapUp, @@ -34,6 +35,7 @@ class PhotoViewGestureDetector extends StatelessWidget { final GestureDragEndCallback? onDragEnd; final GestureDragStartCallback? onDragStart; final GestureDragUpdateCallback? onDragUpdate; + final GestureDragCancelCallback? onDragCancel; final GestureTapUpCallback? onTapUp; final GestureTapDownCallback? onTapDown; @@ -73,7 +75,8 @@ class PhotoViewGestureDetector extends StatelessWidget { instance ..onStart = onDragStart ..onUpdate = onDragUpdate - ..onEnd = onDragEnd; + ..onEnd = onDragEnd + ..onCancel = onDragCancel; }, ); } @@ -203,9 +206,13 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer { void _decideIfWeAcceptEvent(PointerEvent event) { final move = _initialFocalPoint! - _currentFocalPoint!; - final bool shouldMove = validateAxis == Axis.vertical - ? hitDetector!.shouldMove(move, Axis.vertical) - : hitDetector!.shouldMove(move, Axis.horizontal); + + // Accept gesture if movement is possible in the direction the user is swiping + final bool isHorizontalGesture = move.dx.abs() > move.dy.abs(); + final bool shouldMove = isHorizontalGesture + ? hitDetector!.shouldMove(move, Axis.horizontal) + : hitDetector!.shouldMove(move, Axis.vertical); + if (shouldMove || _pointerLocations.keys.length > 1) { final double spanDelta = (_currentSpan! - _initialSpan!).abs(); final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint!).distance; 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 a2ad04e6b5..ee18668f52 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -28,6 +28,7 @@ class ImageWrapper extends StatefulWidget { required this.onDragStart, required this.onDragEnd, required this.onDragUpdate, + required this.onDragCancel, required this.onScaleEnd, required this.onLongPressStart, required this.outerSize, @@ -62,6 +63,7 @@ class ImageWrapper extends StatefulWidget { final PhotoViewImageDragStartCallback? onDragStart; final PhotoViewImageDragEndCallback? onDragEnd; final PhotoViewImageDragUpdateCallback? onDragUpdate; + final VoidCallback? onDragCancel; final PhotoViewImageScaleEndCallback? onScaleEnd; final PhotoViewImageLongPressStartCallback? onLongPressStart; final Size outerSize; @@ -108,6 +110,17 @@ class _ImageWrapperState extends State { } } + // Should be called only when _imageSize is not null + ScaleBoundaries get scaleBoundaries { + return ScaleBoundaries( + widget.minScale ?? 0.0, + widget.maxScale ?? double.infinity, + widget.initialScale ?? PhotoViewComputedScale.contained, + widget.outerSize, + _imageSize!, + ); + } + // retrieve image from the provider void _resolveImage() { final ImageStream newStream = widget.imageProvider.resolve(const ImageConfiguration()); @@ -133,6 +146,7 @@ class _ImageWrapperState extends State { _lastStack = null; _didLoadSynchronously = synchronousCall; + widget.controller.scaleBoundaries = scaleBoundaries; } synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB); @@ -191,6 +205,7 @@ class _ImageWrapperState extends State { onDragStart: widget.onDragStart, onDragEnd: widget.onDragEnd, onDragUpdate: widget.onDragUpdate, + onDragCancel: widget.onDragCancel, onScaleEnd: widget.onScaleEnd, onLongPressStart: widget.onLongPressStart, outerSize: widget.outerSize, @@ -204,14 +219,6 @@ class _ImageWrapperState extends State { ); } - final scaleBoundaries = ScaleBoundaries( - widget.minScale ?? 0.0, - widget.maxScale ?? double.infinity, - widget.initialScale ?? PhotoViewComputedScale.contained, - widget.outerSize, - _imageSize!, - ); - return PhotoViewCore( imageProvider: widget.imageProvider, backgroundDecoration: widget.backgroundDecoration, @@ -229,6 +236,7 @@ class _ImageWrapperState extends State { onDragStart: widget.onDragStart, onDragEnd: widget.onDragEnd, onDragUpdate: widget.onDragUpdate, + onDragCancel: widget.onDragCancel, onScaleEnd: widget.onScaleEnd, onLongPressStart: widget.onLongPressStart, gestureDetectorBehavior: widget.gestureDetectorBehavior, @@ -277,6 +285,7 @@ class CustomChildWrapper extends StatelessWidget { this.onDragStart, this.onDragEnd, this.onDragUpdate, + this.onDragCancel, this.onScaleEnd, this.onLongPressStart, required this.outerSize, @@ -309,6 +318,7 @@ class CustomChildWrapper extends StatelessWidget { final PhotoViewImageDragStartCallback? onDragStart; final PhotoViewImageDragEndCallback? onDragEnd; final PhotoViewImageDragUpdateCallback? onDragUpdate; + final VoidCallback? onDragCancel; final PhotoViewImageScaleEndCallback? onScaleEnd; final PhotoViewImageLongPressStartCallback? onLongPressStart; final Size outerSize; @@ -344,6 +354,7 @@ class CustomChildWrapper extends StatelessWidget { onDragStart: onDragStart, onDragEnd: onDragEnd, onDragUpdate: onDragUpdate, + onDragCancel: onDragCancel, onScaleEnd: onScaleEnd, onLongPressStart: onLongPressStart, gestureDetectorBehavior: gestureDetectorBehavior, diff --git a/mobile/lib/widgets/search/curated_people_row.dart b/mobile/lib/widgets/search/curated_people_row.dart index 74fc3e1c34..9155de2131 100644 --- a/mobile/lib/widgets/search/curated_people_row.dart +++ b/mobile/lib/widgets/search/curated_people_row.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; class CuratedPeopleRow extends StatelessWidget { @@ -29,7 +29,6 @@ class CuratedPeopleRow extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(content.length, (index) { final person = content[index]; - final headers = ApiService.getRequestHeaders(); return Padding( padding: const EdgeInsets.only(right: 16.0), child: Column( @@ -44,7 +43,7 @@ class CuratedPeopleRow extends StatelessWidget { elevation: 3, child: CircleAvatar( maxRadius: imageSize / 2, - backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers), + backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), ), ), diff --git a/mobile/lib/widgets/search/explore_grid.dart b/mobile/lib/widgets/search/explore_grid.dart index a6e1cf5aac..6af20df029 100644 --- a/mobile/lib/widgets/search/explore_grid.dart +++ b/mobile/lib/widgets/search/explore_grid.dart @@ -55,6 +55,7 @@ class ExploreGrid extends StatelessWidget { camera: SearchCameraFilter(), date: SearchDateFilter(), display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: SearchRatingFilter(), mediaType: AssetType.other, ), ), diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index d95d7c7483..3fa443121a 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -33,7 +33,7 @@ class PersonNameEditForm extends HookConsumerWidget { decoration: InputDecoration( hintText: 'name'.tr(), border: const OutlineInputBorder(), - errorText: isError.value ? 'Error occured' : null, + errorText: isError.value ? 'Error occurred' : null, ), ), ), diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index b2a7a18c7c..978b70239c 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -23,7 +23,6 @@ class PeoplePicker extends HookConsumerWidget { final imageSize = 60.0; final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); - final headers = ApiService.getRequestHeaders(); final selectedPeople = useState>(filter ?? {}); return Column( @@ -75,7 +74,7 @@ class PeoplePicker extends HookConsumerWidget { elevation: 3, child: CircleAvatar( maxRadius: imageSize / 2, - backgroundImage: NetworkImage(getFaceThumbnailUrl(person.id), headers: headers), + backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), ), ), ), diff --git a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart new file mode 100644 index 0000000000..5591b0e264 --- /dev/null +++ b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; + +class StarRatingPicker extends HookWidget { + const StarRatingPicker({super.key, required this.onSelect, this.filter}); + final Function(SearchRatingFilter) onSelect; + final SearchRatingFilter? filter; + + @override + Widget build(BuildContext context) { + final selectedRating = useState(filter); + + return RadioGroup( + groupValue: selectedRating.value?.rating, + onChanged: (int? newValue) { + if (newValue == null) return; + final newFilter = SearchRatingFilter(rating: newValue); + selectedRating.value = newFilter; + onSelect(newFilter); + }, + child: Column( + children: List.generate( + 6, + (index) => RadioListTile( + key: Key("star_$index"), + title: Text('rating_count'.t(args: {'count': (index)})), + value: index, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/search/thumbnail_with_info.dart b/mobile/lib/widgets/search/thumbnail_with_info.dart index af9460f929..7ba8257c8a 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/widgets/search/thumbnail_with_info_container.dart'; -import 'package:immich_mobile/services/api.service.dart'; class ThumbnailWithInfo extends StatelessWidget { const ThumbnailWithInfo({ @@ -30,14 +30,7 @@ class ThumbnailWithInfo extends StatelessWidget { child: imageUrl != null ? ClipRRect( borderRadius: BorderRadius.circular(borderRadius), - child: CachedNetworkImage( - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - imageUrl: imageUrl!, - httpHeaders: ApiService.getRequestHeaders(), - errorWidget: (context, url, error) => const Icon(Icons.image_not_supported_outlined), - ), + child: Thumbnail(imageProvider: RemoteImageProvider(url: imageUrl!)), ) : Center(child: Icon(noImageIcon ?? Icons.not_listed_location, color: textAndIconColor)), ); diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index aee28c9449..e86d313294 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -8,10 +8,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; @@ -133,7 +135,7 @@ class AdvancedSettings extends HookConsumerWidget { title: "advanced_settings_enable_alternate_media_filter_title".tr(), subtitle: "advanced_settings_enable_alternate_media_filter_subtitle".tr(), ), - const BetaTimelineListTile(), + if (!Store.isBetaTimelineEnabled) const BetaTimelineListTile(), if (Store.isBetaTimelineEnabled) SettingsSwitchListTile( valueNotifier: readonlyModeEnabled, @@ -153,6 +155,44 @@ class AdvancedSettings extends HookConsumerWidget { ); }, ), + ListTile( + title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), + leading: const Icon(Icons.playlist_remove_rounded), + onTap: () async { + final int clearedBytes; + try { + clearedBytes = await remoteImageApi.clearCache(); + } catch (e) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + "advanced_settings_clear_image_cache_error".tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.themeData.colorScheme.error), + ), + ), + ); + return; + } + + if (clearedBytes < 0) { + return; + } + + // iOS always returns a small non-zero value + final clearedMB = clearedBytes < (256 * 1024) ? "0 MiB" : formatHumanReadableBytes(clearedBytes, 2); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + "advanced_settings_clear_image_cache_success".tr(namedArgs: {'size': clearedMB}), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), + ), + ); + }, + ), + const SizedBox(height: 60), ]; return SettingsSubPageScaffold(settings: advancedSettings); diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index 04786bf916..08e66df48d 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -1,14 +1,14 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; class GroupSettings extends HookConsumerWidget { const GroupSettings({super.key}); @@ -33,12 +33,24 @@ class GroupSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "asset_list_group_by_sub_title".tr()), + SettingGroupTitle( + title: "asset_list_group_by_sub_title".t(context: context), + icon: Icons.group_work_outlined, + ), SettingsRadioListTile( groups: [ - SettingsRadioGroup(title: 'asset_list_layout_settings_group_by_month_day'.tr(), value: GroupAssetsBy.day), - SettingsRadioGroup(title: 'month'.tr(), value: GroupAssetsBy.month), - SettingsRadioGroup(title: 'asset_list_layout_settings_group_automatically'.tr(), value: GroupAssetsBy.auto), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_by_month_day'.t(context: context), + value: GroupAssetsBy.day, + ), + SettingsRadioGroup( + title: 'month'.t(context: context), + value: GroupAssetsBy.month, + ), + SettingsRadioGroup( + title: 'asset_list_layout_settings_group_automatically'.t(context: context), + value: GroupAssetsBy.auto, + ), ], groupBy: groupBy, onRadioChanged: changeGroupValue, 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 bcb4a5ec9c..2d5c9f06eb 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,11 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class LayoutSettings extends HookConsumerWidget { @@ -19,12 +21,16 @@ class LayoutSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "asset_list_layout_sub_title".tr()), - SettingsSwitchListTile( - valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".tr(), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + SettingGroupTitle( + title: "asset_list_layout_sub_title".t(context: context), + icon: Icons.view_module_outlined, ), + if (!Store.isBetaTimelineEnabled) + SettingsSwitchListTile( + valueNotifier: useDynamicLayout, + title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), SettingsSliderListTile( valueNotifier: tilesPerRow, text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart index 5dea38d85e..1555790ff9 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart'; +import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; import 'video_viewer_settings.dart'; @@ -8,7 +9,11 @@ class AssetViewerSettings extends StatelessWidget { @override Widget build(BuildContext context) { - final assetViewerSetting = [const ImageViewerQualitySetting(), const VideoViewerSettings()]; + final assetViewerSetting = [ + const ImageViewerQualitySetting(), + const ImageViewerTapToNavigateSetting(), + const VideoViewerSettings(), + ]; return SettingsSubPageScaffold(settings: assetViewerSetting, showDivider: true); } diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index aed88b90b0..e437b82dd4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -1,10 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -19,21 +18,21 @@ class ImageViewerQualitySetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "setting_image_viewer_title".tr()), - ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), - title: Text('setting_image_viewer_help', style: context.textTheme.bodyMedium).tr(), + SettingGroupTitle( + title: "photos".t(context: context), + icon: Icons.image_outlined, + subtitle: "setting_image_viewer_help".t(context: context), ), SettingsSwitchListTile( valueNotifier: isPreview, - title: "setting_image_viewer_preview_title".tr(), - subtitle: "setting_image_viewer_preview_subtitle".tr(), + title: "setting_image_viewer_preview_title".t(context: context), + subtitle: "setting_image_viewer_preview_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: isOriginal, - title: "setting_image_viewer_original_title".tr(), - subtitle: "setting_image_viewer_original_subtitle".tr(), + title: "setting_image_viewer_original_title".t(context: context), + subtitle: "setting_image_viewer_original_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart new file mode 100644 index 0000000000..759162cab8 --- /dev/null +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -0,0 +1,30 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +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'; +import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class ImageViewerTapToNavigateSetting extends HookConsumerWidget { + const ImageViewerTapToNavigateSetting({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tapToNavigate = useAppSettingsState(AppSettingsEnum.tapToNavigate); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "setting_image_navigation_title".tr()), + SettingsSwitchListTile( + valueNotifier: tapToNavigate, + title: "setting_image_navigation_enable_title".tr(), + subtitle: "setting_image_navigation_enable_subtitle".tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + ], + ); + } +} 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 9a89b7e1e3..c03dcc51b4 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 @@ -1,9 +1,9 @@ -import 'package:easy_localization/easy_localization.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/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -19,23 +19,26 @@ class VideoViewerSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "videos".tr()), + SettingGroupTitle( + title: "videos".t(context: context), + icon: Icons.video_camera_back_outlined, + ), SettingsSwitchListTile( valueNotifier: useAutoPlayVideo, - title: "setting_video_viewer_auto_play_title".tr(), - subtitle: "setting_video_viewer_auto_play_subtitle".tr(), + title: "setting_video_viewer_auto_play_title".t(context: context), + subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useLoopVideo, - title: "setting_video_viewer_looping_title".tr(), - subtitle: "loop_videos_description".tr(), + title: "setting_video_viewer_looping_title".t(context: context), + subtitle: "loop_videos_description".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useOriginalVideo, - title: "setting_video_viewer_original_video_title".tr(), - subtitle: "setting_video_viewer_original_video_subtitle".tr(), + title: "setting_video_viewer_original_video_title".t(context: context), + subtitle: "setting_video_viewer_original_video_subtitle".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 743d38fc48..2c179c42ea 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -16,6 +16,8 @@ import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; class DriftBackupSettings extends ConsumerWidget { @@ -25,36 +27,25 @@ class DriftBackupSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return SettingsSubPageScaffold( settings: [ - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "network_requirements".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), - ), + SettingGroupTitle( + title: "network_requirements".t(context: context), + icon: Icons.cell_tower, ), const _UseWifiForUploadVideosButton(), const _UseWifiForUploadPhotosButton(), if (CurrentPlatform.isAndroid) ...[ const Divider(), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "background_options".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), + SettingGroupTitle( + title: "background_options".t(context: context), + icon: Icons.charging_station_rounded, ), const _BackupOnlyWhenChargingButton(), const _BackupDelaySlider(), ], const Divider(), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - "backup_albums_sync".t(context: context).toUpperCase(), - style: context.textTheme.labelSmall?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.7)), - ), + SettingGroupTitle( + title: "backup_albums_sync".t(context: context), + icon: Icons.sync, ), const _AlbumSyncActionButton(), ], @@ -105,81 +96,67 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> @override Widget build(BuildContext context) { - return ListView( - shrinkWrap: true, - children: [ - StreamBuilder( - stream: Store.watch(StoreKey.syncAlbums), - initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, - builder: (context, snapshot) { - final albumSyncEnable = snapshot.data ?? false; - return Column( - children: [ - ListTile( - title: Text( - "sync_albums".t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text( - "sync_upload_album_setting_subtitle".t(context: context), - style: context.textTheme.labelLarge, - ), - trailing: Switch( - value: albumSyncEnable, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ListView( + shrinkWrap: true, + children: [ + StreamBuilder( + stream: Store.watch(StoreKey.syncAlbums), + initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, + builder: (context, snapshot) { + final albumSyncEnable = snapshot.data ?? false; + return Column( + children: [ + SettingListTile( + title: "sync_albums".t(context: context), + subtitle: "sync_upload_album_setting_subtitle".t(context: context), + trailing: Switch( + value: albumSyncEnable, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue); - if (newValue == true) { - await _manageLinkedAlbums(); - } - }, + if (newValue == true) { + await _manageLinkedAlbums(); + } + }, + ), ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: albumSyncEnable ? 1.0 : 0.0, - child: albumSyncEnable - ? ListTile( - onTap: _manualSyncAlbums, - contentPadding: const EdgeInsets.only(left: 32, right: 16), - title: Text( - "organize_into_albums".t(context: context), - style: context.textTheme.titleSmall?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.normal, - ), - ), - subtitle: Text( - "organize_into_albums_description".t(context: context), - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - trailing: isAlbumSyncInProgress - ? const SizedBox( - width: 32, - height: 32, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ) - : IconButton( - onPressed: _manualSyncAlbums, - icon: const Icon(Icons.sync_rounded), - color: context.colorScheme.onSurface.withValues(alpha: 0.7), - iconSize: 20, - constraints: const BoxConstraints(minWidth: 32, minHeight: 32), - ), - ) - : const SizedBox.shrink(), + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: albumSyncEnable ? 1.0 : 0.0, + child: albumSyncEnable + ? SettingListTile( + onTap: _manualSyncAlbums, + contentPadding: const EdgeInsets.only(left: 32, right: 16), + title: "organize_into_albums".t(context: context), + subtitle: "organize_into_albums_description".t(context: context), + trailing: isAlbumSyncInProgress + ? const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ) + : IconButton( + onPressed: _manualSyncAlbums, + icon: const Icon(Icons.sync_rounded), + color: context.colorScheme.onSurface.withValues(alpha: 0.7), + iconSize: 20, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ) + : const SizedBox.shrink(), + ), ), - ), - ], - ); - }, - ), - ], + ], + ); + }, + ), + ], + ), ); } } @@ -222,24 +199,24 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> { @override Widget build(BuildContext context) { - return ListTile( - title: Text( - widget.titleKey.t(context: context), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), - ), - subtitle: Text(widget.subtitleKey.t(context: context), style: context.textTheme.labelLarge), - trailing: StreamBuilder( - stream: valueStream, - initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, - builder: (context, snapshot) { - final value = snapshot.data ?? false; - return Switch( - value: value, - onChanged: (bool newValue) async { - await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); - }, - ); - }, + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: SettingListTile( + title: widget.titleKey.t(context: context), + subtitle: widget.subtitleKey.t(context: context), + trailing: StreamBuilder( + stream: valueStream, + initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, + builder: (context, snapshot) { + final value = snapshot.data ?? false; + return Switch( + value: value, + onChanged: (bool newValue) async { + await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue); + }, + ); + }, + ), ), ); } @@ -349,12 +326,12 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0), + padding: const EdgeInsets.only(left: 24.0, top: 8.0), child: Text( 'backup_controller_page_background_delay'.tr( namedArgs: {'duration': formatBackupDelaySliderValue(currentValue)}, ), - style: context.textTheme.titleMedium?.copyWith(color: context.primaryColor), + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), ), Slider( 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 index d9a0bae606..be28162b98 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/entity_count_tile.dart @@ -34,33 +34,36 @@ class EntityCountTile extends StatelessWidget { children: [ // Icon and Label Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: context.primaryColor), - const SizedBox(width: 8), + Icon(icon, color: context.primaryColor, size: 14), + const SizedBox(width: 4), Flexible( child: Text( label, - style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16), + style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w500), ), ), ], ), // Number const Spacer(), - 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), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: RichText( + text: TextSpan( + style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode'), + children: [ + TextSpan( + text: zeroPadding(count, maxDigits), + style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)), + ), + TextSpan( + text: count.toString(), + style: TextStyle(color: context.colorScheme.onSurface), + ), + ], + ), ), ), ], diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index d4730951c0..92787077a1 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -5,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -13,9 +15,12 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -25,6 +30,8 @@ class SyncStatusAndActions extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final serverVersion = ref.watch(serverInfoProvider.select((value) => value.serverVersion)); + Future exportDatabase() async { try { // WAL Checkpoint to ensure all changes are written to the database @@ -82,25 +89,27 @@ class SyncStatusAndActions extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text("reset_sqlite".t(context: context)), - content: Text("reset_sqlite_confirmation".t(context: context)), + title: Text(context.t.reset_sqlite), + content: Text(context.t.reset_sqlite_confirmation), actions: [ - TextButton( - onPressed: () => context.pop(), - child: Text("cancel".t(context: context)), - ), + TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel)), TextButton( onPressed: () async { await ref.read(driftProvider).reset(); context.pop(); - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("reset_sqlite_success".t(context: context))), + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: Text(context.t.reset_sqlite_success), + content: Text(context.t.reset_sqlite_done), + actions: [TextButton(onPressed: () => ctx.pop(), child: Text(context.t.ok))], + ), + ), ); }, - child: Text( - "confirm".t(context: context), - style: TextStyle(color: context.colorScheme.error), - ), + child: Text(context.t.confirm, style: TextStyle(color: context.colorScheme.error)), ), ], ); @@ -112,48 +121,47 @@ class SyncStatusAndActions extends HookConsumerWidget { padding: const EdgeInsets.only(top: 16, bottom: 96), children: [ const _SyncStatsCounts(), - const Divider(height: 1, indent: 16, endIndent: 16), - const SizedBox(height: 24), - _SectionHeaderText(text: "jobs".t(context: context)), - ListTile( - title: Text( - "sync_local".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("tap_to_run_job".t(context: context)), + const Divider(height: 10), + const SizedBox(height: 16), + SettingGroupTitle(title: "jobs".t(context: context)), + SettingListTile( + title: "sync_local".t(context: context), + subtitle: "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)), + SettingListTile( + title: "sync_remote".t(context: context), + subtitle: "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), + if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) + SettingListTile( + title: "Sync Cloud Ids".t(context: context), + leading: const Icon(Icons.cloud_circle_rounded), + subtitle: "tap_to_run_job".t(context: context), + trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus), + onTap: ref.read(backgroundSyncProvider).syncCloudIds, ), + SettingListTile( + title: "hash_asset".t(context: context), leading: const Icon(Icons.tag), - subtitle: Text("tap_to_run_job".t(context: context)), + subtitle: "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)), + const Divider(height: 1), + const SizedBox(height: 16), + SettingGroupTitle(title: "actions".t(context: context)), ListTile( title: Text( "clear_file_cache".t(context: context), @@ -194,7 +202,7 @@ class _SyncStatusIcon extends StatelessWidget { @override Widget build(BuildContext context) { return switch (status) { - SyncStatus.idle => const Icon(Icons.pause_circle_outline_rounded), + SyncStatus.idle => const SizedBox.shrink(), 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), @@ -202,26 +210,6 @@ class _SyncStatusIcon extends StatelessWidget { } } -class _SectionHeaderText extends StatelessWidget { - final String text; - - const _SectionHeaderText({required this.text}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - text.toUpperCase(), - style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(200), - ), - ), - ); - } -} - class _SyncStatsCounts extends ConsumerWidget { const _SyncStatsCounts(); @@ -279,9 +267,9 @@ class _SyncStatsCounts extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - _SectionHeaderText(text: "assets".t(context: context)), + SettingGroupTitle(title: "assets".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), // 1. Wrap in IntrinsicHeight child: IntrinsicHeight( child: Flex( @@ -309,9 +297,9 @@ class _SyncStatsCounts extends ConsumerWidget { ), ), ), - _SectionHeaderText(text: "albums".t(context: context)), + SettingGroupTitle(title: "albums".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( child: Flex( direction: Axis.horizontal, @@ -337,9 +325,9 @@ class _SyncStatsCounts extends ConsumerWidget { ), ), ), - _SectionHeaderText(text: "other".t(context: context)), + SettingGroupTitle(title: "other".t(context: context)), Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( child: Flex( direction: Axis.horizontal, @@ -368,7 +356,7 @@ class _SyncStatsCounts extends ConsumerWidget { // To be removed once the experimental feature is stable if (CurrentPlatform.isAndroid && appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ - _SectionHeaderText(text: "trash".t(context: context)), + SettingGroupTitle(title: "trash".t(context: context)), Consumer( builder: (context, ref, _) { final counts = ref.watch(trashedAssetsCountProvider); diff --git a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart index 480665e614..21e0edb34c 100644 --- a/mobile/lib/widgets/settings/beta_timeline_list_tile.dart +++ b/mobile/lib/widgets/settings/beta_timeline_list_tile.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; class BetaTimelineListTile extends ConsumerWidget { const BetaTimelineListTile({super.key}); @@ -56,8 +57,8 @@ class BetaTimelineListTile extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 4.0), - child: ListTile( - title: Text("new_timeline".t(context: context)), + child: SettingListTile( + title: "new_timeline".t(context: context), trailing: Switch.adaptive( value: betaTimelineValue, onChanged: onSwitchChanged, diff --git a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart index c3bb64faf6..d7e547054e 100644 --- a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart +++ b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custom_proxy_headers_settings.dart @@ -1,9 +1,8 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; class CustomProxyHeaderSettings extends StatelessWidget { @@ -15,11 +14,11 @@ class CustomProxyHeaderSettings extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 20), dense: true, title: Text( - IntlKeys.advanced_settings_proxy_headers_title.tr(), + context.t.advanced_settings_proxy_headers_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), subtitle: Text( - IntlKeys.advanced_settings_proxy_headers_subtitle.tr(), + context.t.advanced_settings_proxy_headers_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onTap: () => context.pushRoute(const HeaderSettingsRoute()), diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart new file mode 100644 index 0000000000..ee7ee20b00 --- /dev/null +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -0,0 +1,907 @@ +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/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/cleanup.provider.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +class FreeUpSpaceSettings extends ConsumerStatefulWidget { + const FreeUpSpaceSettings({super.key}); + + @override + ConsumerState createState() => _FreeUpSpaceSettingsState(); +} + +class _FreeUpSpaceSettingsState extends ConsumerState { + CleanupStep _currentStep = CleanupStep.selectDate; + bool _hasScanned = false; + bool _isKeepSettingsExpanded = false; + + @override + void initState() { + super.initState(); + WakelockPlus.enable(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initializeAlbumDefaults(); + }); + } + + Future _initializeAlbumDefaults() async { + final albums = await ref.read(localAlbumProvider.future); + final existingAlbumIds = albums.map((a) => a.id).toSet(); + final albumsWithNames = albums.map((a) => (a.id, a.name)).toList(); + + final notifier = ref.read(cleanupProvider.notifier); + notifier.applyDefaultAlbumSelections(albumsWithNames); + notifier.cleanupStaleAlbumIds(existingAlbumIds); + } + + void _resetState() { + ref.read(cleanupProvider.notifier).reset(); + _hasScanned = false; + } + + CleanupStep get _calculatedStep { + final state = ref.read(cleanupProvider); + + if (state.assetsToDelete.isNotEmpty) { + return CleanupStep.delete; + } + + if (state.selectedDate != null) { + return CleanupStep.scan; + } + + return CleanupStep.selectDate; + } + + void _goToScanStep() { + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + setState(() => _currentStep = CleanupStep.scan); + _scanAssets(); + } + + void _setPresetDate(int daysAgo) { + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + final date = DateTime.now().subtract(Duration(days: daysAgo)); + ref.read(cleanupProvider.notifier).setSelectedDate(date); + setState(() => _hasScanned = false); + } + + bool _isPresetSelected(int? daysAgo) { + final state = ref.read(cleanupProvider); + if (state.selectedDate == null) return false; + + final expectedDate = daysAgo != null ? DateTime.now().subtract(Duration(days: daysAgo)) : DateTime(2000); + + // Check if dates match (ignoring time component) + return state.selectedDate!.year == expectedDate.year && + state.selectedDate!.month == expectedDate.month && + state.selectedDate!.day == expectedDate.day; + } + + Future _selectDate() async { + final state = ref.read(cleanupProvider); + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: state.selectedDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + + if (picked != null) { + ref.read(cleanupProvider.notifier).setSelectedDate(picked); + setState(() => _hasScanned = false); + } + } + + void _onKeepSettingsChanged() { + setState(() { + _hasScanned = false; + _currentStep = CleanupStep.scan; + }); + } + + Future _scanAssets() async { + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + + await ref.read(cleanupProvider.notifier).scanAssets(); + final state = ref.read(cleanupProvider); + + setState(() { + _hasScanned = true; + if (state.assetsToDelete.isNotEmpty) { + _currentStep = CleanupStep.delete; + } + }); + } + + Future _deleteAssets() async { + final state = ref.read(cleanupProvider); + + if (state.assetsToDelete.isEmpty || state.selectedDate == null) { + return; + } + + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + final confirmed = await showDialog( + context: context, + builder: (ctx) => + _DeleteConfirmationDialog(assetCount: state.assetsToDelete.length, cutoffDate: state.selectedDate!), + ); + + if (confirmed != true) { + return; + } + + final deletedCount = await ref.read(cleanupProvider.notifier).deleteAssets(); + + if (mounted && deletedCount > 0) { + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + + await showDialog( + context: context, + builder: (ctx) => _DeleteSuccessDialog(deletedCount: deletedCount), + ); + + if (mounted) { + context.router.popUntilRoot(); + } + return; + } + + setState(() => _currentStep = CleanupStep.selectDate); + } + + void _showAssetsPreview(List assets) { + ref.read(hapticFeedbackProvider.notifier).mediumImpact(); + context.pushRoute(CleanupPreviewRoute(assets: assets)); + } + + @override + dispose() { + super.dispose(); + WakelockPlus.disable(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(cleanupProvider); + final hasDate = state.selectedDate != null; + final hasAssets = _hasScanned && state.assetsToDelete.isNotEmpty; + final subtitleStyle = context.textTheme.bodyMedium!.copyWith( + color: context.textTheme.bodyMedium!.color!.withAlpha(215), + ); + + StepStyle styleForState(StepState stepState, {bool isDestructive = false}) { + switch (stepState) { + case StepState.complete: + return StepStyle( + color: context.colorScheme.primary, + indexStyle: TextStyle(color: context.colorScheme.onPrimary, fontWeight: FontWeight.w500), + ); + case StepState.disabled: + return StepStyle( + color: context.colorScheme.onSurface.withValues(alpha: 0.38), + indexStyle: TextStyle(color: context.colorScheme.surface, fontWeight: FontWeight.w500), + ); + case StepState.indexed: + case StepState.editing: + case StepState.error: + if (isDestructive) { + return StepStyle( + color: context.colorScheme.error, + indexStyle: TextStyle(color: context.colorScheme.onError, fontWeight: FontWeight.w500), + ); + } + return StepStyle( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + indexStyle: TextStyle(color: context.colorScheme.surface, fontWeight: FontWeight.w500), + ); + } + } + + final step1State = hasDate ? StepState.complete : StepState.indexed; + final step2State = hasAssets + ? StepState.complete + : hasDate + ? StepState.indexed + : StepState.disabled; + final step3State = hasAssets ? StepState.indexed : StepState.disabled; + + final hasKeepSettings = + state.keepFavorites || state.keepAlbumIds.isNotEmpty || state.keepMediaType != AssetKeepType.none; + + String getKeepSettingsSummary() { + final parts = []; + + if (state.keepMediaType == AssetKeepType.photosOnly) { + parts.add('all_photos'.t(context: context)); + } else if (state.keepMediaType == AssetKeepType.videosOnly) { + parts.add('all_videos'.t(context: context)); + } + + if (state.keepFavorites) { + parts.add('favorites'.t(context: context)); + } + + if (state.keepAlbumIds.isNotEmpty) { + parts.add('keep_albums_count'.t(context: context, args: {'count': state.keepAlbumIds.length.toString()})); + } + + if (parts.isEmpty) { + return 'none'.t(context: context); + } + + return parts.join(', '); + } + + return PopScope( + onPopInvokedWithResult: (didPop, result) { + if (didPop) { + _resetState(); + } + }, + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerLow, + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)), + ), + child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium), + ), + ), + + // Keep on device settings card + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide( + color: hasKeepSettings + ? context.colorScheme.primary.withValues(alpha: 0.5) + : context.colorScheme.outlineVariant, + width: hasKeepSettings ? 1.5 : 1, + ), + ), + color: hasKeepSettings + ? context.colorScheme.primaryContainer.withValues(alpha: 0.15) + : context.colorScheme.surfaceContainerLow, + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + initiallyExpanded: _isKeepSettingsExpanded, + onExpansionChanged: (expanded) { + setState(() => _isKeepSettingsExpanded = expanded); + }, + leading: Icon( + hasKeepSettings ? Icons.bookmark : Icons.bookmark_border, + color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, + ), + title: Text( + 'keep_on_device'.t(context: context), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + color: hasKeepSettings ? context.colorScheme.primary : null, + ), + ), + subtitle: Text( + hasKeepSettings + ? 'keeping'.t(context: context, args: {'items': getKeepSettingsSummary()}) + : 'keep_on_device_hint'.t(context: context), + style: context.textTheme.bodySmall?.copyWith( + color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, + ), + ), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('keep_description'.t(context: context), style: subtitleStyle), + const SizedBox(height: 4), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text( + 'keep_favorites'.t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), + + value: state.keepFavorites, + onChanged: (value) { + ref.read(cleanupProvider.notifier).setKeepFavorites(value); + _onKeepSettingsChanged(); + }, + ), + const SizedBox(height: 8), + _KeepAlbumsSection( + albumIds: state.keepAlbumIds, + onAlbumToggled: (albumId) { + ref.read(cleanupProvider.notifier).toggleKeepAlbum(albumId); + _onKeepSettingsChanged(); + }, + ), + const SizedBox(height: 16), + Text( + 'always_keep'.t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), + const SizedBox(height: 4), + SegmentedButton( + showSelectedIcon: false, + segments: [ + const ButtonSegment(value: AssetKeepType.none, label: Text('—')), + ButtonSegment( + value: AssetKeepType.photosOnly, + label: Text('photos'.t(context: context)), + icon: const Icon(Icons.photo), + ), + ButtonSegment( + value: AssetKeepType.videosOnly, + label: Text('videos'.t(context: context)), + icon: const Icon(Icons.videocam), + ), + ], + selected: {state.keepMediaType}, + onSelectionChanged: (selection) { + ref.read(cleanupProvider.notifier).setKeepMediaType(selection.first); + _onKeepSettingsChanged(); + }, + ), + if (state.keepMediaType != AssetKeepType.none) ...[ + const SizedBox(height: 8), + Text( + state.keepMediaType == AssetKeepType.photosOnly + ? 'always_keep_photos_hint'.t(context: context) + : 'always_keep_videos_hint'.t(context: context), + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 8), + + Stepper( + physics: const NeverScrollableScrollPhysics(), + currentStep: _currentStep.index, + onStepTapped: (step) { + // Only allow going back or to completed steps + if (step <= _calculatedStep.index) { + setState(() => _currentStep = CleanupStep.values[step]); + } + }, + controlsBuilder: (_, __) => const SizedBox.shrink(), + steps: [ + // Step 1: Select Cutoff Date + Step( + stepStyle: styleForState(step1State), + title: Text( + 'select_cutoff_date'.t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: step1State == StepState.complete + ? context.colorScheme.primary + : context.colorScheme.onSurface, + ), + ), + subtitle: hasDate + ? Text( + DateFormat.yMMMd().format(state.selectedDate!), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ) + : null, + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('cutoff_date_description'.t(context: context), style: subtitleStyle), + const SizedBox(height: 16), + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + childAspectRatio: 1.4, + children: [ + _DatePresetCard( + value: '30', + unit: 'cutoff_day'.t(context: context, args: {'count': '30'}), + onTap: () => _setPresetDate(30), + isSelected: _isPresetSelected(30), + ), + _DatePresetCard( + value: '60', + unit: 'cutoff_day'.t(context: context, args: {'count': '60'}), + + onTap: () => _setPresetDate(60), + isSelected: _isPresetSelected(60), + ), + _DatePresetCard( + value: '90', + unit: 'cutoff_day'.t(context: context, args: {'count': '90'}), + + onTap: () => _setPresetDate(90), + isSelected: _isPresetSelected(90), + ), + _DatePresetCard( + value: '1', + unit: 'cutoff_year'.t(context: context, args: {'count': '1'}), + onTap: () => _setPresetDate(365), + isSelected: _isPresetSelected(365), + ), + _DatePresetCard( + value: '2', + unit: 'cutoff_year'.t(context: context, args: {'count': '2'}), + onTap: () => _setPresetDate(730), + isSelected: _isPresetSelected(730), + ), + _DatePresetCard( + value: '3', + unit: 'cutoff_year'.t(context: context, args: {'count': '3'}), + onTap: () => _setPresetDate(1095), + isSelected: _isPresetSelected(1095), + ), + ], + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: _selectDate, + icon: const Icon(Icons.calendar_today), + label: Text('custom_date'.t(context: context)), + style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: hasDate ? _goToScanStep : null, + icon: const Icon(Icons.arrow_forward), + label: Text('continue'.t(context: context)), + style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), + ), + ], + ), + isActive: true, + state: step1State, + ), + + // Step 2: Scan Assets + Step( + stepStyle: styleForState(step2State), + title: Text( + 'scan'.t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: step2State == StepState.complete + ? context.colorScheme.primary + : step2State == StepState.disabled + ? context.colorScheme.onSurface.withValues(alpha: 0.38) + : context.colorScheme.onSurface, + ), + ), + subtitle: _hasScanned + ? Text( + state.totalBytes > 0 + ? 'cleanup_found_assets_with_size'.t( + context: context, + args: { + 'count': state.assetsToDelete.length.toString(), + 'size': formatBytes(state.totalBytes), + }, + ) + : 'cleanup_found_assets'.t( + context: context, + args: {'count': state.assetsToDelete.length.toString()}, + ), + style: context.textTheme.bodyMedium?.copyWith( + color: state.assetsToDelete.isNotEmpty + ? context.colorScheme.primary + : context.colorScheme.onSurface.withValues(alpha: 0.6), + fontWeight: FontWeight.w500, + ), + ) + : null, + content: Column( + children: [ + Text('cleanup_step3_description'.t(context: context), style: subtitleStyle), + if (CurrentPlatform.isIOS) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: context.colorScheme.primaryContainer.withValues(alpha: 0.3), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + child: Row( + children: [ + Icon(Icons.info_outline, color: context.colorScheme.primary), + const SizedBox(width: 12), + Expanded( + child: Text( + 'cleanup_icloud_shared_albums_excluded'.t(context: context), + style: context.textTheme.labelLarge, + ), + ), + ], + ), + ), + ], + const SizedBox(height: 16), + state.isScanning + ? SizedBox( + width: 28, + height: 28, + child: CircularProgressIndicator( + strokeWidth: 2, + backgroundColor: context.colorScheme.primary.withAlpha(50), + ), + ) + : ElevatedButton.icon( + onPressed: state.isScanning ? null : _scanAssets, + icon: const Icon(Icons.search), + label: Text(_hasScanned ? 'rescan'.t(context: context) : 'scan'.t(context: context)), + style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), + ), + if (_hasScanned && state.assetsToDelete.isEmpty) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Row( + children: [ + const Icon(Icons.info, color: Colors.orange), + const SizedBox(width: 12), + Expanded( + child: Text( + 'cleanup_no_assets_found'.t(context: context), + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ), + ], + ], + ), + isActive: hasDate, + state: step2State, + ), + + // Step 3: Delete Assets + Step( + stepStyle: styleForState(step3State, isDestructive: true), + title: Text( + 'move_to_device_trash'.t(context: context), + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: step3State == StepState.disabled + ? context.colorScheme.onSurface.withValues(alpha: 0.38) + : context.colorScheme.error, + ), + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.errorContainer.withValues(alpha: 0.3), + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: context.colorScheme.error.withValues(alpha: 0.3)), + ), + child: hasAssets + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'cleanup_step4_summary'.t( + context: context, + args: { + 'count': state.assetsToDelete.length.toString(), + 'date': DateFormat.yMMMd().format(state.selectedDate!), + }, + ), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15), + ), + ], + ) + : null, + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: () => _showAssetsPreview(state.assetsToDelete), + icon: const Icon(Icons.preview), + label: Text('preview'.t(context: context)), + style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), + ), + const SizedBox(height: 12), + ElevatedButton.icon( + onPressed: state.isDeleting ? null : _deleteAssets, + icon: state.isDeleting + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.delete_forever), + label: Text( + state.isDeleting + ? 'cleanup_deleting'.t(context: context) + : 'move_to_device_trash'.t(context: context), + ), + style: ElevatedButton.styleFrom( + backgroundColor: context.colorScheme.error, + foregroundColor: context.colorScheme.onError, + minimumSize: const Size(double.infinity, 56), + textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + ), + ], + ), + isActive: hasAssets, + state: step3State, + ), + ], + ), + const SizedBox(height: 60), + ], + ), + ), + ); + } +} + +class _DeleteConfirmationDialog extends StatelessWidget { + final int assetCount; + final DateTime cutoffDate; + + const _DeleteConfirmationDialog({required this.assetCount, required this.cutoffDate}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('cleanup_confirm_prompt_title'.t(context: context)), + content: Text( + 'cleanup_confirm_description'.t( + context: context, + args: {'count': assetCount.toString(), 'date': DateFormat.yMMMd().format(cutoffDate)}, + ), + style: context.textTheme.labelLarge?.copyWith(fontSize: 15), + ), + actions: [ + TextButton( + onPressed: () => context.pop(false), + child: Text('cancel'.t(context: context)), + ), + ElevatedButton( + onPressed: () => context.pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: context.colorScheme.error, + foregroundColor: context.colorScheme.onError, + ), + child: Text('confirm'.t(context: context)), + ), + ], + ); + } +} + +class _DeleteSuccessDialog extends StatelessWidget { + final int deletedCount; + + const _DeleteSuccessDialog({required this.deletedCount}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: Icon(Icons.check_circle, color: context.colorScheme.primary, size: 48), + title: Text('success'.t(context: context)), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'cleanup_deleted_assets'.t(context: context, args: {'count': deletedCount.toString()}), + style: context.textTheme.labelLarge?.copyWith(fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + 'cleanup_trash_hint'.t(context: context), + style: context.textTheme.labelLarge?.copyWith(fontSize: 16, color: context.primaryColor), + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () => context.pop(), + child: Text('done'.t(context: context)), + ), + ], + ); + } +} + +class _DatePresetCard extends StatelessWidget { + final String value; + final String unit; + final VoidCallback onTap; + final bool isSelected; + + const _DatePresetCard({required this.value, required this.unit, required this.onTap, required this.isSelected}); + + @override + Widget build(BuildContext context) { + return Material( + color: isSelected ? context.colorScheme.primaryContainer.withAlpha(100) : context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(12)), + border: Border.all(color: isSelected ? context.colorScheme.primary : Colors.transparent, width: 1), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + value, + style: context.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: isSelected ? context.colorScheme.primary : context.colorScheme.onSurface, + ), + ), + Text( + unit, + style: context.textTheme.bodySmall?.copyWith( + color: isSelected + ? context.colorScheme.primary + : context.colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _KeepAlbumsSection extends ConsumerWidget { + final Set albumIds; + final ValueChanged onAlbumToggled; + + const _KeepAlbumsSection({required this.albumIds, required this.onAlbumToggled}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albumsAsync = ref.watch(localAlbumProvider); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'keep_albums'.t(context: context), + style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), + ), + + const SizedBox(height: 8), + albumsAsync.when( + loading: () => const Center( + child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2)), + ), + error: (error, stack) => Text( + 'error_loading_albums'.t(context: context), + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), + ), + data: (albums) { + if (albums.isEmpty) { + return Text( + 'no_albums_found'.t(context: context), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ); + } + return Container( + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outlineVariant), + borderRadius: const BorderRadius.all(Radius.circular(12)), + ), + constraints: const BoxConstraints(maxHeight: 200), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: ListView.builder( + shrinkWrap: true, + itemCount: albums.length, + itemBuilder: (context, index) { + final album = albums[index]; + final isSelected = albumIds.contains(album.id); + return _AlbumTile(album: album, isSelected: isSelected, onToggle: () => onAlbumToggled(album.id)); + }, + ), + ), + ); + }, + ), + if (albumIds.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + 'keep_albums_count'.t(context: context, args: {'count': albumIds.length.toString()}), + style: context.textTheme.bodySmall?.copyWith( + color: context.colorScheme.primary, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ); + } +} + +class _AlbumTile extends StatelessWidget { + final LocalAlbum album; + final bool isSelected; + final VoidCallback onToggle; + + const _AlbumTile({required this.album, required this.isSelected, required this.onToggle}); + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + leading: Icon( + isSelected ? Icons.check_circle : Icons.circle_outlined, + color: isSelected ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, + size: 20, + ), + title: Text( + album.name, + style: context.textTheme.bodyMedium?.copyWith(color: isSelected ? context.colorScheme.primary : null), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + album.assetCount.toString(), + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceVariant), + ), + onTap: onToggle, + ); + } +} diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index a712ce416c..735971e0c2 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState { autovalidateMode: AutovalidateMode.onUserInteraction, validator: validateUrl, keyboardType: TextInputType.url, - style: const TextStyle(fontFamily: 'Inconsolata', fontWeight: FontWeight.w600, fontSize: 14), + style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 14), decoration: InputDecoration( hintText: 'http(s)://immich.domain.com', contentPadding: const EdgeInsets.all(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 8cc6079961..ba21acf49c 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,12 +1,12 @@ import 'dart:convert'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; @@ -54,7 +54,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { saveEndpointList(); } - Widget proxyDecorator(Widget child, int index, Animation animation) { + Widget proxyDecorator(Widget child, int _, Animation animation) { return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { @@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("external_network_sheet_info".tr(), style: context.textTheme.bodyMedium), + child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.add), - label: Text('add_endpoint'.tr().toUpperCase()), + label: Text('add_endpoint'.t(context: context)), onPressed: enabled ? () { entries.value = [ 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 21e26c8f1f..c89c8e149e 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -5,6 +5,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/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; @@ -155,7 +156,7 @@ class LocalNetworkPreference extends HookConsumerWidget { style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), - fontFamily: 'Inconsolata', + fontFamily: 'GoogleSansCode', ), ), trailing: IconButton( @@ -167,15 +168,14 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.lan_rounded), - title: Text("server_endpoint".tr()), + title: Text("server_endpoint".t(context: context)), subtitle: localEndpointText.value.isEmpty ? const Text("http://local-ip:2283") : Text( localEndpointText.value, style: context.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100), - fontFamily: 'Inconsolata', + fontFamily: 'GoogleSansCode', ), ), trailing: IconButton( @@ -190,7 +190,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'.t(context: context)), 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 272b83c9aa..981bec2c0c 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -3,6 +3,7 @@ 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/translate_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -10,6 +11,7 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; class NetworkingSettings extends HookConsumerWidget { @@ -87,12 +89,10 @@ class NetworkingSettings extends HookConsumerWidget { return ListView( padding: const EdgeInsets.only(bottom: 96), children: [ - Padding( - 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, - ), + const SizedBox(height: 8), + SettingGroupTitle( + title: "current_server_address".t(context: context), + icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -108,12 +108,7 @@ class NetworkingSettings extends HookConsumerWidget { : const Icon(Icons.circle_outlined), title: Text( currentEndpoint ?? "--", - style: TextStyle( - fontSize: 16, - fontFamily: 'Inconsolata', - fontWeight: FontWeight.bold, - color: context.primaryColor, - ), + style: TextStyle(fontSize: 14, fontFamily: 'GoogleSansCode', color: context.primaryColor), ), ), ), @@ -128,14 +123,16 @@ class NetworkingSettings extends HookConsumerWidget { title: "automatic_endpoint_switching_title".tr(), subtitle: "automatic_endpoint_switching_subtitle".tr(), ), - Padding( - padding: const EdgeInsets.only(top: 8, left: 16, bottom: 16), - child: NetworkPreferenceTitle(title: "local_network".tr().toUpperCase(), icon: Icons.home_outlined), + const SizedBox(height: 8), + SettingGroupTitle( + title: "local_network".t(context: context), + icon: Icons.home_outlined, ), LocalNetworkPreference(enabled: featureEnabled.value), - Padding( - padding: const EdgeInsets.only(top: 32, left: 16, bottom: 16), - child: NetworkPreferenceTitle(title: "external_network".tr().toUpperCase(), icon: Icons.dns_outlined), + const SizedBox(height: 16), + SettingGroupTitle( + title: "external_network".t(context: context), + icon: Icons.dns_outlined, ), ExternalNetworkPreference(enabled: featureEnabled.value), ], @@ -143,30 +140,6 @@ class NetworkingSettings extends HookConsumerWidget { } } -class NetworkPreferenceTitle extends StatelessWidget { - const NetworkPreferenceTitle({super.key, required this.icon, required this.title}); - - final IconData icon; - final String title; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Icon(icon, color: context.colorScheme.onSurface.withAlpha(150)), - const SizedBox(width: 8), - Text( - title, - style: context.textTheme.displaySmall?.copyWith( - color: context.colorScheme.onSurface.withAlpha(200), - fontWeight: FontWeight.w500, - ), - ), - ], - ); - } -} - class NetworkStatusIcon extends StatelessWidget { const NetworkStatusIcon({super.key, required this.status, this.enabled = true}) : super(); @@ -175,10 +148,10 @@ class NetworkStatusIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: _buildIcon(context)); + return AnimatedSwitcher(duration: const Duration(milliseconds: 200), child: buildIcon(context)); } - Widget _buildIcon(BuildContext context) => switch (status) { + Widget buildIcon(BuildContext context) => switch (status) { AuxCheckStatus.loading => Padding( padding: const EdgeInsets.only(left: 4.0), child: SizedBox( diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index 49f57a5e94..5e745dd61d 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -1,9 +1,9 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -22,10 +22,13 @@ class HapticSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "haptic_feedback_title".tr()), + SettingGroupTitle( + title: "haptic_feedback_title".t(context: context), + icon: Icons.vibration_outlined, + ), SettingsSwitchListTile( valueNotifier: isHapticFeedbackEnabled, - title: 'haptic_feedback_switch'.tr(), + title: 'enabled'.t(context: context), onChanged: onHapticFeedbackChange, ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 123f7c9921..fc20fb7bed 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -1,12 +1,12 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -74,23 +74,26 @@ class ThemeSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "theme".tr()), + SettingGroupTitle( + title: "theme".t(context: context), + icon: Icons.color_lens_outlined, + ), SettingsSwitchListTile( valueNotifier: isSystemTheme, - title: 'theme_setting_system_theme_switch'.tr(), + title: 'theme_setting_system_theme_switch'.t(context: context), onChanged: onSystemThemeChange, ), if (currentTheme.value != ThemeMode.system) SettingsSwitchListTile( valueNotifier: isDarkTheme, - title: 'map_settings_dark_mode'.tr(), + title: 'map_settings_dark_mode'.t(context: context), onChanged: onThemeChange, ), const PrimaryColorSetting(), SettingsSwitchListTile( valueNotifier: applyThemeToBackgroundProvider, - title: "theme_setting_colorful_interface_title".tr(), - subtitle: 'theme_setting_colorful_interface_subtitle'.tr(), + title: "theme_setting_colorful_interface_title".t(context: context), + subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), onChanged: onSurfaceColorSettingChange, ), ], diff --git a/mobile/lib/widgets/settings/setting_group_title.dart b/mobile/lib/widgets/settings/setting_group_title.dart new file mode 100644 index 0000000000..48b1a9bfba --- /dev/null +++ b/mobile/lib/widgets/settings/setting_group_title.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; + +class SettingGroupTitle extends StatelessWidget { + final String title; + final String? subtitle; + final IconData? icon; + final EdgeInsetsGeometry? contentPadding; + + const SettingGroupTitle({super.key, required this.title, this.icon, this.subtitle, this.contentPadding}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: contentPadding ?? const EdgeInsets.only(left: 20.0, right: 20.0, bottom: 8.0), + child: Column( + children: [ + Row( + children: [ + if (icon != null) ...[ + Icon(icon, color: context.colorScheme.onSurfaceSecondary, size: 20), + const SizedBox(width: 8), + ], + Text(title, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary)), + ], + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: context.textTheme.bodyMedium!.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), + ), + ], + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/setting_list_tile.dart b/mobile/lib/widgets/settings/setting_list_tile.dart new file mode 100644 index 0000000000..17f44f8a85 --- /dev/null +++ b/mobile/lib/widgets/settings/setting_list_tile.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class SettingListTile extends StatelessWidget { + final String title; + final String? subtitle; + final Widget? leading; + final Widget? trailing; + final VoidCallback? onTap; + final EdgeInsetsGeometry? contentPadding; + + const SettingListTile({ + required this.title, + this.subtitle, + this.leading, + this.trailing, + this.onTap, + this.contentPadding, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5)), + subtitle: subtitle != null + ? Text( + subtitle!, + style: context.textTheme.bodyMedium!.copyWith(color: context.textTheme.bodyMedium!.color!.withAlpha(215)), + ) + : null, + leading: leading, + trailing: trailing, + onTap: onTap, + contentPadding: contentPadding, + ); + } +} diff --git a/mobile/lib/widgets/settings/settings_card.dart b/mobile/lib/widgets/settings/settings_card.dart index 36eff7bae1..b5dcaac1ca 100644 --- a/mobile/lib/widgets/settings/settings_card.dart +++ b/mobile/lib/widgets/settings/settings_card.dart @@ -36,11 +36,8 @@ class SettingsCard extends StatelessWidget { 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), + title: Text(title, style: context.textTheme.titleMedium!.copyWith(color: context.primaryColor)), + subtitle: Text(subtitle, style: context.textTheme.bodyMedium), onTap: () => context.pushRoute(settingRoute), ), ), diff --git a/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart b/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart index b4cb67239e..78f483f0a9 100644 --- a/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart +++ b/mobile/lib/widgets/settings/settings_sub_page_scaffold.dart @@ -9,13 +9,11 @@ class SettingsSubPageScaffold extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 20), + padding: const EdgeInsets.symmetric(vertical: 16), itemCount: settings.length, itemBuilder: (ctx, index) => settings[index], separatorBuilder: (context, index) => showDivider - ? const Column( - children: [SizedBox(height: 5), Divider(height: 10, indent: 15, endIndent: 15), SizedBox(height: 15)], - ) + ? const Column(children: [SizedBox(height: 5), Divider(height: 10), SizedBox(height: 15)]) : const SizedBox(height: 10), ); } diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index dc31acf0a4..fa210ee720 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -1,14 +1,13 @@ -import 'dart:io'; - import 'package:easy_localization/easy_localization.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:immich_mobile/platform/network_api.g.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart'; +import 'package:logging/logging.dart'; class SslClientCertSettings extends StatefulWidget { const SslClientCertSettings({super.key, required this.isLoggedIn}); @@ -20,10 +19,12 @@ class SslClientCertSettings extends StatefulWidget { } class _SslClientCertSettingsState extends State { - _SslClientCertSettingsState() : isCertExist = SSLClientCertStoreVal.load() != null; + final _log = Logger("SslClientCertSettings"); bool isCertExist; + _SslClientCertSettingsState() : isCertExist = SSLClientCertStoreVal.load() != null; + @override Widget build(BuildContext context) { return ListTile( @@ -41,16 +42,12 @@ class _SslClientCertSettingsState extends State { const SizedBox(height: 6), Row( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ + ElevatedButton(onPressed: widget.isLoggedIn ? null : importCert, child: Text("client_cert_import".tr())), ElevatedButton( - onPressed: widget.isLoggedIn ? null : () => importCert(context), - child: Text("client_cert_import".tr()), - ), - const SizedBox(width: 15), - ElevatedButton( - onPressed: widget.isLoggedIn || !isCertExist ? null : () async => await removeCert(context), + onPressed: widget.isLoggedIn || !isCertExist ? null : removeCert, child: Text("remove".tr()), ), ], @@ -60,71 +57,52 @@ class _SslClientCertSettingsState extends State { ); } - void showMessage(BuildContext context, String message) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - content: Text(message), - actions: [TextButton(onPressed: () => ctx.pop(), child: Text("client_cert_dialog_msg_confirm".tr()))], + void showMessage(String message) { + context.showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + content: Text(message, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor)), ), ); } - Future storeCert(BuildContext context, Uint8List data, String? password) async { - if (password != null && password.isEmpty) { - password = null; - } - final cert = SSLClientCertStoreVal(data, password); - // Test whether the certificate is valid - final isCertValid = HttpSSLCertOverride.setClientCert(SecurityContext(withTrustedRoots: true), cert); - if (!isCertValid) { - showMessage(context, "client_cert_invalid_msg".tr()); - return; - } - await cert.save(); - HttpSSLOptions.apply(); - setState(() => isCertExist = true); - showMessage(context, "client_cert_import_success_msg".tr()); - } - - void setPassword(BuildContext context, Uint8List data) { - final password = TextEditingController(); - showDialog( - context: context, - barrierDismissible: false, - builder: (ctx) => AlertDialog( - content: TextField( - controller: password, - obscureText: true, - obscuringCharacter: "*", - decoration: InputDecoration(hintText: "client_cert_enter_password".tr()), - ), - actions: [ - TextButton( - onPressed: () async => {ctx.pop(), await storeCert(context, data, password.text)}, - child: Text("client_cert_dialog_msg_confirm".tr()), - ), - ], - ), - ); - } - - Future importCert(BuildContext ctx) async { - FilePickerResult? res = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['p12', 'pfx'], - ); - if (res != null) { - File file = File(res.files.single.path!); - final bytes = await file.readAsBytes(); - setPassword(ctx, bytes); + Future importCert() async { + try { + final styling = ClientCertPrompt( + title: "client_cert_password_title".tr(), + message: "client_cert_password_message".tr(), + cancel: "cancel".tr(), + confirm: "confirm".tr(), + ); + final cert = await networkApi.selectCertificate(styling); + await SSLClientCertStoreVal(cert.data, cert.password).save(); + HttpSSLOptions.apply(); + setState(() => isCertExist = true); + showMessage("client_cert_import_success_msg".tr()); + } catch (e) { + if (_isCancellation(e)) { + return; + } + _log.severe("Error importing client cert", e); + showMessage("client_cert_invalid_msg".tr()); } } - Future removeCert(BuildContext context) async { - await SSLClientCertStoreVal.delete(); - HttpSSLOptions.apply(); - setState(() => isCertExist = false); - showMessage(context, "client_cert_remove_msg".tr()); + Future removeCert() async { + try { + await networkApi.removeCertificate(); + await SSLClientCertStoreVal.delete(); + HttpSSLOptions.apply(); + setState(() => isCertExist = false); + showMessage("client_cert_remove_msg".tr()); + } catch (e) { + if (_isCancellation(e)) { + return; + } + _log.severe("Error removing client cert", e); + showMessage("client_cert_invalid_msg".tr()); + } } + + bool _isCancellation(Object e) => e is PlatformException && e.code.toLowerCase().contains("cancel"); } diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index cbd6e1f077..19da80b833 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -78,7 +78,10 @@ class SharedLinkItem extends ConsumerWidget { return; } - Clipboard.setData(ClipboardData(text: "${serverUrl}share/${sharedLink.key}")).then((_) { + final hasSlug = sharedLink.slug?.isNotEmpty == true; + final urlPath = hasSlug ? sharedLink.slug : sharedLink.key; + final basePath = hasSlug ? 's' : 'share'; + Clipboard.setData(ClipboardData(text: "$serverUrl$basePath/$urlPath")).then((_) { context.scaffoldMessenger.showSnackBar( SnackBar( content: Text( diff --git a/mobile/makefile b/mobile/makefile index b90e95c902..3a0a263687 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -7,15 +7,19 @@ build: pigeon: dart run pigeon --input pigeon/native_sync_api.dart - dart run pigeon --input pigeon/thumbnail_api.dart + dart run pigeon --input pigeon/local_image_api.dart + dart run pigeon --input pigeon/remote_image_api.dart dart run pigeon --input pigeon/background_worker_api.dart dart run pigeon --input pigeon/background_worker_lock_api.dart dart run pigeon --input pigeon/connectivity_api.dart + dart run pigeon --input pigeon/network_api.dart dart format lib/platform/native_sync_api.g.dart - dart format lib/platform/thumbnail_api.g.dart + dart format lib/platform/local_image_api.g.dart + dart format lib/platform/remote_image_api.g.dart dart format lib/platform/background_worker_api.g.dart dart format lib/platform/background_worker_lock_api.g.dart dart format lib/platform/connectivity_api.g.dart + dart format lib/platform/network_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs @@ -33,11 +37,11 @@ migration: dart run drift_dev make-migrations translation: - npm --prefix ../web run format:i18n + pnpm --prefix ../i18n run format:fix 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 + dart format lib/generated/translations.g.dart analyze: dart analyze --fatal-infos diff --git a/mobile/mise.toml b/mobile/mise.toml index cdafd1cc18..88b8902053 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -16,7 +16,15 @@ sources = [ "infrastructure/**/*.drift", ] outputs = { auto = true } -run = "dart run build_runner build --delete-conflicting-outputs" +run = [ + "dart run build_runner build --delete-conflicting-outputs", + "dart format lib/routing/router.gr.dart", +] + +[tasks."codegen:watch"] +alias = "watch" +description = "Watch and auto-generate dart code" +run = "dart run build_runner watch --delete-conflicting-outputs" [tasks."codegen:pigeon"] alias = "pigeon" @@ -32,13 +40,7 @@ depends = [ [tasks."codegen:translation"] alias = "translation" description = "Generate translations from i18n JSONs" -run = [ - { task = "//i18n:format-fix" }, - { tasks = [ - "i18n:loader", - "i18n:keys", - ] }, -] +run = [{ task = "//:i18n:format-fix" }, { tasks = ["i18n:loader", "i18n:keys"] }] [tasks."codegen:app-icon"] description = "Generate app icons" @@ -158,10 +160,10 @@ run = [ description = "Generate i18n keys" hide = true sources = ["i18n/en.json"] -outputs = "lib/generated/intl_keys.g.dart" +outputs = "lib/generated/translations.g.dart" run = [ "dart run bin/generate_keys.dart", - "dart format lib/generated/intl_keys.g.dart", + "dart format lib/generated/translations.g.dart", ] [tasks."analyze:dart"] diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8d4f8a429b..eddebc994f 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 2.4.0 +- API version: 2.5.6 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -100,8 +100,11 @@ Class | Method | HTTP request | Description *AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets +*AssetsApi* | [**deleteBulkAssetMetadata**](doc//AssetsApi.md#deletebulkassetmetadata) | **DELETE** /assets/metadata | Delete asset metadata *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset +*AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Apply edits to an existing asset *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID +*AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key @@ -110,11 +113,13 @@ Class | Method | HTTP request | Description *AssetsApi* | [**getAssetTile**](doc//AssetsApi.md#getassettile) | **GET** /assets/{id}/tiles/{level}/{col}/{row} | Get an image tile *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video +*AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | Update assets +*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password @@ -134,6 +139,11 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**unlockAuthSession**](doc//AuthenticationApi.md#unlockauthsession) | **POST** /auth/session/unlock | Unlock auth session *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | Validate access token *AuthenticationAdminApi* | [**unlinkAllOAuthAccountsAdmin**](doc//AuthenticationAdminApi.md#unlinkalloauthaccountsadmin) | **POST** /admin/auth/unlink-all | Unlink all OAuth accounts +*DatabaseBackupsAdminApi* | [**deleteDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#deletedatabasebackup) | **DELETE** /admin/database-backups | Delete database backup +*DatabaseBackupsAdminApi* | [**downloadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#downloaddatabasebackup) | **GET** /admin/database-backups/{filename} | Download database backup +*DatabaseBackupsAdminApi* | [**listDatabaseBackups**](doc//DatabaseBackupsAdminApi.md#listdatabasebackups) | **GET** /admin/database-backups | List database backups +*DatabaseBackupsAdminApi* | [**startDatabaseRestoreFlow**](doc//DatabaseBackupsAdminApi.md#startdatabaserestoreflow) | **POST** /admin/database-backups/start-restore | Start database backup restore flow +*DatabaseBackupsAdminApi* | [**uploadDatabaseBackup**](doc//DatabaseBackupsAdminApi.md#uploaddatabasebackup) | **POST** /admin/database-backups/upload | Upload database backup *DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner *DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user @@ -162,6 +172,8 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings +*MaintenanceAdminApi* | [**detectPriorInstall**](doc//MaintenanceAdminApi.md#detectpriorinstall) | **GET** /admin/maintenance/detect-install | Detect existing install +*MaintenanceAdminApi* | [**getMaintenanceStatus**](doc//MaintenanceAdminApi.md#getmaintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status *MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode *MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers @@ -200,6 +212,7 @@ Class | Method | HTTP request | Description *PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people *PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person *PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin +*PluginsApi* | [**getPluginTriggers**](doc//PluginsApi.md#getplugintriggers) | **GET** /plugins/triggers | List all plugin triggers *PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins *QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue *QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue @@ -244,6 +257,7 @@ Class | Method | HTTP request | Description *SharedLinksApi* | [**getSharedLinkById**](doc//SharedLinksApi.md#getsharedlinkbyid) | **GET** /shared-links/{id} | Retrieve a shared link *SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} | Delete a shared link *SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets | Remove assets from a shared link +*SharedLinksApi* | [**sharedLinkLogin**](doc//SharedLinksApi.md#sharedlinklogin) | **POST** /shared-links/login | Shared link login *SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} | Update a shared link *StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks | Create a stack *StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | Delete a stack @@ -344,6 +358,12 @@ Class | Method | HTTP request | Description - [AssetCopyDto](doc//AssetCopyDto.md) - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) + - [AssetEditAction](doc//AssetEditAction.md) + - [AssetEditActionItemDto](doc//AssetEditActionItemDto.md) + - [AssetEditActionItemDtoParameters](doc//AssetEditActionItemDtoParameters.md) + - [AssetEditActionItemResponseDto](doc//AssetEditActionItemResponseDto.md) + - [AssetEditsCreateDto](doc//AssetEditsCreateDto.md) + - [AssetEditsResponseDto](doc//AssetEditsResponseDto.md) - [AssetFaceCreateDto](doc//AssetFaceCreateDto.md) - [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md) - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) @@ -358,7 +378,11 @@ Class | Method | HTTP request | Description - [AssetMediaResponseDto](doc//AssetMediaResponseDto.md) - [AssetMediaSize](doc//AssetMediaSize.md) - [AssetMediaStatus](doc//AssetMediaStatus.md) - - [AssetMetadataKey](doc//AssetMetadataKey.md) + - [AssetMetadataBulkDeleteDto](doc//AssetMetadataBulkDeleteDto.md) + - [AssetMetadataBulkDeleteItemDto](doc//AssetMetadataBulkDeleteItemDto.md) + - [AssetMetadataBulkResponseDto](doc//AssetMetadataBulkResponseDto.md) + - [AssetMetadataBulkUpsertDto](doc//AssetMetadataBulkUpsertDto.md) + - [AssetMetadataBulkUpsertItemDto](doc//AssetMetadataBulkUpsertItemDto.md) - [AssetMetadataResponseDto](doc//AssetMetadataResponseDto.md) - [AssetMetadataUpsertDto](doc//AssetMetadataUpsertDto.md) - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) @@ -387,7 +411,12 @@ Class | Method | HTTP request | Description - [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) + - [CropParameters](doc//CropParameters.md) - [DatabaseBackupConfig](doc//DatabaseBackupConfig.md) + - [DatabaseBackupDeleteDto](doc//DatabaseBackupDeleteDto.md) + - [DatabaseBackupDto](doc//DatabaseBackupDto.md) + - [DatabaseBackupListResponseDto](doc//DatabaseBackupListResponseDto.md) + - [DownloadArchiveDto](doc//DownloadArchiveDto.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponse](doc//DownloadResponse.md) @@ -417,7 +446,10 @@ Class | Method | HTTP request | Description - [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md) - [MaintenanceAction](doc//MaintenanceAction.md) - [MaintenanceAuthDto](doc//MaintenanceAuthDto.md) + - [MaintenanceDetectInstallResponseDto](doc//MaintenanceDetectInstallResponseDto.md) + - [MaintenanceDetectInstallStorageFolderDto](doc//MaintenanceDetectInstallStorageFolderDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) + - [MaintenanceStatusResponseDto](doc//MaintenanceStatusResponseDto.md) - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) @@ -431,6 +463,8 @@ Class | Method | HTTP request | Description - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) + - [MirrorAxis](doc//MirrorAxis.md) + - [MirrorParameters](doc//MirrorParameters.md) - [NotificationCreateDto](doc//NotificationCreateDto.md) - [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md) - [NotificationDto](doc//NotificationDto.md) @@ -466,9 +500,10 @@ Class | Method | HTTP request | Description - [PinCodeSetupDto](doc//PinCodeSetupDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md) - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - - [PluginContext](doc//PluginContext.md) + - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) - [PluginResponseDto](doc//PluginResponseDto.md) + - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerType](doc//PluginTriggerType.md) - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) @@ -490,6 +525,7 @@ Class | Method | HTTP request | Description - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) + - [RotateParameters](doc//RotateParameters.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchExploreItem](doc//SearchExploreItem.md) @@ -518,6 +554,7 @@ Class | Method | HTTP request | Description - [SetMaintenanceModeDto](doc//SetMaintenanceModeDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkEditDto](doc//SharedLinkEditDto.md) + - [SharedLinkLoginDto](doc//SharedLinkLoginDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkType](doc//SharedLinkType.md) - [SharedLinksResponse](doc//SharedLinksResponse.md) @@ -529,6 +566,7 @@ Class | Method | HTTP request | Description - [StackResponseDto](doc//StackResponseDto.md) - [StackUpdateDto](doc//StackUpdateDto.md) - [StatisticsSearchDto](doc//StatisticsSearchDto.md) + - [StorageFolder](doc//StorageFolder.md) - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) @@ -539,9 +577,12 @@ Class | Method | HTTP request | Description - [SyncAlbumUserV1](doc//SyncAlbumUserV1.md) - [SyncAlbumV1](doc//SyncAlbumV1.md) - [SyncAssetDeleteV1](doc//SyncAssetDeleteV1.md) + - [SyncAssetEditDeleteV1](doc//SyncAssetEditDeleteV1.md) + - [SyncAssetEditV1](doc//SyncAssetEditV1.md) - [SyncAssetExifV1](doc//SyncAssetExifV1.md) - [SyncAssetFaceDeleteV1](doc//SyncAssetFaceDeleteV1.md) - [SyncAssetFaceV1](doc//SyncAssetFaceV1.md) + - [SyncAssetFaceV2](doc//SyncAssetFaceV2.md) - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) - [SyncAssetV1](doc//SyncAssetV1.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 21730074aa..253e8a6811 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -36,6 +36,7 @@ part 'api/albums_api.dart'; part 'api/assets_api.dart'; part 'api/authentication_api.dart'; part 'api/authentication_admin_api.dart'; +part 'api/database_backups_admin_api.dart'; part 'api/deprecated_api.dart'; part 'api/download_api.dart'; part 'api/duplicates_api.dart'; @@ -95,6 +96,12 @@ part 'model/asset_bulk_upload_check_result.dart'; part 'model/asset_copy_dto.dart'; part 'model/asset_delta_sync_dto.dart'; part 'model/asset_delta_sync_response_dto.dart'; +part 'model/asset_edit_action.dart'; +part 'model/asset_edit_action_item_dto.dart'; +part 'model/asset_edit_action_item_dto_parameters.dart'; +part 'model/asset_edit_action_item_response_dto.dart'; +part 'model/asset_edits_create_dto.dart'; +part 'model/asset_edits_response_dto.dart'; part 'model/asset_face_create_dto.dart'; part 'model/asset_face_delete_dto.dart'; part 'model/asset_face_response_dto.dart'; @@ -109,7 +116,11 @@ part 'model/asset_jobs_dto.dart'; part 'model/asset_media_response_dto.dart'; part 'model/asset_media_size.dart'; part 'model/asset_media_status.dart'; -part 'model/asset_metadata_key.dart'; +part 'model/asset_metadata_bulk_delete_dto.dart'; +part 'model/asset_metadata_bulk_delete_item_dto.dart'; +part 'model/asset_metadata_bulk_response_dto.dart'; +part 'model/asset_metadata_bulk_upsert_dto.dart'; +part 'model/asset_metadata_bulk_upsert_item_dto.dart'; part 'model/asset_metadata_response_dto.dart'; part 'model/asset_metadata_upsert_dto.dart'; part 'model/asset_metadata_upsert_item_dto.dart'; @@ -138,7 +149,12 @@ part 'model/contributor_count_response_dto.dart'; part 'model/create_album_dto.dart'; part 'model/create_library_dto.dart'; part 'model/create_profile_image_response_dto.dart'; +part 'model/crop_parameters.dart'; part 'model/database_backup_config.dart'; +part 'model/database_backup_delete_dto.dart'; +part 'model/database_backup_dto.dart'; +part 'model/database_backup_list_response_dto.dart'; +part 'model/download_archive_dto.dart'; part 'model/download_archive_info.dart'; part 'model/download_info_dto.dart'; part 'model/download_response.dart'; @@ -168,7 +184,10 @@ part 'model/logout_response_dto.dart'; part 'model/machine_learning_availability_checks_dto.dart'; part 'model/maintenance_action.dart'; part 'model/maintenance_auth_dto.dart'; +part 'model/maintenance_detect_install_response_dto.dart'; +part 'model/maintenance_detect_install_storage_folder_dto.dart'; part 'model/maintenance_login_dto.dart'; +part 'model/maintenance_status_response_dto.dart'; part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; @@ -182,6 +201,8 @@ part 'model/memory_type.dart'; part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; +part 'model/mirror_axis.dart'; +part 'model/mirror_parameters.dart'; part 'model/notification_create_dto.dart'; part 'model/notification_delete_all_dto.dart'; part 'model/notification_dto.dart'; @@ -217,9 +238,10 @@ part 'model/pin_code_reset_dto.dart'; part 'model/pin_code_setup_dto.dart'; part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; -part 'model/plugin_context.dart'; +part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; part 'model/plugin_response_dto.dart'; +part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_type.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; @@ -241,6 +263,7 @@ part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; +part 'model/rotate_parameters.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_response_dto.dart'; part 'model/search_explore_item.dart'; @@ -269,6 +292,7 @@ part 'model/session_update_dto.dart'; part 'model/set_maintenance_mode_dto.dart'; part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; +part 'model/shared_link_login_dto.dart'; part 'model/shared_link_response_dto.dart'; part 'model/shared_link_type.dart'; part 'model/shared_links_response.dart'; @@ -280,6 +304,7 @@ part 'model/stack_create_dto.dart'; part 'model/stack_response_dto.dart'; part 'model/stack_update_dto.dart'; part 'model/statistics_search_dto.dart'; +part 'model/storage_folder.dart'; part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; @@ -290,9 +315,12 @@ 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_edit_delete_v1.dart'; +part 'model/sync_asset_edit_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_face_v2.dart'; part 'model/sync_asset_metadata_delete_v1.dart'; part 'model/sync_asset_metadata_v1.dart'; part 'model/sync_asset_v1.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index b92f95be72..697598ac97 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -130,14 +130,19 @@ class ActivitiesApi { /// Parameters: /// /// * [String] albumId (required): + /// Album ID /// /// * [String] assetId: + /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: + /// Filter by activity level /// /// * [ReactionType] type: + /// Filter by activity type /// /// * [String] userId: + /// Filter by user ID Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities'; @@ -184,14 +189,19 @@ class ActivitiesApi { /// Parameters: /// /// * [String] albumId (required): + /// Album ID /// /// * [String] assetId: + /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: + /// Filter by activity level /// /// * [ReactionType] type: + /// Filter by activity type /// /// * [String] userId: + /// Filter by user ID Future?> getActivities(String albumId, { String? assetId, ReactionLevel? level, ReactionType? type, String? userId, }) async { final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, level: level, type: type, userId: userId, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -219,8 +229,10 @@ class ActivitiesApi { /// Parameters: /// /// * [String] albumId (required): + /// Album ID /// /// * [String] assetId: + /// Asset ID (if activity is for an asset) Future getActivityStatisticsWithHttpInfo(String albumId, { String? assetId, }) async { // ignore: prefer_const_declarations final apiPath = r'/activities/statistics'; @@ -258,8 +270,10 @@ class ActivitiesApi { /// Parameters: /// /// * [String] albumId (required): + /// Album ID /// /// * [String] assetId: + /// Asset ID (if activity is for an asset) Future getActivityStatistics(String albumId, { String? assetId, }) async { final response = await getActivityStatisticsWithHttpInfo(albumId, assetId: assetId, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index 1042a2850f..e2db95b9e0 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -347,6 +347,7 @@ class AlbumsApi { /// * [String] slug: /// /// * [bool] withoutAssets: + /// Exclude assets from response Future getAlbumInfoWithHttpInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums/{id}' @@ -396,6 +397,7 @@ class AlbumsApi { /// * [String] slug: /// /// * [bool] withoutAssets: + /// Exclude assets from response Future getAlbumInfo(String id, { String? key, String? slug, bool? withoutAssets, }) async { final response = await getAlbumInfoWithHttpInfo(id, key: key, slug: slug, withoutAssets: withoutAssets, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -468,9 +470,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] assetId: - /// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums + /// Filter albums containing this asset ID (ignores shared parameter) /// /// * [bool] shared: + /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums Future getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async { // ignore: prefer_const_declarations final apiPath = r'/albums'; @@ -510,9 +513,10 @@ class AlbumsApi { /// Parameters: /// /// * [String] assetId: - /// Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums + /// Filter albums containing this asset ID (ignores shared parameter) /// /// * [bool] shared: + /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums Future?> getAllAlbums({ String? assetId, bool? shared, }) async { final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 43cb05a8a3..a8bdb5305b 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -185,13 +185,15 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// - /// * [AssetMetadataKey] key (required): - Future deleteAssetMetadataWithHttpInfo(String id, AssetMetadataKey key,) async { + /// * [String] key (required): + /// Metadata key + Future deleteAssetMetadataWithHttpInfo(String id, String key,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) - .replaceAll('{key}', key.toString()); + .replaceAll('{key}', key); // ignore: prefer_final_locals Object? postBody; @@ -221,9 +223,11 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// - /// * [AssetMetadataKey] key (required): - Future deleteAssetMetadata(String id, AssetMetadataKey key,) async { + /// * [String] key (required): + /// Metadata key + Future deleteAssetMetadata(String id, String key,) async { final response = await deleteAssetMetadataWithHttpInfo(id, key,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -278,6 +282,54 @@ class AssetsApi { } } + /// Delete asset metadata + /// + /// Delete metadata key-value pairs for multiple assets. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): + Future deleteBulkAssetMetadataWithHttpInfo(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/metadata'; + + // ignore: prefer_final_locals + Object? postBody = assetMetadataBulkDeleteDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Delete asset metadata + /// + /// Delete metadata key-value pairs for multiple assets. + /// + /// Parameters: + /// + /// * [AssetMetadataBulkDeleteDto] assetMetadataBulkDeleteDto (required): + Future deleteBulkAssetMetadata(AssetMetadataBulkDeleteDto assetMetadataBulkDeleteDto,) async { + final response = await deleteBulkAssetMetadataWithHttpInfo(assetMetadataBulkDeleteDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Download original asset /// /// Downloads the original file of the specified asset. @@ -288,10 +340,13 @@ class AssetsApi { /// /// * [String] id (required): /// + /// * [bool] edited: + /// Return edited asset if available + /// /// * [String] key: /// /// * [String] slug: - Future downloadAssetWithHttpInfo(String id, { String? key, String? slug, }) async { + Future downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/original' .replaceAll('{id}', id); @@ -303,6 +358,9 @@ class AssetsApi { final headerParams = {}; final formParams = {}; + if (edited != null) { + queryParams.addAll(_queryParams('', 'edited', edited)); + } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } @@ -332,11 +390,14 @@ class AssetsApi { /// /// * [String] id (required): /// + /// * [bool] edited: + /// Return edited asset if available + /// /// * [String] key: /// /// * [String] slug: - Future downloadAsset(String id, { String? key, String? slug, }) async { - final response = await downloadAssetWithHttpInfo(id, key: key, slug: slug, ); + Future downloadAsset(String id, { bool? edited, String? key, String? slug, }) async { + final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -350,6 +411,67 @@ class AssetsApi { return null; } + /// Apply edits to an existing asset + /// + /// Apply a series of edit actions (crop, rotate, mirror) to the specified asset. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAssetWithHttpInfo(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/edits' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetEditsCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Apply edits to an existing asset + /// + /// Apply a series of edit actions (crop, rotate, mirror) to the specified asset. + /// + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [AssetEditsCreateDto] assetEditsCreateDto (required): + Future editAsset(String id, AssetEditsCreateDto assetEditsCreateDto,) async { + final response = await editAssetWithHttpInfo(id, assetEditsCreateDto,); + 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), 'AssetEditsResponseDto',) as AssetEditsResponseDto; + + } + return null; + } + /// Retrieve assets by device ID /// /// Get all asset of a device that are in the database, ID only. @@ -359,6 +481,7 @@ class AssetsApi { /// Parameters: /// /// * [String] deviceId (required): + /// Device ID Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/device/{deviceId}' @@ -392,6 +515,7 @@ class AssetsApi { /// Parameters: /// /// * [String] deviceId (required): + /// Device ID Future?> getAllUserAssetsByDeviceId(String deviceId,) async { final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); if (response.statusCode >= HttpStatus.badRequest) { @@ -410,6 +534,63 @@ class AssetsApi { return null; } + /// Retrieve edits for an existing asset + /// + /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future getAssetEditsWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/edits' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve edits for an existing asset + /// + /// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset. + /// + /// Parameters: + /// + /// * [String] id (required): + Future getAssetEdits(String id,) async { + final response = await getAssetEditsWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetEditsResponseDto',) as AssetEditsResponseDto; + + } + return null; + } + /// Retrieve an asset /// /// Retrieve detailed information about a specific asset. @@ -551,13 +732,15 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// - /// * [AssetMetadataKey] key (required): - Future getAssetMetadataByKeyWithHttpInfo(String id, AssetMetadataKey key,) async { + /// * [String] key (required): + /// Metadata key + Future getAssetMetadataByKeyWithHttpInfo(String id, String key,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' .replaceAll('{id}', id) - .replaceAll('{key}', key.toString()); + .replaceAll('{key}', key); // ignore: prefer_final_locals Object? postBody; @@ -587,9 +770,11 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// - /// * [AssetMetadataKey] key (required): - Future getAssetMetadataByKey(String id, AssetMetadataKey key,) async { + /// * [String] key (required): + /// Metadata key + Future getAssetMetadataByKey(String id, String key,) async { final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -673,10 +858,13 @@ class AssetsApi { /// Parameters: /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isTrashed: + /// Filter by trash status /// /// * [AssetVisibility] visibility: + /// Filter by visibility Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -719,10 +907,13 @@ class AssetsApi { /// Parameters: /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isTrashed: + /// Filter by trash status /// /// * [AssetVisibility] visibility: + /// Filter by visibility Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -834,6 +1025,7 @@ class AssetsApi { /// Parameters: /// /// * [num] count: + /// Number of random assets to return Future getRandomWithHttpInfo({ num? count, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/random'; @@ -870,6 +1062,7 @@ class AssetsApi { /// Parameters: /// /// * [num] count: + /// Number of random assets to return Future?> getRandom({ num? count, }) async { final response = await getRandomWithHttpInfo( count: count, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -960,6 +1153,55 @@ class AssetsApi { return null; } + /// Remove edits from an existing asset + /// + /// Removes all edit actions (crop, rotate, mirror) associated with the specified asset. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future removeAssetEditsWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/edits' + .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, + ); + } + + /// Remove edits from an existing asset + /// + /// Removes all edit actions (crop, rotate, mirror) associated with the specified asset. + /// + /// Parameters: + /// + /// * [String] id (required): + Future removeAssetEdits(String id,) async { + final response = await removeAssetEditsWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Replace asset /// /// Replace the asset with new file, without changing its id. @@ -971,22 +1213,29 @@ class AssetsApi { /// * [String] id (required): /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): + /// File modification date /// /// * [String] key: /// /// * [String] slug: /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/original' @@ -1063,22 +1312,29 @@ class AssetsApi { /// * [String] id (required): /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): + /// File modification date /// /// * [String] key: /// /// * [String] slug: /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -1315,6 +1571,65 @@ class AssetsApi { } } + /// Upsert asset metadata + /// + /// Upsert metadata key-value pairs for multiple assets. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): + Future updateBulkAssetMetadataWithHttpInfo(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/metadata'; + + // ignore: prefer_final_locals + Object? postBody = assetMetadataBulkUpsertDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Upsert asset metadata + /// + /// Upsert metadata key-value pairs for multiple assets. + /// + /// Parameters: + /// + /// * [AssetMetadataBulkUpsertDto] assetMetadataBulkUpsertDto (required): + Future?> updateBulkAssetMetadata(AssetMetadataBulkUpsertDto assetMetadataBulkUpsertDto,) async { + final response = await updateBulkAssetMetadataWithHttpInfo(assetMetadataBulkUpsertDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Upload asset /// /// Uploads a new asset to the server. @@ -1324,16 +1639,19 @@ class AssetsApi { /// Parameters: /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): - /// - /// * [List] metadata (required): + /// File modification date /// /// * [String] key: /// @@ -1343,17 +1661,26 @@ class AssetsApi { /// sha1 checksum that can be used for duplicate detection before the file is uploaded /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename /// /// * [bool] isFavorite: + /// Mark as favorite /// /// * [String] livePhotoVideoId: + /// Live photo video ID + /// + /// * [List] metadata: + /// Asset metadata items /// /// * [MultipartFile] sidecarData: + /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + /// Asset visibility + Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1451,16 +1778,19 @@ class AssetsApi { /// Parameters: /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): - /// - /// * [List] metadata (required): + /// File modification date /// /// * [String] key: /// @@ -1470,18 +1800,27 @@ class AssetsApi { /// sha1 checksum that can be used for duplicate detection before the file is uploaded /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename /// /// * [bool] isFavorite: + /// Mark as favorite /// /// * [String] livePhotoVideoId: + /// Live photo video ID + /// + /// * [List] metadata: + /// Asset metadata items /// /// * [MultipartFile] sidecarData: + /// Sidecar file data /// /// * [AssetVisibility] visibility: - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, List metadata, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, metadata, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, ); + /// Asset visibility + Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { + final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1497,7 +1836,7 @@ class AssetsApi { /// View asset thumbnail /// - /// Retrieve the thumbnail image for the specified asset. + /// Retrieve the thumbnail image for the specified asset. Viewing the fullsize thumbnail might redirect to downloadAsset, which requires a different permission. /// /// Note: This method returns the HTTP [Response]. /// @@ -1505,12 +1844,16 @@ class AssetsApi { /// /// * [String] id (required): /// + /// * [bool] edited: + /// Return edited asset if available + /// /// * [String] key: /// /// * [AssetMediaSize] size: + /// Asset media size /// /// * [String] slug: - Future viewAssetWithHttpInfo(String id, { String? key, AssetMediaSize? size, String? slug, }) async { + Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/thumbnail' .replaceAll('{id}', id); @@ -1522,6 +1865,9 @@ class AssetsApi { final headerParams = {}; final formParams = {}; + if (edited != null) { + queryParams.addAll(_queryParams('', 'edited', edited)); + } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } @@ -1548,19 +1894,23 @@ class AssetsApi { /// View asset thumbnail /// - /// Retrieve the thumbnail image for the specified asset. + /// Retrieve the thumbnail image for the specified asset. Viewing the fullsize thumbnail might redirect to downloadAsset, which requires a different permission. /// /// Parameters: /// /// * [String] id (required): /// + /// * [bool] edited: + /// Return edited asset if available + /// /// * [String] key: /// /// * [AssetMediaSize] size: + /// Asset media size /// /// * [String] slug: - Future viewAsset(String id, { String? key, AssetMediaSize? size, String? slug, }) async { - final response = await viewAssetWithHttpInfo(id, key: key, size: size, slug: slug, ); + Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { + final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart new file mode 100644 index 0000000000..fbd485f86f --- /dev/null +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -0,0 +1,269 @@ +// +// 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 DatabaseBackupsAdminApi { + DatabaseBackupsAdminApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Delete database backup + /// + /// Delete a backup by its filename + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): + Future deleteDatabaseBackupWithHttpInfo(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups'; + + // ignore: prefer_final_locals + Object? postBody = databaseBackupDeleteDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Delete database backup + /// + /// Delete a backup by its filename + /// + /// Parameters: + /// + /// * [DatabaseBackupDeleteDto] databaseBackupDeleteDto (required): + Future deleteDatabaseBackup(DatabaseBackupDeleteDto databaseBackupDeleteDto,) async { + final response = await deleteDatabaseBackupWithHttpInfo(databaseBackupDeleteDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Download database backup + /// + /// Downloads the database backup file + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] filename (required): + Future downloadDatabaseBackupWithHttpInfo(String filename,) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/{filename}' + .replaceAll('{filename}', filename); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Download database backup + /// + /// Downloads the database backup file + /// + /// Parameters: + /// + /// * [String] filename (required): + Future downloadDatabaseBackup(String filename,) async { + final response = await downloadDatabaseBackupWithHttpInfo(filename,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile; + + } + return null; + } + + /// List database backups + /// + /// Get the list of the successful and failed backups + /// + /// Note: This method returns the HTTP [Response]. + Future listDatabaseBackupsWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List database backups + /// + /// Get the list of the successful and failed backups + Future listDatabaseBackups() async { + final response = await listDatabaseBackupsWithHttpInfo(); + 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), 'DatabaseBackupListResponseDto',) as DatabaseBackupListResponseDto; + + } + return null; + } + + /// Start database backup restore flow + /// + /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) + /// + /// Note: This method returns the HTTP [Response]. + Future startDatabaseRestoreFlowWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/start-restore'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Start database backup restore flow + /// + /// Put Immich into maintenance mode to restore a backup (Immich must not be configured) + Future startDatabaseRestoreFlow() async { + final response = await startDatabaseRestoreFlowWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Upload database backup + /// + /// Uploads .sql/.sql.gz file to restore backup from + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [MultipartFile] file: + Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/database-backups/upload'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['multipart/form-data']; + + bool hasFields = false; + final mp = MultipartRequest('POST', Uri.parse(apiPath)); + if (file != null) { + hasFields = true; + mp.fields[r'file'] = file.field; + mp.files.add(file); + } + if (hasFields) { + postBody = mp; + } + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Upload database backup + /// + /// Uploads .sql/.sql.gz file to restore backup from + /// + /// Parameters: + /// + /// * [MultipartFile] file: + Future uploadDatabaseBackup({ MultipartFile? file, }) async { + final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } +} diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index d0d92d804d..33bcaf062c 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -82,6 +82,7 @@ class DeprecatedApi { /// Parameters: /// /// * [String] deviceId (required): + /// Device ID Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/device/{deviceId}' @@ -115,6 +116,7 @@ class DeprecatedApi { /// Parameters: /// /// * [String] deviceId (required): + /// Device ID Future?> getAllUserAssetsByDeviceId(String deviceId,) async { final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); if (response.statusCode >= HttpStatus.badRequest) { @@ -305,6 +307,7 @@ class DeprecatedApi { /// Parameters: /// /// * [num] count: + /// Number of random assets to return Future getRandomWithHttpInfo({ num? count, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/random'; @@ -341,6 +344,7 @@ class DeprecatedApi { /// Parameters: /// /// * [num] count: + /// Number of random assets to return Future?> getRandom({ num? count, }) async { final response = await getRandomWithHttpInfo( count: count, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -370,22 +374,29 @@ class DeprecatedApi { /// * [String] id (required): /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): + /// File modification date /// /// * [String] key: /// /// * [String] slug: /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/original' @@ -462,22 +473,29 @@ class DeprecatedApi { /// * [String] id (required): /// /// * [MultipartFile] assetData (required): + /// Asset file data /// /// * [String] deviceAssetId (required): + /// Device asset ID /// /// * [String] deviceId (required): + /// Device ID /// /// * [DateTime] fileCreatedAt (required): + /// File creation date /// /// * [DateTime] fileModifiedAt (required): + /// File modification date /// /// * [String] key: /// /// * [String] slug: /// /// * [String] duration: + /// Duration (for videos) /// /// * [String] filename: + /// Filename Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -502,6 +520,7 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -537,6 +556,7 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/download_api.dart b/mobile/openapi/lib/api/download_api.dart index 5245622753..4d0c5c8165 100644 --- a/mobile/openapi/lib/api/download_api.dart +++ b/mobile/openapi/lib/api/download_api.dart @@ -24,17 +24,17 @@ class DownloadApi { /// /// Parameters: /// - /// * [AssetIdsDto] assetIdsDto (required): + /// * [DownloadArchiveDto] downloadArchiveDto (required): /// /// * [String] key: /// /// * [String] slug: - Future downloadArchiveWithHttpInfo(AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { + Future downloadArchiveWithHttpInfo(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { // ignore: prefer_const_declarations final apiPath = r'/download/archive'; // ignore: prefer_final_locals - Object? postBody = assetIdsDto; + Object? postBody = downloadArchiveDto; final queryParams = []; final headerParams = {}; @@ -67,13 +67,13 @@ class DownloadApi { /// /// Parameters: /// - /// * [AssetIdsDto] assetIdsDto (required): + /// * [DownloadArchiveDto] downloadArchiveDto (required): /// /// * [String] key: /// /// * [String] slug: - Future downloadArchive(AssetIdsDto assetIdsDto, { String? key, String? slug, }) async { - final response = await downloadArchiveWithHttpInfo(assetIdsDto, key: key, slug: slug, ); + Future downloadArchive(DownloadArchiveDto downloadArchiveDto, { String? key, String? slug, }) async { + final response = await downloadArchiveWithHttpInfo(downloadArchiveDto, key: key, slug: slug, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/faces_api.dart b/mobile/openapi/lib/api/faces_api.dart index 1d2e7401e8..43d63b47b9 100644 --- a/mobile/openapi/lib/api/faces_api.dart +++ b/mobile/openapi/lib/api/faces_api.dart @@ -126,6 +126,7 @@ class FacesApi { /// Parameters: /// /// * [String] id (required): + /// Face ID Future getFacesWithHttpInfo(String id,) async { // ignore: prefer_const_declarations final apiPath = r'/faces'; @@ -160,6 +161,7 @@ class FacesApi { /// Parameters: /// /// * [String] id (required): + /// Face ID Future?> getFaces(String id,) async { final response = await getFacesWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 9dda59a883..41517f8144 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -121,6 +121,7 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -156,6 +157,7 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index 7e46f96c6e..0f953f1634 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -16,6 +16,102 @@ class MaintenanceAdminApi { final ApiClient apiClient; + /// Detect existing install + /// + /// Collect integrity checks and other heuristics about local data. + /// + /// Note: This method returns the HTTP [Response]. + Future detectPriorInstallWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/maintenance/detect-install'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Detect existing install + /// + /// Collect integrity checks and other heuristics about local data. + Future detectPriorInstall() async { + final response = await detectPriorInstallWithHttpInfo(); + 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), 'MaintenanceDetectInstallResponseDto',) as MaintenanceDetectInstallResponseDto; + + } + return null; + } + + /// Get maintenance mode status + /// + /// Fetch information about the currently running maintenance action. + /// + /// Note: This method returns the HTTP [Response]. + Future getMaintenanceStatusWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/admin/maintenance/status'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Get maintenance mode status + /// + /// Fetch information about the currently running maintenance action. + Future getMaintenanceStatus() async { + final response = await getMaintenanceStatusWithHttpInfo(); + 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), 'MaintenanceStatusResponseDto',) as MaintenanceStatusResponseDto; + + } + return null; + } + /// Log into maintenance mode /// /// Login with maintenance token or cookie to receive current information and perform further actions. diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 6302ac304e..4ce62bd96c 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -25,16 +25,22 @@ class MapApi { /// Parameters: /// /// * [DateTime] fileCreatedAfter: + /// Filter assets created after this date /// /// * [DateTime] fileCreatedBefore: + /// Filter assets created before this date /// /// * [bool] isArchived: + /// Filter by archived status /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] withPartners: + /// Include partner assets /// /// * [bool] withSharedAlbums: + /// Include shared album assets Future getMapMarkersWithHttpInfo({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { // ignore: prefer_const_declarations final apiPath = r'/map/markers'; @@ -86,16 +92,22 @@ class MapApi { /// Parameters: /// /// * [DateTime] fileCreatedAfter: + /// Filter assets created after this date /// /// * [DateTime] fileCreatedBefore: + /// Filter assets created before this date /// /// * [bool] isArchived: + /// Filter by archived status /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] withPartners: + /// Include partner assets /// /// * [bool] withSharedAlbums: + /// Include shared album assets Future?> getMapMarkers({ DateTime? fileCreatedAfter, DateTime? fileCreatedBefore, bool? isArchived, bool? isFavorite, bool? withPartners, bool? withSharedAlbums, }) async { final response = await getMapMarkersWithHttpInfo( fileCreatedAfter: fileCreatedAfter, fileCreatedBefore: fileCreatedBefore, isArchived: isArchived, isFavorite: isFavorite, withPartners: withPartners, withSharedAlbums: withSharedAlbums, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -123,8 +135,10 @@ class MapApi { /// Parameters: /// /// * [double] lat (required): + /// Latitude (-90 to 90) /// /// * [double] lon (required): + /// Longitude (-180 to 180) Future reverseGeocodeWithHttpInfo(double lat, double lon,) async { // ignore: prefer_const_declarations final apiPath = r'/map/reverse-geocode'; @@ -160,8 +174,10 @@ class MapApi { /// Parameters: /// /// * [double] lat (required): + /// Latitude (-90 to 90) /// /// * [double] lon (required): + /// Longitude (-180 to 180) Future?> reverseGeocode(double lat, double lon,) async { final response = await reverseGeocodeWithHttpInfo(lat, lon,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 314595e84e..913205428e 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -251,17 +251,22 @@ class MemoriesApi { /// Parameters: /// /// * [DateTime] for_: + /// Filter by date /// /// * [bool] isSaved: + /// Filter by saved status /// /// * [bool] isTrashed: + /// Include trashed memories /// /// * [MemorySearchOrder] order: + /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: + /// Memory type Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -313,17 +318,22 @@ class MemoriesApi { /// Parameters: /// /// * [DateTime] for_: + /// Filter by date /// /// * [bool] isSaved: + /// Filter by saved status /// /// * [bool] isTrashed: + /// Include trashed memories /// /// * [MemorySearchOrder] order: + /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: + /// Memory type Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -412,17 +422,22 @@ class MemoriesApi { /// Parameters: /// /// * [DateTime] for_: + /// Filter by date /// /// * [bool] isSaved: + /// Filter by saved status /// /// * [bool] isTrashed: + /// Include trashed memories /// /// * [MemorySearchOrder] order: + /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: + /// Memory type Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -474,17 +489,22 @@ class MemoriesApi { /// Parameters: /// /// * [DateTime] for_: + /// Filter by date /// /// * [bool] isSaved: + /// Filter by saved status /// /// * [bool] isTrashed: + /// Include trashed memories /// /// * [MemorySearchOrder] order: + /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: + /// Memory type Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index 2de59a0a76..d4e2b1d80f 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -179,12 +179,16 @@ class NotificationsApi { /// Parameters: /// /// * [String] id: + /// Filter by notification ID /// /// * [NotificationLevel] level: + /// Filter by notification level /// /// * [NotificationType] type: + /// Filter by notification type /// /// * [bool] unread: + /// Filter by unread status Future getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { // ignore: prefer_const_declarations final apiPath = r'/notifications'; @@ -230,12 +234,16 @@ class NotificationsApi { /// Parameters: /// /// * [String] id: + /// Filter by notification ID /// /// * [NotificationLevel] level: + /// Filter by notification level /// /// * [NotificationType] type: + /// Filter by notification type /// /// * [bool] unread: + /// Filter by unread status Future?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async { final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 7d18f6d867..3b15b90909 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -138,6 +138,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): + /// Partner direction Future getPartnersWithHttpInfo(PartnerDirection direction,) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -172,6 +173,7 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): + /// Partner direction Future?> getPartners(PartnerDirection direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index c38e61584e..c8c1821423 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -178,8 +178,10 @@ class PeopleApi { /// Parameters: /// /// * [String] closestAssetId: + /// Closest asset ID for similarity search /// /// * [String] closestPersonId: + /// Closest person ID for similarity search /// /// * [num] page: /// Page number for pagination @@ -188,6 +190,7 @@ class PeopleApi { /// Number of items per page /// /// * [bool] withHidden: + /// Include hidden people Future getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async { // ignore: prefer_const_declarations final apiPath = r'/people'; @@ -236,8 +239,10 @@ class PeopleApi { /// Parameters: /// /// * [String] closestAssetId: + /// Closest asset ID for similarity search /// /// * [String] closestPersonId: + /// Closest person ID for similarity search /// /// * [num] page: /// Page number for pagination @@ -246,6 +251,7 @@ class PeopleApi { /// Number of items per page /// /// * [bool] withHidden: + /// Include hidden people Future getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async { final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/plugins_api.dart b/mobile/openapi/lib/api/plugins_api.dart index 264d3049e8..5735fba379 100644 --- a/mobile/openapi/lib/api/plugins_api.dart +++ b/mobile/openapi/lib/api/plugins_api.dart @@ -73,6 +73,57 @@ class PluginsApi { return null; } + /// List all plugin triggers + /// + /// Retrieve a list of all available plugin triggers. + /// + /// Note: This method returns the HTTP [Response]. + Future getPluginTriggersWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/plugins/triggers'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List all plugin triggers + /// + /// Retrieve a list of all available plugin triggers. + Future?> getPluginTriggers() async { + final response = await getPluginTriggersWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// List all plugins /// /// Retrieve a list of plugins available to the authenticated user. diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index 50575ed706..ecb556e434 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -25,6 +25,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -60,6 +61,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -78,6 +80,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name Future getQueueWithHttpInfo(QueueName name,) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' @@ -111,6 +114,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name Future getQueue(QueueName name,) async { final response = await getQueueWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { @@ -135,8 +139,10 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [List] status: + /// Filter jobs by status Future getQueueJobsWithHttpInfo(QueueName name, { List? status, }) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}/jobs' @@ -174,8 +180,10 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [List] status: + /// Filter jobs by status Future?> getQueueJobs(QueueName name, { List? status, }) async { final response = await getQueueJobsWithHttpInfo(name, status: status, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -254,6 +262,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { @@ -289,6 +298,7 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): + /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index ee5f64753c..085958de66 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -127,18 +127,25 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): + /// Suggestion type /// /// * [String] country: + /// Filter by country /// /// * [bool] includeNull: + /// Include null values in suggestions /// /// * [String] lensModel: + /// Filter by lens model /// /// * [String] make: + /// Filter by camera make /// /// * [String] model: + /// Filter by camera model /// /// * [String] state: + /// Filter by state/province Future getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/suggestions'; @@ -191,18 +198,25 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): + /// Suggestion type /// /// * [String] country: + /// Filter by country /// /// * [bool] includeNull: + /// Include null values in suggestions /// /// * [String] lensModel: + /// Filter by lens model /// /// * [String] make: + /// Filter by camera make /// /// * [String] model: + /// Filter by camera model /// /// * [String] state: + /// Filter by state/province Future?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { final response = await getSearchSuggestionsWithHttpInfo(type, country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -342,68 +356,100 @@ class SearchApi { /// Parameters: /// /// * [List] albumIds: + /// Filter by album IDs /// /// * [String] city: + /// Filter by city name /// /// * [String] country: + /// Filter by country name /// /// * [DateTime] createdAfter: + /// Filter by creation date (after) /// /// * [DateTime] createdBefore: + /// Filter by creation date (before) /// /// * [String] deviceId: + /// Device ID to filter by /// /// * [bool] isEncoded: + /// Filter by encoded status /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isMotion: + /// Filter by motion photo status /// /// * [bool] isNotInAlbum: + /// Filter assets not in any album /// /// * [bool] isOffline: + /// Filter by offline status /// /// * [String] lensModel: + /// Filter by lens model /// /// * [String] libraryId: + /// Library ID to filter by /// /// * [String] make: + /// Filter by camera make /// /// * [int] minFileSize: + /// Minimum file size in bytes /// /// * [String] model: + /// Filter by camera model /// /// * [String] ocr: + /// Filter by OCR text content /// /// * [List] personIds: + /// Filter by person IDs /// /// * [num] rating: + /// Filter by rating [1-5], or null for unrated /// /// * [num] size: + /// Number of results to return /// /// * [String] state: + /// Filter by state/province name /// /// * [List] tagIds: + /// Filter by tag IDs /// /// * [DateTime] takenAfter: + /// Filter by taken date (after) /// /// * [DateTime] takenBefore: + /// Filter by taken date (before) /// /// * [DateTime] trashedAfter: + /// Filter by trash date (after) /// /// * [DateTime] trashedBefore: + /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: + /// Asset type filter /// /// * [DateTime] updatedAfter: + /// Filter by update date (after) /// /// * [DateTime] updatedBefore: + /// Filter by update date (before) /// /// * [AssetVisibility] visibility: + /// Filter by visibility /// /// * [bool] withDeleted: + /// Include deleted assets /// /// * [bool] withExif: + /// Include EXIF data in response Future searchLargeAssetsWithHttpInfo({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/large-assets'; @@ -533,68 +579,100 @@ class SearchApi { /// Parameters: /// /// * [List] albumIds: + /// Filter by album IDs /// /// * [String] city: + /// Filter by city name /// /// * [String] country: + /// Filter by country name /// /// * [DateTime] createdAfter: + /// Filter by creation date (after) /// /// * [DateTime] createdBefore: + /// Filter by creation date (before) /// /// * [String] deviceId: + /// Device ID to filter by /// /// * [bool] isEncoded: + /// Filter by encoded status /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isMotion: + /// Filter by motion photo status /// /// * [bool] isNotInAlbum: + /// Filter assets not in any album /// /// * [bool] isOffline: + /// Filter by offline status /// /// * [String] lensModel: + /// Filter by lens model /// /// * [String] libraryId: + /// Library ID to filter by /// /// * [String] make: + /// Filter by camera make /// /// * [int] minFileSize: + /// Minimum file size in bytes /// /// * [String] model: + /// Filter by camera model /// /// * [String] ocr: + /// Filter by OCR text content /// /// * [List] personIds: + /// Filter by person IDs /// /// * [num] rating: + /// Filter by rating [1-5], or null for unrated /// /// * [num] size: + /// Number of results to return /// /// * [String] state: + /// Filter by state/province name /// /// * [List] tagIds: + /// Filter by tag IDs /// /// * [DateTime] takenAfter: + /// Filter by taken date (after) /// /// * [DateTime] takenBefore: + /// Filter by taken date (before) /// /// * [DateTime] trashedAfter: + /// Filter by trash date (after) /// /// * [DateTime] trashedBefore: + /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: + /// Asset type filter /// /// * [DateTime] updatedAfter: + /// Filter by update date (after) /// /// * [DateTime] updatedBefore: + /// Filter by update date (before) /// /// * [AssetVisibility] visibility: + /// Filter by visibility /// /// * [bool] withDeleted: + /// Include deleted assets /// /// * [bool] withExif: + /// Include EXIF data in response Future?> searchLargeAssets({ List? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List? personIds, num? rating, num? size, String? state, List? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -622,8 +700,10 @@ class SearchApi { /// Parameters: /// /// * [String] name (required): + /// Person name to search for /// /// * [bool] withHidden: + /// Include hidden people Future searchPersonWithHttpInfo(String name, { bool? withHidden, }) async { // ignore: prefer_const_declarations final apiPath = r'/search/person'; @@ -661,8 +741,10 @@ class SearchApi { /// Parameters: /// /// * [String] name (required): + /// Person name to search for /// /// * [bool] withHidden: + /// Include hidden people Future?> searchPerson(String name, { bool? withHidden, }) async { final response = await searchPersonWithHttpInfo(name, withHidden: withHidden, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -690,6 +772,7 @@ class SearchApi { /// Parameters: /// /// * [String] name (required): + /// Place name to search for Future searchPlacesWithHttpInfo(String name,) async { // ignore: prefer_const_declarations final apiPath = r'/search/places'; @@ -724,6 +807,7 @@ class SearchApi { /// Parameters: /// /// * [String] name (required): + /// Place name to search for Future?> searchPlaces(String name,) async { final response = await searchPlacesWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 79106e5db6..37eeffcf46 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -160,7 +160,11 @@ class SharedLinksApi { /// Parameters: /// /// * [String] albumId: - Future getAllSharedLinksWithHttpInfo({ String? albumId, }) async { + /// Filter by album ID + /// + /// * [String] id: + /// Filter by shared link ID + Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -174,6 +178,9 @@ class SharedLinksApi { if (albumId != null) { queryParams.addAll(_queryParams('', 'albumId', albumId)); } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } const contentTypes = []; @@ -196,8 +203,12 @@ class SharedLinksApi { /// Parameters: /// /// * [String] albumId: - Future?> getAllSharedLinks({ String? albumId, }) async { - final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, ); + /// Filter by album ID + /// + /// * [String] id: + /// Filter by shared link ID + Future?> getAllSharedLinks({ String? albumId, String? id, }) async { + final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, id: id, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -225,10 +236,12 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] password: + /// Link password /// /// * [String] slug: /// /// * [String] token: + /// Access token Future getMySharedLinkWithHttpInfo({ String? key, String? password, String? slug, String? token, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links/me'; @@ -276,10 +289,12 @@ class SharedLinksApi { /// * [String] key: /// /// * [String] password: + /// Link password /// /// * [String] slug: /// /// * [String] token: + /// Access token Future getMySharedLink({ String? key, String? password, String? slug, String? token, }) async { final response = await getMySharedLinkWithHttpInfo( key: key, password: password, slug: slug, token: token, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -480,6 +495,77 @@ class SharedLinksApi { return null; } + /// Shared link login + /// + /// Login to a password protected shared link + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [SharedLinkLoginDto] sharedLinkLoginDto (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/shared-links/login'; + + // ignore: prefer_final_locals + Object? postBody = sharedLinkLoginDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (key != null) { + queryParams.addAll(_queryParams('', 'key', key)); + } + if (slug != null) { + queryParams.addAll(_queryParams('', 'slug', slug)); + } + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Shared link login + /// + /// Login to a password protected shared link + /// + /// Parameters: + /// + /// * [SharedLinkLoginDto] sharedLinkLoginDto (required): + /// + /// * [String] key: + /// + /// * [String] slug: + Future sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async { + final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto; + + } + return null; + } + /// Update a shared link /// /// Update an existing shared link by its ID. diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart index 66fa1881ac..a691af2a7d 100644 --- a/mobile/openapi/lib/api/stacks_api.dart +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -289,6 +289,7 @@ class StacksApi { /// Parameters: /// /// * [String] primaryAssetId: + /// Filter by primary asset ID Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async { // ignore: prefer_const_declarations final apiPath = r'/stacks'; @@ -325,6 +326,7 @@ class StacksApi { /// Parameters: /// /// * [String] primaryAssetId: + /// Filter by primary asset ID Future?> searchStacks({ String? primaryAssetId, }) async { final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 2afcea20ff..f82c362ff7 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -30,6 +30,9 @@ class TimelineApi { /// * [String] albumId: /// Filter assets belonging to a specific album /// + /// * [String] bbox: + /// Bounding box coordinates as west,south,east,north (WGS84) + /// /// * [bool] isFavorite: /// Filter by favorite status (true for favorites only, false for non-favorites only) /// @@ -63,7 +66,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/bucket'; @@ -77,6 +80,9 @@ class TimelineApi { if (albumId != null) { queryParams.addAll(_queryParams('', 'albumId', albumId)); } + if (bbox != null) { + queryParams.addAll(_queryParams('', 'bbox', bbox)); + } if (isFavorite != null) { queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); } @@ -141,6 +147,9 @@ class TimelineApi { /// * [String] albumId: /// Filter assets belonging to a specific album /// + /// * [String] bbox: + /// Bounding box coordinates as west,south,east,north (WGS84) + /// /// * [bool] isFavorite: /// Filter by favorite status (true for favorites only, false for non-favorites only) /// @@ -174,8 +183,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -200,6 +209,9 @@ class TimelineApi { /// * [String] albumId: /// Filter assets belonging to a specific album /// + /// * [String] bbox: + /// Bounding box coordinates as west,south,east,north (WGS84) + /// /// * [bool] isFavorite: /// Filter by favorite status (true for favorites only, false for non-favorites only) /// @@ -233,7 +245,7 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final apiPath = r'/timeline/buckets'; @@ -247,6 +259,9 @@ class TimelineApi { if (albumId != null) { queryParams.addAll(_queryParams('', 'albumId', albumId)); } + if (bbox != null) { + queryParams.addAll(_queryParams('', 'bbox', bbox)); + } if (isFavorite != null) { queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); } @@ -307,6 +322,9 @@ class TimelineApi { /// * [String] albumId: /// Filter assets belonging to a specific album /// + /// * [String] bbox: + /// Bounding box coordinates as west,south,east,north (WGS84) + /// /// * [bool] isFavorite: /// Filter by favorite status (true for favorites only, false for non-favorites only) /// @@ -340,8 +358,8 @@ class TimelineApi { /// /// * [bool] withStacked: /// Include stacked assets in the response. When true, only primary assets from stacks are returned. - Future?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 842a3ebc5b..59a4b60096 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -318,10 +318,13 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isTrashed: + /// Filter by trash status /// /// * [AssetVisibility] visibility: + /// Filter by visibility Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' @@ -367,10 +370,13 @@ class UsersAdminApi { /// * [String] id (required): /// /// * [bool] isFavorite: + /// Filter by favorite status /// /// * [bool] isTrashed: + /// Filter by trash status /// /// * [AssetVisibility] visibility: + /// Filter by visibility Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -452,8 +458,10 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id: + /// User ID filter /// /// * [bool] withDeleted: + /// Include deleted users Future searchUsersAdminWithHttpInfo({ String? id, bool? withDeleted, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users'; @@ -493,8 +501,10 @@ class UsersAdminApi { /// Parameters: /// /// * [String] id: + /// User ID filter /// /// * [bool] withDeleted: + /// Include deleted users Future?> searchUsersAdmin({ String? id, bool? withDeleted, }) async { final response = await searchUsersAdminWithHttpInfo( id: id, withDeleted: withDeleted, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index f398d9c813..7ccae02c76 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -25,6 +25,7 @@ class UsersApi { /// Parameters: /// /// * [MultipartFile] file (required): + /// Profile image file Future createProfileImageWithHttpInfo(MultipartFile file,) async { // ignore: prefer_const_declarations final apiPath = r'/users/profile-image'; @@ -67,6 +68,7 @@ class UsersApi { /// Parameters: /// /// * [MultipartFile] file (required): + /// Profile image file Future createProfileImage(MultipartFile file,) async { final response = await createProfileImageWithHttpInfo(file,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 041be67015..bfe469e7c0 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -238,6 +238,18 @@ class ApiClient { return AssetDeltaSyncDto.fromJson(value); case 'AssetDeltaSyncResponseDto': return AssetDeltaSyncResponseDto.fromJson(value); + case 'AssetEditAction': + return AssetEditActionTypeTransformer().decode(value); + case 'AssetEditActionItemDto': + return AssetEditActionItemDto.fromJson(value); + case 'AssetEditActionItemDtoParameters': + return AssetEditActionItemDtoParameters.fromJson(value); + case 'AssetEditActionItemResponseDto': + return AssetEditActionItemResponseDto.fromJson(value); + case 'AssetEditsCreateDto': + return AssetEditsCreateDto.fromJson(value); + case 'AssetEditsResponseDto': + return AssetEditsResponseDto.fromJson(value); case 'AssetFaceCreateDto': return AssetFaceCreateDto.fromJson(value); case 'AssetFaceDeleteDto': @@ -266,8 +278,16 @@ class ApiClient { return AssetMediaSizeTypeTransformer().decode(value); case 'AssetMediaStatus': return AssetMediaStatusTypeTransformer().decode(value); - case 'AssetMetadataKey': - return AssetMetadataKeyTypeTransformer().decode(value); + case 'AssetMetadataBulkDeleteDto': + return AssetMetadataBulkDeleteDto.fromJson(value); + case 'AssetMetadataBulkDeleteItemDto': + return AssetMetadataBulkDeleteItemDto.fromJson(value); + case 'AssetMetadataBulkResponseDto': + return AssetMetadataBulkResponseDto.fromJson(value); + case 'AssetMetadataBulkUpsertDto': + return AssetMetadataBulkUpsertDto.fromJson(value); + case 'AssetMetadataBulkUpsertItemDto': + return AssetMetadataBulkUpsertItemDto.fromJson(value); case 'AssetMetadataResponseDto': return AssetMetadataResponseDto.fromJson(value); case 'AssetMetadataUpsertDto': @@ -324,8 +344,18 @@ class ApiClient { return CreateLibraryDto.fromJson(value); case 'CreateProfileImageResponseDto': return CreateProfileImageResponseDto.fromJson(value); + case 'CropParameters': + return CropParameters.fromJson(value); case 'DatabaseBackupConfig': return DatabaseBackupConfig.fromJson(value); + case 'DatabaseBackupDeleteDto': + return DatabaseBackupDeleteDto.fromJson(value); + case 'DatabaseBackupDto': + return DatabaseBackupDto.fromJson(value); + case 'DatabaseBackupListResponseDto': + return DatabaseBackupListResponseDto.fromJson(value); + case 'DownloadArchiveDto': + return DownloadArchiveDto.fromJson(value); case 'DownloadArchiveInfo': return DownloadArchiveInfo.fromJson(value); case 'DownloadInfoDto': @@ -384,8 +414,14 @@ class ApiClient { return MaintenanceActionTypeTransformer().decode(value); case 'MaintenanceAuthDto': return MaintenanceAuthDto.fromJson(value); + case 'MaintenanceDetectInstallResponseDto': + return MaintenanceDetectInstallResponseDto.fromJson(value); + case 'MaintenanceDetectInstallStorageFolderDto': + return MaintenanceDetectInstallStorageFolderDto.fromJson(value); case 'MaintenanceLoginDto': return MaintenanceLoginDto.fromJson(value); + case 'MaintenanceStatusResponseDto': + return MaintenanceStatusResponseDto.fromJson(value); case 'ManualJobName': return ManualJobNameTypeTransformer().decode(value); case 'MapMarkerResponseDto': @@ -412,6 +448,10 @@ class ApiClient { return MergePersonDto.fromJson(value); case 'MetadataSearchDto': return MetadataSearchDto.fromJson(value); + case 'MirrorAxis': + return MirrorAxisTypeTransformer().decode(value); + case 'MirrorParameters': + return MirrorParameters.fromJson(value); case 'NotificationCreateDto': return NotificationCreateDto.fromJson(value); case 'NotificationDeleteAllDto': @@ -482,12 +522,14 @@ class ApiClient { return PlacesResponseDto.fromJson(value); case 'PluginActionResponseDto': return PluginActionResponseDto.fromJson(value); - case 'PluginContext': - return PluginContextTypeTransformer().decode(value); + case 'PluginContextType': + return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); case 'PluginResponseDto': return PluginResponseDto.fromJson(value); + case 'PluginTriggerResponseDto': + return PluginTriggerResponseDto.fromJson(value); case 'PluginTriggerType': return PluginTriggerTypeTypeTransformer().decode(value); case 'PurchaseResponse': @@ -530,6 +572,8 @@ class ApiClient { return ReactionTypeTypeTransformer().decode(value); case 'ReverseGeocodingStateResponseDto': return ReverseGeocodingStateResponseDto.fromJson(value); + case 'RotateParameters': + return RotateParameters.fromJson(value); case 'SearchAlbumResponseDto': return SearchAlbumResponseDto.fromJson(value); case 'SearchAssetResponseDto': @@ -586,6 +630,8 @@ class ApiClient { return SharedLinkCreateDto.fromJson(value); case 'SharedLinkEditDto': return SharedLinkEditDto.fromJson(value); + case 'SharedLinkLoginDto': + return SharedLinkLoginDto.fromJson(value); case 'SharedLinkResponseDto': return SharedLinkResponseDto.fromJson(value); case 'SharedLinkType': @@ -608,6 +654,8 @@ class ApiClient { return StackUpdateDto.fromJson(value); case 'StatisticsSearchDto': return StatisticsSearchDto.fromJson(value); + case 'StorageFolder': + return StorageFolderTypeTransformer().decode(value); case 'SyncAckDeleteDto': return SyncAckDeleteDto.fromJson(value); case 'SyncAckDto': @@ -628,12 +676,18 @@ class ApiClient { return SyncAlbumV1.fromJson(value); case 'SyncAssetDeleteV1': return SyncAssetDeleteV1.fromJson(value); + case 'SyncAssetEditDeleteV1': + return SyncAssetEditDeleteV1.fromJson(value); + case 'SyncAssetEditV1': + return SyncAssetEditV1.fromJson(value); case 'SyncAssetExifV1': return SyncAssetExifV1.fromJson(value); case 'SyncAssetFaceDeleteV1': return SyncAssetFaceDeleteV1.fromJson(value); case 'SyncAssetFaceV1': return SyncAssetFaceV1.fromJson(value); + case 'SyncAssetFaceV2': + return SyncAssetFaceV2.fromJson(value); case 'SyncAssetMetadataDeleteV1': return SyncAssetMetadataDeleteV1.fromJson(value); case 'SyncAssetMetadataV1': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 2c97eeb314..830325a5b6 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -58,6 +58,9 @@ String parameterToString(dynamic value) { if (value is AlbumUserRole) { return AlbumUserRoleTypeTransformer().encode(value).toString(); } + if (value is AssetEditAction) { + return AssetEditActionTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } @@ -67,9 +70,6 @@ String parameterToString(dynamic value) { if (value is AssetMediaStatus) { return AssetMediaStatusTypeTransformer().encode(value).toString(); } - if (value is AssetMetadataKey) { - return AssetMetadataKeyTypeTransformer().encode(value).toString(); - } if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } @@ -112,6 +112,9 @@ String parameterToString(dynamic value) { if (value is MemoryType) { return MemoryTypeTypeTransformer().encode(value).toString(); } + if (value is MirrorAxis) { + return MirrorAxisTypeTransformer().encode(value).toString(); + } if (value is NotificationLevel) { return NotificationLevelTypeTransformer().encode(value).toString(); } @@ -127,8 +130,8 @@ String parameterToString(dynamic value) { if (value is Permission) { return PermissionTypeTransformer().encode(value).toString(); } - if (value is PluginContext) { - return PluginContextTypeTransformer().encode(value).toString(); + if (value is PluginContextType) { + return PluginContextTypeTypeTransformer().encode(value).toString(); } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); @@ -157,6 +160,9 @@ String parameterToString(dynamic value) { if (value is SourceType) { return SourceTypeTypeTransformer().encode(value).toString(); } + if (value is StorageFolder) { + return StorageFolderTypeTransformer().encode(value).toString(); + } if (value is SyncEntityType) { return SyncEntityTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index ce4b4a0176..fb4b6d084e 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -19,8 +19,10 @@ class ActivityCreateDto { required this.type, }); + /// Album ID String albumId; + /// Asset ID (if activity is for an asset) /// /// 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 @@ -29,6 +31,7 @@ class ActivityCreateDto { /// String? assetId; + /// Comment text (required if type is comment) /// /// 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 @@ -37,6 +40,7 @@ class ActivityCreateDto { /// String? comment; + /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index 25fb0f53f8..dadb45d8ac 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -21,14 +21,19 @@ class ActivityResponseDto { required this.user, }); + /// Asset ID (if activity is for an asset) String? assetId; + /// Comment text (for comment activities) String? comment; + /// Creation date DateTime createdAt; + /// Activity ID String id; + /// Activity type ReactionType type; UserResponseDto user; diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 27c478230d..15ad2a170e 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -17,8 +17,10 @@ class ActivityStatisticsResponseDto { required this.likes, }); + /// Number of comments int comments; + /// Number of likes int likes; @override diff --git a/mobile/openapi/lib/model/add_users_dto.dart b/mobile/openapi/lib/model/add_users_dto.dart index 531c1ec785..1dad234811 100644 --- a/mobile/openapi/lib/model/add_users_dto.dart +++ b/mobile/openapi/lib/model/add_users_dto.dart @@ -16,6 +16,7 @@ class AddUsersDto { this.albumUsers = const [], }); + /// Album users to add List albumUsers; @override diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart index 298bf318a2..6daba2a796 100644 --- a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -16,6 +16,7 @@ class AdminOnboardingUpdateDto { required this.isOnboarded, }); + /// Is admin onboarded bool isOnboarded; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 2f53706e7a..43e686fbdc 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -34,22 +34,28 @@ class AlbumResponseDto { required this.updatedAt, }); + /// Album name String albumName; + /// Thumbnail asset ID String? albumThumbnailAssetId; List albumUsers; + /// Number of assets int assetCount; List assets; List contributorCounts; + /// Creation date DateTime createdAt; + /// Album description String description; + /// End date (latest asset) /// /// 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 @@ -58,12 +64,16 @@ class AlbumResponseDto { /// DateTime? endDate; + /// Has shared link bool hasSharedLink; + /// Album ID String id; + /// Activity feed enabled bool isActivityEnabled; + /// Last modified asset timestamp /// /// 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 @@ -72,6 +82,7 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; + /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -82,10 +93,13 @@ class AlbumResponseDto { UserResponseDto owner; + /// Owner user ID String ownerId; + /// Is shared album bool shared; + /// Start date (earliest asset) /// /// 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 @@ -94,6 +108,7 @@ class AlbumResponseDto { /// DateTime? startDate; + /// Last update date DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 9e19002cf1..127334e687 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -18,10 +18,13 @@ class AlbumStatisticsResponseDto { required this.shared, }); + /// Number of non-shared albums int notShared; + /// Number of owned albums int owned; + /// Number of shared albums int shared; @override diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index e1f24377d7..c448a0b4b7 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -17,8 +17,10 @@ class AlbumUserAddDto { required this.userId, }); + /// Album user role AlbumUserRole role; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 93a0661b30..8006748341 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -17,8 +17,10 @@ class AlbumUserCreateDto { required this.userId, }); + /// Album user role AlbumUserRole role; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index bbae03fba7..8d0c01cfb8 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -17,6 +17,7 @@ class AlbumUserResponseDto { required this.user, }); + /// Album user role AlbumUserRole role; UserResponseDto user; diff --git a/mobile/openapi/lib/model/album_user_role.dart b/mobile/openapi/lib/model/album_user_role.dart index c0d61cd7f5..d797fdc2e8 100644 --- a/mobile/openapi/lib/model/album_user_role.dart +++ b/mobile/openapi/lib/model/album_user_role.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Album user role class AlbumUserRole { /// Instantiate a new enum with the provided [value]. const AlbumUserRole._(this.value); diff --git a/mobile/openapi/lib/model/albums_add_assets_dto.dart b/mobile/openapi/lib/model/albums_add_assets_dto.dart index bdbf68980c..d6aa3db1c1 100644 --- a/mobile/openapi/lib/model/albums_add_assets_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_dto.dart @@ -17,8 +17,10 @@ class AlbumsAddAssetsDto { this.assetIds = const [], }); + /// Album IDs List albumIds; + /// Asset IDs List assetIds; @override diff --git a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart index 4ad2c5e150..743a9f0645 100644 --- a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart @@ -17,6 +17,7 @@ class AlbumsAddAssetsResponseDto { required this.success, }); + /// Error reason /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -25,6 +26,7 @@ class AlbumsAddAssetsResponseDto { /// BulkIdErrorReason? error; + /// Operation success bool success; @override diff --git a/mobile/openapi/lib/model/albums_response.dart b/mobile/openapi/lib/model/albums_response.dart index 4f9a8eb8f2..520ee171c1 100644 --- a/mobile/openapi/lib/model/albums_response.dart +++ b/mobile/openapi/lib/model/albums_response.dart @@ -16,6 +16,7 @@ class AlbumsResponse { this.defaultAssetOrder = AssetOrder.desc, }); + /// Default asset order for albums AssetOrder defaultAssetOrder; @override diff --git a/mobile/openapi/lib/model/albums_update.dart b/mobile/openapi/lib/model/albums_update.dart index d61b5c1398..107c65dd1e 100644 --- a/mobile/openapi/lib/model/albums_update.dart +++ b/mobile/openapi/lib/model/albums_update.dart @@ -16,6 +16,7 @@ class AlbumsUpdate { this.defaultAssetOrder, }); + /// Default asset order for albums /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index 848774e9c9..e64b127820 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -17,6 +17,7 @@ class APIKeyCreateDto { this.permissions = const [], }); + /// API key name /// /// 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 @@ -25,6 +26,7 @@ class APIKeyCreateDto { /// String? name; + /// List of permissions List permissions; @override diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index cdaa70e37d..7540c4bb26 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -19,6 +19,7 @@ class APIKeyCreateResponseDto { APIKeyResponseDto apiKey; + /// API key secret (only shown once) String secret; @override diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index fd0d91f673..32ba543342 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -20,14 +20,19 @@ class APIKeyResponseDto { required this.updatedAt, }); + /// Creation date DateTime createdAt; + /// API key ID String id; + /// API key name String name; + /// List of permissions List permissions; + /// Last update date DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 7f32c95118..ba107bcda2 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -17,6 +17,7 @@ class APIKeyUpdateDto { this.permissions = const [], }); + /// API key name /// /// 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 @@ -25,6 +26,7 @@ class APIKeyUpdateDto { /// String? name; + /// List of permissions List permissions; @override diff --git a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart index c4453054b1..055ef16015 100644 --- a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart @@ -17,6 +17,7 @@ class AssetBulkDeleteDto { this.ids = const [], }); + /// Force delete even if in use /// /// 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 @@ -25,6 +26,7 @@ class AssetBulkDeleteDto { /// bool? force; + /// IDs to process List ids; @override diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index d7e75ae365..99bac7abfa 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -26,6 +26,7 @@ class AssetBulkUpdateDto { this.visibility, }); + /// Original date and time /// /// 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 @@ -34,6 +35,7 @@ class AssetBulkUpdateDto { /// String? dateTimeOriginal; + /// Relative time offset in seconds /// /// 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 @@ -42,6 +44,7 @@ class AssetBulkUpdateDto { /// num? dateTimeRelative; + /// Asset description /// /// 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 @@ -50,10 +53,13 @@ class AssetBulkUpdateDto { /// String? description; + /// Duplicate ID String? duplicateId; + /// Asset IDs to update List ids; + /// Mark as favorite /// /// 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 @@ -62,6 +68,7 @@ class AssetBulkUpdateDto { /// bool? isFavorite; + /// Latitude coordinate /// /// 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 @@ -70,6 +77,7 @@ class AssetBulkUpdateDto { /// num? latitude; + /// Longitude coordinate /// /// 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 @@ -78,16 +86,13 @@ class AssetBulkUpdateDto { /// num? longitude; + /// Rating in range [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Time zone (IANA timezone) /// /// 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 @@ -96,6 +101,7 @@ class AssetBulkUpdateDto { /// String? timeZone; + /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -211,7 +217,9 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), timeZone: mapValueOfType(json, r'timeZone'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart index 36c13bfdf6..66f46795e8 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart @@ -16,6 +16,7 @@ class AssetBulkUploadCheckDto { this.assets = const [], }); + /// Assets to check List assets; @override diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart index 13dfa340fa..65f81926e3 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart @@ -17,9 +17,10 @@ class AssetBulkUploadCheckItem { required this.id, }); - /// base64 or hex encoded sha1 hash + /// Base64 or hex encoded SHA1 hash String checksum; + /// Asset ID String id; @override diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart index 8c3651e9fa..b37bb0de8a 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart @@ -16,6 +16,7 @@ class AssetBulkUploadCheckResponseDto { this.results = const [], }); + /// Upload check results List results; @override diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index 88e46dae7d..b56370f689 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -20,8 +20,10 @@ class AssetBulkUploadCheckResult { this.reason, }); + /// Upload action AssetBulkUploadCheckResultActionEnum action; + /// Existing asset ID if duplicate /// /// 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 @@ -30,8 +32,10 @@ class AssetBulkUploadCheckResult { /// String? assetId; + /// Asset ID String id; + /// Whether existing asset is trashed /// /// 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 @@ -40,6 +44,7 @@ class AssetBulkUploadCheckResult { /// bool? isTrashed; + /// Rejection reason if rejected AssetBulkUploadCheckResultReasonEnum? reason; @override @@ -150,7 +155,7 @@ class AssetBulkUploadCheckResult { }; } - +/// Upload action class AssetBulkUploadCheckResultActionEnum { /// Instantiate a new enum with the provided [value]. const AssetBulkUploadCheckResultActionEnum._(this.value); @@ -224,7 +229,7 @@ class AssetBulkUploadCheckResultActionEnumTypeTransformer { } - +/// Rejection reason if rejected class AssetBulkUploadCheckResultReasonEnum { /// Instantiate a new enum with the provided [value]. const AssetBulkUploadCheckResultReasonEnum._(this.value); diff --git a/mobile/openapi/lib/model/asset_copy_dto.dart b/mobile/openapi/lib/model/asset_copy_dto.dart index ba19cb1dbc..2e68c5c113 100644 --- a/mobile/openapi/lib/model/asset_copy_dto.dart +++ b/mobile/openapi/lib/model/asset_copy_dto.dart @@ -22,18 +22,25 @@ class AssetCopyDto { required this.targetId, }); + /// Copy album associations bool albums; + /// Copy favorite status bool favorite; + /// Copy shared links bool sharedLinks; + /// Copy sidecar file bool sidecar; + /// Source asset ID String sourceId; + /// Copy stack association bool stack; + /// Target asset ID String targetId; @override diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart index 845aadcdcd..22c09752d2 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_dto.dart @@ -17,8 +17,10 @@ class AssetDeltaSyncDto { this.userIds = const [], }); + /// Sync assets updated after this date DateTime updatedAfter; + /// User IDs to sync List userIds; @override diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart index a64e1a2fbe..7351840b11 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart @@ -18,10 +18,13 @@ class AssetDeltaSyncResponseDto { this.upserted = const [], }); + /// Deleted asset IDs List deleted; + /// Whether full sync is needed bool needsFullSync; + /// Upserted assets List upserted; @override diff --git a/mobile/openapi/lib/model/asset_edit_action.dart b/mobile/openapi/lib/model/asset_edit_action.dart new file mode 100644 index 0000000000..3754cb4501 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action.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; + +/// Type of edit action to perform +class AssetEditAction { + /// Instantiate a new enum with the provided [value]. + const AssetEditAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const crop = AssetEditAction._(r'crop'); + static const rotate = AssetEditAction._(r'rotate'); + static const mirror = AssetEditAction._(r'mirror'); + + /// List of all possible values in this [enum][AssetEditAction]. + static const values = [ + crop, + rotate, + mirror, + ]; + + static AssetEditAction? fromJson(dynamic value) => AssetEditActionTypeTransformer().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 = AssetEditAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetEditAction] to String, +/// and [decode] dynamic data back to [AssetEditAction]. +class AssetEditActionTypeTransformer { + factory AssetEditActionTypeTransformer() => _instance ??= const AssetEditActionTypeTransformer._(); + + const AssetEditActionTypeTransformer._(); + + String encode(AssetEditAction data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetEditAction. + /// + /// 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. + AssetEditAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'crop': return AssetEditAction.crop; + case r'rotate': return AssetEditAction.rotate; + case r'mirror': return AssetEditAction.mirror; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetEditActionTypeTransformer] instance. + static AssetEditActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart new file mode 100644 index 0000000000..7829de4bd5 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_item_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 AssetEditActionItemDto { + /// Returns a new [AssetEditActionItemDto] instance. + AssetEditActionItemDto({ + required this.action, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + AssetEditActionItemDtoParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDto && + other.action == action && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionItemDto[action=$action, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionItemDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionItemDto( + action: AssetEditAction.fromJson(json[r'action'])!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + ); + } + 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 = AssetEditActionItemDto.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 = AssetEditActionItemDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionItemDto-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] = AssetEditActionItemDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'parameters', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart new file mode 100644 index 0000000000..fc67aa022f --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -0,0 +1,153 @@ +// +// 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 AssetEditActionItemDtoParameters { + /// Returns a new [AssetEditActionItemDtoParameters] instance. + AssetEditActionItemDtoParameters({ + required this.height, + required this.width, + required this.x, + required this.y, + required this.angle, + required this.axis, + }); + + /// Height of the crop + /// + /// Minimum value: 1 + num height; + + /// Width of the crop + /// + /// Minimum value: 1 + num width; + + /// Top-Left X coordinate of crop + /// + /// Minimum value: 0 + num x; + + /// Top-Left Y coordinate of crop + /// + /// Minimum value: 0 + num y; + + /// Rotation angle in degrees + num angle; + + /// Axis to mirror along + MirrorAxis axis; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemDtoParameters && + other.height == height && + other.width == width && + other.x == x && + other.y == y && + other.angle == angle && + other.axis == axis; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (height.hashCode) + + (width.hashCode) + + (x.hashCode) + + (y.hashCode) + + (angle.hashCode) + + (axis.hashCode); + + @override + String toString() => 'AssetEditActionItemDtoParameters[height=$height, width=$width, x=$x, y=$y, angle=$angle, axis=$axis]'; + + Map toJson() { + final json = {}; + json[r'height'] = this.height; + json[r'width'] = this.width; + json[r'x'] = this.x; + json[r'y'] = this.y; + json[r'angle'] = this.angle; + json[r'axis'] = this.axis; + return json; + } + + /// Returns a new [AssetEditActionItemDtoParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionItemDtoParameters? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemDtoParameters"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionItemDtoParameters( + height: num.parse('${json[r'height']}'), + width: num.parse('${json[r'width']}'), + x: num.parse('${json[r'x']}'), + y: num.parse('${json[r'y']}'), + angle: num.parse('${json[r'angle']}'), + axis: MirrorAxis.fromJson(json[r'axis'])!, + ); + } + 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 = AssetEditActionItemDtoParameters.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 = AssetEditActionItemDtoParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionItemDtoParameters-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] = AssetEditActionItemDtoParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'height', + 'width', + 'x', + 'y', + 'angle', + 'axis', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart new file mode 100644 index 0000000000..a23a1ef5f3 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -0,0 +1,116 @@ +// +// 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 AssetEditActionItemResponseDto { + /// Returns a new [AssetEditActionItemResponseDto] instance. + AssetEditActionItemResponseDto({ + required this.action, + required this.id, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + String id; + + AssetEditActionItemDtoParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionItemResponseDto && + other.action == action && + other.id == id && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (id.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionItemResponseDto[action=$action, id=$id, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'id'] = this.id; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionItemResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionItemResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionItemResponseDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionItemResponseDto( + action: AssetEditAction.fromJson(json[r'action'])!, + id: mapValueOfType(json, r'id')!, + parameters: AssetEditActionItemDtoParameters.fromJson(json[r'parameters'])!, + ); + } + 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 = AssetEditActionItemResponseDto.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 = AssetEditActionItemResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionItemResponseDto-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] = AssetEditActionItemResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'id', + 'parameters', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edits_create_dto.dart b/mobile/openapi/lib/model/asset_edits_create_dto.dart new file mode 100644 index 0000000000..9f6fc66904 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edits_create_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetEditsCreateDto { + /// Returns a new [AssetEditsCreateDto] instance. + AssetEditsCreateDto({ + this.edits = const [], + }); + + /// List of edit actions to apply (crop, rotate, or mirror) + List edits; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditsCreateDto && + _deepEquality.equals(other.edits, edits); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (edits.hashCode); + + @override + String toString() => 'AssetEditsCreateDto[edits=$edits]'; + + Map toJson() { + final json = {}; + json[r'edits'] = this.edits; + return json; + } + + /// Returns a new [AssetEditsCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditsCreateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsCreateDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditsCreateDto( + edits: AssetEditActionItemDto.listFromJson(json[r'edits']), + ); + } + 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 = AssetEditsCreateDto.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 = AssetEditsCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditsCreateDto-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] = AssetEditsCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'edits', + }; +} + diff --git a/mobile/openapi/lib/model/asset_edits_response_dto.dart b/mobile/openapi/lib/model/asset_edits_response_dto.dart new file mode 100644 index 0000000000..322b4c0a4c --- /dev/null +++ b/mobile/openapi/lib/model/asset_edits_response_dto.dart @@ -0,0 +1,109 @@ +// +// 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 AssetEditsResponseDto { + /// Returns a new [AssetEditsResponseDto] instance. + AssetEditsResponseDto({ + required this.assetId, + this.edits = const [], + }); + + /// Asset ID these edits belong to + String assetId; + + /// List of edit actions applied to the asset + List edits; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditsResponseDto && + other.assetId == assetId && + _deepEquality.equals(other.edits, edits); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (edits.hashCode); + + @override + String toString() => 'AssetEditsResponseDto[assetId=$assetId, edits=$edits]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'edits'] = this.edits; + return json; + } + + /// Returns a new [AssetEditsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsResponseDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditsResponseDto( + assetId: mapValueOfType(json, r'assetId')!, + edits: AssetEditActionItemResponseDto.listFromJson(json[r'edits']), + ); + } + 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 = AssetEditsResponseDto.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 = AssetEditsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditsResponseDto-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] = AssetEditsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'edits', + }; +} + diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 29e8244a96..3ecc20c699 100644 --- a/mobile/openapi/lib/model/asset_face_create_dto.dart +++ b/mobile/openapi/lib/model/asset_face_create_dto.dart @@ -23,20 +23,28 @@ class AssetFaceCreateDto { required this.y, }); + /// Asset ID String assetId; + /// Face bounding box height int height; + /// Image height in pixels int imageHeight; + /// Image width in pixels int imageWidth; + /// Person ID String personId; + /// Face bounding box width int width; + /// Face bounding box X coordinate int x; + /// Face bounding box Y coordinate int y; @override diff --git a/mobile/openapi/lib/model/asset_face_delete_dto.dart b/mobile/openapi/lib/model/asset_face_delete_dto.dart index 2e53b0699c..a1f3731bea 100644 --- a/mobile/openapi/lib/model/asset_face_delete_dto.dart +++ b/mobile/openapi/lib/model/asset_face_delete_dto.dart @@ -16,6 +16,7 @@ class AssetFaceDeleteDto { required this.force, }); + /// Force delete even if person has other faces bool force; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index c05b511649..61d972a0c4 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -24,22 +24,31 @@ class AssetFaceResponseDto { this.sourceType, }); + /// Bounding box X1 coordinate int boundingBoxX1; + /// Bounding box X2 coordinate int boundingBoxX2; + /// Bounding box Y1 coordinate int boundingBoxY1; + /// Bounding box Y2 coordinate int boundingBoxY2; + /// Face ID String id; + /// Image height in pixels int imageHeight; + /// Image width in pixels int imageWidth; + /// Person associated with face PersonResponseDto? person; + /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_face_update_dto.dart b/mobile/openapi/lib/model/asset_face_update_dto.dart index 71bdde8e9a..1027627552 100644 --- a/mobile/openapi/lib/model/asset_face_update_dto.dart +++ b/mobile/openapi/lib/model/asset_face_update_dto.dart @@ -16,6 +16,7 @@ class AssetFaceUpdateDto { this.data = const [], }); + /// Face update items List data; @override diff --git a/mobile/openapi/lib/model/asset_face_update_item.dart b/mobile/openapi/lib/model/asset_face_update_item.dart index c2c4803259..a81b21e139 100644 --- a/mobile/openapi/lib/model/asset_face_update_item.dart +++ b/mobile/openapi/lib/model/asset_face_update_item.dart @@ -17,8 +17,10 @@ class AssetFaceUpdateItem { required this.personId, }); + /// Asset ID String assetId; + /// Person ID String personId; @override diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 8bf07e1534..1ae5cef07e 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -23,20 +23,28 @@ class AssetFaceWithoutPersonResponseDto { this.sourceType, }); + /// Bounding box X1 coordinate int boundingBoxX1; + /// Bounding box X2 coordinate int boundingBoxX2; + /// Bounding box Y1 coordinate int boundingBoxY1; + /// Bounding box Y2 coordinate int boundingBoxY2; + /// Face ID String id; + /// Image height in pixels int imageHeight; + /// Image width in pixels int imageWidth; + /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart index 7151094b95..3fabb1cac6 100644 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_full_sync_dto.dart @@ -19,6 +19,7 @@ class AssetFullSyncDto { this.userId, }); + /// Last asset ID (pagination) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -27,11 +28,15 @@ class AssetFullSyncDto { /// String? lastId; + /// Maximum number of assets to return + /// /// Minimum value: 1 int limit; + /// Sync assets updated until this date DateTime updatedUntil; + /// Filter by user ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_ids_dto.dart b/mobile/openapi/lib/model/asset_ids_dto.dart index b44888f396..85e5cc3aee 100644 --- a/mobile/openapi/lib/model/asset_ids_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_dto.dart @@ -16,6 +16,7 @@ class AssetIdsDto { this.assetIds = const [], }); + /// Asset IDs List assetIds; @override diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index ff63091caa..9745283021 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -18,10 +18,13 @@ class AssetIdsResponseDto { required this.success, }); + /// Asset ID String assetId; + /// Error reason if failed AssetIdsResponseDtoErrorEnum? error; + /// Whether operation succeeded bool success; @override @@ -116,7 +119,7 @@ class AssetIdsResponseDto { }; } - +/// Error reason if failed class AssetIdsResponseDtoErrorEnum { /// Instantiate a new enum with the provided [value]. const AssetIdsResponseDtoErrorEnum._(this.value); diff --git a/mobile/openapi/lib/model/asset_job_name.dart b/mobile/openapi/lib/model/asset_job_name.dart index 11e0555b86..7625677bb5 100644 --- a/mobile/openapi/lib/model/asset_job_name.dart +++ b/mobile/openapi/lib/model/asset_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Job name class AssetJobName { /// Instantiate a new enum with the provided [value]. const AssetJobName._(this.value); diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 0f8bfab009..0aa5544a3a 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -17,8 +17,10 @@ class AssetJobsDto { required this.name, }); + /// Asset IDs List assetIds; + /// Job name AssetJobName name; @override diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index 75428ec5f6..905e738b6e 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -17,8 +17,10 @@ class AssetMediaResponseDto { required this.status, }); + /// Asset media ID String id; + /// Upload status AssetMediaStatus status; @override diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index aa7e2a6f5c..087d19da1f 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -23,12 +23,14 @@ class AssetMediaSize { String toJson() => value; + static const original = AssetMediaSize._(r'original'); static const fullsize = AssetMediaSize._(r'fullsize'); static const preview = AssetMediaSize._(r'preview'); static const thumbnail = AssetMediaSize._(r'thumbnail'); /// List of all possible values in this [enum][AssetMediaSize]. static const values = [ + original, fullsize, preview, thumbnail, @@ -70,6 +72,7 @@ class AssetMediaSizeTypeTransformer { AssetMediaSize? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { + case r'original': return AssetMediaSize.original; case r'fullsize': return AssetMediaSize.fullsize; case r'preview': return AssetMediaSize.preview; case r'thumbnail': return AssetMediaSize.thumbnail; diff --git a/mobile/openapi/lib/model/asset_media_status.dart b/mobile/openapi/lib/model/asset_media_status.dart index 42fec08cc7..b45918e5c3 100644 --- a/mobile/openapi/lib/model/asset_media_status.dart +++ b/mobile/openapi/lib/model/asset_media_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Upload status class AssetMediaStatus { /// Instantiate a new enum with the provided [value]. const AssetMediaStatus._(this.value); diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart new file mode 100644 index 0000000000..6376ebc531 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetMetadataBulkDeleteDto { + /// Returns a new [AssetMetadataBulkDeleteDto] instance. + AssetMetadataBulkDeleteDto({ + this.items = const [], + }); + + /// Metadata items to delete + List items; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkDeleteDto && + _deepEquality.equals(other.items, items); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (items.hashCode); + + @override + String toString() => 'AssetMetadataBulkDeleteDto[items=$items]'; + + Map toJson() { + final json = {}; + json[r'items'] = this.items; + return json; + } + + /// Returns a new [AssetMetadataBulkDeleteDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataBulkDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataBulkDeleteDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataBulkDeleteDto( + items: AssetMetadataBulkDeleteItemDto.listFromJson(json[r'items']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetMetadataBulkDeleteDto.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 = AssetMetadataBulkDeleteDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataBulkDeleteDto-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] = AssetMetadataBulkDeleteDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'items', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart new file mode 100644 index 0000000000..90417b79e0 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart @@ -0,0 +1,109 @@ +// +// 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 AssetMetadataBulkDeleteItemDto { + /// Returns a new [AssetMetadataBulkDeleteItemDto] instance. + AssetMetadataBulkDeleteItemDto({ + required this.assetId, + required this.key, + }); + + /// Asset ID + String assetId; + + /// Metadata key + String key; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkDeleteItemDto && + other.assetId == assetId && + other.key == key; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (key.hashCode); + + @override + String toString() => 'AssetMetadataBulkDeleteItemDto[assetId=$assetId, key=$key]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'key'] = this.key; + return json; + } + + /// Returns a new [AssetMetadataBulkDeleteItemDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataBulkDeleteItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataBulkDeleteItemDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataBulkDeleteItemDto( + assetId: mapValueOfType(json, r'assetId')!, + key: mapValueOfType(json, r'key')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetMetadataBulkDeleteItemDto.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 = AssetMetadataBulkDeleteItemDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataBulkDeleteItemDto-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] = AssetMetadataBulkDeleteItemDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'key', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart new file mode 100644 index 0000000000..b79a693726 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -0,0 +1,127 @@ +// +// 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 AssetMetadataBulkResponseDto { + /// Returns a new [AssetMetadataBulkResponseDto] instance. + AssetMetadataBulkResponseDto({ + required this.assetId, + required this.key, + required this.updatedAt, + required this.value, + }); + + /// Asset ID + String assetId; + + /// Metadata key + String key; + + /// Last update date + DateTime updatedAt; + + /// Metadata value (object) + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto && + other.assetId == assetId && + other.key == key && + other.updatedAt == updatedAt && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (key.hashCode) + + (updatedAt.hashCode) + + (value.hashCode); + + @override + String toString() => 'AssetMetadataBulkResponseDto[assetId=$assetId, key=$key, updatedAt=$updatedAt, value=$value]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'key'] = this.key; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'value'] = this.value; + return json; + } + + /// Returns a new [AssetMetadataBulkResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataBulkResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataBulkResponseDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataBulkResponseDto( + assetId: mapValueOfType(json, r'assetId')!, + key: mapValueOfType(json, r'key')!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + value: mapValueOfType(json, r'value')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetMetadataBulkResponseDto.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 = AssetMetadataBulkResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataBulkResponseDto-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] = AssetMetadataBulkResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'key', + 'updatedAt', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart new file mode 100644 index 0000000000..a5e770b02a --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetMetadataBulkUpsertDto { + /// Returns a new [AssetMetadataBulkUpsertDto] instance. + AssetMetadataBulkUpsertDto({ + this.items = const [], + }); + + /// Metadata items to upsert + List items; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertDto && + _deepEquality.equals(other.items, items); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (items.hashCode); + + @override + String toString() => 'AssetMetadataBulkUpsertDto[items=$items]'; + + Map toJson() { + final json = {}; + json[r'items'] = this.items; + return json; + } + + /// Returns a new [AssetMetadataBulkUpsertDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataBulkUpsertDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataBulkUpsertDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataBulkUpsertDto( + items: AssetMetadataBulkUpsertItemDto.listFromJson(json[r'items']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetMetadataBulkUpsertDto.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 = AssetMetadataBulkUpsertDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataBulkUpsertDto-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] = AssetMetadataBulkUpsertDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'items', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart new file mode 100644 index 0000000000..caaf379b30 --- /dev/null +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -0,0 +1,118 @@ +// +// 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 AssetMetadataBulkUpsertItemDto { + /// Returns a new [AssetMetadataBulkUpsertItemDto] instance. + AssetMetadataBulkUpsertItemDto({ + required this.assetId, + required this.key, + required this.value, + }); + + /// Asset ID + String assetId; + + /// Metadata key + String key; + + /// Metadata value (object) + Object value; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto && + other.assetId == assetId && + other.key == key && + other.value == value; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (key.hashCode) + + (value.hashCode); + + @override + String toString() => 'AssetMetadataBulkUpsertItemDto[assetId=$assetId, key=$key, value=$value]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'key'] = this.key; + json[r'value'] = this.value; + return json; + } + + /// Returns a new [AssetMetadataBulkUpsertItemDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetMetadataBulkUpsertItemDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMetadataBulkUpsertItemDto"); + if (value is Map) { + final json = value.cast(); + + return AssetMetadataBulkUpsertItemDto( + assetId: mapValueOfType(json, r'assetId')!, + key: mapValueOfType(json, r'key')!, + value: mapValueOfType(json, r'value')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetMetadataBulkUpsertItemDto.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 = AssetMetadataBulkUpsertItemDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetMetadataBulkUpsertItemDto-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] = AssetMetadataBulkUpsertItemDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetId', + 'key', + 'value', + }; +} + diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index af5769b9bb..2c3faab178 100644 --- a/mobile/openapi/lib/model/asset_metadata_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_response_dto.dart @@ -18,10 +18,13 @@ class AssetMetadataResponseDto { required this.value, }); - AssetMetadataKey key; + /// Metadata key + String key; + /// Last update date DateTime updatedAt; + /// Metadata value (object) Object value; @override @@ -57,7 +60,7 @@ class AssetMetadataResponseDto { final json = value.cast(); return AssetMetadataResponseDto( - key: AssetMetadataKey.fromJson(json[r'key'])!, + key: mapValueOfType(json, r'key')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!, value: mapValueOfType(json, r'value')!, ); diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart index 45d044feb0..b1473d4826 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_dto.dart @@ -16,6 +16,7 @@ class AssetMetadataUpsertDto { this.items = const [], }); + /// Metadata items to upsert List items; @override diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart index 4b7e6579a1..8a6bcb9b01 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -17,8 +17,10 @@ class AssetMetadataUpsertItemDto { required this.value, }); - AssetMetadataKey key; + /// Metadata key + String key; + /// Metadata value (object) Object value; @override @@ -51,7 +53,7 @@ class AssetMetadataUpsertItemDto { final json = value.cast(); return AssetMetadataUpsertItemDto( - key: AssetMetadataKey.fromJson(json[r'key'])!, + key: mapValueOfType(json, r'key')!, value: mapValueOfType(json, r'value')!, ); } diff --git a/mobile/openapi/lib/model/asset_order.dart b/mobile/openapi/lib/model/asset_order.dart index ca04e2b78f..21edd95ff6 100644 --- a/mobile/openapi/lib/model/asset_order.dart +++ b/mobile/openapi/lib/model/asset_order.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset sort order class AssetOrder { /// Instantiate a new enum with the provided [value]. const AssetOrder._(this.value); diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 8d49986359..078dd0bdaf 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -23,8 +23,10 @@ class AssetResponseDto { required this.fileCreatedAt, required this.fileModifiedAt, required this.hasMetadata, + required this.height, required this.id, required this.isArchived, + required this.isEdited, required this.isFavorite, required this.isOffline, required this.isTrashed, @@ -45,20 +47,25 @@ class AssetResponseDto { this.unassignedFaces = const [], required this.updatedAt, required this.visibility, + required this.width, }); - /// base64 encoded sha1 hash + /// Base64 encoded SHA1 hash String checksum; /// The UTC timestamp when the asset was originally uploaded to Immich. DateTime createdAt; + /// Device asset ID String deviceAssetId; + /// Device ID String deviceId; + /// Duplicate group ID String? duplicateId; + /// Video duration (for videos) String duration; /// @@ -75,27 +82,43 @@ class AssetResponseDto { /// The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. DateTime fileModifiedAt; + /// Whether asset has metadata bool hasMetadata; + /// Asset height + num? height; + + /// Asset ID String id; + /// Is archived bool isArchived; + /// Is edited + bool isEdited; + + /// Is favorite bool isFavorite; + /// Is offline bool isOffline; + /// Is trashed bool isTrashed; + /// Library ID String? libraryId; + /// Live photo video ID String? livePhotoVideoId; /// The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months. DateTime localDateTime; + /// Original file name String originalFileName; + /// Original MIME type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -104,6 +127,7 @@ class AssetResponseDto { /// String? originalMimeType; + /// Original file path String originalPath; /// @@ -114,10 +138,12 @@ class AssetResponseDto { /// UserResponseDto? owner; + /// Owner user ID String ownerId; List people; + /// Is resized /// /// 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 @@ -130,8 +156,10 @@ class AssetResponseDto { List tags; + /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; + /// Asset type AssetTypeEnum type; List unassignedFaces; @@ -139,8 +167,12 @@ class AssetResponseDto { /// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. DateTime updatedAt; + /// Asset visibility AssetVisibility visibility; + /// Asset width + num? width; + @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.checksum == checksum && @@ -153,8 +185,10 @@ class AssetResponseDto { other.fileCreatedAt == fileCreatedAt && other.fileModifiedAt == fileModifiedAt && other.hasMetadata == hasMetadata && + other.height == height && other.id == id && other.isArchived == isArchived && + other.isEdited == isEdited && other.isFavorite == isFavorite && other.isOffline == isOffline && other.isTrashed == isTrashed && @@ -174,7 +208,8 @@ class AssetResponseDto { other.type == type && _deepEquality.equals(other.unassignedFaces, unassignedFaces) && other.updatedAt == updatedAt && - other.visibility == visibility; + other.visibility == visibility && + other.width == width; @override int get hashCode => @@ -189,8 +224,10 @@ class AssetResponseDto { (fileCreatedAt.hashCode) + (fileModifiedAt.hashCode) + (hasMetadata.hashCode) + + (height == null ? 0 : height!.hashCode) + (id.hashCode) + (isArchived.hashCode) + + (isEdited.hashCode) + (isFavorite.hashCode) + (isOffline.hashCode) + (isTrashed.hashCode) + @@ -210,10 +247,11 @@ class AssetResponseDto { (type.hashCode) + (unassignedFaces.hashCode) + (updatedAt.hashCode) + - (visibility.hashCode); + (visibility.hashCode) + + (width == null ? 0 : width!.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]'; + String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; @@ -235,8 +273,14 @@ class AssetResponseDto { json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); json[r'hasMetadata'] = this.hasMetadata; + if (this.height != null) { + json[r'height'] = this.height; + } else { + // json[r'height'] = null; + } json[r'id'] = this.id; json[r'isArchived'] = this.isArchived; + json[r'isEdited'] = this.isEdited; json[r'isFavorite'] = this.isFavorite; json[r'isOffline'] = this.isOffline; json[r'isTrashed'] = this.isTrashed; @@ -285,6 +329,11 @@ class AssetResponseDto { json[r'unassignedFaces'] = this.unassignedFaces; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'visibility'] = this.visibility; + if (this.width != null) { + json[r'width'] = this.width; + } else { + // json[r'width'] = null; + } return json; } @@ -307,8 +356,12 @@ class AssetResponseDto { fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, hasMetadata: mapValueOfType(json, r'hasMetadata')!, + height: json[r'height'] == null + ? null + : num.parse('${json[r'height']}'), id: mapValueOfType(json, r'id')!, isArchived: mapValueOfType(json, r'isArchived')!, + isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isOffline: mapValueOfType(json, r'isOffline')!, isTrashed: mapValueOfType(json, r'isTrashed')!, @@ -329,6 +382,9 @@ class AssetResponseDto { unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), updatedAt: mapDateTime(json, r'updatedAt', r'')!, visibility: AssetVisibility.fromJson(json[r'visibility'])!, + width: json[r'width'] == null + ? null + : num.parse('${json[r'width']}'), ); } return null; @@ -384,8 +440,10 @@ class AssetResponseDto { 'fileCreatedAt', 'fileModifiedAt', 'hasMetadata', + 'height', 'id', 'isArchived', + 'isEdited', 'isFavorite', 'isOffline', 'isTrashed', @@ -397,6 +455,7 @@ class AssetResponseDto { 'type', 'updatedAt', 'visibility', + 'width', }; } diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index bb4becb129..229e7aa710 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -18,10 +18,13 @@ class AssetStackResponseDto { required this.primaryAssetId, }); + /// Number of assets in stack int assetCount; + /// Stack ID String id; + /// Primary asset ID String primaryAssetId; @override diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index d11ce55a5c..201550c87f 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -18,10 +18,13 @@ class AssetStatsResponseDto { required this.videos, }); + /// Number of images int images; + /// Total number of assets int total; + /// Number of videos int videos; @override diff --git a/mobile/openapi/lib/model/asset_type_enum.dart b/mobile/openapi/lib/model/asset_type_enum.dart index 1022beb24e..b6e0351198 100644 --- a/mobile/openapi/lib/model/asset_type_enum.dart +++ b/mobile/openapi/lib/model/asset_type_enum.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset type class AssetTypeEnum { /// Instantiate a new enum with the provided [value]. const AssetTypeEnum._(this.value); diff --git a/mobile/openapi/lib/model/asset_visibility.dart b/mobile/openapi/lib/model/asset_visibility.dart index 498bf17c38..6290dffb2e 100644 --- a/mobile/openapi/lib/model/asset_visibility.dart +++ b/mobile/openapi/lib/model/asset_visibility.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset visibility class AssetVisibility { /// Instantiate a new enum with the provided [value]. const AssetVisibility._(this.value); diff --git a/mobile/openapi/lib/model/audio_codec.dart b/mobile/openapi/lib/model/audio_codec.dart index ea1e96f36e..095c616995 100644 --- a/mobile/openapi/lib/model/audio_codec.dart +++ b/mobile/openapi/lib/model/audio_codec.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Target audio codec class AudioCodec { /// Instantiate a new enum with the provided [value]. const AudioCodec._(this.value); diff --git a/mobile/openapi/lib/model/auth_status_response_dto.dart b/mobile/openapi/lib/model/auth_status_response_dto.dart index 4e823506ee..23b9d40525 100644 --- a/mobile/openapi/lib/model/auth_status_response_dto.dart +++ b/mobile/openapi/lib/model/auth_status_response_dto.dart @@ -20,6 +20,7 @@ class AuthStatusResponseDto { this.pinExpiresAt, }); + /// Session expiration date /// /// 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 @@ -28,12 +29,16 @@ class AuthStatusResponseDto { /// String? expiresAt; + /// Is elevated session bool isElevated; + /// Has password set bool password; + /// Has PIN code set bool pinCode; + /// PIN expiration date /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index 875eb138a8..a817832dab 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -16,6 +16,7 @@ class AvatarUpdate { this.color, }); + /// Avatar color /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/bulk_id_error_reason.dart b/mobile/openapi/lib/model/bulk_id_error_reason.dart index cdaf70217e..ea56e9dbba 100644 --- a/mobile/openapi/lib/model/bulk_id_error_reason.dart +++ b/mobile/openapi/lib/model/bulk_id_error_reason.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Error reason class BulkIdErrorReason { /// Instantiate a new enum with the provided [value]. const BulkIdErrorReason._(this.value); diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index 67a587e8d0..cd122785dd 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -18,10 +18,13 @@ class BulkIdResponseDto { required this.success, }); + /// Error reason if failed BulkIdResponseDtoErrorEnum? error; + /// ID String id; + /// Whether operation succeeded bool success; @override @@ -116,7 +119,7 @@ class BulkIdResponseDto { }; } - +/// Error reason if failed class BulkIdResponseDtoErrorEnum { /// Instantiate a new enum with the provided [value]. const BulkIdResponseDtoErrorEnum._(this.value); diff --git a/mobile/openapi/lib/model/bulk_ids_dto.dart b/mobile/openapi/lib/model/bulk_ids_dto.dart index 6a7f8ceeec..7e7864a285 100644 --- a/mobile/openapi/lib/model/bulk_ids_dto.dart +++ b/mobile/openapi/lib/model/bulk_ids_dto.dart @@ -16,6 +16,7 @@ class BulkIdsDto { this.ids = const [], }); + /// IDs to process List ids; @override diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart index d49f1ad3d7..0b7f0738fe 100644 --- a/mobile/openapi/lib/model/cast_response.dart +++ b/mobile/openapi/lib/model/cast_response.dart @@ -16,6 +16,7 @@ class CastResponse { this.gCastEnabled = false, }); + /// Whether Google Cast is enabled bool gCastEnabled; @override diff --git a/mobile/openapi/lib/model/cast_update.dart b/mobile/openapi/lib/model/cast_update.dart index 8707639132..8dbf80f171 100644 --- a/mobile/openapi/lib/model/cast_update.dart +++ b/mobile/openapi/lib/model/cast_update.dart @@ -16,6 +16,7 @@ class CastUpdate { this.gCastEnabled, }); + /// Whether Google Cast is enabled /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/change_password_dto.dart b/mobile/openapi/lib/model/change_password_dto.dart index 4a897f4079..3dd6e437da 100644 --- a/mobile/openapi/lib/model/change_password_dto.dart +++ b/mobile/openapi/lib/model/change_password_dto.dart @@ -18,10 +18,13 @@ class ChangePasswordDto { required this.password, }); + /// Invalidate all other sessions bool invalidateSessions; + /// New password (min 8 characters) String newPassword; + /// Current password String password; @override diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart index 42ce6d5c3e..6e4a471092 100644 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_dto.dart @@ -17,8 +17,10 @@ class CheckExistingAssetsDto { required this.deviceId, }); + /// Device asset IDs to check List deviceAssetIds; + /// Device ID String deviceId; @override diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart index ad93578ebc..9fb13f100f 100644 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart @@ -16,6 +16,7 @@ class CheckExistingAssetsResponseDto { this.existingIds = const [], }); + /// Existing asset IDs List existingIds; @override diff --git a/mobile/openapi/lib/model/clip_config.dart b/mobile/openapi/lib/model/clip_config.dart index b500d20f2e..915e4975ed 100644 --- a/mobile/openapi/lib/model/clip_config.dart +++ b/mobile/openapi/lib/model/clip_config.dart @@ -17,8 +17,10 @@ class CLIPConfig { required this.modelName, }); + /// Whether the task is enabled bool enabled; + /// Name of the model to use String modelName; @override diff --git a/mobile/openapi/lib/model/colorspace.dart b/mobile/openapi/lib/model/colorspace.dart index e0c1658be5..e871e140fb 100644 --- a/mobile/openapi/lib/model/colorspace.dart +++ b/mobile/openapi/lib/model/colorspace.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Colorspace class Colorspace { /// Instantiate a new enum with the provided [value]. const Colorspace._(this.value); diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index e0e16ee427..1bef8f29d8 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -17,8 +17,10 @@ class ContributorCountResponseDto { required this.userId, }); + /// Number of assets contributed int assetCount; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/cq_mode.dart b/mobile/openapi/lib/model/cq_mode.dart index f660fabf1f..efd788b5fb 100644 --- a/mobile/openapi/lib/model/cq_mode.dart +++ b/mobile/openapi/lib/model/cq_mode.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// CQ mode class CQMode { /// Instantiate a new enum with the provided [value]. const CQMode._(this.value); diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index ff8c1df647..183a41c772 100644 --- a/mobile/openapi/lib/model/create_album_dto.dart +++ b/mobile/openapi/lib/model/create_album_dto.dart @@ -19,12 +19,16 @@ class CreateAlbumDto { this.description, }); + /// Album name String albumName; + /// Album users List albumUsers; + /// Initial asset IDs List assetIds; + /// Album description /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 2b8085be6f..69942fee5c 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -19,10 +19,13 @@ class CreateLibraryDto { required this.ownerId, }); + /// Exclusion patterns (max 128) Set exclusionPatterns; + /// Import paths (max 128) Set importPaths; + /// Library name /// /// 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 @@ -31,6 +34,7 @@ class CreateLibraryDto { /// String? name; + /// Owner user ID String ownerId; @override diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index ee98142e86..20d7cbd5e7 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -18,10 +18,13 @@ class CreateProfileImageResponseDto { required this.userId, }); + /// Profile image change date DateTime profileChangedAt; + /// Profile image file path String profileImagePath; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/crop_parameters.dart b/mobile/openapi/lib/model/crop_parameters.dart new file mode 100644 index 0000000000..8c5b884596 --- /dev/null +++ b/mobile/openapi/lib/model/crop_parameters.dart @@ -0,0 +1,135 @@ +// +// 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 CropParameters { + /// Returns a new [CropParameters] instance. + CropParameters({ + required this.height, + required this.width, + required this.x, + required this.y, + }); + + /// Height of the crop + /// + /// Minimum value: 1 + num height; + + /// Width of the crop + /// + /// Minimum value: 1 + num width; + + /// Top-Left X coordinate of crop + /// + /// Minimum value: 0 + num x; + + /// Top-Left Y coordinate of crop + /// + /// Minimum value: 0 + num y; + + @override + bool operator ==(Object other) => identical(this, other) || other is CropParameters && + other.height == height && + other.width == width && + other.x == x && + other.y == y; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (height.hashCode) + + (width.hashCode) + + (x.hashCode) + + (y.hashCode); + + @override + String toString() => 'CropParameters[height=$height, width=$width, x=$x, y=$y]'; + + Map toJson() { + final json = {}; + json[r'height'] = this.height; + json[r'width'] = this.width; + json[r'x'] = this.x; + json[r'y'] = this.y; + return json; + } + + /// Returns a new [CropParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CropParameters? fromJson(dynamic value) { + upgradeDto(value, "CropParameters"); + if (value is Map) { + final json = value.cast(); + + return CropParameters( + height: num.parse('${json[r'height']}'), + width: num.parse('${json[r'width']}'), + x: num.parse('${json[r'x']}'), + y: num.parse('${json[r'y']}'), + ); + } + 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 = CropParameters.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 = CropParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CropParameters-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] = CropParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'height', + 'width', + 'x', + 'y', + }; +} + diff --git a/mobile/openapi/lib/model/database_backup_config.dart b/mobile/openapi/lib/model/database_backup_config.dart index d82128bd44..419968c3f3 100644 --- a/mobile/openapi/lib/model/database_backup_config.dart +++ b/mobile/openapi/lib/model/database_backup_config.dart @@ -18,10 +18,14 @@ class DatabaseBackupConfig { required this.keepLastAmount, }); + /// Cron expression String cronExpression; + /// Enabled bool enabled; + /// Keep last amount + /// /// Minimum value: 1 num keepLastAmount; diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart new file mode 100644 index 0000000000..8bc33a81dc --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -0,0 +1,101 @@ +// +// 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 DatabaseBackupDeleteDto { + /// Returns a new [DatabaseBackupDeleteDto] instance. + DatabaseBackupDeleteDto({ + this.backups = const [], + }); + + List backups; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDeleteDto && + _deepEquality.equals(other.backups, backups); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (backups.hashCode); + + @override + String toString() => 'DatabaseBackupDeleteDto[backups=$backups]'; + + Map toJson() { + final json = {}; + json[r'backups'] = this.backups; + return json; + } + + /// Returns a new [DatabaseBackupDeleteDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupDeleteDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupDeleteDto( + backups: json[r'backups'] is Iterable + ? (json[r'backups'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = DatabaseBackupDeleteDto.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 = DatabaseBackupDeleteDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupDeleteDto-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] = DatabaseBackupDeleteDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'backups', + }; +} + diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart new file mode 100644 index 0000000000..4bf231587b --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DatabaseBackupDto { + /// Returns a new [DatabaseBackupDto] instance. + DatabaseBackupDto({ + required this.filename, + required this.filesize, + }); + + String filename; + + num filesize; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupDto && + other.filename == filename && + other.filesize == filesize; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (filename.hashCode) + + (filesize.hashCode); + + @override + String toString() => 'DatabaseBackupDto[filename=$filename, filesize=$filesize]'; + + Map toJson() { + final json = {}; + json[r'filename'] = this.filename; + json[r'filesize'] = this.filesize; + return json; + } + + /// Returns a new [DatabaseBackupDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupDto( + filename: mapValueOfType(json, r'filename')!, + filesize: num.parse('${json[r'filesize']}'), + ); + } + 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 = DatabaseBackupDto.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 = DatabaseBackupDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupDto-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] = DatabaseBackupDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'filename', + 'filesize', + }; +} + diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart new file mode 100644 index 0000000000..16985dd605 --- /dev/null +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class DatabaseBackupListResponseDto { + /// Returns a new [DatabaseBackupListResponseDto] instance. + DatabaseBackupListResponseDto({ + this.backups = const [], + }); + + List backups; + + @override + bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupListResponseDto && + _deepEquality.equals(other.backups, backups); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (backups.hashCode); + + @override + String toString() => 'DatabaseBackupListResponseDto[backups=$backups]'; + + Map toJson() { + final json = {}; + json[r'backups'] = this.backups; + return json; + } + + /// Returns a new [DatabaseBackupListResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DatabaseBackupListResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DatabaseBackupListResponseDto"); + if (value is Map) { + final json = value.cast(); + + return DatabaseBackupListResponseDto( + backups: DatabaseBackupDto.listFromJson(json[r'backups']), + ); + } + 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 = DatabaseBackupListResponseDto.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 = DatabaseBackupListResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DatabaseBackupListResponseDto-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] = DatabaseBackupListResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'backups', + }; +} + diff --git a/mobile/openapi/lib/model/download_archive_dto.dart b/mobile/openapi/lib/model/download_archive_dto.dart new file mode 100644 index 0000000000..20e8527f18 --- /dev/null +++ b/mobile/openapi/lib/model/download_archive_dto.dart @@ -0,0 +1,120 @@ +// +// 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 DownloadArchiveDto { + /// Returns a new [DownloadArchiveDto] instance. + DownloadArchiveDto({ + this.assetIds = const [], + this.edited, + }); + + /// Asset IDs + List assetIds; + + /// Download edited asset if available + /// + /// 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? edited; + + @override + bool operator ==(Object other) => identical(this, other) || other is DownloadArchiveDto && + _deepEquality.equals(other.assetIds, assetIds) && + other.edited == edited; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode) + + (edited == null ? 0 : edited!.hashCode); + + @override + String toString() => 'DownloadArchiveDto[assetIds=$assetIds, edited=$edited]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + if (this.edited != null) { + json[r'edited'] = this.edited; + } else { + // json[r'edited'] = null; + } + return json; + } + + /// Returns a new [DownloadArchiveDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static DownloadArchiveDto? fromJson(dynamic value) { + upgradeDto(value, "DownloadArchiveDto"); + if (value is Map) { + final json = value.cast(); + + return DownloadArchiveDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + edited: mapValueOfType(json, r'edited'), + ); + } + 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 = DownloadArchiveDto.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 = DownloadArchiveDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of DownloadArchiveDto-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] = DownloadArchiveDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetIds', + }; +} + diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index 5f3fd1a8c1..97a3346a67 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -17,8 +17,10 @@ class DownloadArchiveInfo { required this.size, }); + /// Asset IDs in this archive List assetIds; + /// Archive size in bytes int size; @override diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index 6f4777975c..a1ba44920e 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -19,6 +19,7 @@ class DownloadInfoDto { this.userId, }); + /// Album ID to download /// /// 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 @@ -27,6 +28,8 @@ class DownloadInfoDto { /// String? albumId; + /// Archive size limit in bytes + /// /// Minimum value: 1 /// /// Please note: This property should have been non-nullable! Since the specification file @@ -36,8 +39,10 @@ class DownloadInfoDto { /// int? archiveSize; + /// Asset IDs to download List assetIds; + /// User ID to download assets from /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 041da44b71..32e9487475 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -17,8 +17,10 @@ class DownloadResponse { this.includeEmbeddedVideos = false, }); + /// Maximum archive size in bytes int archiveSize; + /// Whether to include embedded videos in downloads bool includeEmbeddedVideos; @override diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index 5c6bd11266..81912e1d30 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -17,8 +17,10 @@ class DownloadResponseDto { required this.totalSize, }); + /// Archive information List archives; + /// Total size in bytes int totalSize; @override diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 8df825a922..4acc1c8bd3 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -17,6 +17,8 @@ class DownloadUpdate { this.includeEmbeddedVideos, }); + /// Maximum archive size in bytes + /// /// Minimum value: 1 /// /// Please note: This property should have been non-nullable! Since the specification file @@ -26,6 +28,7 @@ class DownloadUpdate { /// int? archiveSize; + /// Whether to include embedded videos in downloads /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/duplicate_detection_config.dart b/mobile/openapi/lib/model/duplicate_detection_config.dart index e4fc352028..43233826ef 100644 --- a/mobile/openapi/lib/model/duplicate_detection_config.dart +++ b/mobile/openapi/lib/model/duplicate_detection_config.dart @@ -17,8 +17,11 @@ class DuplicateDetectionConfig { required this.maxDistance, }); + /// Whether the task is enabled bool enabled; + /// Maximum distance threshold for duplicate detection + /// /// Minimum value: 0.001 /// Maximum value: 0.1 double maxDistance; diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart index 6ac7c46871..6c85dc8013 100644 --- a/mobile/openapi/lib/model/duplicate_response_dto.dart +++ b/mobile/openapi/lib/model/duplicate_response_dto.dart @@ -17,8 +17,10 @@ class DuplicateResponseDto { required this.duplicateId, }); + /// Duplicate assets List assets; + /// Duplicate group ID String duplicateId; @override diff --git a/mobile/openapi/lib/model/email_notifications_response.dart b/mobile/openapi/lib/model/email_notifications_response.dart index d6dcfb9273..08a3d580c6 100644 --- a/mobile/openapi/lib/model/email_notifications_response.dart +++ b/mobile/openapi/lib/model/email_notifications_response.dart @@ -18,10 +18,13 @@ class EmailNotificationsResponse { required this.enabled, }); + /// Whether to receive email notifications for album invites bool albumInvite; + /// Whether to receive email notifications for album updates bool albumUpdate; + /// Whether email notifications are enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/email_notifications_update.dart b/mobile/openapi/lib/model/email_notifications_update.dart index dad0a52fde..e158e45598 100644 --- a/mobile/openapi/lib/model/email_notifications_update.dart +++ b/mobile/openapi/lib/model/email_notifications_update.dart @@ -18,6 +18,7 @@ class EmailNotificationsUpdate { this.enabled, }); + /// Whether to receive email notifications for album invites /// /// 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 @@ -26,6 +27,7 @@ class EmailNotificationsUpdate { /// bool? albumInvite; + /// Whether to receive email notifications for album updates /// /// 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 @@ -34,6 +36,7 @@ class EmailNotificationsUpdate { /// bool? albumUpdate; + /// Whether email notifications are enabled /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 17397b2081..6bb58a8ab9 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -37,48 +37,70 @@ class ExifResponseDto { this.timeZone, }); + /// City name String? city; + /// Country name String? country; + /// Original date/time DateTime? dateTimeOriginal; + /// Image description String? description; + /// Image height in pixels num? exifImageHeight; + /// Image width in pixels num? exifImageWidth; + /// Exposure time String? exposureTime; + /// F-number (aperture) num? fNumber; + /// File size in bytes int? fileSizeInByte; + /// Focal length in mm num? focalLength; + /// ISO sensitivity num? iso; + /// GPS latitude num? latitude; + /// Lens model String? lensModel; + /// GPS longitude num? longitude; + /// Camera make String? make; + /// Camera model String? model; + /// Modification date/time DateTime? modifyDate; + /// Image orientation String? orientation; + /// Projection type String? projectionType; + /// Rating num? rating; + /// State/province name String? state; + /// Time zone String? timeZone; @override diff --git a/mobile/openapi/lib/model/face_dto.dart b/mobile/openapi/lib/model/face_dto.dart index c84a518b8c..ec5f5c8a6c 100644 --- a/mobile/openapi/lib/model/face_dto.dart +++ b/mobile/openapi/lib/model/face_dto.dart @@ -16,6 +16,7 @@ class FaceDto { required this.id, }); + /// Face ID String id; @override diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 439efbbfae..4b9d7a6e9e 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -20,19 +20,27 @@ class FacialRecognitionConfig { required this.modelName, }); + /// Whether the task is enabled bool enabled; + /// Maximum distance threshold for face recognition + /// /// Minimum value: 0.1 /// Maximum value: 2 double maxDistance; + /// Minimum number of faces required for recognition + /// /// Minimum value: 1 int minFaces; + /// Minimum confidence score for face detection + /// /// Minimum value: 0.1 /// Maximum value: 1 double minScore; + /// Name of the model to use String modelName; @override diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 248b64b054..906a95a83c 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -17,8 +17,10 @@ class FoldersResponse { this.sidebarWeb = false, }); + /// Whether folders are enabled bool enabled; + /// Whether folders appear in web sidebar bool sidebarWeb; @override diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart index 0234717754..edd58014d4 100644 --- a/mobile/openapi/lib/model/folders_update.dart +++ b/mobile/openapi/lib/model/folders_update.dart @@ -17,6 +17,7 @@ class FoldersUpdate { this.sidebarWeb, }); + /// Whether folders are enabled /// /// 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 @@ -25,6 +26,7 @@ class FoldersUpdate { /// bool? enabled; + /// Whether folders appear in web sidebar /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/image_format.dart b/mobile/openapi/lib/model/image_format.dart index 479b519e24..1a0dde5def 100644 --- a/mobile/openapi/lib/model/image_format.dart +++ b/mobile/openapi/lib/model/image_format.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Image format class ImageFormat { /// Instantiate a new enum with the provided [value]. const ImageFormat._(this.value); diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index fe6743cba0..3a3412384e 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -16,6 +16,7 @@ class JobCreateDto { required this.name, }); + /// Job name ManualJobName name; @override diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart index 038a17a8e6..96b9339b7d 100644 --- a/mobile/openapi/lib/model/job_name.dart +++ b/mobile/openapi/lib/model/job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Job name class JobName { /// Instantiate a new enum with the provided [value]. const JobName._(this.value); @@ -29,6 +29,7 @@ class JobName { static const assetDetectFaces = JobName._(r'AssetDetectFaces'); static const assetDetectDuplicatesQueueAll = JobName._(r'AssetDetectDuplicatesQueueAll'); static const assetDetectDuplicates = JobName._(r'AssetDetectDuplicates'); + static const assetEditThumbnailGeneration = JobName._(r'AssetEditThumbnailGeneration'); static const assetEncodeVideoQueueAll = JobName._(r'AssetEncodeVideoQueueAll'); static const assetEncodeVideo = JobName._(r'AssetEncodeVideo'); static const assetEmptyTrash = JobName._(r'AssetEmptyTrash'); @@ -87,6 +88,7 @@ class JobName { assetDetectFaces, assetDetectDuplicatesQueueAll, assetDetectDuplicates, + assetEditThumbnailGeneration, assetEncodeVideoQueueAll, assetEncodeVideo, assetEmptyTrash, @@ -180,6 +182,7 @@ class JobNameTypeTransformer { case r'AssetDetectFaces': return JobName.assetDetectFaces; case r'AssetDetectDuplicatesQueueAll': return JobName.assetDetectDuplicatesQueueAll; case r'AssetDetectDuplicates': return JobName.assetDetectDuplicates; + case r'AssetEditThumbnailGeneration': return JobName.assetEditThumbnailGeneration; case r'AssetEncodeVideoQueueAll': return JobName.assetEncodeVideoQueueAll; case r'AssetEncodeVideo': return JobName.assetEncodeVideo; case r'AssetEmptyTrash': return JobName.assetEmptyTrash; diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index af354bef9e..73a0187ddd 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -16,6 +16,8 @@ class JobSettingsDto { required this.concurrency, }); + /// Concurrency + /// /// Minimum value: 1 int concurrency; diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index 3cf1248508..aa9158e591 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -24,22 +24,31 @@ class LibraryResponseDto { required this.updatedAt, }); + /// Number of assets int assetCount; + /// Creation date DateTime createdAt; + /// Exclusion patterns List exclusionPatterns; + /// Library ID String id; + /// Import paths List importPaths; + /// Library name String name; + /// Owner user ID String ownerId; + /// Last refresh date DateTime? refreshedAt; + /// Last update date DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index afe67da31a..6eec3ae8d7 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -19,12 +19,16 @@ class LibraryStatsResponseDto { this.videos = 0, }); + /// Number of photos int photos; + /// Total number of assets int total; + /// Storage usage in bytes int usage; + /// Number of videos int videos; @override diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index d27d579bb4..ea1fee9d7a 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -17,8 +17,10 @@ class LicenseKeyDto { required this.licenseKey, }); + /// Activation key String activationKey; + /// License key (format: IM(SV|CL)(-XXXX){8}) String licenseKey; @override diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart index 6d3009433f..84ff72c1eb 100644 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ b/mobile/openapi/lib/model/license_response_dto.dart @@ -18,10 +18,13 @@ class LicenseResponseDto { required this.licenseKey, }); + /// Activation date DateTime activatedAt; + /// Activation key String activationKey; + /// License key (format: IM(SV|CL)(-XXXX){8}) String licenseKey; @override diff --git a/mobile/openapi/lib/model/login_credential_dto.dart b/mobile/openapi/lib/model/login_credential_dto.dart index 7e892ab5fb..1fdfdc3d40 100644 --- a/mobile/openapi/lib/model/login_credential_dto.dart +++ b/mobile/openapi/lib/model/login_credential_dto.dart @@ -17,8 +17,10 @@ class LoginCredentialDto { required this.password, }); + /// User email String email; + /// User password String password; @override diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index 82a4f9b3ed..c6938c2393 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -23,20 +23,28 @@ class LoginResponseDto { required this.userId, }); + /// Access token String accessToken; + /// Is admin user bool isAdmin; + /// Is onboarded bool isOnboarded; + /// User name String name; + /// Profile image path String profileImagePath; + /// Should change password bool shouldChangePassword; + /// User email String userEmail; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/logout_response_dto.dart b/mobile/openapi/lib/model/logout_response_dto.dart index aa94904e2a..b50db2c28b 100644 --- a/mobile/openapi/lib/model/logout_response_dto.dart +++ b/mobile/openapi/lib/model/logout_response_dto.dart @@ -17,8 +17,10 @@ class LogoutResponseDto { required this.successful, }); + /// Redirect URI String redirectUri; + /// Logout successful bool successful; @override diff --git a/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart b/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart index 84b3181426..dc0cf5fac0 100644 --- a/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart +++ b/mobile/openapi/lib/model/machine_learning_availability_checks_dto.dart @@ -18,6 +18,7 @@ class MachineLearningAvailabilityChecksDto { required this.timeout, }); + /// Enabled bool enabled; num interval; diff --git a/mobile/openapi/lib/model/maintenance_action.dart b/mobile/openapi/lib/model/maintenance_action.dart index 9be628961f..ebf5ec0f71 100644 --- a/mobile/openapi/lib/model/maintenance_action.dart +++ b/mobile/openapi/lib/model/maintenance_action.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Maintenance action class MaintenanceAction { /// Instantiate a new enum with the provided [value]. const MaintenanceAction._(this.value); @@ -25,11 +25,15 @@ class MaintenanceAction { static const start = MaintenanceAction._(r'start'); static const end = MaintenanceAction._(r'end'); + static const selectDatabaseRestore = MaintenanceAction._(r'select_database_restore'); + static const restoreDatabase = MaintenanceAction._(r'restore_database'); /// List of all possible values in this [enum][MaintenanceAction]. static const values = [ start, end, + selectDatabaseRestore, + restoreDatabase, ]; static MaintenanceAction? fromJson(dynamic value) => MaintenanceActionTypeTransformer().decode(value); @@ -70,6 +74,8 @@ class MaintenanceActionTypeTransformer { switch (data) { case r'start': return MaintenanceAction.start; case r'end': return MaintenanceAction.end; + case r'select_database_restore': return MaintenanceAction.selectDatabaseRestore; + case r'restore_database': return MaintenanceAction.restoreDatabase; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/maintenance_auth_dto.dart b/mobile/openapi/lib/model/maintenance_auth_dto.dart index 919da5502b..f9511bdd2b 100644 --- a/mobile/openapi/lib/model/maintenance_auth_dto.dart +++ b/mobile/openapi/lib/model/maintenance_auth_dto.dart @@ -16,6 +16,7 @@ class MaintenanceAuthDto { required this.username, }); + /// Maintenance username String username; @override diff --git a/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart new file mode 100644 index 0000000000..1c364a6fdc --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_detect_install_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MaintenanceDetectInstallResponseDto { + /// Returns a new [MaintenanceDetectInstallResponseDto] instance. + MaintenanceDetectInstallResponseDto({ + this.storage = const [], + }); + + List storage; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallResponseDto && + _deepEquality.equals(other.storage, storage); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (storage.hashCode); + + @override + String toString() => 'MaintenanceDetectInstallResponseDto[storage=$storage]'; + + Map toJson() { + final json = {}; + json[r'storage'] = this.storage; + return json; + } + + /// Returns a new [MaintenanceDetectInstallResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceDetectInstallResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceDetectInstallResponseDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceDetectInstallResponseDto( + storage: MaintenanceDetectInstallStorageFolderDto.listFromJson(json[r'storage']), + ); + } + 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 = MaintenanceDetectInstallResponseDto.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 = MaintenanceDetectInstallResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceDetectInstallResponseDto-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] = MaintenanceDetectInstallResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'storage', + }; +} + diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart new file mode 100644 index 0000000000..ad524914b4 --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -0,0 +1,127 @@ +// +// 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 MaintenanceDetectInstallStorageFolderDto { + /// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance. + MaintenanceDetectInstallStorageFolderDto({ + required this.files, + required this.folder, + required this.readable, + required this.writable, + }); + + /// Number of files in the folder + num files; + + /// Storage folder + StorageFolder folder; + + /// Whether the folder is readable + bool readable; + + /// Whether the folder is writable + bool writable; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceDetectInstallStorageFolderDto && + other.files == files && + other.folder == folder && + other.readable == readable && + other.writable == writable; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (files.hashCode) + + (folder.hashCode) + + (readable.hashCode) + + (writable.hashCode); + + @override + String toString() => 'MaintenanceDetectInstallStorageFolderDto[files=$files, folder=$folder, readable=$readable, writable=$writable]'; + + Map toJson() { + final json = {}; + json[r'files'] = this.files; + json[r'folder'] = this.folder; + json[r'readable'] = this.readable; + json[r'writable'] = this.writable; + return json; + } + + /// Returns a new [MaintenanceDetectInstallStorageFolderDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceDetectInstallStorageFolderDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceDetectInstallStorageFolderDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceDetectInstallStorageFolderDto( + files: num.parse('${json[r'files']}'), + folder: StorageFolder.fromJson(json[r'folder'])!, + readable: mapValueOfType(json, r'readable')!, + writable: mapValueOfType(json, r'writable')!, + ); + } + 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 = MaintenanceDetectInstallStorageFolderDto.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 = MaintenanceDetectInstallStorageFolderDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceDetectInstallStorageFolderDto-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] = MaintenanceDetectInstallStorageFolderDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'files', + 'folder', + 'readable', + 'writable', + }; +} + diff --git a/mobile/openapi/lib/model/maintenance_login_dto.dart b/mobile/openapi/lib/model/maintenance_login_dto.dart index 45f56bd3ba..64cf6b234b 100644 --- a/mobile/openapi/lib/model/maintenance_login_dto.dart +++ b/mobile/openapi/lib/model/maintenance_login_dto.dart @@ -16,6 +16,7 @@ class MaintenanceLoginDto { this.token, }); + /// Maintenance token /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart new file mode 100644 index 0000000000..52dbb5b95b --- /dev/null +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -0,0 +1,159 @@ +// +// 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 MaintenanceStatusResponseDto { + /// Returns a new [MaintenanceStatusResponseDto] instance. + MaintenanceStatusResponseDto({ + required this.action, + required this.active, + this.error, + this.progress, + this.task, + }); + + /// Maintenance action + MaintenanceAction action; + + bool active; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? error; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? progress; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? task; + + @override + bool operator ==(Object other) => identical(this, other) || other is MaintenanceStatusResponseDto && + other.action == action && + other.active == active && + other.error == error && + other.progress == progress && + other.task == task; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (active.hashCode) + + (error == null ? 0 : error!.hashCode) + + (progress == null ? 0 : progress!.hashCode) + + (task == null ? 0 : task!.hashCode); + + @override + String toString() => 'MaintenanceStatusResponseDto[action=$action, active=$active, error=$error, progress=$progress, task=$task]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'active'] = this.active; + if (this.error != null) { + json[r'error'] = this.error; + } else { + // json[r'error'] = null; + } + if (this.progress != null) { + json[r'progress'] = this.progress; + } else { + // json[r'progress'] = null; + } + if (this.task != null) { + json[r'task'] = this.task; + } else { + // json[r'task'] = null; + } + return json; + } + + /// Returns a new [MaintenanceStatusResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MaintenanceStatusResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MaintenanceStatusResponseDto"); + if (value is Map) { + final json = value.cast(); + + return MaintenanceStatusResponseDto( + action: MaintenanceAction.fromJson(json[r'action'])!, + active: mapValueOfType(json, r'active')!, + error: mapValueOfType(json, r'error'), + progress: num.parse('${json[r'progress']}'), + task: mapValueOfType(json, r'task'), + ); + } + 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 = MaintenanceStatusResponseDto.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 = MaintenanceStatusResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MaintenanceStatusResponseDto-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] = MaintenanceStatusResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'active', + }; +} + diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index 311215ad9e..d09790a81a 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Job name class ManualJobName { /// Instantiate a new enum with the provided [value]. const ManualJobName._(this.value); diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index 74ac51a271..c0a47a5458 100644 --- a/mobile/openapi/lib/model/map_marker_response_dto.dart +++ b/mobile/openapi/lib/model/map_marker_response_dto.dart @@ -21,16 +21,22 @@ class MapMarkerResponseDto { required this.state, }); + /// City name String? city; + /// Country name String? country; + /// Asset ID String id; + /// Latitude double lat; + /// Longitude double lon; + /// State/Province name String? state; @override diff --git a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart index 6d8757d39f..85435485e6 100644 --- a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart +++ b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart @@ -18,10 +18,13 @@ class MapReverseGeocodeResponseDto { required this.state, }); + /// City name String? city; + /// Country name String? country; + /// State/Province name String? state; @override diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index cb42f596a6..63d4094cd0 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -17,8 +17,10 @@ class MemoriesResponse { this.enabled = true, }); + /// Memory duration in seconds int duration; + /// Whether memories are enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index 39c46ffd2f..d27cef022d 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -17,6 +17,8 @@ class MemoriesUpdate { this.enabled, }); + /// Memory duration in seconds + /// /// Minimum value: 1 /// /// Please note: This property should have been non-nullable! Since the specification file @@ -26,6 +28,7 @@ class MemoriesUpdate { /// int? duration; + /// Whether memories are enabled /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 15985f2f1c..5b8eeed8fb 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -15,16 +15,29 @@ class MemoryCreateDto { MemoryCreateDto({ this.assetIds = const [], required this.data, + this.hideAt, this.isSaved, required this.memoryAt, this.seenAt, + this.showAt, required this.type, }); + /// Asset IDs to associate with memory List assetIds; OnThisDayDto data; + /// Date when memory should be hidden + /// + /// 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. + /// + DateTime? hideAt; + + /// Is memory saved /// /// 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 @@ -33,8 +46,10 @@ class MemoryCreateDto { /// bool? isSaved; + /// Memory date DateTime memoryAt; + /// Date when memory was seen /// /// 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 @@ -43,15 +58,27 @@ class MemoryCreateDto { /// DateTime? seenAt; + /// Date when memory should be shown + /// + /// 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. + /// + DateTime? showAt; + + /// Memory type MemoryType type; @override bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && _deepEquality.equals(other.assetIds, assetIds) && other.data == data && + other.hideAt == hideAt && other.isSaved == isSaved && other.memoryAt == memoryAt && other.seenAt == seenAt && + other.showAt == showAt && other.type == type; @override @@ -59,18 +86,25 @@ class MemoryCreateDto { // ignore: unnecessary_parenthesis (assetIds.hashCode) + (data.hashCode) + + (hideAt == null ? 0 : hideAt!.hashCode) + (isSaved == null ? 0 : isSaved!.hashCode) + (memoryAt.hashCode) + (seenAt == null ? 0 : seenAt!.hashCode) + + (showAt == null ? 0 : showAt!.hashCode) + (type.hashCode); @override - String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, type=$type]'; + String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, hideAt=$hideAt, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, showAt=$showAt, type=$type]'; Map toJson() { final json = {}; json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; + if (this.hideAt != null) { + json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + } else { + // json[r'hideAt'] = null; + } if (this.isSaved != null) { json[r'isSaved'] = this.isSaved; } else { @@ -81,6 +115,11 @@ class MemoryCreateDto { 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; return json; @@ -99,9 +138,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, + hideAt: mapDateTime(json, r'hideAt', r''), isSaved: mapValueOfType(json, r'isSaved'), memoryAt: mapDateTime(json, r'memoryAt', r'')!, seenAt: mapDateTime(json, r'seenAt', r''), + showAt: mapDateTime(json, r'showAt', r''), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 7d50259e24..1835095cf7 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -30,10 +30,12 @@ class MemoryResponseDto { List assets; + /// Creation date DateTime createdAt; OnThisDayDto data; + /// Deletion date /// /// 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 @@ -42,6 +44,7 @@ class MemoryResponseDto { /// DateTime? deletedAt; + /// Date when memory should be hidden /// /// 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 @@ -50,14 +53,19 @@ class MemoryResponseDto { /// DateTime? hideAt; + /// Memory ID String id; + /// Is memory saved bool isSaved; + /// Memory date DateTime memoryAt; + /// Owner user ID String ownerId; + /// Date when memory was seen /// /// 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 @@ -66,6 +74,7 @@ class MemoryResponseDto { /// DateTime? seenAt; + /// Date when memory should be shown /// /// 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 @@ -74,8 +83,10 @@ class MemoryResponseDto { /// DateTime? showAt; + /// Memory type MemoryType type; + /// Last update date DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart index a9a10ad327..bde78de481 100644 --- a/mobile/openapi/lib/model/memory_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/memory_statistics_response_dto.dart @@ -16,6 +16,7 @@ class MemoryStatisticsResponseDto { required this.total, }); + /// Total number of memories int total; @override diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index e750f9faad..4905b161bf 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -18,6 +18,7 @@ class MemoryUpdateDto { this.seenAt, }); + /// Is memory saved /// /// 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 @@ -26,6 +27,7 @@ class MemoryUpdateDto { /// bool? isSaved; + /// Memory date /// /// 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 @@ -34,6 +36,7 @@ class MemoryUpdateDto { /// DateTime? memoryAt; + /// Date when memory was seen /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/merge_person_dto.dart b/mobile/openapi/lib/model/merge_person_dto.dart index fd225276b6..8a647890c3 100644 --- a/mobile/openapi/lib/model/merge_person_dto.dart +++ b/mobile/openapi/lib/model/merge_person_dto.dart @@ -16,6 +16,7 @@ class MergePersonDto { this.ids = const [], }); + /// Person IDs to merge List ids; @override diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 7d8d2b1314..81f8d41527 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -59,8 +59,10 @@ class MetadataSearchDto { this.withStacked, }); + /// Filter by album IDs List albumIds; + /// Filter by file checksum /// /// 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 @@ -69,10 +71,13 @@ class MetadataSearchDto { /// String? checksum; + /// Filter by city name String? city; + /// Filter by country name String? country; + /// Filter by creation date (after) /// /// 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 @@ -81,6 +86,7 @@ class MetadataSearchDto { /// DateTime? createdAfter; + /// Filter by creation date (before) /// /// 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 @@ -89,6 +95,7 @@ class MetadataSearchDto { /// DateTime? createdBefore; + /// Filter by description text /// /// 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 @@ -97,6 +104,7 @@ class MetadataSearchDto { /// String? description; + /// Filter by device asset ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -105,6 +113,7 @@ class MetadataSearchDto { /// String? deviceAssetId; + /// Device ID to filter by /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -113,6 +122,7 @@ class MetadataSearchDto { /// String? deviceId; + /// Filter by encoded video file path /// /// 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 @@ -121,6 +131,7 @@ class MetadataSearchDto { /// String? encodedVideoPath; + /// Filter by asset ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -129,6 +140,7 @@ class MetadataSearchDto { /// String? id; + /// Filter by encoded status /// /// 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 @@ -137,6 +149,7 @@ class MetadataSearchDto { /// bool? isEncoded; + /// Filter by favorite status /// /// 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 @@ -145,6 +158,7 @@ class MetadataSearchDto { /// bool? isFavorite; + /// Filter by motion photo status /// /// 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 @@ -153,6 +167,7 @@ class MetadataSearchDto { /// bool? isMotion; + /// Filter assets not in any album /// /// 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 @@ -161,6 +176,7 @@ class MetadataSearchDto { /// bool? isNotInAlbum; + /// Filter by offline status /// /// 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 @@ -169,10 +185,13 @@ class MetadataSearchDto { /// bool? isOffline; + /// Filter by lens model String? lensModel; + /// Library ID to filter by String? libraryId; + /// Filter by camera make /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -181,8 +200,10 @@ class MetadataSearchDto { /// String? make; + /// Filter by camera model String? model; + /// Filter by OCR text content /// /// 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 @@ -191,8 +212,10 @@ class MetadataSearchDto { /// String? ocr; + /// Sort order AssetOrder order; + /// Filter by original file name /// /// 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 @@ -201,6 +224,7 @@ class MetadataSearchDto { /// String? originalFileName; + /// Filter by original file path /// /// 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 @@ -209,6 +233,8 @@ class MetadataSearchDto { /// String? originalPath; + /// Page number + /// /// Minimum value: 1 /// /// Please note: This property should have been non-nullable! Since the specification file @@ -218,8 +244,10 @@ class MetadataSearchDto { /// num? page; + /// Filter by person IDs List personIds; + /// Filter by preview file path /// /// 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 @@ -228,16 +256,14 @@ class MetadataSearchDto { /// String? previewPath; + /// Filter by rating [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -248,10 +274,13 @@ class MetadataSearchDto { /// num? size; + /// Filter by state/province name String? state; + /// Filter by tag IDs List? tagIds; + /// Filter by taken date (after) /// /// 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 @@ -260,6 +289,7 @@ class MetadataSearchDto { /// DateTime? takenAfter; + /// Filter by taken date (before) /// /// 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 @@ -268,6 +298,7 @@ class MetadataSearchDto { /// DateTime? takenBefore; + /// Filter by thumbnail file path /// /// 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 @@ -276,6 +307,7 @@ class MetadataSearchDto { /// String? thumbnailPath; + /// Filter by trash date (after) /// /// 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 @@ -284,6 +316,7 @@ class MetadataSearchDto { /// DateTime? trashedAfter; + /// Filter by trash date (before) /// /// 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 @@ -292,6 +325,7 @@ class MetadataSearchDto { /// DateTime? trashedBefore; + /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -300,6 +334,7 @@ class MetadataSearchDto { /// AssetTypeEnum? type; + /// Filter by update date (after) /// /// 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 @@ -308,6 +343,7 @@ class MetadataSearchDto { /// DateTime? updatedAfter; + /// Filter by update date (before) /// /// 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 @@ -316,6 +352,7 @@ class MetadataSearchDto { /// DateTime? updatedBefore; + /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -324,6 +361,7 @@ class MetadataSearchDto { /// AssetVisibility? visibility; + /// Include deleted assets /// /// 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 @@ -332,6 +370,7 @@ class MetadataSearchDto { /// bool? withDeleted; + /// Include EXIF data in response /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -340,6 +379,7 @@ class MetadataSearchDto { /// bool? withExif; + /// Include assets with people /// /// 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 @@ -348,6 +388,7 @@ class MetadataSearchDto { /// bool? withPeople; + /// Include stacked assets /// /// 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 @@ -707,7 +748,9 @@ class MetadataSearchDto { ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], previewPath: mapValueOfType(json, r'previewPath'), - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), tagIds: json[r'tagIds'] is Iterable diff --git a/mobile/openapi/lib/model/asset_metadata_key.dart b/mobile/openapi/lib/model/mirror_axis.dart similarity index 52% rename from mobile/openapi/lib/model/asset_metadata_key.dart rename to mobile/openapi/lib/model/mirror_axis.dart index 70186cd41c..4deeeb047c 100644 --- a/mobile/openapi/lib/model/asset_metadata_key.dart +++ b/mobile/openapi/lib/model/mirror_axis.dart @@ -10,10 +10,10 @@ part of openapi.api; - -class AssetMetadataKey { +/// Axis to mirror along +class MirrorAxis { /// Instantiate a new enum with the provided [value]. - const AssetMetadataKey._(this.value); + const MirrorAxis._(this.value); /// The underlying value of this enum member. final String value; @@ -23,20 +23,22 @@ class AssetMetadataKey { String toJson() => value; - static const mobileApp = AssetMetadataKey._(r'mobile-app'); + static const horizontal = MirrorAxis._(r'horizontal'); + static const vertical = MirrorAxis._(r'vertical'); - /// List of all possible values in this [enum][AssetMetadataKey]. - static const values = [ - mobileApp, + /// List of all possible values in this [enum][MirrorAxis]. + static const values = [ + horizontal, + vertical, ]; - static AssetMetadataKey? fromJson(dynamic value) => AssetMetadataKeyTypeTransformer().decode(value); + static MirrorAxis? fromJson(dynamic value) => MirrorAxisTypeTransformer().decode(value); - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AssetMetadataKey.fromJson(row); + final value = MirrorAxis.fromJson(row); if (value != null) { result.add(value); } @@ -46,16 +48,16 @@ class AssetMetadataKey { } } -/// Transformation class that can [encode] an instance of [AssetMetadataKey] to String, -/// and [decode] dynamic data back to [AssetMetadataKey]. -class AssetMetadataKeyTypeTransformer { - factory AssetMetadataKeyTypeTransformer() => _instance ??= const AssetMetadataKeyTypeTransformer._(); +/// Transformation class that can [encode] an instance of [MirrorAxis] to String, +/// and [decode] dynamic data back to [MirrorAxis]. +class MirrorAxisTypeTransformer { + factory MirrorAxisTypeTransformer() => _instance ??= const MirrorAxisTypeTransformer._(); - const AssetMetadataKeyTypeTransformer._(); + const MirrorAxisTypeTransformer._(); - String encode(AssetMetadataKey data) => data.value; + String encode(MirrorAxis data) => data.value; - /// Decodes a [dynamic value][data] to a AssetMetadataKey. + /// Decodes a [dynamic value][data] to a MirrorAxis. /// /// 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] @@ -63,10 +65,11 @@ class AssetMetadataKeyTypeTransformer { /// /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, /// and users are still using an old app with the old code. - AssetMetadataKey? decode(dynamic data, {bool allowNull = true}) { + MirrorAxis? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { - case r'mobile-app': return AssetMetadataKey.mobileApp; + case r'horizontal': return MirrorAxis.horizontal; + case r'vertical': return MirrorAxis.vertical; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); @@ -76,7 +79,7 @@ class AssetMetadataKeyTypeTransformer { return null; } - /// Singleton [AssetMetadataKeyTypeTransformer] instance. - static AssetMetadataKeyTypeTransformer? _instance; + /// Singleton [MirrorAxisTypeTransformer] instance. + static MirrorAxisTypeTransformer? _instance; } diff --git a/mobile/openapi/lib/model/mirror_parameters.dart b/mobile/openapi/lib/model/mirror_parameters.dart new file mode 100644 index 0000000000..e8b8db685b --- /dev/null +++ b/mobile/openapi/lib/model/mirror_parameters.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MirrorParameters { + /// Returns a new [MirrorParameters] instance. + MirrorParameters({ + required this.axis, + }); + + /// Axis to mirror along + MirrorAxis axis; + + @override + bool operator ==(Object other) => identical(this, other) || other is MirrorParameters && + other.axis == axis; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (axis.hashCode); + + @override + String toString() => 'MirrorParameters[axis=$axis]'; + + Map toJson() { + final json = {}; + json[r'axis'] = this.axis; + return json; + } + + /// Returns a new [MirrorParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MirrorParameters? fromJson(dynamic value) { + upgradeDto(value, "MirrorParameters"); + if (value is Map) { + final json = value.cast(); + + return MirrorParameters( + axis: MirrorAxis.fromJson(json[r'axis'])!, + ); + } + 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 = MirrorParameters.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 = MirrorParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MirrorParameters-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] = MirrorParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'axis', + }; +} + diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart index 07985353b2..1288da8670 100644 --- a/mobile/openapi/lib/model/notification_create_dto.dart +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -22,6 +22,7 @@ class NotificationCreateDto { required this.userId, }); + /// Additional notification data /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -30,8 +31,10 @@ class NotificationCreateDto { /// Object? data; + /// Notification description String? description; + /// Notification level /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -40,10 +43,13 @@ class NotificationCreateDto { /// NotificationLevel? level; + /// Date when notification was read DateTime? readAt; + /// Notification title String title; + /// Notification type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -52,6 +58,7 @@ class NotificationCreateDto { /// NotificationType? type; + /// User ID to send notification to String userId; @override diff --git a/mobile/openapi/lib/model/notification_delete_all_dto.dart b/mobile/openapi/lib/model/notification_delete_all_dto.dart index 4be1b89e92..1b398a4f33 100644 --- a/mobile/openapi/lib/model/notification_delete_all_dto.dart +++ b/mobile/openapi/lib/model/notification_delete_all_dto.dart @@ -16,6 +16,7 @@ class NotificationDeleteAllDto { this.ids = const [], }); + /// Notification IDs to delete List ids; @override diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart index 4f730b4e50..30d43de115 100644 --- a/mobile/openapi/lib/model/notification_dto.dart +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -23,8 +23,10 @@ class NotificationDto { required this.type, }); + /// Creation date DateTime createdAt; + /// Additional notification data /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -33,6 +35,7 @@ class NotificationDto { /// Object? data; + /// Notification description /// /// 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 @@ -41,10 +44,13 @@ class NotificationDto { /// String? description; + /// Notification ID String id; + /// Notification level NotificationLevel level; + /// Date when notification was read /// /// 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 @@ -53,8 +59,10 @@ class NotificationDto { /// DateTime? readAt; + /// Notification title String title; + /// Notification type NotificationType type; @override diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart index a6393b275a..a157058324 100644 --- a/mobile/openapi/lib/model/notification_update_all_dto.dart +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -17,8 +17,10 @@ class NotificationUpdateAllDto { this.readAt, }); + /// Notification IDs to update List ids; + /// Date when notifications were read DateTime? readAt; @override diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart index e76496eb97..eddf9c7e12 100644 --- a/mobile/openapi/lib/model/notification_update_dto.dart +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -16,6 +16,7 @@ class NotificationUpdateDto { this.readAt, }); + /// Date when notification was read DateTime? readAt; @override diff --git a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart index 869c3be753..7eedc45673 100644 --- a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart +++ b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart @@ -16,6 +16,7 @@ class OAuthAuthorizeResponseDto { required this.url, }); + /// OAuth authorization URL String url; @override diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart index ea8cac31a0..d94374935a 100644 --- a/mobile/openapi/lib/model/o_auth_callback_dto.dart +++ b/mobile/openapi/lib/model/o_auth_callback_dto.dart @@ -18,6 +18,7 @@ class OAuthCallbackDto { required this.url, }); + /// OAuth code verifier (PKCE) /// /// 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 @@ -26,6 +27,7 @@ class OAuthCallbackDto { /// String? codeVerifier; + /// OAuth state parameter /// /// 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 @@ -34,6 +36,7 @@ class OAuthCallbackDto { /// String? state; + /// OAuth callback URL String url; @override diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart index bb3e8d448d..1c9ce8d5b8 100644 --- a/mobile/openapi/lib/model/o_auth_config_dto.dart +++ b/mobile/openapi/lib/model/o_auth_config_dto.dart @@ -18,6 +18,7 @@ class OAuthConfigDto { this.state, }); + /// OAuth code challenge (PKCE) /// /// 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 @@ -26,8 +27,10 @@ class OAuthConfigDto { /// String? codeChallenge; + /// OAuth redirect URI String redirectUri; + /// OAuth state parameter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart index fc528888b3..77466d61d9 100644 --- a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Token endpoint auth method class OAuthTokenEndpointAuthMethod { /// Instantiate a new enum with the provided [value]. const OAuthTokenEndpointAuthMethod._(this.value); diff --git a/mobile/openapi/lib/model/ocr_config.dart b/mobile/openapi/lib/model/ocr_config.dart index 51746c4924..d97cd5ffca 100644 --- a/mobile/openapi/lib/model/ocr_config.dart +++ b/mobile/openapi/lib/model/ocr_config.dart @@ -20,19 +20,27 @@ class OcrConfig { required this.modelName, }); + /// Whether the task is enabled bool enabled; + /// Maximum resolution for OCR processing + /// /// Minimum value: 1 int maxResolution; + /// Minimum confidence score for text detection + /// /// Minimum value: 0.1 /// Maximum value: 1 double minDetectionScore; + /// Minimum confidence score for text recognition + /// /// Minimum value: 0.1 /// Maximum value: 1 double minRecognitionScore; + /// Name of the model to use String modelName; @override diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index bfcc4fd630..93ec956f58 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -16,6 +16,8 @@ class OnThisDayDto { required this.year, }); + /// Year for on this day memory + /// /// Minimum value: 1 num year; diff --git a/mobile/openapi/lib/model/onboarding_dto.dart b/mobile/openapi/lib/model/onboarding_dto.dart index 670b6a5c68..8499bc9b9a 100644 --- a/mobile/openapi/lib/model/onboarding_dto.dart +++ b/mobile/openapi/lib/model/onboarding_dto.dart @@ -16,6 +16,7 @@ class OnboardingDto { required this.isOnboarded, }); + /// Is user onboarded bool isOnboarded; @override diff --git a/mobile/openapi/lib/model/onboarding_response_dto.dart b/mobile/openapi/lib/model/onboarding_response_dto.dart index 033466e96b..2b0dbe2b96 100644 --- a/mobile/openapi/lib/model/onboarding_response_dto.dart +++ b/mobile/openapi/lib/model/onboarding_response_dto.dart @@ -16,6 +16,7 @@ class OnboardingResponseDto { required this.isOnboarded, }); + /// Is user onboarded bool isOnboarded; @override diff --git a/mobile/openapi/lib/model/partner_create_dto.dart b/mobile/openapi/lib/model/partner_create_dto.dart index 09d60c5c77..30aa96ff30 100644 --- a/mobile/openapi/lib/model/partner_create_dto.dart +++ b/mobile/openapi/lib/model/partner_create_dto.dart @@ -16,6 +16,7 @@ class PartnerCreateDto { required this.sharedWithId, }); + /// User ID to share with String sharedWithId; @override diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index f61df86b42..5789938d18 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,12 +22,16 @@ class PartnerResponseDto { required this.profileImagePath, }); + /// Avatar color UserAvatarColor avatarColor; + /// User email String email; + /// User ID String id; + /// Show in timeline /// /// 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 @@ -36,10 +40,13 @@ class PartnerResponseDto { /// bool? inTimeline; + /// User name String name; + /// Profile change date DateTime profileChangedAt; + /// Profile image path String profileImagePath; @override diff --git a/mobile/openapi/lib/model/partner_update_dto.dart b/mobile/openapi/lib/model/partner_update_dto.dart index 25cf217764..db3516e3a1 100644 --- a/mobile/openapi/lib/model/partner_update_dto.dart +++ b/mobile/openapi/lib/model/partner_update_dto.dart @@ -16,6 +16,7 @@ class PartnerUpdateDto { required this.inTimeline, }); + /// Show partner assets in timeline bool inTimeline; @override diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index 1312c73874..c09560e08c 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -17,8 +17,10 @@ class PeopleResponse { this.sidebarWeb = false, }); + /// Whether people are enabled bool enabled; + /// Whether people appear in web sidebar bool sidebarWeb; @override diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index 901c38ade9..f345657e73 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -19,6 +19,7 @@ class PeopleResponseDto { required this.total, }); + /// Whether there are more pages /// /// 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 @@ -27,10 +28,13 @@ class PeopleResponseDto { /// bool? hasNextPage; + /// Number of hidden people int hidden; + /// List of people List people; + /// Total number of people int total; @override diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart index fb4eeeb434..fe16479bac 100644 --- a/mobile/openapi/lib/model/people_update.dart +++ b/mobile/openapi/lib/model/people_update.dart @@ -17,6 +17,7 @@ class PeopleUpdate { this.sidebarWeb, }); + /// Whether people are enabled /// /// 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 @@ -25,6 +26,7 @@ class PeopleUpdate { /// bool? enabled; + /// Whether people appear in web sidebar /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/people_update_dto.dart b/mobile/openapi/lib/model/people_update_dto.dart index f771084f75..c9ce74d659 100644 --- a/mobile/openapi/lib/model/people_update_dto.dart +++ b/mobile/openapi/lib/model/people_update_dto.dart @@ -16,6 +16,7 @@ class PeopleUpdateDto { this.people = const [], }); + /// People to update List people; @override diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart index ce324b859e..5e20aeb464 100644 --- a/mobile/openapi/lib/model/people_update_item.dart +++ b/mobile/openapi/lib/model/people_update_item.dart @@ -22,12 +22,13 @@ class PeopleUpdateItem { this.name, }); - /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. + /// Person date of birth DateTime? birthDate; + /// Person color (hex) String? color; - /// Asset is used to get the feature face thumbnail. + /// Asset ID used for feature face thumbnail /// /// 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 @@ -36,9 +37,10 @@ class PeopleUpdateItem { /// String? featureFaceAssetId; - /// Person id. + /// Person ID String id; + /// Mark as favorite /// /// 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 @@ -47,7 +49,7 @@ class PeopleUpdateItem { /// bool? isFavorite; - /// Person visibility + /// Person visibility (hidden) /// /// 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 @@ -56,7 +58,7 @@ class PeopleUpdateItem { /// bool? isHidden; - /// Person name. + /// Person name /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 3b9a3964b6..9092ede786 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// List of permissions class Permission { /// Instantiate a new enum with the provided [value]. const Permission._(this.value); @@ -43,6 +43,10 @@ class Permission { static const assetPeriodUpload = Permission._(r'asset.upload'); static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodCopy = Permission._(r'asset.copy'); + static const assetPeriodDerive = Permission._(r'asset.derive'); + static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get'); + static const assetPeriodEditPeriodCreate = Permission._(r'asset.edit.create'); + static const assetPeriodEditPeriodDelete = Permission._(r'asset.edit.delete'); static const albumPeriodCreate = Permission._(r'album.create'); static const albumPeriodRead = Permission._(r'album.read'); static const albumPeriodUpdate = Permission._(r'album.update'); @@ -58,12 +62,17 @@ class Permission { static const authPeriodChangePassword = Permission._(r'auth.changePassword'); static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); static const archivePeriodRead = Permission._(r'archive.read'); + static const backupPeriodList = Permission._(r'backup.list'); + static const backupPeriodDownload = Permission._(r'backup.download'); + static const backupPeriodUpload = Permission._(r'backup.upload'); + static const backupPeriodDelete = Permission._(r'backup.delete'); 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 folderPeriodRead = Permission._(r'folder.read'); static const jobPeriodCreate = Permission._(r'job.create'); static const jobPeriodRead = Permission._(r'job.read'); static const libraryPeriodCreate = Permission._(r'library.create'); @@ -74,6 +83,8 @@ class Permission { static const timelinePeriodRead = Permission._(r'timeline.read'); static const timelinePeriodDownload = Permission._(r'timeline.download'); static const maintenance = Permission._(r'maintenance'); + static const mapPeriodRead = Permission._(r'map.read'); + static const mapPeriodSearch = Permission._(r'map.search'); static const memoryPeriodCreate = Permission._(r'memory.create'); static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodUpdate = Permission._(r'memory.update'); @@ -191,6 +202,10 @@ class Permission { assetPeriodUpload, assetPeriodReplace, assetPeriodCopy, + assetPeriodDerive, + assetPeriodEditPeriodGet, + assetPeriodEditPeriodCreate, + assetPeriodEditPeriodDelete, albumPeriodCreate, albumPeriodRead, albumPeriodUpdate, @@ -206,12 +221,17 @@ class Permission { authPeriodChangePassword, authDevicePeriodDelete, archivePeriodRead, + backupPeriodList, + backupPeriodDownload, + backupPeriodUpload, + backupPeriodDelete, duplicatePeriodRead, duplicatePeriodDelete, facePeriodCreate, facePeriodRead, facePeriodUpdate, facePeriodDelete, + folderPeriodRead, jobPeriodCreate, jobPeriodRead, libraryPeriodCreate, @@ -222,6 +242,8 @@ class Permission { timelinePeriodRead, timelinePeriodDownload, maintenance, + mapPeriodRead, + mapPeriodSearch, memoryPeriodCreate, memoryPeriodRead, memoryPeriodUpdate, @@ -374,6 +396,10 @@ class PermissionTypeTransformer { case r'asset.upload': return Permission.assetPeriodUpload; case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.copy': return Permission.assetPeriodCopy; + case r'asset.derive': return Permission.assetPeriodDerive; + case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet; + case r'asset.edit.create': return Permission.assetPeriodEditPeriodCreate; + case r'asset.edit.delete': return Permission.assetPeriodEditPeriodDelete; case r'album.create': return Permission.albumPeriodCreate; case r'album.read': return Permission.albumPeriodRead; case r'album.update': return Permission.albumPeriodUpdate; @@ -389,12 +415,17 @@ class PermissionTypeTransformer { case r'auth.changePassword': return Permission.authPeriodChangePassword; case r'authDevice.delete': return Permission.authDevicePeriodDelete; case r'archive.read': return Permission.archivePeriodRead; + case r'backup.list': return Permission.backupPeriodList; + case r'backup.download': return Permission.backupPeriodDownload; + case r'backup.upload': return Permission.backupPeriodUpload; + case r'backup.delete': return Permission.backupPeriodDelete; 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'folder.read': return Permission.folderPeriodRead; case r'job.create': return Permission.jobPeriodCreate; case r'job.read': return Permission.jobPeriodRead; case r'library.create': return Permission.libraryPeriodCreate; @@ -405,6 +436,8 @@ class PermissionTypeTransformer { case r'timeline.read': return Permission.timelinePeriodRead; case r'timeline.download': return Permission.timelinePeriodDownload; case r'maintenance': return Permission.maintenance; + case r'map.read': return Permission.mapPeriodRead; + case r'map.search': return Permission.mapPeriodSearch; case r'memory.create': return Permission.memoryPeriodCreate; case r'memory.read': return Permission.memoryPeriodRead; case r'memory.update': return Permission.memoryPeriodUpdate; diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart index 87b426eaed..f2ba702c2f 100644 --- a/mobile/openapi/lib/model/person_create_dto.dart +++ b/mobile/openapi/lib/model/person_create_dto.dart @@ -20,11 +20,13 @@ class PersonCreateDto { this.name, }); - /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. + /// Person date of birth DateTime? birthDate; + /// Person color (hex) String? color; + /// Mark as favorite /// /// 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 @@ -33,7 +35,7 @@ class PersonCreateDto { /// bool? isFavorite; - /// Person visibility + /// Person visibility (hidden) /// /// 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 @@ -42,7 +44,7 @@ class PersonCreateDto { /// bool? isHidden; - /// Person name. + /// Person name /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index a6ad5e0c24..455dfb98d6 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -23,8 +23,10 @@ class PersonResponseDto { this.updatedAt, }); + /// Person date of birth DateTime? birthDate; + /// Person color (hex) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -33,8 +35,10 @@ class PersonResponseDto { /// String? color; + /// Person ID String id; + /// Is favorite /// /// 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 @@ -43,12 +47,16 @@ class PersonResponseDto { /// bool? isFavorite; + /// Is hidden bool isHidden; + /// Person name String name; + /// Thumbnail path String thumbnailPath; + /// Last update date /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index d9f84e9f4c..d2b45c8ccb 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -16,6 +16,7 @@ class PersonStatisticsResponseDto { required this.assets, }); + /// Number of assets int assets; @override diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index 6736b4e177..b56940e51d 100644 --- a/mobile/openapi/lib/model/person_update_dto.dart +++ b/mobile/openapi/lib/model/person_update_dto.dart @@ -21,12 +21,13 @@ class PersonUpdateDto { this.name, }); - /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. + /// Person date of birth DateTime? birthDate; + /// Person color (hex) String? color; - /// Asset is used to get the feature face thumbnail. + /// Asset ID used for feature face thumbnail /// /// 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 @@ -35,6 +36,7 @@ class PersonUpdateDto { /// String? featureFaceAssetId; + /// Mark as favorite /// /// 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 @@ -43,7 +45,7 @@ class PersonUpdateDto { /// bool? isFavorite; - /// Person visibility + /// Person visibility (hidden) /// /// 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 @@ -52,7 +54,7 @@ class PersonUpdateDto { /// bool? isHidden; - /// Person name. + /// Person name /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index 9b2e40cf56..f31c04b69f 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -24,8 +24,10 @@ class PersonWithFacesResponseDto { this.updatedAt, }); + /// Person date of birth DateTime? birthDate; + /// Person color (hex) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -34,10 +36,13 @@ class PersonWithFacesResponseDto { /// String? color; + /// Face detections List faces; + /// Person ID String id; + /// Is favorite /// /// 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 @@ -46,12 +51,16 @@ class PersonWithFacesResponseDto { /// bool? isFavorite; + /// Is hidden bool isHidden; + /// Person name String name; + /// Thumbnail path String thumbnailPath; + /// Last update date /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/pin_code_change_dto.dart b/mobile/openapi/lib/model/pin_code_change_dto.dart index 2e9967aa6b..068cc9e91b 100644 --- a/mobile/openapi/lib/model/pin_code_change_dto.dart +++ b/mobile/openapi/lib/model/pin_code_change_dto.dart @@ -18,8 +18,10 @@ class PinCodeChangeDto { this.pinCode, }); + /// New PIN code (4-6 digits) String newPinCode; + /// User password (required if PIN code is not provided) /// /// 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 @@ -28,6 +30,7 @@ class PinCodeChangeDto { /// String? password; + /// New PIN code (4-6 digits) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/pin_code_reset_dto.dart b/mobile/openapi/lib/model/pin_code_reset_dto.dart index 3585348675..c37be76f18 100644 --- a/mobile/openapi/lib/model/pin_code_reset_dto.dart +++ b/mobile/openapi/lib/model/pin_code_reset_dto.dart @@ -17,6 +17,7 @@ class PinCodeResetDto { this.pinCode, }); + /// User password (required if PIN code is not provided) /// /// 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 @@ -25,6 +26,7 @@ class PinCodeResetDto { /// String? password; + /// New PIN code (4-6 digits) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/pin_code_setup_dto.dart b/mobile/openapi/lib/model/pin_code_setup_dto.dart index 09933790de..e2f08f102b 100644 --- a/mobile/openapi/lib/model/pin_code_setup_dto.dart +++ b/mobile/openapi/lib/model/pin_code_setup_dto.dart @@ -16,6 +16,7 @@ class PinCodeSetupDto { required this.pinCode, }); + /// PIN code (4-6 digits) String pinCode; @override diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart index 4f77788263..94aa58eba4 100644 --- a/mobile/openapi/lib/model/places_response_dto.dart +++ b/mobile/openapi/lib/model/places_response_dto.dart @@ -20,6 +20,7 @@ class PlacesResponseDto { required this.name, }); + /// Administrative level 1 name (state/province) /// /// 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 @@ -28,6 +29,7 @@ class PlacesResponseDto { /// String? admin1name; + /// Administrative level 2 name (county/district) /// /// 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 @@ -36,10 +38,13 @@ class PlacesResponseDto { /// String? admin2name; + /// Latitude coordinate num latitude; + /// Longitude coordinate num longitude; + /// Place name String name; @override diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 75b23fc8a4..34fa314ba9 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -22,18 +22,25 @@ class PluginActionResponseDto { required this.title, }); + /// Action description String description; + /// Action ID String id; + /// Method name String methodName; + /// Plugin ID String pluginId; + /// Action schema Object? schema; - List supportedContexts; + /// Supported contexts + List supportedContexts; + /// Action title String title; @override @@ -90,7 +97,7 @@ class PluginActionResponseDto { methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, schema: mapValueOfType(json, r'schema'), - supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), + supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); } diff --git a/mobile/openapi/lib/model/plugin_context.dart b/mobile/openapi/lib/model/plugin_context_type.dart similarity index 51% rename from mobile/openapi/lib/model/plugin_context.dart rename to mobile/openapi/lib/model/plugin_context_type.dart index efb701c7d0..6f4ac91fdb 100644 --- a/mobile/openapi/lib/model/plugin_context.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,10 +10,10 @@ part of openapi.api; - -class PluginContext { +/// Context type +class PluginContextType { /// Instantiate a new enum with the provided [value]. - const PluginContext._(this.value); + const PluginContextType._(this.value); /// The underlying value of this enum member. final String value; @@ -23,24 +23,24 @@ class PluginContext { String toJson() => value; - static const asset = PluginContext._(r'asset'); - static const album = PluginContext._(r'album'); - static const person = PluginContext._(r'person'); + static const asset = PluginContextType._(r'asset'); + static const album = PluginContextType._(r'album'); + static const person = PluginContextType._(r'person'); - /// List of all possible values in this [enum][PluginContext]. - static const values = [ + /// List of all possible values in this [enum][PluginContextType]. + static const values = [ asset, album, person, ]; - static PluginContext? fromJson(dynamic value) => PluginContextTypeTransformer().decode(value); + static PluginContextType? fromJson(dynamic value) => PluginContextTypeTypeTransformer().decode(value); - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = PluginContext.fromJson(row); + final value = PluginContextType.fromJson(row); if (value != null) { result.add(value); } @@ -50,16 +50,16 @@ class PluginContext { } } -/// Transformation class that can [encode] an instance of [PluginContext] to String, -/// and [decode] dynamic data back to [PluginContext]. -class PluginContextTypeTransformer { - factory PluginContextTypeTransformer() => _instance ??= const PluginContextTypeTransformer._(); +/// Transformation class that can [encode] an instance of [PluginContextType] to String, +/// and [decode] dynamic data back to [PluginContextType]. +class PluginContextTypeTypeTransformer { + factory PluginContextTypeTypeTransformer() => _instance ??= const PluginContextTypeTypeTransformer._(); - const PluginContextTypeTransformer._(); + const PluginContextTypeTypeTransformer._(); - String encode(PluginContext data) => data.value; + String encode(PluginContextType data) => data.value; - /// Decodes a [dynamic value][data] to a PluginContext. + /// Decodes a [dynamic value][data] to a PluginContextType. /// /// 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] @@ -67,12 +67,12 @@ class PluginContextTypeTransformer { /// /// 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. - PluginContext? decode(dynamic data, {bool allowNull = true}) { + PluginContextType? decode(dynamic data, {bool allowNull = true}) { if (data != null) { switch (data) { - case r'asset': return PluginContext.asset; - case r'album': return PluginContext.album; - case r'person': return PluginContext.person; + case r'asset': return PluginContextType.asset; + case r'album': return PluginContextType.album; + case r'person': return PluginContextType.person; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); @@ -82,7 +82,7 @@ class PluginContextTypeTransformer { return null; } - /// Singleton [PluginContextTypeTransformer] instance. - static PluginContextTypeTransformer? _instance; + /// Singleton [PluginContextTypeTypeTransformer] instance. + static PluginContextTypeTypeTransformer? _instance; } diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index 8ed6acec78..ea6411a9c1 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -22,18 +22,25 @@ class PluginFilterResponseDto { required this.title, }); + /// Filter description String description; + /// Filter ID String id; + /// Method name String methodName; + /// Plugin ID String pluginId; + /// Filter schema Object? schema; - List supportedContexts; + /// Supported contexts + List supportedContexts; + /// Filter title String title; @override @@ -90,7 +97,7 @@ class PluginFilterResponseDto { methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, schema: mapValueOfType(json, r'schema'), - supportedContexts: PluginContext.listFromJson(json[r'supportedContexts']), + supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); } diff --git a/mobile/openapi/lib/model/plugin_response_dto.dart b/mobile/openapi/lib/model/plugin_response_dto.dart index afa6f3e1ab..7a99896475 100644 --- a/mobile/openapi/lib/model/plugin_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_response_dto.dart @@ -25,24 +25,34 @@ class PluginResponseDto { required this.version, }); + /// Plugin actions List actions; + /// Plugin author String author; + /// Creation date String createdAt; + /// Plugin description String description; + /// Plugin filters List filters; + /// Plugin ID String id; + /// Plugin name String name; + /// Plugin title String title; + /// Last update date String updatedAt; + /// Plugin version String version; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart new file mode 100644 index 0000000000..16a9604bcd --- /dev/null +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -0,0 +1,109 @@ +// +// 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 PluginTriggerResponseDto { + /// Returns a new [PluginTriggerResponseDto] instance. + PluginTriggerResponseDto({ + required this.contextType, + required this.type, + }); + + /// Context type + PluginContextType contextType; + + /// Trigger type + PluginTriggerType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginTriggerResponseDto && + other.contextType == contextType && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (contextType.hashCode) + + (type.hashCode); + + @override + String toString() => 'PluginTriggerResponseDto[contextType=$contextType, type=$type]'; + + Map toJson() { + final json = {}; + json[r'contextType'] = this.contextType; + json[r'type'] = this.type; + return json; + } + + /// Returns a new [PluginTriggerResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginTriggerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PluginTriggerResponseDto"); + if (value is Map) { + final json = value.cast(); + + return PluginTriggerResponseDto( + contextType: PluginContextType.fromJson(json[r'contextType'])!, + type: PluginTriggerType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginTriggerResponseDto.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 = PluginTriggerResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginTriggerResponseDto-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] = PluginTriggerResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'contextType', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index b200f1b9e6..9ae64acf6c 100644 --- a/mobile/openapi/lib/model/plugin_trigger_type.dart +++ b/mobile/openapi/lib/model/plugin_trigger_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Trigger type class PluginTriggerType { /// Instantiate a new enum with the provided [value]. const PluginTriggerType._(this.value); diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart index a117206977..e55c286629 100644 --- a/mobile/openapi/lib/model/purchase_response.dart +++ b/mobile/openapi/lib/model/purchase_response.dart @@ -17,8 +17,10 @@ class PurchaseResponse { required this.showSupportBadge, }); + /// Date until which to hide buy button String hideBuyButtonUntil; + /// Whether to show support badge bool showSupportBadge; @override diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart index 69057e6c55..913faf9bc4 100644 --- a/mobile/openapi/lib/model/purchase_update.dart +++ b/mobile/openapi/lib/model/purchase_update.dart @@ -17,6 +17,7 @@ class PurchaseUpdate { this.showSupportBadge, }); + /// Date until which to hide buy button /// /// 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 @@ -25,6 +26,7 @@ class PurchaseUpdate { /// String? hideBuyButtonUntil; + /// Whether to show support badge /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/queue_command.dart b/mobile/openapi/lib/model/queue_command.dart index f03ec6eccd..3cf689a02d 100644 --- a/mobile/openapi/lib/model/queue_command.dart +++ b/mobile/openapi/lib/model/queue_command.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue command to execute class QueueCommand { /// Instantiate a new enum with the provided [value]. const QueueCommand._(this.value); diff --git a/mobile/openapi/lib/model/queue_command_dto.dart b/mobile/openapi/lib/model/queue_command_dto.dart index ded848c12f..9e1eea15db 100644 --- a/mobile/openapi/lib/model/queue_command_dto.dart +++ b/mobile/openapi/lib/model/queue_command_dto.dart @@ -17,8 +17,10 @@ class QueueCommandDto { this.force, }); + /// Queue command to execute QueueCommand command; + /// Force the command execution (if applicable) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart index 1bfaa56195..2ce63784eb 100644 --- a/mobile/openapi/lib/model/queue_job_response_dto.dart +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -19,8 +19,10 @@ class QueueJobResponseDto { required this.timestamp, }); + /// Job data payload Object data; + /// Job ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -29,8 +31,10 @@ class QueueJobResponseDto { /// String? id; + /// Job name JobName name; + /// Job creation timestamp int timestamp; @override diff --git a/mobile/openapi/lib/model/queue_name.dart b/mobile/openapi/lib/model/queue_name.dart index bcc4159fce..d94304d0d3 100644 --- a/mobile/openapi/lib/model/queue_name.dart +++ b/mobile/openapi/lib/model/queue_name.dart @@ -40,6 +40,7 @@ class QueueName { static const backupDatabase = QueueName._(r'backupDatabase'); static const ocr = QueueName._(r'ocr'); static const workflow = QueueName._(r'workflow'); + static const editor = QueueName._(r'editor'); /// List of all possible values in this [enum][QueueName]. static const values = [ @@ -60,6 +61,7 @@ class QueueName { backupDatabase, ocr, workflow, + editor, ]; static QueueName? fromJson(dynamic value) => QueueNameTypeTransformer().decode(value); @@ -115,6 +117,7 @@ class QueueNameTypeTransformer { case r'backupDatabase': return QueueName.backupDatabase; case r'ocr': return QueueName.ocr; case r'workflow': return QueueName.workflow; + case r'editor': return QueueName.editor; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index c5d4ed8e3d..ac9244514c 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -18,8 +18,10 @@ class QueueResponseDto { required this.statistics, }); + /// Whether the queue is paused bool isPaused; + /// Queue name QueueName name; QueueStatisticsDto statistics; diff --git a/mobile/openapi/lib/model/queue_statistics_dto.dart b/mobile/openapi/lib/model/queue_statistics_dto.dart index c27c4a5892..c9a37ee30a 100644 --- a/mobile/openapi/lib/model/queue_statistics_dto.dart +++ b/mobile/openapi/lib/model/queue_statistics_dto.dart @@ -21,16 +21,22 @@ class QueueStatisticsDto { required this.waiting, }); + /// Number of active jobs int active; + /// Number of completed jobs int completed; + /// Number of delayed jobs int delayed; + /// Number of failed jobs int failed; + /// Number of paused jobs int paused; + /// Number of waiting jobs int waiting; @override diff --git a/mobile/openapi/lib/model/queue_status_legacy_dto.dart b/mobile/openapi/lib/model/queue_status_legacy_dto.dart index 88c4eac340..de6ce63319 100644 --- a/mobile/openapi/lib/model/queue_status_legacy_dto.dart +++ b/mobile/openapi/lib/model/queue_status_legacy_dto.dart @@ -17,8 +17,10 @@ class QueueStatusLegacyDto { required this.isPaused, }); + /// Whether the queue is currently active (has running jobs) bool isActive; + /// Whether the queue is paused bool isPaused; @override diff --git a/mobile/openapi/lib/model/queue_update_dto.dart b/mobile/openapi/lib/model/queue_update_dto.dart index ce89e51878..28aafe95f7 100644 --- a/mobile/openapi/lib/model/queue_update_dto.dart +++ b/mobile/openapi/lib/model/queue_update_dto.dart @@ -16,6 +16,7 @@ class QueueUpdateDto { this.isPaused, }); + /// Whether to pause the queue /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/queues_response_legacy_dto.dart b/mobile/openapi/lib/model/queues_response_legacy_dto.dart index 4aab6d863b..c7bc23cb4d 100644 --- a/mobile/openapi/lib/model/queues_response_legacy_dto.dart +++ b/mobile/openapi/lib/model/queues_response_legacy_dto.dart @@ -16,6 +16,7 @@ class QueuesResponseLegacyDto { required this.backgroundTask, required this.backupDatabase, required this.duplicateDetection, + required this.editor, required this.faceDetection, required this.facialRecognition, required this.library_, @@ -38,6 +39,8 @@ class QueuesResponseLegacyDto { QueueResponseLegacyDto duplicateDetection; + QueueResponseLegacyDto editor; + QueueResponseLegacyDto faceDetection; QueueResponseLegacyDto facialRecognition; @@ -71,6 +74,7 @@ class QueuesResponseLegacyDto { other.backgroundTask == backgroundTask && other.backupDatabase == backupDatabase && other.duplicateDetection == duplicateDetection && + other.editor == editor && other.faceDetection == faceDetection && other.facialRecognition == facialRecognition && other.library_ == library_ && @@ -92,6 +96,7 @@ class QueuesResponseLegacyDto { (backgroundTask.hashCode) + (backupDatabase.hashCode) + (duplicateDetection.hashCode) + + (editor.hashCode) + (faceDetection.hashCode) + (facialRecognition.hashCode) + (library_.hashCode) + @@ -108,13 +113,14 @@ class QueuesResponseLegacyDto { (workflow.hashCode); @override - String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; + String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, editor=$editor, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; Map toJson() { final json = {}; json[r'backgroundTask'] = this.backgroundTask; json[r'backupDatabase'] = this.backupDatabase; json[r'duplicateDetection'] = this.duplicateDetection; + json[r'editor'] = this.editor; json[r'faceDetection'] = this.faceDetection; json[r'facialRecognition'] = this.facialRecognition; json[r'library'] = this.library_; @@ -144,6 +150,7 @@ class QueuesResponseLegacyDto { backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!, backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!, duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!, + editor: QueueResponseLegacyDto.fromJson(json[r'editor'])!, faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!, facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!, library_: QueueResponseLegacyDto.fromJson(json[r'library'])!, @@ -208,6 +215,7 @@ class QueuesResponseLegacyDto { 'backgroundTask', 'backupDatabase', 'duplicateDetection', + 'editor', 'faceDetection', 'facialRecognition', 'library', diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 96d670fd96..4166fc9f3c 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -48,12 +48,16 @@ class RandomSearchDto { this.withStacked, }); + /// Filter by album IDs List albumIds; + /// Filter by city name String? city; + /// Filter by country name String? country; + /// Filter by creation date (after) /// /// 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 @@ -62,6 +66,7 @@ class RandomSearchDto { /// DateTime? createdAfter; + /// Filter by creation date (before) /// /// 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 @@ -70,6 +75,7 @@ class RandomSearchDto { /// DateTime? createdBefore; + /// Device ID to filter by /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -78,6 +84,7 @@ class RandomSearchDto { /// String? deviceId; + /// Filter by encoded status /// /// 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 @@ -86,6 +93,7 @@ class RandomSearchDto { /// bool? isEncoded; + /// Filter by favorite status /// /// 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 @@ -94,6 +102,7 @@ class RandomSearchDto { /// bool? isFavorite; + /// Filter by motion photo status /// /// 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 @@ -102,6 +111,7 @@ class RandomSearchDto { /// bool? isMotion; + /// Filter assets not in any album /// /// 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 @@ -110,6 +120,7 @@ class RandomSearchDto { /// bool? isNotInAlbum; + /// Filter by offline status /// /// 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 @@ -118,10 +129,13 @@ class RandomSearchDto { /// bool? isOffline; + /// Filter by lens model String? lensModel; + /// Library ID to filter by String? libraryId; + /// Filter by camera make /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -130,8 +144,10 @@ class RandomSearchDto { /// String? make; + /// Filter by camera model String? model; + /// Filter by OCR text content /// /// 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 @@ -140,18 +156,17 @@ class RandomSearchDto { /// String? ocr; + /// Filter by person IDs List personIds; + /// Filter by rating [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -162,10 +177,13 @@ class RandomSearchDto { /// num? size; + /// Filter by state/province name String? state; + /// Filter by tag IDs List? tagIds; + /// Filter by taken date (after) /// /// 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 @@ -174,6 +192,7 @@ class RandomSearchDto { /// DateTime? takenAfter; + /// Filter by taken date (before) /// /// 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 @@ -182,6 +201,7 @@ class RandomSearchDto { /// DateTime? takenBefore; + /// Filter by trash date (after) /// /// 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 @@ -190,6 +210,7 @@ class RandomSearchDto { /// DateTime? trashedAfter; + /// Filter by trash date (before) /// /// 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 @@ -198,6 +219,7 @@ class RandomSearchDto { /// DateTime? trashedBefore; + /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -206,6 +228,7 @@ class RandomSearchDto { /// AssetTypeEnum? type; + /// Filter by update date (after) /// /// 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 @@ -214,6 +237,7 @@ class RandomSearchDto { /// DateTime? updatedAfter; + /// Filter by update date (before) /// /// 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 @@ -222,6 +246,7 @@ class RandomSearchDto { /// DateTime? updatedBefore; + /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -230,6 +255,7 @@ class RandomSearchDto { /// AssetVisibility? visibility; + /// Include deleted assets /// /// 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 @@ -238,6 +264,7 @@ class RandomSearchDto { /// bool? withDeleted; + /// Include EXIF data in response /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -246,6 +273,7 @@ class RandomSearchDto { /// bool? withExif; + /// Include assets with people /// /// 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 @@ -254,6 +282,7 @@ class RandomSearchDto { /// bool? withPeople; + /// Include stacked assets /// /// 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 @@ -530,7 +559,9 @@ class RandomSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), tagIds: json[r'tagIds'] is Iterable diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index 8e1951277a..4346fa5c58 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -16,6 +16,7 @@ class RatingsResponse { this.enabled = false, }); + /// Whether ratings are enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/ratings_update.dart b/mobile/openapi/lib/model/ratings_update.dart index 5d9f9a655f..8079172e21 100644 --- a/mobile/openapi/lib/model/ratings_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -16,6 +16,7 @@ class RatingsUpdate { this.enabled, }); + /// Whether ratings are enabled /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart index 5b3648b46b..6ad8c1a7b9 100644 --- a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -17,8 +17,10 @@ class ReverseGeocodingStateResponseDto { required this.lastUpdate, }); + /// Last import file name String? lastImportFileName; + /// Last update timestamp String? lastUpdate; @override diff --git a/mobile/openapi/lib/model/rotate_parameters.dart b/mobile/openapi/lib/model/rotate_parameters.dart new file mode 100644 index 0000000000..33609e83e5 --- /dev/null +++ b/mobile/openapi/lib/model/rotate_parameters.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class RotateParameters { + /// Returns a new [RotateParameters] instance. + RotateParameters({ + required this.angle, + }); + + /// Rotation angle in degrees + num angle; + + @override + bool operator ==(Object other) => identical(this, other) || other is RotateParameters && + other.angle == angle; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (angle.hashCode); + + @override + String toString() => 'RotateParameters[angle=$angle]'; + + Map toJson() { + final json = {}; + json[r'angle'] = this.angle; + return json; + } + + /// Returns a new [RotateParameters] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static RotateParameters? fromJson(dynamic value) { + upgradeDto(value, "RotateParameters"); + if (value is Map) { + final json = value.cast(); + + return RotateParameters( + angle: num.parse('${json[r'angle']}'), + ); + } + 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 = RotateParameters.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 = RotateParameters.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of RotateParameters-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] = RotateParameters.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'angle', + }; +} + diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index e9b47e85ec..8841251e4a 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -19,12 +19,14 @@ class SearchAlbumResponseDto { required this.total, }); + /// Number of albums in this page int count; List facets; List items; + /// Total number of matching albums int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index 3d214e61d9..acb81f28e2 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -20,14 +20,17 @@ class SearchAssetResponseDto { required this.total, }); + /// Number of assets in this page int count; List facets; List items; + /// Next page token String? nextPage; + /// Total number of matching assets int total; @override diff --git a/mobile/openapi/lib/model/search_explore_item.dart b/mobile/openapi/lib/model/search_explore_item.dart index d44b2cd704..4089011879 100644 --- a/mobile/openapi/lib/model/search_explore_item.dart +++ b/mobile/openapi/lib/model/search_explore_item.dart @@ -19,6 +19,7 @@ class SearchExploreItem { AssetResponseDto data; + /// Explore value String value; @override diff --git a/mobile/openapi/lib/model/search_explore_response_dto.dart b/mobile/openapi/lib/model/search_explore_response_dto.dart index 3b5d4f9849..07ce26c9b8 100644 --- a/mobile/openapi/lib/model/search_explore_response_dto.dart +++ b/mobile/openapi/lib/model/search_explore_response_dto.dart @@ -17,6 +17,7 @@ class SearchExploreResponseDto { this.items = const [], }); + /// Explore field name String fieldName; List items; diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index f8eee84485..8318fbfb3b 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -17,8 +17,10 @@ class SearchFacetCountResponseDto { required this.value, }); + /// Number of assets with this facet value int count; + /// Facet value String value; @override diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index aeec873c8d..43b5ac5c81 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,8 +17,10 @@ class SearchFacetResponseDto { required this.fieldName, }); + /// Facet counts List counts; + /// Facet field name String fieldName; @override diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart index 84f31373d8..5aebe4d6a9 100644 --- a/mobile/openapi/lib/model/search_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -16,6 +16,7 @@ class SearchStatisticsResponseDto { required this.total, }); + /// Total number of matching assets int total; @override diff --git a/mobile/openapi/lib/model/server_about_response_dto.dart b/mobile/openapi/lib/model/server_about_response_dto.dart index 5d53d5fdee..1ae53763fe 100644 --- a/mobile/openapi/lib/model/server_about_response_dto.dart +++ b/mobile/openapi/lib/model/server_about_response_dto.dart @@ -36,6 +36,7 @@ class ServerAboutResponseDto { required this.versionUrl, }); + /// Build identifier /// /// 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 @@ -44,6 +45,7 @@ class ServerAboutResponseDto { /// String? build; + /// Build image name /// /// 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 @@ -52,6 +54,7 @@ class ServerAboutResponseDto { /// String? buildImage; + /// Build image URL /// /// 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 @@ -60,6 +63,7 @@ class ServerAboutResponseDto { /// String? buildImageUrl; + /// Build URL /// /// 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 @@ -68,6 +72,7 @@ class ServerAboutResponseDto { /// String? buildUrl; + /// ExifTool version /// /// 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 @@ -76,6 +81,7 @@ class ServerAboutResponseDto { /// String? exiftool; + /// FFmpeg version /// /// 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 @@ -84,6 +90,7 @@ class ServerAboutResponseDto { /// String? ffmpeg; + /// ImageMagick version /// /// 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 @@ -92,6 +99,7 @@ class ServerAboutResponseDto { /// String? imagemagick; + /// libvips version /// /// 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 @@ -100,8 +108,10 @@ class ServerAboutResponseDto { /// String? libvips; + /// Whether the server is licensed bool licensed; + /// Node.js version /// /// 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 @@ -110,6 +120,7 @@ class ServerAboutResponseDto { /// String? nodejs; + /// Repository name /// /// 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 @@ -118,6 +129,7 @@ class ServerAboutResponseDto { /// String? repository; + /// Repository URL /// /// 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 @@ -126,6 +138,7 @@ class ServerAboutResponseDto { /// String? repositoryUrl; + /// Source commit hash /// /// 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 @@ -134,6 +147,7 @@ class ServerAboutResponseDto { /// String? sourceCommit; + /// Source reference (branch/tag) /// /// 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 @@ -142,6 +156,7 @@ class ServerAboutResponseDto { /// String? sourceRef; + /// Source URL /// /// 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 @@ -150,6 +165,7 @@ class ServerAboutResponseDto { /// String? sourceUrl; + /// Third-party bug/feature URL /// /// 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 @@ -158,6 +174,7 @@ class ServerAboutResponseDto { /// String? thirdPartyBugFeatureUrl; + /// Third-party documentation URL /// /// 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 @@ -166,6 +183,7 @@ class ServerAboutResponseDto { /// String? thirdPartyDocumentationUrl; + /// Third-party source URL /// /// 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 @@ -174,6 +192,7 @@ class ServerAboutResponseDto { /// String? thirdPartySourceUrl; + /// Third-party support URL /// /// 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 @@ -182,8 +201,10 @@ class ServerAboutResponseDto { /// String? thirdPartySupportUrl; + /// Server version String version; + /// URL to version information String versionUrl; @override diff --git a/mobile/openapi/lib/model/server_apk_links_dto.dart b/mobile/openapi/lib/model/server_apk_links_dto.dart index 086a2f172b..2227018468 100644 --- a/mobile/openapi/lib/model/server_apk_links_dto.dart +++ b/mobile/openapi/lib/model/server_apk_links_dto.dart @@ -19,12 +19,16 @@ class ServerApkLinksDto { required this.x8664, }); + /// APK download link for ARM64 v8a architecture String arm64v8a; + /// APK download link for ARM EABI v7a architecture String armeabiv7a; + /// APK download link for universal architecture String universal; + /// APK download link for x86_64 architecture String x8664; @override diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 8e701472b1..fec096d51a 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -26,26 +26,37 @@ class ServerConfigDto { required this.userDeleteDelay, }); + /// External domain URL String externalDomain; + /// Whether the server has been initialized bool isInitialized; + /// Whether the admin has completed onboarding bool isOnboarded; + /// Login page message String loginPageMessage; + /// Whether maintenance mode is active bool maintenanceMode; + /// Map dark style URL String mapDarkStyleUrl; + /// Map light style URL String mapLightStyleUrl; + /// OAuth button text String oauthButtonText; + /// Whether public user registration is enabled bool publicUsers; + /// Number of days before trashed assets are permanently deleted int trashDays; + /// Delay in days before deleted users are permanently removed int userDeleteDelay; @override diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 7b5980ca13..79494b74eb 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -30,34 +30,49 @@ class ServerFeaturesDto { required this.trash, }); + /// Whether config file is available bool configFile; + /// Whether duplicate detection is enabled bool duplicateDetection; + /// Whether email notifications are enabled bool email; + /// Whether facial recognition is enabled bool facialRecognition; + /// Whether face import is enabled bool importFaces; + /// Whether map feature is enabled bool map; + /// Whether OAuth is enabled bool oauth; + /// Whether OAuth auto-launch is enabled bool oauthAutoLaunch; + /// Whether OCR is enabled bool ocr; + /// Whether password login is enabled bool passwordLogin; + /// Whether reverse geocoding is enabled bool reverseGeocoding; + /// Whether search is enabled bool search; + /// Whether sidecar files are supported bool sidecar; + /// Whether smart search is enabled bool smartSearch; + /// Whether trash feature is enabled bool trash; @override diff --git a/mobile/openapi/lib/model/server_media_types_response_dto.dart b/mobile/openapi/lib/model/server_media_types_response_dto.dart index 506cbb44b4..6a2aaeb9e1 100644 --- a/mobile/openapi/lib/model/server_media_types_response_dto.dart +++ b/mobile/openapi/lib/model/server_media_types_response_dto.dart @@ -18,10 +18,13 @@ class ServerMediaTypesResponseDto { this.video = const [], }); + /// Supported image MIME types List image; + /// Supported sidecar MIME types List sidecar; + /// Supported video MIME types List video; @override diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index 531fa8f03e..ef2fa458e2 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -21,16 +21,21 @@ class ServerStatsResponseDto { this.videos = 0, }); + /// Total number of photos int photos; + /// Total storage usage in bytes int usage; List usageByUser; + /// Storage usage for photos in bytes int usagePhotos; + /// Storage usage for videos in bytes int usageVideos; + /// Total number of videos int videos; @override diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 8d12e77834..476b048b4d 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -22,18 +22,25 @@ class ServerStorageResponseDto { required this.diskUseRaw, }); + /// Available disk space (human-readable format) String diskAvailable; + /// Available disk space in bytes int diskAvailableRaw; + /// Total disk size (human-readable format) String diskSize; + /// Total disk size in bytes int diskSizeRaw; + /// Disk usage percentage (0-100) double diskUsagePercentage; + /// Used disk space (human-readable format) String diskUse; + /// Used disk space in bytes int diskUseRaw; @override diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart index 69e1b2d2c8..957cf84d55 100644 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ b/mobile/openapi/lib/model/server_theme_dto.dart @@ -16,6 +16,7 @@ class ServerThemeDto { required this.customCss, }); + /// Custom CSS for theming String customCss; @override diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart index c81cb0e8b9..c3b7049016 100644 --- a/mobile/openapi/lib/model/server_version_history_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -18,10 +18,13 @@ class ServerVersionHistoryResponseDto { required this.version, }); + /// When this version was first seen DateTime createdAt; + /// Version history entry ID String id; + /// Version string String version; @override diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index 751347fabd..a13cd81ad7 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -18,10 +18,13 @@ class ServerVersionResponseDto { required this.patch_, }); + /// Major version number int major; + /// Minor version number int minor; + /// Patch version number int patch_; @override diff --git a/mobile/openapi/lib/model/session_create_dto.dart b/mobile/openapi/lib/model/session_create_dto.dart index aacf1150a5..3874bc3303 100644 --- a/mobile/openapi/lib/model/session_create_dto.dart +++ b/mobile/openapi/lib/model/session_create_dto.dart @@ -18,6 +18,7 @@ class SessionCreateDto { this.duration, }); + /// Device OS /// /// 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 @@ -26,6 +27,7 @@ class SessionCreateDto { /// String? deviceOS; + /// Device type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -34,7 +36,7 @@ class SessionCreateDto { /// String? deviceType; - /// session duration, in seconds + /// Session duration in seconds /// /// Minimum value: 1 /// diff --git a/mobile/openapi/lib/model/session_create_response_dto.dart b/mobile/openapi/lib/model/session_create_response_dto.dart index e16597f3b5..f35232b0e8 100644 --- a/mobile/openapi/lib/model/session_create_response_dto.dart +++ b/mobile/openapi/lib/model/session_create_response_dto.dart @@ -25,16 +25,22 @@ class SessionCreateResponseDto { required this.updatedAt, }); + /// App version String? appVersion; + /// Creation date String createdAt; + /// Is current session bool current; + /// Device OS String deviceOS; + /// Device type String deviceType; + /// Expiration date /// /// 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 @@ -43,12 +49,16 @@ class SessionCreateResponseDto { /// String? expiresAt; + /// Session ID String id; + /// Is pending sync reset bool isPendingSyncReset; + /// Session token String token; + /// Last update date String updatedAt; @override diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index 85acb8a358..ed84160827 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -24,16 +24,22 @@ class SessionResponseDto { required this.updatedAt, }); + /// App version String? appVersion; + /// Creation date String createdAt; + /// Is current session bool current; + /// Device OS String deviceOS; + /// Device type String deviceType; + /// Expiration date /// /// 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 @@ -42,10 +48,13 @@ class SessionResponseDto { /// String? expiresAt; + /// Session ID String id; + /// Is pending sync reset bool isPendingSyncReset; + /// Last update date String updatedAt; @override diff --git a/mobile/openapi/lib/model/session_unlock_dto.dart b/mobile/openapi/lib/model/session_unlock_dto.dart index 4cfeb14385..48ee75fb05 100644 --- a/mobile/openapi/lib/model/session_unlock_dto.dart +++ b/mobile/openapi/lib/model/session_unlock_dto.dart @@ -17,6 +17,7 @@ class SessionUnlockDto { this.pinCode, }); + /// User password (required if PIN code is not provided) /// /// 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 @@ -25,6 +26,7 @@ class SessionUnlockDto { /// String? password; + /// New PIN code (4-6 digits) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/session_update_dto.dart b/mobile/openapi/lib/model/session_update_dto.dart index cd170b1baa..3ab430deaa 100644 --- a/mobile/openapi/lib/model/session_update_dto.dart +++ b/mobile/openapi/lib/model/session_update_dto.dart @@ -16,6 +16,7 @@ class SessionUpdateDto { this.isPendingSyncReset, }); + /// Reset pending sync state /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index c724337529..14bf584bb9 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -14,25 +14,43 @@ class SetMaintenanceModeDto { /// Returns a new [SetMaintenanceModeDto] instance. SetMaintenanceModeDto({ required this.action, + this.restoreBackupFilename, }); + /// Maintenance action MaintenanceAction action; + /// Restore backup filename + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? restoreBackupFilename; + @override bool operator ==(Object other) => identical(this, other) || other is SetMaintenanceModeDto && - other.action == action; + other.action == action && + other.restoreBackupFilename == restoreBackupFilename; @override int get hashCode => // ignore: unnecessary_parenthesis - (action.hashCode); + (action.hashCode) + + (restoreBackupFilename == null ? 0 : restoreBackupFilename!.hashCode); @override - String toString() => 'SetMaintenanceModeDto[action=$action]'; + String toString() => 'SetMaintenanceModeDto[action=$action, restoreBackupFilename=$restoreBackupFilename]'; Map toJson() { final json = {}; json[r'action'] = this.action; + if (this.restoreBackupFilename != null) { + json[r'restoreBackupFilename'] = this.restoreBackupFilename; + } else { + // json[r'restoreBackupFilename'] = null; + } return json; } @@ -46,6 +64,7 @@ class SetMaintenanceModeDto { return SetMaintenanceModeDto( action: MaintenanceAction.fromJson(json[r'action'])!, + restoreBackupFilename: mapValueOfType(json, r'restoreBackupFilename'), ); } return null; diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 644227bd6e..2675ad4beb 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -25,6 +25,7 @@ class SharedLinkCreateDto { required this.type, }); + /// Album ID (for album sharing) /// /// 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 @@ -33,8 +34,10 @@ class SharedLinkCreateDto { /// String? albumId; + /// Allow downloads bool allowDownload; + /// Allow uploads /// /// 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 @@ -43,18 +46,25 @@ class SharedLinkCreateDto { /// bool? allowUpload; + /// Asset IDs (for individual assets) List assetIds; + /// Link description String? description; + /// Expiration date DateTime? expiresAt; + /// Link password String? password; + /// Show metadata bool showMetadata; + /// Custom URL slug String? slug; + /// Shared link type SharedLinkType type; @override diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index f13bc6977b..b22232add6 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -23,6 +23,7 @@ class SharedLinkEditDto { this.slug, }); + /// Allow downloads /// /// 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 @@ -31,6 +32,7 @@ class SharedLinkEditDto { /// bool? allowDownload; + /// Allow uploads /// /// 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 @@ -39,7 +41,7 @@ class SharedLinkEditDto { /// bool? allowUpload; - /// Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. + /// Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. /// /// 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 @@ -48,12 +50,16 @@ class SharedLinkEditDto { /// bool? changeExpiryTime; + /// Link description String? description; + /// Expiration date DateTime? expiresAt; + /// Link password String? password; + /// Show metadata /// /// 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 @@ -62,6 +68,7 @@ class SharedLinkEditDto { /// bool? showMetadata; + /// Custom URL slug String? slug; @override diff --git a/mobile/openapi/lib/model/shared_link_login_dto.dart b/mobile/openapi/lib/model/shared_link_login_dto.dart new file mode 100644 index 0000000000..1ab1bc9349 --- /dev/null +++ b/mobile/openapi/lib/model/shared_link_login_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SharedLinkLoginDto { + /// Returns a new [SharedLinkLoginDto] instance. + SharedLinkLoginDto({ + required this.password, + }); + + /// Shared link password + String password; + + @override + bool operator ==(Object other) => identical(this, other) || other is SharedLinkLoginDto && + other.password == password; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (password.hashCode); + + @override + String toString() => 'SharedLinkLoginDto[password=$password]'; + + Map toJson() { + final json = {}; + json[r'password'] = this.password; + return json; + } + + /// Returns a new [SharedLinkLoginDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SharedLinkLoginDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkLoginDto"); + if (value is Map) { + final json = value.cast(); + + return SharedLinkLoginDto( + password: mapValueOfType(json, r'password')!, + ); + } + 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 = SharedLinkLoginDto.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 = SharedLinkLoginDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SharedLinkLoginDto-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] = SharedLinkLoginDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'password', + }; +} + diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d81e1dfa31..d9aec48c39 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -38,32 +38,45 @@ class SharedLinkResponseDto { /// AlbumResponseDto? album; + /// Allow downloads bool allowDownload; + /// Allow uploads bool allowUpload; List assets; + /// Creation date DateTime createdAt; + /// Link description String? description; + /// Expiration date DateTime? expiresAt; + /// Shared link ID String id; + /// Encryption key (base64url) String key; + /// Has password String? password; + /// Show metadata bool showMetadata; + /// Custom URL slug String? slug; + /// Access token String? token; + /// Shared link type SharedLinkType type; + /// Owner user ID String userId; @override diff --git a/mobile/openapi/lib/model/shared_link_type.dart b/mobile/openapi/lib/model/shared_link_type.dart index efab97c209..6a17a9c763 100644 --- a/mobile/openapi/lib/model/shared_link_type.dart +++ b/mobile/openapi/lib/model/shared_link_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Shared link type class SharedLinkType { /// Instantiate a new enum with the provided [value]. const SharedLinkType._(this.value); diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart index 80875e6174..510e94e43f 100644 --- a/mobile/openapi/lib/model/shared_links_response.dart +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -17,8 +17,10 @@ class SharedLinksResponse { this.sidebarWeb = false, }); + /// Whether shared links are enabled bool enabled; + /// Whether shared links appear in web sidebar bool sidebarWeb; @override diff --git a/mobile/openapi/lib/model/shared_links_update.dart b/mobile/openapi/lib/model/shared_links_update.dart index 5d9eda3001..8e792b4f49 100644 --- a/mobile/openapi/lib/model/shared_links_update.dart +++ b/mobile/openapi/lib/model/shared_links_update.dart @@ -17,6 +17,7 @@ class SharedLinksUpdate { this.sidebarWeb, }); + /// Whether shared links are enabled /// /// 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 @@ -25,6 +26,7 @@ class SharedLinksUpdate { /// bool? enabled; + /// Whether shared links appear in web sidebar /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart index 7e0ff4045c..54c8fa07d2 100644 --- a/mobile/openapi/lib/model/sign_up_dto.dart +++ b/mobile/openapi/lib/model/sign_up_dto.dart @@ -18,10 +18,13 @@ class SignUpDto { required this.password, }); + /// User email String email; + /// User name String name; + /// User password String password; @override diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 24f040a92b..5f8214467f 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -50,12 +50,16 @@ class SmartSearchDto { this.withExif, }); + /// Filter by album IDs List albumIds; + /// Filter by city name String? city; + /// Filter by country name String? country; + /// Filter by creation date (after) /// /// 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 @@ -64,6 +68,7 @@ class SmartSearchDto { /// DateTime? createdAfter; + /// Filter by creation date (before) /// /// 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 @@ -72,6 +77,7 @@ class SmartSearchDto { /// DateTime? createdBefore; + /// Device ID to filter by /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -80,6 +86,7 @@ class SmartSearchDto { /// String? deviceId; + /// Filter by encoded status /// /// 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 @@ -88,6 +95,7 @@ class SmartSearchDto { /// bool? isEncoded; + /// Filter by favorite status /// /// 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 @@ -96,6 +104,7 @@ class SmartSearchDto { /// bool? isFavorite; + /// Filter by motion photo status /// /// 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 @@ -104,6 +113,7 @@ class SmartSearchDto { /// bool? isMotion; + /// Filter assets not in any album /// /// 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 @@ -112,6 +122,7 @@ class SmartSearchDto { /// bool? isNotInAlbum; + /// Filter by offline status /// /// 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 @@ -120,6 +131,7 @@ class SmartSearchDto { /// bool? isOffline; + /// Search language code /// /// 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 @@ -128,10 +140,13 @@ class SmartSearchDto { /// String? language; + /// Filter by lens model String? lensModel; + /// Library ID to filter by String? libraryId; + /// Filter by camera make /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -140,8 +155,10 @@ class SmartSearchDto { /// String? make; + /// Filter by camera model String? model; + /// Filter by OCR text content /// /// 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 @@ -150,6 +167,8 @@ class SmartSearchDto { /// String? ocr; + /// Page number + /// /// Minimum value: 1 /// /// Please note: This property should have been non-nullable! Since the specification file @@ -159,8 +178,10 @@ class SmartSearchDto { /// num? page; + /// Filter by person IDs List personIds; + /// Natural language search query /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -169,6 +190,7 @@ class SmartSearchDto { /// String? query; + /// Asset ID to use as search reference /// /// 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 @@ -177,16 +199,14 @@ class SmartSearchDto { /// String? queryAssetId; + /// Filter by rating [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -197,10 +217,13 @@ class SmartSearchDto { /// num? size; + /// Filter by state/province name String? state; + /// Filter by tag IDs List? tagIds; + /// Filter by taken date (after) /// /// 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 @@ -209,6 +232,7 @@ class SmartSearchDto { /// DateTime? takenAfter; + /// Filter by taken date (before) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -217,6 +241,7 @@ class SmartSearchDto { /// DateTime? takenBefore; + /// Filter by trash date (after) /// /// 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 @@ -225,6 +250,7 @@ class SmartSearchDto { /// DateTime? trashedAfter; + /// Filter by trash date (before) /// /// 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 @@ -233,6 +259,7 @@ class SmartSearchDto { /// DateTime? trashedBefore; + /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -241,6 +268,7 @@ class SmartSearchDto { /// AssetTypeEnum? type; + /// Filter by update date (after) /// /// 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 @@ -249,6 +277,7 @@ class SmartSearchDto { /// DateTime? updatedAfter; + /// Filter by update date (before) /// /// 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 @@ -257,6 +286,7 @@ class SmartSearchDto { /// DateTime? updatedBefore; + /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -265,6 +295,7 @@ class SmartSearchDto { /// AssetVisibility? visibility; + /// Include deleted assets /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -273,6 +304,7 @@ class SmartSearchDto { /// bool? withDeleted; + /// Include EXIF data in response /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -567,7 +599,9 @@ class SmartSearchDto { : const [], query: mapValueOfType(json, r'query'), queryAssetId: mapValueOfType(json, r'queryAssetId'), - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), tagIds: json[r'tagIds'] is Iterable diff --git a/mobile/openapi/lib/model/source_type.dart b/mobile/openapi/lib/model/source_type.dart index 4da5aba495..ed164172a3 100644 --- a/mobile/openapi/lib/model/source_type.dart +++ b/mobile/openapi/lib/model/source_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Face detection source type class SourceType { /// Instantiate a new enum with the provided [value]. const SourceType._(this.value); diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart index cb51081eb1..6b08c83401 100644 --- a/mobile/openapi/lib/model/stack_create_dto.dart +++ b/mobile/openapi/lib/model/stack_create_dto.dart @@ -16,7 +16,7 @@ class StackCreateDto { this.assetIds = const [], }); - /// first asset becomes the primary + /// Asset IDs (first becomes primary, min 2) List assetIds; @override diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index b6cb747caf..638dfb5255 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,10 +18,13 @@ class StackResponseDto { required this.primaryAssetId, }); + /// Stack assets List assets; + /// Stack ID String id; + /// Primary asset ID String primaryAssetId; @override diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart index 0101499edf..e81c204f97 100644 --- a/mobile/openapi/lib/model/stack_update_dto.dart +++ b/mobile/openapi/lib/model/stack_update_dto.dart @@ -16,6 +16,7 @@ class StackUpdateDto { this.primaryAssetId, }); + /// Primary asset ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index e0965352e0..d5bbf448a3 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -44,12 +44,16 @@ class StatisticsSearchDto { this.visibility, }); + /// Filter by album IDs List albumIds; + /// Filter by city name String? city; + /// Filter by country name String? country; + /// Filter by creation date (after) /// /// 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 @@ -58,6 +62,7 @@ class StatisticsSearchDto { /// DateTime? createdAfter; + /// Filter by creation date (before) /// /// 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 @@ -66,6 +71,7 @@ class StatisticsSearchDto { /// DateTime? createdBefore; + /// Filter by description text /// /// 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 @@ -74,6 +80,7 @@ class StatisticsSearchDto { /// String? description; + /// Device ID to filter by /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -82,6 +89,7 @@ class StatisticsSearchDto { /// String? deviceId; + /// Filter by encoded status /// /// 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 @@ -90,6 +98,7 @@ class StatisticsSearchDto { /// bool? isEncoded; + /// Filter by favorite status /// /// 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 @@ -98,6 +107,7 @@ class StatisticsSearchDto { /// bool? isFavorite; + /// Filter by motion photo status /// /// 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 @@ -106,6 +116,7 @@ class StatisticsSearchDto { /// bool? isMotion; + /// Filter assets not in any album /// /// 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 @@ -114,6 +125,7 @@ class StatisticsSearchDto { /// bool? isNotInAlbum; + /// Filter by offline status /// /// 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 @@ -122,10 +134,13 @@ class StatisticsSearchDto { /// bool? isOffline; + /// Filter by lens model String? lensModel; + /// Library ID to filter by String? libraryId; + /// Filter by camera make /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -134,8 +149,10 @@ class StatisticsSearchDto { /// String? make; + /// Filter by camera model String? model; + /// Filter by OCR text content /// /// 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 @@ -144,22 +161,22 @@ class StatisticsSearchDto { /// String? ocr; + /// Filter by person IDs List personIds; + /// Filter by rating [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Filter by state/province name String? state; + /// Filter by tag IDs List? tagIds; + /// Filter by taken date (after) /// /// 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 @@ -168,6 +185,7 @@ class StatisticsSearchDto { /// DateTime? takenAfter; + /// Filter by taken date (before) /// /// 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 @@ -176,6 +194,7 @@ class StatisticsSearchDto { /// DateTime? takenBefore; + /// Filter by trash date (after) /// /// 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 @@ -184,6 +203,7 @@ class StatisticsSearchDto { /// DateTime? trashedAfter; + /// Filter by trash date (before) /// /// 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 @@ -192,6 +212,7 @@ class StatisticsSearchDto { /// DateTime? trashedBefore; + /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -200,6 +221,7 @@ class StatisticsSearchDto { /// AssetTypeEnum? type; + /// Filter by update date (after) /// /// 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 @@ -208,6 +230,7 @@ class StatisticsSearchDto { /// DateTime? updatedAfter; + /// Filter by update date (before) /// /// 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 @@ -216,6 +239,7 @@ class StatisticsSearchDto { /// DateTime? updatedBefore; + /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -465,7 +489,9 @@ class StatisticsSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), state: mapValueOfType(json, r'state'), tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) diff --git a/mobile/openapi/lib/model/storage_folder.dart b/mobile/openapi/lib/model/storage_folder.dart new file mode 100644 index 0000000000..8579d48f28 --- /dev/null +++ b/mobile/openapi/lib/model/storage_folder.dart @@ -0,0 +1,97 @@ +// +// 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; + +/// Storage folder +class StorageFolder { + /// Instantiate a new enum with the provided [value]. + const StorageFolder._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const encodedVideo = StorageFolder._(r'encoded-video'); + static const library_ = StorageFolder._(r'library'); + static const upload = StorageFolder._(r'upload'); + static const profile = StorageFolder._(r'profile'); + static const thumbs = StorageFolder._(r'thumbs'); + static const backups = StorageFolder._(r'backups'); + + /// List of all possible values in this [enum][StorageFolder]. + static const values = [ + encodedVideo, + library_, + upload, + profile, + thumbs, + backups, + ]; + + static StorageFolder? fromJson(dynamic value) => StorageFolderTypeTransformer().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 = StorageFolder.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [StorageFolder] to String, +/// and [decode] dynamic data back to [StorageFolder]. +class StorageFolderTypeTransformer { + factory StorageFolderTypeTransformer() => _instance ??= const StorageFolderTypeTransformer._(); + + const StorageFolderTypeTransformer._(); + + String encode(StorageFolder data) => data.value; + + /// Decodes a [dynamic value][data] to a StorageFolder. + /// + /// 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. + StorageFolder? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'encoded-video': return StorageFolder.encodedVideo; + case r'library': return StorageFolder.library_; + case r'upload': return StorageFolder.upload; + case r'profile': return StorageFolder.profile; + case r'thumbs': return StorageFolder.thumbs; + case r'backups': return StorageFolder.backups; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [StorageFolderTypeTransformer] instance. + static StorageFolderTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/sync_ack_delete_dto.dart b/mobile/openapi/lib/model/sync_ack_delete_dto.dart index 998f812f2e..b72ae8c5a6 100644 --- a/mobile/openapi/lib/model/sync_ack_delete_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_delete_dto.dart @@ -16,6 +16,7 @@ class SyncAckDeleteDto { this.types = const [], }); + /// Sync entity types to delete acks for List types; @override diff --git a/mobile/openapi/lib/model/sync_ack_dto.dart b/mobile/openapi/lib/model/sync_ack_dto.dart index c7fafa17d2..747f671557 100644 --- a/mobile/openapi/lib/model/sync_ack_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_dto.dart @@ -17,8 +17,10 @@ class SyncAckDto { required this.type, }); + /// Acknowledgment ID String ack; + /// Sync entity type SyncEntityType type; @override diff --git a/mobile/openapi/lib/model/sync_ack_set_dto.dart b/mobile/openapi/lib/model/sync_ack_set_dto.dart index 0d9eedc389..531a9dc763 100644 --- a/mobile/openapi/lib/model/sync_ack_set_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_set_dto.dart @@ -16,6 +16,7 @@ class SyncAckSetDto { this.acks = const [], }); + /// Acknowledgment IDs (max 1000) List acks; @override diff --git a/mobile/openapi/lib/model/sync_album_delete_v1.dart b/mobile/openapi/lib/model/sync_album_delete_v1.dart index ae5ba3da5d..a6fdf5c68c 100644 --- a/mobile/openapi/lib/model/sync_album_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_album_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAlbumDeleteV1 { required this.albumId, }); + /// Album ID String albumId; @override 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 index d18c850b2a..08952b90ed 100644 --- a/mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart @@ -17,8 +17,10 @@ class SyncAlbumToAssetDeleteV1 { required this.assetId, }); + /// Album ID String albumId; + /// Asset ID String assetId; @override diff --git a/mobile/openapi/lib/model/sync_album_to_asset_v1.dart b/mobile/openapi/lib/model/sync_album_to_asset_v1.dart index 6908f320f8..5f38b35088 100644 --- a/mobile/openapi/lib/model/sync_album_to_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_album_to_asset_v1.dart @@ -17,8 +17,10 @@ class SyncAlbumToAssetV1 { required this.assetId, }); + /// Album ID String albumId; + /// Asset ID String assetId; @override diff --git a/mobile/openapi/lib/model/sync_album_user_delete_v1.dart b/mobile/openapi/lib/model/sync_album_user_delete_v1.dart index f2b0fbee26..526bcc6b6e 100644 --- a/mobile/openapi/lib/model/sync_album_user_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_delete_v1.dart @@ -17,8 +17,10 @@ class SyncAlbumUserDeleteV1 { required this.userId, }); + /// Album ID String albumId; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart index 0b4968b34d..3fc8972069 100644 --- a/mobile/openapi/lib/model/sync_album_user_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -18,10 +18,13 @@ class SyncAlbumUserV1 { required this.userId, }); + /// Album ID String albumId; + /// Album user role AlbumUserRole role; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart index 8ac8246d46..6c89d93724 100644 --- a/mobile/openapi/lib/model/sync_album_v1.dart +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -24,22 +24,30 @@ class SyncAlbumV1 { required this.updatedAt, }); + /// Created at DateTime createdAt; + /// Album description String description; + /// Album ID String id; + /// Is activity enabled bool isActivityEnabled; + /// Album name String name; AssetOrder order; + /// Owner ID String ownerId; + /// Thumbnail asset ID String? thumbnailAssetId; + /// Updated at DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/sync_asset_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_delete_v1.dart index c1787caf04..1d5a947774 100644 --- a/mobile/openapi/lib/model/sync_asset_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetDeleteV1 { required this.assetId, }); + /// Asset ID String assetId; @override diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart new file mode 100644 index 0000000000..68af280290 --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_edit_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 SyncAssetEditDeleteV1 { + /// Returns a new [SyncAssetEditDeleteV1] instance. + SyncAssetEditDeleteV1({ + required this.editId, + }); + + String editId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditDeleteV1 && + other.editId == editId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (editId.hashCode); + + @override + String toString() => 'SyncAssetEditDeleteV1[editId=$editId]'; + + Map toJson() { + final json = {}; + json[r'editId'] = this.editId; + return json; + } + + /// Returns a new [SyncAssetEditDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetEditDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetEditDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetEditDeleteV1( + editId: mapValueOfType(json, r'editId')!, + ); + } + 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 = SyncAssetEditDeleteV1.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 = SyncAssetEditDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetEditDeleteV1-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] = SyncAssetEditDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'editId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart new file mode 100644 index 0000000000..3cc2673bfc --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_edit_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 SyncAssetEditV1 { + /// Returns a new [SyncAssetEditV1] instance. + SyncAssetEditV1({ + required this.action, + required this.assetId, + required this.id, + required this.parameters, + required this.sequence, + }); + + AssetEditAction action; + + String assetId; + + String id; + + Object parameters; + + int sequence; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetEditV1 && + other.action == action && + other.assetId == assetId && + other.id == id && + other.parameters == parameters && + other.sequence == sequence; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (assetId.hashCode) + + (id.hashCode) + + (parameters.hashCode) + + (sequence.hashCode); + + @override + String toString() => 'SyncAssetEditV1[action=$action, assetId=$assetId, id=$id, parameters=$parameters, sequence=$sequence]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'assetId'] = this.assetId; + json[r'id'] = this.id; + json[r'parameters'] = this.parameters; + json[r'sequence'] = this.sequence; + return json; + } + + /// Returns a new [SyncAssetEditV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetEditV1? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetEditV1"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetEditV1( + action: AssetEditAction.fromJson(json[r'action'])!, + assetId: mapValueOfType(json, r'assetId')!, + id: mapValueOfType(json, r'id')!, + parameters: mapValueOfType(json, r'parameters')!, + sequence: mapValueOfType(json, r'sequence')!, + ); + } + 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 = SyncAssetEditV1.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 = SyncAssetEditV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetEditV1-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] = SyncAssetEditV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'action', + 'assetId', + 'id', + 'parameters', + 'sequence', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index d4fdc9249d..ff9efdfea3 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -40,54 +40,79 @@ class SyncAssetExifV1 { required this.timeZone, }); + /// Asset ID String assetId; + /// City String? city; + /// Country String? country; + /// Date time original DateTime? dateTimeOriginal; + /// Description String? description; + /// Exif image height int? exifImageHeight; + /// Exif image width int? exifImageWidth; + /// Exposure time String? exposureTime; + /// F number double? fNumber; + /// File size in byte int? fileSizeInByte; + /// Focal length double? focalLength; + /// FPS double? fps; + /// ISO int? iso; + /// Latitude double? latitude; + /// Lens model String? lensModel; + /// Longitude double? longitude; + /// Make String? make; + /// Model String? model; + /// Modify date DateTime? modifyDate; + /// Orientation String? orientation; + /// Profile description String? profileDescription; + /// Projection type String? projectionType; + /// Rating int? rating; + /// State String? state; + /// Time zone String? timeZone; @override diff --git a/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart index 0992bfdcba..9cfb8814a7 100644 --- a/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetFaceDeleteV1 { required this.assetFaceId, }); + /// Asset face ID String assetFaceId; @override diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart index 60d1766e34..647a07d5eb 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -25,6 +25,7 @@ class SyncAssetFaceV1 { required this.sourceType, }); + /// Asset ID String assetId; int boundingBoxX1; @@ -35,14 +36,17 @@ class SyncAssetFaceV1 { int boundingBoxY2; + /// Asset face ID String id; int imageHeight; int imageWidth; + /// Person ID String? personId; + /// Source type String sourceType; @override diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart new file mode 100644 index 0000000000..688d71229f --- /dev/null +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -0,0 +1,201 @@ +// +// 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 SyncAssetFaceV2 { + /// Returns a new [SyncAssetFaceV2] instance. + SyncAssetFaceV2({ + required this.assetId, + required this.boundingBoxX1, + required this.boundingBoxX2, + required this.boundingBoxY1, + required this.boundingBoxY2, + required this.deletedAt, + required this.id, + required this.imageHeight, + required this.imageWidth, + required this.isVisible, + required this.personId, + required this.sourceType, + }); + + /// Asset ID + String assetId; + + int boundingBoxX1; + + int boundingBoxX2; + + int boundingBoxY1; + + int boundingBoxY2; + + /// Face deleted at + DateTime? deletedAt; + + /// Asset face ID + String id; + + int imageHeight; + + int imageWidth; + + /// Is the face visible in the asset + bool isVisible; + + /// Person ID + String? personId; + + /// Source type + String sourceType; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncAssetFaceV2 && + other.assetId == assetId && + other.boundingBoxX1 == boundingBoxX1 && + other.boundingBoxX2 == boundingBoxX2 && + other.boundingBoxY1 == boundingBoxY1 && + other.boundingBoxY2 == boundingBoxY2 && + other.deletedAt == deletedAt && + other.id == id && + other.imageHeight == imageHeight && + other.imageWidth == imageWidth && + other.isVisible == isVisible && + other.personId == personId && + other.sourceType == sourceType; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (boundingBoxX1.hashCode) + + (boundingBoxX2.hashCode) + + (boundingBoxY1.hashCode) + + (boundingBoxY2.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (id.hashCode) + + (imageHeight.hashCode) + + (imageWidth.hashCode) + + (isVisible.hashCode) + + (personId == null ? 0 : personId!.hashCode) + + (sourceType.hashCode); + + @override + String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, 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; + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'id'] = this.id; + json[r'imageHeight'] = this.imageHeight; + json[r'imageWidth'] = this.imageWidth; + json[r'isVisible'] = this.isVisible; + if (this.personId != null) { + json[r'personId'] = this.personId; + } else { + // json[r'personId'] = null; + } + json[r'sourceType'] = this.sourceType; + return json; + } + + /// Returns a new [SyncAssetFaceV2] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncAssetFaceV2? fromJson(dynamic value) { + upgradeDto(value, "SyncAssetFaceV2"); + if (value is Map) { + final json = value.cast(); + + return SyncAssetFaceV2( + 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')!, + deletedAt: mapDateTime(json, r'deletedAt', r''), + id: mapValueOfType(json, r'id')!, + imageHeight: mapValueOfType(json, r'imageHeight')!, + imageWidth: mapValueOfType(json, r'imageWidth')!, + isVisible: mapValueOfType(json, r'isVisible')!, + 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 = SyncAssetFaceV2.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 = SyncAssetFaceV2.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncAssetFaceV2-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] = SyncAssetFaceV2.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', + 'deletedAt', + 'id', + 'imageHeight', + 'imageWidth', + 'isVisible', + 'personId', + 'sourceType', + }; +} + diff --git a/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart index c9a7ef4670..326555ef13 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart @@ -17,9 +17,11 @@ class SyncAssetMetadataDeleteV1 { required this.key, }); + /// Asset ID String assetId; - AssetMetadataKey key; + /// Key + String key; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataDeleteV1 && @@ -52,7 +54,7 @@ class SyncAssetMetadataDeleteV1 { return SyncAssetMetadataDeleteV1( assetId: mapValueOfType(json, r'assetId')!, - key: AssetMetadataKey.fromJson(json[r'key'])!, + key: mapValueOfType(json, r'key')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 720fcef947..4a66623939 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart @@ -18,10 +18,13 @@ class SyncAssetMetadataV1 { required this.value, }); + /// Asset ID String assetId; - AssetMetadataKey key; + /// Key + String key; + /// Value Object value; @override @@ -58,7 +61,7 @@ class SyncAssetMetadataV1 { return SyncAssetMetadataV1( assetId: mapValueOfType(json, r'assetId')!, - key: AssetMetadataKey.fromJson(json[r'key'])!, + key: mapValueOfType(json, r'key')!, value: mapValueOfType(json, r'value')!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index f0d5097ea4..debde4488e 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -18,7 +18,9 @@ class SyncAssetV1 { required this.duration, required this.fileCreatedAt, required this.fileModifiedAt, + required this.height, required this.id, + required this.isEdited, required this.isFavorite, required this.libraryId, required this.livePhotoVideoId, @@ -29,40 +31,66 @@ class SyncAssetV1 { required this.thumbhash, required this.type, required this.visibility, + required this.width, }); + /// Checksum String checksum; + /// Deleted at DateTime? deletedAt; + /// Duration String? duration; + /// File created at DateTime? fileCreatedAt; + /// File modified at DateTime? fileModifiedAt; + /// Asset height + int? height; + + /// Asset ID String id; + /// Is edited + bool isEdited; + + /// Is favorite bool isFavorite; + /// Library ID String? libraryId; + /// Live photo video ID String? livePhotoVideoId; + /// Local date time DateTime? localDateTime; + /// Original file name String originalFileName; + /// Owner ID String ownerId; + /// Stack ID String? stackId; + /// Thumbhash String? thumbhash; + /// Asset type AssetTypeEnum type; + /// Asset visibility AssetVisibility visibility; + /// Asset width + int? width; + @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 && other.checksum == checksum && @@ -70,7 +98,9 @@ class SyncAssetV1 { other.duration == duration && other.fileCreatedAt == fileCreatedAt && other.fileModifiedAt == fileModifiedAt && + other.height == height && other.id == id && + other.isEdited == isEdited && other.isFavorite == isFavorite && other.libraryId == libraryId && other.livePhotoVideoId == livePhotoVideoId && @@ -80,7 +110,8 @@ class SyncAssetV1 { other.stackId == stackId && other.thumbhash == thumbhash && other.type == type && - other.visibility == visibility; + other.visibility == visibility && + other.width == width; @override int get hashCode => @@ -90,7 +121,9 @@ class SyncAssetV1 { (duration == null ? 0 : duration!.hashCode) + (fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) + (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + + (height == null ? 0 : height!.hashCode) + (id.hashCode) + + (isEdited.hashCode) + (isFavorite.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + @@ -100,10 +133,11 @@ class SyncAssetV1 { (stackId == null ? 0 : stackId!.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + - (visibility.hashCode); + (visibility.hashCode) + + (width == null ? 0 : width!.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]'; Map toJson() { final json = {}; @@ -127,8 +161,14 @@ class SyncAssetV1 { json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String(); } else { // json[r'fileModifiedAt'] = null; + } + if (this.height != null) { + json[r'height'] = this.height; + } else { + // json[r'height'] = null; } json[r'id'] = this.id; + json[r'isEdited'] = this.isEdited; json[r'isFavorite'] = this.isFavorite; if (this.libraryId != null) { json[r'libraryId'] = this.libraryId; @@ -159,6 +199,11 @@ class SyncAssetV1 { } json[r'type'] = this.type; json[r'visibility'] = this.visibility; + if (this.width != null) { + json[r'width'] = this.width; + } else { + // json[r'width'] = null; + } return json; } @@ -176,7 +221,9 @@ class SyncAssetV1 { duration: mapValueOfType(json, r'duration'), fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), + height: mapValueOfType(json, r'height'), id: mapValueOfType(json, r'id')!, + isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), @@ -187,6 +234,7 @@ class SyncAssetV1 { thumbhash: mapValueOfType(json, r'thumbhash'), type: AssetTypeEnum.fromJson(json[r'type'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!, + width: mapValueOfType(json, r'width'), ); } return null; @@ -239,7 +287,9 @@ class SyncAssetV1 { 'duration', 'fileCreatedAt', 'fileModifiedAt', + 'height', 'id', + 'isEdited', 'isFavorite', 'libraryId', 'livePhotoVideoId', @@ -250,6 +300,7 @@ class SyncAssetV1 { 'thumbhash', 'type', 'visibility', + 'width', }; } diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart index 1dab7f47e3..0edd804c6a 100644 --- a/mobile/openapi/lib/model/sync_auth_user_v1.dart +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -28,30 +28,41 @@ class SyncAuthUserV1 { required this.storageLabel, }); + /// User avatar color UserAvatarColor? avatarColor; + /// User deleted at DateTime? deletedAt; + /// User email String email; + /// User has profile image bool hasProfileImage; + /// User ID String id; + /// User is admin bool isAdmin; + /// User name String name; + /// User OAuth ID String oauthId; + /// User pin code String? pinCode; + /// User profile changed at DateTime profileChangedAt; int? quotaSizeInBytes; int quotaUsageInBytes; + /// User storage label String? storageLabel; @override diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index 1b4ca91f3b..e8db2dc4d3 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sync entity type class SyncEntityType { /// Instantiate a new enum with the provided [value]. const SyncEntityType._(this.value); @@ -29,6 +29,8 @@ class SyncEntityType { static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); + static const assetEditV1 = SyncEntityType._(r'AssetEditV1'); + static const assetEditDeleteV1 = SyncEntityType._(r'AssetEditDeleteV1'); static const assetMetadataV1 = SyncEntityType._(r'AssetMetadataV1'); static const assetMetadataDeleteV1 = SyncEntityType._(r'AssetMetadataDeleteV1'); static const partnerV1 = SyncEntityType._(r'PartnerV1'); @@ -64,6 +66,7 @@ class SyncEntityType { static const personV1 = SyncEntityType._(r'PersonV1'); static const personDeleteV1 = SyncEntityType._(r'PersonDeleteV1'); static const assetFaceV1 = SyncEntityType._(r'AssetFaceV1'); + static const assetFaceV2 = SyncEntityType._(r'AssetFaceV2'); static const assetFaceDeleteV1 = SyncEntityType._(r'AssetFaceDeleteV1'); static const userMetadataV1 = SyncEntityType._(r'UserMetadataV1'); static const userMetadataDeleteV1 = SyncEntityType._(r'UserMetadataDeleteV1'); @@ -79,6 +82,8 @@ class SyncEntityType { assetV1, assetDeleteV1, assetExifV1, + assetEditV1, + assetEditDeleteV1, assetMetadataV1, assetMetadataDeleteV1, partnerV1, @@ -114,6 +119,7 @@ class SyncEntityType { personV1, personDeleteV1, assetFaceV1, + assetFaceV2, assetFaceDeleteV1, userMetadataV1, userMetadataDeleteV1, @@ -164,6 +170,8 @@ class SyncEntityTypeTypeTransformer { case r'AssetV1': return SyncEntityType.assetV1; case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; case r'AssetExifV1': return SyncEntityType.assetExifV1; + case r'AssetEditV1': return SyncEntityType.assetEditV1; + case r'AssetEditDeleteV1': return SyncEntityType.assetEditDeleteV1; case r'AssetMetadataV1': return SyncEntityType.assetMetadataV1; case r'AssetMetadataDeleteV1': return SyncEntityType.assetMetadataDeleteV1; case r'PartnerV1': return SyncEntityType.partnerV1; @@ -199,6 +207,7 @@ class SyncEntityTypeTypeTransformer { case r'PersonV1': return SyncEntityType.personV1; case r'PersonDeleteV1': return SyncEntityType.personDeleteV1; case r'AssetFaceV1': return SyncEntityType.assetFaceV1; + case r'AssetFaceV2': return SyncEntityType.assetFaceV2; case r'AssetFaceDeleteV1': return SyncEntityType.assetFaceDeleteV1; case r'UserMetadataV1': return SyncEntityType.userMetadataV1; case r'UserMetadataDeleteV1': return SyncEntityType.userMetadataDeleteV1; diff --git a/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart b/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart index a9af77e929..c37682d02d 100644 --- a/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_asset_delete_v1.dart @@ -17,8 +17,10 @@ class SyncMemoryAssetDeleteV1 { required this.memoryId, }); + /// Asset ID String assetId; + /// Memory ID String memoryId; @override diff --git a/mobile/openapi/lib/model/sync_memory_asset_v1.dart b/mobile/openapi/lib/model/sync_memory_asset_v1.dart index d26e3c9a29..2cfab98afd 100644 --- a/mobile/openapi/lib/model/sync_memory_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_asset_v1.dart @@ -17,8 +17,10 @@ class SyncMemoryAssetV1 { required this.memoryId, }); + /// Asset ID String assetId; + /// Memory ID String memoryId; @override diff --git a/mobile/openapi/lib/model/sync_memory_delete_v1.dart b/mobile/openapi/lib/model/sync_memory_delete_v1.dart index 9702da5aaf..d5f63ec8fa 100644 --- a/mobile/openapi/lib/model/sync_memory_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_delete_v1.dart @@ -16,6 +16,7 @@ class SyncMemoryDeleteV1 { required this.memoryId, }); + /// Memory ID String memoryId; @override diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart index 2ae2b01fd7..c506738d97 100644 --- a/mobile/openapi/lib/model/sync_memory_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -27,28 +27,40 @@ class SyncMemoryV1 { required this.updatedAt, }); + /// Created at DateTime createdAt; + /// Data Object data; + /// Deleted at DateTime? deletedAt; + /// Hide at DateTime? hideAt; + /// Memory ID String id; + /// Is saved bool isSaved; + /// Memory at DateTime memoryAt; + /// Owner ID String ownerId; + /// Seen at DateTime? seenAt; + /// Show at DateTime? showAt; + /// Memory type MemoryType type; + /// Updated at DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/sync_partner_delete_v1.dart b/mobile/openapi/lib/model/sync_partner_delete_v1.dart index f5e10d6576..64dfb4eb98 100644 --- a/mobile/openapi/lib/model/sync_partner_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_partner_delete_v1.dart @@ -17,8 +17,10 @@ class SyncPartnerDeleteV1 { required this.sharedWithId, }); + /// Shared by ID String sharedById; + /// Shared with ID String sharedWithId; @override diff --git a/mobile/openapi/lib/model/sync_partner_v1.dart b/mobile/openapi/lib/model/sync_partner_v1.dart index e551c4c83d..9f9c3d14c1 100644 --- a/mobile/openapi/lib/model/sync_partner_v1.dart +++ b/mobile/openapi/lib/model/sync_partner_v1.dart @@ -18,10 +18,13 @@ class SyncPartnerV1 { required this.sharedWithId, }); + /// In timeline bool inTimeline; + /// Shared by ID String sharedById; + /// Shared with ID String sharedWithId; @override diff --git a/mobile/openapi/lib/model/sync_person_delete_v1.dart b/mobile/openapi/lib/model/sync_person_delete_v1.dart index 002f5c5b83..526bc26187 100644 --- a/mobile/openapi/lib/model/sync_person_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_person_delete_v1.dart @@ -16,6 +16,7 @@ class SyncPersonDeleteV1 { required this.personId, }); + /// Person ID String personId; @override diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index 6749beb3e1..fc2c36aa8c 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -25,24 +25,34 @@ class SyncPersonV1 { required this.updatedAt, }); + /// Birth date DateTime? birthDate; + /// Color String? color; + /// Created at DateTime createdAt; + /// Face asset ID String? faceAssetId; + /// Person ID String id; + /// Is favorite bool isFavorite; + /// Is hidden bool isHidden; + /// Person name String name; + /// Owner ID String ownerId; + /// Updated at DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index c3dc1c4d61..671081c0a5 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sync request types class SyncRequestType { /// Instantiate a new enum with the provided [value]. const SyncRequestType._(this.value); @@ -30,6 +30,7 @@ class SyncRequestType { static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); static const assetsV1 = SyncRequestType._(r'AssetsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); + static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1'); static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); static const authUsersV1 = SyncRequestType._(r'AuthUsersV1'); static const memoriesV1 = SyncRequestType._(r'MemoriesV1'); @@ -42,6 +43,7 @@ class SyncRequestType { static const usersV1 = SyncRequestType._(r'UsersV1'); static const peopleV1 = SyncRequestType._(r'PeopleV1'); static const assetFacesV1 = SyncRequestType._(r'AssetFacesV1'); + static const assetFacesV2 = SyncRequestType._(r'AssetFacesV2'); static const userMetadataV1 = SyncRequestType._(r'UserMetadataV1'); /// List of all possible values in this [enum][SyncRequestType]. @@ -53,6 +55,7 @@ class SyncRequestType { albumAssetExifsV1, assetsV1, assetExifsV1, + assetEditsV1, assetMetadataV1, authUsersV1, memoriesV1, @@ -65,6 +68,7 @@ class SyncRequestType { usersV1, peopleV1, assetFacesV1, + assetFacesV2, userMetadataV1, ]; @@ -111,6 +115,7 @@ class SyncRequestTypeTypeTransformer { case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; case r'AssetsV1': return SyncRequestType.assetsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1; + case r'AssetEditsV1': return SyncRequestType.assetEditsV1; case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; case r'AuthUsersV1': return SyncRequestType.authUsersV1; case r'MemoriesV1': return SyncRequestType.memoriesV1; @@ -123,6 +128,7 @@ class SyncRequestTypeTypeTransformer { case r'UsersV1': return SyncRequestType.usersV1; case r'PeopleV1': return SyncRequestType.peopleV1; case r'AssetFacesV1': return SyncRequestType.assetFacesV1; + case r'AssetFacesV2': return SyncRequestType.assetFacesV2; case r'UserMetadataV1': return SyncRequestType.userMetadataV1; default: if (!allowNull) { diff --git a/mobile/openapi/lib/model/sync_stack_delete_v1.dart b/mobile/openapi/lib/model/sync_stack_delete_v1.dart index 22c6d99a52..2a7398291a 100644 --- a/mobile/openapi/lib/model/sync_stack_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_delete_v1.dart @@ -16,6 +16,7 @@ class SyncStackDeleteV1 { required this.stackId, }); + /// Stack ID String stackId; @override diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart index c65affe8c0..e4487ccfaf 100644 --- a/mobile/openapi/lib/model/sync_stack_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -20,14 +20,19 @@ class SyncStackV1 { required this.updatedAt, }); + /// Created at DateTime createdAt; + /// Stack ID String id; + /// Owner ID String ownerId; + /// Primary asset ID String primaryAssetId; + /// Updated at DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/sync_stream_dto.dart b/mobile/openapi/lib/model/sync_stream_dto.dart index 9884eef342..932477cb15 100644 --- a/mobile/openapi/lib/model/sync_stream_dto.dart +++ b/mobile/openapi/lib/model/sync_stream_dto.dart @@ -17,6 +17,7 @@ class SyncStreamDto { this.types = const [], }); + /// Reset sync state /// /// 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 @@ -25,6 +26,7 @@ class SyncStreamDto { /// bool? reset; + /// Sync request types List types; @override diff --git a/mobile/openapi/lib/model/sync_user_delete_v1.dart b/mobile/openapi/lib/model/sync_user_delete_v1.dart index 09411cb79d..bbbdc147dd 100644 --- a/mobile/openapi/lib/model/sync_user_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_delete_v1.dart @@ -16,6 +16,7 @@ class SyncUserDeleteV1 { required this.userId, }); + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart index f39acc617b..61340a8f82 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -17,8 +17,10 @@ class SyncUserMetadataDeleteV1 { required this.userId, }); + /// User metadata key UserMetadataKey key; + /// User ID String userId; @override diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart index cf39b6d960..23803d0be4 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -18,10 +18,13 @@ class SyncUserMetadataV1 { required this.value, }); + /// User metadata key UserMetadataKey key; + /// User ID String userId; + /// User metadata value Object value; @override diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index b9fad5ae8c..6d425130a3 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -22,18 +22,25 @@ class SyncUserV1 { required this.profileChangedAt, }); + /// User avatar color UserAvatarColor? avatarColor; + /// User deleted at DateTime? deletedAt; + /// User email String email; + /// User has profile image bool hasProfileImage; + /// User ID String id; + /// User name String name; + /// User profile changed at DateTime profileChangedAt; @override diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 0acfc9e8fb..6c7acbd218 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -36,54 +36,80 @@ class SystemConfigFFmpegDto { required this.twoPass, }); + /// Transcode hardware acceleration TranscodeHWAccel accel; + /// Accelerated decode bool accelDecode; + /// Accepted audio codecs List acceptedAudioCodecs; + /// Accepted containers List acceptedContainers; + /// Accepted video codecs List acceptedVideoCodecs; + /// B-frames + /// /// Minimum value: -1 /// Maximum value: 16 int bframes; + /// CQ mode CQMode cqMode; + /// CRF + /// /// Minimum value: 0 /// Maximum value: 51 int crf; + /// GOP size + /// /// Minimum value: 0 int gopSize; + /// Max bitrate String maxBitrate; + /// Preferred hardware device String preferredHwDevice; + /// Preset String preset; + /// References + /// /// Minimum value: 0 /// Maximum value: 6 int refs; + /// Target audio codec AudioCodec targetAudioCodec; + /// Target resolution String targetResolution; + /// Target video codec VideoCodec targetVideoCodec; + /// Temporal AQ bool temporalAQ; + /// Threads + /// /// Minimum value: 0 int threads; + /// Tone mapping ToneMapping tonemap; + /// Transcode policy TranscodePolicy transcode; + /// Two pass bool twoPass; @override diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart index 4e18eb8de2..f57303c310 100644 --- a/mobile/openapi/lib/model/system_config_faces_dto.dart +++ b/mobile/openapi/lib/model/system_config_faces_dto.dart @@ -16,6 +16,7 @@ class SystemConfigFacesDto { required this.import_, }); + /// Import bool import_; @override diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart index fbeb704b27..b5640f82c8 100644 --- a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -15,13 +15,21 @@ class SystemConfigGeneratedFullsizeImageDto { SystemConfigGeneratedFullsizeImageDto({ required this.enabled, required this.format, + this.progressive = false, required this.quality, }); + /// Enabled bool enabled; + /// Image format ImageFormat format; + /// Progressive + bool progressive; + + /// Quality + /// /// Minimum value: 1 /// Maximum value: 100 int quality; @@ -30,6 +38,7 @@ class SystemConfigGeneratedFullsizeImageDto { bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedFullsizeImageDto && other.enabled == enabled && other.format == format && + other.progressive == progressive && other.quality == quality; @override @@ -37,15 +46,17 @@ class SystemConfigGeneratedFullsizeImageDto { // ignore: unnecessary_parenthesis (enabled.hashCode) + (format.hashCode) + + (progressive.hashCode) + (quality.hashCode); @override - String toString() => 'SystemConfigGeneratedFullsizeImageDto[enabled=$enabled, format=$format, quality=$quality]'; + String toString() => 'SystemConfigGeneratedFullsizeImageDto[enabled=$enabled, format=$format, progressive=$progressive, quality=$quality]'; Map toJson() { final json = {}; json[r'enabled'] = this.enabled; json[r'format'] = this.format; + json[r'progressive'] = this.progressive; json[r'quality'] = this.quality; return json; } @@ -61,6 +72,7 @@ class SystemConfigGeneratedFullsizeImageDto { return SystemConfigGeneratedFullsizeImageDto( enabled: mapValueOfType(json, r'enabled')!, format: ImageFormat.fromJson(json[r'format'])!, + progressive: mapValueOfType(json, r'progressive') ?? false, quality: mapValueOfType(json, r'quality')!, ); } diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart index 2192a7cb0c..3e8fed2c68 100644 --- a/mobile/openapi/lib/model/system_config_generated_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -14,22 +14,31 @@ class SystemConfigGeneratedImageDto { /// Returns a new [SystemConfigGeneratedImageDto] instance. SystemConfigGeneratedImageDto({ required this.format, + this.progressive = false, required this.quality, required this.size, }); + /// Image format ImageFormat format; + bool progressive; + + /// Quality + /// /// Minimum value: 1 /// Maximum value: 100 int quality; + /// Size + /// /// Minimum value: 1 int size; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto && other.format == format && + other.progressive == progressive && other.quality == quality && other.size == size; @@ -37,15 +46,17 @@ class SystemConfigGeneratedImageDto { int get hashCode => // ignore: unnecessary_parenthesis (format.hashCode) + + (progressive.hashCode) + (quality.hashCode) + (size.hashCode); @override - String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]'; + String toString() => 'SystemConfigGeneratedImageDto[format=$format, progressive=$progressive, quality=$quality, size=$size]'; Map toJson() { final json = {}; json[r'format'] = this.format; + json[r'progressive'] = this.progressive; json[r'quality'] = this.quality; json[r'size'] = this.size; return json; @@ -61,6 +72,7 @@ class SystemConfigGeneratedImageDto { return SystemConfigGeneratedImageDto( format: ImageFormat.fromJson(json[r'format'])!, + progressive: mapValueOfType(json, r'progressive') ?? false, quality: mapValueOfType(json, r'quality')!, size: mapValueOfType(json, r'size')!, ); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 783eaa7d46..217a666a67 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -20,8 +20,10 @@ class SystemConfigImageDto { required this.thumbnail, }); + /// Colorspace Colorspace colorspace; + /// Extract embedded bool extractEmbedded; SystemConfigGeneratedFullsizeImageDto fullsize; diff --git a/mobile/openapi/lib/model/system_config_job_dto.dart b/mobile/openapi/lib/model/system_config_job_dto.dart index 461420b3e3..d54db6809f 100644 --- a/mobile/openapi/lib/model/system_config_job_dto.dart +++ b/mobile/openapi/lib/model/system_config_job_dto.dart @@ -14,6 +14,7 @@ class SystemConfigJobDto { /// Returns a new [SystemConfigJobDto] instance. SystemConfigJobDto({ required this.backgroundTask, + required this.editor, required this.faceDetection, required this.library_, required this.metadataExtraction, @@ -30,6 +31,8 @@ class SystemConfigJobDto { JobSettingsDto backgroundTask; + JobSettingsDto editor; + JobSettingsDto faceDetection; JobSettingsDto library_; @@ -57,6 +60,7 @@ class SystemConfigJobDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigJobDto && other.backgroundTask == backgroundTask && + other.editor == editor && other.faceDetection == faceDetection && other.library_ == library_ && other.metadataExtraction == metadataExtraction && @@ -74,6 +78,7 @@ class SystemConfigJobDto { int get hashCode => // ignore: unnecessary_parenthesis (backgroundTask.hashCode) + + (editor.hashCode) + (faceDetection.hashCode) + (library_.hashCode) + (metadataExtraction.hashCode) + @@ -88,11 +93,12 @@ class SystemConfigJobDto { (workflow.hashCode); @override - String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; + String toString() => 'SystemConfigJobDto[backgroundTask=$backgroundTask, editor=$editor, faceDetection=$faceDetection, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; Map toJson() { final json = {}; json[r'backgroundTask'] = this.backgroundTask; + json[r'editor'] = this.editor; json[r'faceDetection'] = this.faceDetection; json[r'library'] = this.library_; json[r'metadataExtraction'] = this.metadataExtraction; @@ -118,6 +124,7 @@ class SystemConfigJobDto { return SystemConfigJobDto( backgroundTask: JobSettingsDto.fromJson(json[r'backgroundTask'])!, + editor: JobSettingsDto.fromJson(json[r'editor'])!, faceDetection: JobSettingsDto.fromJson(json[r'faceDetection'])!, library_: JobSettingsDto.fromJson(json[r'library'])!, metadataExtraction: JobSettingsDto.fromJson(json[r'metadataExtraction'])!, @@ -178,6 +185,7 @@ class SystemConfigJobDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'backgroundTask', + 'editor', 'faceDetection', 'library', 'metadataExtraction', diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 6a6558b4b3..28ea603c2a 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -19,6 +19,7 @@ class SystemConfigLibraryScanDto { String cronExpression; + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_library_watch_dto.dart b/mobile/openapi/lib/model/system_config_library_watch_dto.dart index 1a1f5d7126..b4f171bd25 100644 --- a/mobile/openapi/lib/model/system_config_library_watch_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_watch_dto.dart @@ -16,6 +16,7 @@ class SystemConfigLibraryWatchDto { required this.enabled, }); + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_logging_dto.dart b/mobile/openapi/lib/model/system_config_logging_dto.dart index f025221eff..54278893db 100644 --- a/mobile/openapi/lib/model/system_config_logging_dto.dart +++ b/mobile/openapi/lib/model/system_config_logging_dto.dart @@ -17,6 +17,7 @@ class SystemConfigLoggingDto { required this.level, }); + /// Enabled bool enabled; LogLevel level; diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index da689936f8..2a0f1ffbc6 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -28,6 +28,7 @@ class SystemConfigMachineLearningDto { DuplicateDetectionConfig duplicateDetection; + /// Enabled bool enabled; FacialRecognitionConfig facialRecognition; diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index d53d5711db..109babd374 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -20,6 +20,7 @@ class SystemConfigMapDto { String darkStyle; + /// Enabled bool enabled; String lightStyle; diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart index c63d2abc1b..ec2b400dfd 100644 --- a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart +++ b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart @@ -16,6 +16,7 @@ class SystemConfigNewVersionCheckDto { required this.enabled, }); + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart index ab7b4b37c2..cfb18b181e 100644 --- a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -21,16 +21,21 @@ class SystemConfigNightlyTasksDto { required this.syncQuotaUsage, }); + /// Cluster new faces bool clusterNewFaces; + /// Database cleanup bool databaseCleanup; + /// Generate memories bool generateMemories; + /// Missing thumbnails bool missingThumbnails; String startTime; + /// Sync quota usage bool syncQuotaUsage; @override 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 c8f91be1f1..82195e498b 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -33,42 +33,61 @@ class SystemConfigOAuthDto { required this.tokenEndpointAuthMethod, }); + /// Auto launch bool autoLaunch; + /// Auto register bool autoRegister; + /// Button text String buttonText; + /// Client ID String clientId; + /// Client secret String clientSecret; + /// Default storage quota + /// /// Minimum value: 0 int? defaultStorageQuota; + /// Enabled bool enabled; + /// Issuer URL String issuerUrl; + /// Mobile override enabled bool mobileOverrideEnabled; + /// Mobile redirect URI String mobileRedirectUri; + /// Profile signing algorithm String profileSigningAlgorithm; + /// Role claim String roleClaim; + /// Scope String scope; String signingAlgorithm; + /// Storage label claim String storageLabelClaim; + /// Storage quota claim String storageQuotaClaim; + /// Timeout + /// /// Minimum value: 1 int timeout; + /// Token endpoint auth method OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; @override diff --git a/mobile/openapi/lib/model/system_config_password_login_dto.dart b/mobile/openapi/lib/model/system_config_password_login_dto.dart index 69c8942bb6..1328a6acaa 100644 --- a/mobile/openapi/lib/model/system_config_password_login_dto.dart +++ b/mobile/openapi/lib/model/system_config_password_login_dto.dart @@ -16,6 +16,7 @@ class SystemConfigPasswordLoginDto { required this.enabled, }); + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart index 6c1673d46c..0374e19be1 100644 --- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart +++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart @@ -16,6 +16,7 @@ class SystemConfigReverseGeocodingDto { required this.enabled, }); + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_server_dto.dart b/mobile/openapi/lib/model/system_config_server_dto.dart index 8099292dd0..200f75f7c6 100644 --- a/mobile/openapi/lib/model/system_config_server_dto.dart +++ b/mobile/openapi/lib/model/system_config_server_dto.dart @@ -18,10 +18,13 @@ class SystemConfigServerDto { required this.publicUsers, }); + /// External domain String externalDomain; + /// Login page message String loginPageMessage; + /// Public users bool publicUsers; @override diff --git a/mobile/openapi/lib/model/system_config_smtp_dto.dart b/mobile/openapi/lib/model/system_config_smtp_dto.dart index fcde49cf35..a3d14cda63 100644 --- a/mobile/openapi/lib/model/system_config_smtp_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_dto.dart @@ -19,10 +19,13 @@ class SystemConfigSmtpDto { required this.transport, }); + /// Whether SMTP email notifications are enabled bool enabled; + /// Email address to send from String from; + /// Email address for replies String replyTo; SystemConfigSmtpTransportDto transport; diff --git a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart index 46307046b4..9e16e5badf 100644 --- a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart @@ -21,18 +21,25 @@ class SystemConfigSmtpTransportDto { required this.username, }); + /// SMTP server hostname String host; + /// Whether to ignore SSL certificate errors bool ignoreCert; + /// SMTP password String password; + /// SMTP server port + /// /// Minimum value: 0 /// Maximum value: 65535 num port; + /// Whether to use secure connection (TLS/SSL) bool secure; + /// SMTP username String username; @override diff --git a/mobile/openapi/lib/model/system_config_storage_template_dto.dart b/mobile/openapi/lib/model/system_config_storage_template_dto.dart index 596aafc195..f9f37e48ad 100644 --- a/mobile/openapi/lib/model/system_config_storage_template_dto.dart +++ b/mobile/openapi/lib/model/system_config_storage_template_dto.dart @@ -18,10 +18,13 @@ class SystemConfigStorageTemplateDto { required this.template, }); + /// Enabled bool enabled; + /// Hash verification enabled bool hashVerificationEnabled; + /// Template String template; @override diff --git a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart index f8586d344c..6f81513039 100644 --- a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart @@ -23,20 +23,28 @@ class SystemConfigTemplateStorageOptionDto { this.yearOptions = const [], }); + /// Available day format options for storage template List dayOptions; + /// Available hour format options for storage template List hourOptions; + /// Available minute format options for storage template List minuteOptions; + /// Available month format options for storage template List monthOptions; + /// Available preset template options List presetOptions; + /// Available second format options for storage template List secondOptions; + /// Available week format options for storage template List weekOptions; + /// Available year format options for storage template List yearOptions; @override diff --git a/mobile/openapi/lib/model/system_config_theme_dto.dart b/mobile/openapi/lib/model/system_config_theme_dto.dart index a97c2cf84c..fca38f71fb 100644 --- a/mobile/openapi/lib/model/system_config_theme_dto.dart +++ b/mobile/openapi/lib/model/system_config_theme_dto.dart @@ -16,6 +16,7 @@ class SystemConfigThemeDto { required this.customCss, }); + /// Custom CSS for theming String customCss; @override diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 51b39e9a55..9bdaef92d3 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -17,9 +17,12 @@ class SystemConfigTrashDto { required this.enabled, }); + /// Days + /// /// Minimum value: 0 int days; + /// Enabled bool enabled; @override diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index 8e6bd3c9c3..a7313560e6 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -16,6 +16,8 @@ class SystemConfigUserDto { required this.deleteDelay, }); + /// Delete delay + /// /// Minimum value: 1 int deleteDelay; diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart index 26a575e193..16abc3bcdc 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart @@ -17,8 +17,10 @@ class TagBulkAssetsDto { this.tagIds = const [], }); + /// Asset IDs List assetIds; + /// Tag IDs List tagIds; @override diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index 009f26bfe4..5566846e3c 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -16,6 +16,7 @@ class TagBulkAssetsResponseDto { required this.count, }); + /// Number of assets tagged int count; @override diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index 9a5171074d..fd6a10163c 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -18,6 +18,7 @@ class TagCreateDto { this.parentId, }); + /// Tag color (hex) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -26,8 +27,10 @@ class TagCreateDto { /// String? color; + /// Tag name String name; + /// Parent tag ID String? parentId; @override diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index cd684b163a..9a71912153 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -22,6 +22,7 @@ class TagResponseDto { required this.value, }); + /// Tag color (hex) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -30,12 +31,16 @@ class TagResponseDto { /// String? color; + /// Creation date DateTime createdAt; + /// Tag ID String id; + /// Tag name String name; + /// Parent tag ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -44,8 +49,10 @@ class TagResponseDto { /// String? parentId; + /// Last update date DateTime updatedAt; + /// Tag value (full path) String value; @override diff --git a/mobile/openapi/lib/model/tag_update_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart index ab1adb127b..98cb6af523 100644 --- a/mobile/openapi/lib/model/tag_update_dto.dart +++ b/mobile/openapi/lib/model/tag_update_dto.dart @@ -16,6 +16,7 @@ class TagUpdateDto { this.color, }); + /// Tag color (hex) String? color; @override diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart index d60a00f466..3581ef1e8f 100644 --- a/mobile/openapi/lib/model/tag_upsert_dto.dart +++ b/mobile/openapi/lib/model/tag_upsert_dto.dart @@ -16,6 +16,7 @@ class TagUpsertDto { this.tags = const [], }); + /// Tag names to upsert List tags; @override diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 2470edf979..1e4a4bd109 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -17,8 +17,10 @@ class TagsResponse { this.sidebarWeb = true, }); + /// Whether tags are enabled bool enabled; + /// Whether tags appear in web sidebar bool sidebarWeb; @override diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart index d992369140..e42357e3d4 100644 --- a/mobile/openapi/lib/model/tags_update.dart +++ b/mobile/openapi/lib/model/tags_update.dart @@ -17,6 +17,7 @@ class TagsUpdate { this.sidebarWeb, }); + /// Whether tags are enabled /// /// 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 @@ -25,6 +26,7 @@ class TagsUpdate { /// bool? enabled; + /// Whether tags appear in web sidebar /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/template_dto.dart b/mobile/openapi/lib/model/template_dto.dart index f818e0508a..b1eab848ed 100644 --- a/mobile/openapi/lib/model/template_dto.dart +++ b/mobile/openapi/lib/model/template_dto.dart @@ -16,6 +16,7 @@ class TemplateDto { required this.template, }); + /// Template name String template; @override diff --git a/mobile/openapi/lib/model/template_response_dto.dart b/mobile/openapi/lib/model/template_response_dto.dart index 3c3224a54b..f19c1eae7d 100644 --- a/mobile/openapi/lib/model/template_response_dto.dart +++ b/mobile/openapi/lib/model/template_response_dto.dart @@ -17,8 +17,10 @@ class TemplateResponseDto { required this.name, }); + /// Template HTML content String html; + /// Template name String name; @override diff --git a/mobile/openapi/lib/model/test_email_response_dto.dart b/mobile/openapi/lib/model/test_email_response_dto.dart index 33e6c042d8..e14783f3c4 100644 --- a/mobile/openapi/lib/model/test_email_response_dto.dart +++ b/mobile/openapi/lib/model/test_email_response_dto.dart @@ -16,6 +16,7 @@ class TestEmailResponseDto { required this.messageId, }); + /// Email message ID String messageId; @override diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 58032b7c51..720323cd14 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -42,7 +42,7 @@ class TimeBucketAssetResponseDto { /// Array of video durations in HH:MM:SS format (null for images) List duration; - /// Array of file creation timestamps in UTC (ISO 8601 format, without timezone) + /// Array of file creation timestamps in UTC List fileCreatedAt; /// Array of asset IDs in the time bucket diff --git a/mobile/openapi/lib/model/tone_mapping.dart b/mobile/openapi/lib/model/tone_mapping.dart index e05aea2b77..a1db2f5c9c 100644 --- a/mobile/openapi/lib/model/tone_mapping.dart +++ b/mobile/openapi/lib/model/tone_mapping.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Tone mapping class ToneMapping { /// Instantiate a new enum with the provided [value]. const ToneMapping._(this.value); diff --git a/mobile/openapi/lib/model/transcode_hw_accel.dart b/mobile/openapi/lib/model/transcode_hw_accel.dart index de5006341e..22d20de320 100644 --- a/mobile/openapi/lib/model/transcode_hw_accel.dart +++ b/mobile/openapi/lib/model/transcode_hw_accel.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Transcode hardware acceleration class TranscodeHWAccel { /// Instantiate a new enum with the provided [value]. const TranscodeHWAccel._(this.value); diff --git a/mobile/openapi/lib/model/transcode_policy.dart b/mobile/openapi/lib/model/transcode_policy.dart index 6e9617428a..ab3a876a93 100644 --- a/mobile/openapi/lib/model/transcode_policy.dart +++ b/mobile/openapi/lib/model/transcode_policy.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Transcode policy class TranscodePolicy { /// Instantiate a new enum with the provided [value]. const TranscodePolicy._(this.value); diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 2df154d06c..7edd5d032a 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -16,6 +16,7 @@ class TrashResponseDto { required this.count, }); + /// Number of items in trash int count; @override diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 8353dba14e..46ce8b0ecc 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -20,6 +20,7 @@ class UpdateAlbumDto { this.order, }); + /// Album name /// /// 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 @@ -28,6 +29,7 @@ class UpdateAlbumDto { /// String? albumName; + /// Album thumbnail asset ID /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -36,6 +38,7 @@ class UpdateAlbumDto { /// String? albumThumbnailAssetId; + /// Album description /// /// 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 @@ -44,6 +47,7 @@ class UpdateAlbumDto { /// String? description; + /// Enable activity feed /// /// 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 @@ -52,6 +56,7 @@ class UpdateAlbumDto { /// bool? isActivityEnabled; + /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 43218cae6e..9d934eb465 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -16,6 +16,7 @@ class UpdateAlbumUserDto { required this.role, }); + /// Album user role AlbumUserRole role; @override diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 7b364f1387..8526995934 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -23,6 +23,7 @@ class UpdateAssetDto { this.visibility, }); + /// Original date and time /// /// 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 @@ -31,6 +32,7 @@ class UpdateAssetDto { /// String? dateTimeOriginal; + /// Asset description /// /// 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 @@ -39,6 +41,7 @@ class UpdateAssetDto { /// String? description; + /// Mark as favorite /// /// 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 @@ -47,6 +50,7 @@ class UpdateAssetDto { /// bool? isFavorite; + /// Latitude coordinate /// /// 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 @@ -55,8 +59,10 @@ class UpdateAssetDto { /// num? latitude; + /// Live photo video ID String? livePhotoVideoId; + /// Longitude coordinate /// /// 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 @@ -65,16 +71,13 @@ class UpdateAssetDto { /// num? longitude; + /// Rating in range [1-5], or null for unrated + /// /// Minimum value: -1 /// Maximum value: 5 - /// - /// 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. - /// num? rating; + /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -169,7 +172,9 @@ class UpdateAssetDto { latitude: num.parse('${json[r'latitude']}'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), longitude: num.parse('${json[r'longitude']}'), - rating: num.parse('${json[r'rating']}'), + rating: json[r'rating'] == null + ? null + : num.parse('${json[r'rating']}'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 6a4f36906f..628bdc0055 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -18,10 +18,13 @@ class UpdateLibraryDto { this.name, }); + /// Exclusion patterns (max 128) Set exclusionPatterns; + /// Import paths (max 128) Set importPaths; + /// Library name /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 80235915fe..da1fe600a5 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -23,20 +23,28 @@ class UsageByUserDto { required this.videos, }); + /// Number of photos int photos; + /// User quota size in bytes (null if unlimited) int? quotaSizeInBytes; + /// Total storage usage in bytes int usage; + /// Storage usage for photos in bytes int usagePhotos; + /// Storage usage for videos in bytes int usageVideos; + /// User ID String userId; + /// User name String userName; + /// Number of videos int videos; @override diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 8c8b70fbce..485b2e00e5 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -19,15 +19,19 @@ class UserAdminCreateDto { required this.name, this.notify, required this.password, + this.pinCode, this.quotaSizeInBytes, this.shouldChangePassword, this.storageLabel, }); + /// Avatar color UserAvatarColor? avatarColor; + /// User email String email; + /// Grant admin privileges /// /// 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 @@ -36,8 +40,10 @@ class UserAdminCreateDto { /// bool? isAdmin; + /// User name String name; + /// Send notification email /// /// 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 @@ -46,11 +52,18 @@ class UserAdminCreateDto { /// bool? notify; + /// User password String password; + /// PIN code + String? pinCode; + + /// Storage quota in bytes + /// /// Minimum value: 0 int? quotaSizeInBytes; + /// Require password change on next login /// /// 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 @@ -59,6 +72,7 @@ class UserAdminCreateDto { /// bool? shouldChangePassword; + /// Storage label String? storageLabel; @override @@ -69,6 +83,7 @@ class UserAdminCreateDto { other.name == name && other.notify == notify && other.password == password && + other.pinCode == pinCode && other.quotaSizeInBytes == quotaSizeInBytes && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel; @@ -82,12 +97,13 @@ class UserAdminCreateDto { (name.hashCode) + (notify == null ? 0 : notify!.hashCode) + (password.hashCode) + + (pinCode == null ? 0 : pinCode!.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, isAdmin=$isAdmin, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, isAdmin=$isAdmin, name=$name, notify=$notify, password=$password, pinCode=$pinCode, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -109,6 +125,11 @@ class UserAdminCreateDto { // json[r'notify'] = null; } json[r'password'] = this.password; + if (this.pinCode != null) { + json[r'pinCode'] = this.pinCode; + } else { + // json[r'pinCode'] = null; + } if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; } else { @@ -142,6 +163,7 @@ class UserAdminCreateDto { name: mapValueOfType(json, r'name')!, notify: mapValueOfType(json, r'notify'), password: mapValueOfType(json, r'password')!, + pinCode: mapValueOfType(json, r'pinCode'), quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), storageLabel: mapValueOfType(json, r'storageLabel'), diff --git a/mobile/openapi/lib/model/user_admin_delete_dto.dart b/mobile/openapi/lib/model/user_admin_delete_dto.dart index 2cf68ad7b2..6be70f37b7 100644 --- a/mobile/openapi/lib/model/user_admin_delete_dto.dart +++ b/mobile/openapi/lib/model/user_admin_delete_dto.dart @@ -16,6 +16,7 @@ class UserAdminDeleteDto { this.force, }); + /// Force delete even if user has assets /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index e5ae8e1d4e..706f65cf35 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,38 +32,55 @@ class UserAdminResponseDto { required this.updatedAt, }); + /// Avatar color UserAvatarColor avatarColor; + /// Creation date DateTime createdAt; + /// Deletion date DateTime? deletedAt; + /// User email String email; + /// User ID String id; + /// Is admin user bool isAdmin; + /// User license UserLicense? license; + /// User name String name; + /// OAuth ID String oauthId; + /// Profile change date DateTime profileChangedAt; + /// Profile image path String profileImagePath; + /// Storage quota in bytes int? quotaSizeInBytes; + /// Storage usage in bytes int? quotaUsageInBytes; + /// Require password change on next login bool shouldChangePassword; + /// User status UserStatus status; + /// Storage label String? storageLabel; + /// Last update date DateTime updatedAt; @override diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 9605552d20..3cce65745f 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,8 +24,10 @@ class UserAdminUpdateDto { this.storageLabel, }); + /// Avatar color UserAvatarColor? avatarColor; + /// User email /// /// 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 @@ -34,6 +36,7 @@ class UserAdminUpdateDto { /// String? email; + /// Grant admin privileges /// /// 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 @@ -42,6 +45,7 @@ class UserAdminUpdateDto { /// bool? isAdmin; + /// User name /// /// 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 @@ -50,6 +54,7 @@ class UserAdminUpdateDto { /// String? name; + /// User password /// /// 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 @@ -58,11 +63,15 @@ class UserAdminUpdateDto { /// String? password; + /// PIN code String? pinCode; + /// Storage quota in bytes + /// /// Minimum value: 0 int? quotaSizeInBytes; + /// Require password change on next login /// /// 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 @@ -71,6 +80,7 @@ class UserAdminUpdateDto { /// bool? shouldChangePassword; + /// Storage label String? storageLabel; @override diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart index 4cd7dd3204..4fcf518550 100644 --- a/mobile/openapi/lib/model/user_avatar_color.dart +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Avatar color class UserAvatarColor { /// Instantiate a new enum with the provided [value]. const UserAvatarColor._(this.value); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index 9bed8d5c43..f02dc73bef 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -18,10 +18,13 @@ class UserLicense { required this.licenseKey, }); + /// Activation date DateTime activatedAt; + /// Activation key String activationKey; + /// License key String licenseKey; @override diff --git a/mobile/openapi/lib/model/user_metadata_key.dart b/mobile/openapi/lib/model/user_metadata_key.dart index 845b5ae9bb..2b4c11a73d 100644 --- a/mobile/openapi/lib/model/user_metadata_key.dart +++ b/mobile/openapi/lib/model/user_metadata_key.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// User metadata key class UserMetadataKey { /// Instantiate a new enum with the provided [value]. const UserMetadataKey._(this.value); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index a02da29948..bf0e2cbf09 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,16 +21,22 @@ class UserResponseDto { required this.profileImagePath, }); + /// Avatar color UserAvatarColor avatarColor; + /// User email String email; + /// User ID String id; + /// User name String name; + /// Profile change date DateTime profileChangedAt; + /// Profile image path String profileImagePath; @override diff --git a/mobile/openapi/lib/model/user_status.dart b/mobile/openapi/lib/model/user_status.dart index 596abf324e..130bd650f2 100644 --- a/mobile/openapi/lib/model/user_status.dart +++ b/mobile/openapi/lib/model/user_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// User status class UserStatus { /// Instantiate a new enum with the provided [value]. const UserStatus._(this.value); diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 779e07ffa6..066c435eb3 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,8 +19,10 @@ class UserUpdateMeDto { this.password, }); + /// Avatar color UserAvatarColor? avatarColor; + /// User email /// /// 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 @@ -29,6 +31,7 @@ class UserUpdateMeDto { /// String? email; + /// User name /// /// 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 @@ -37,6 +40,7 @@ class UserUpdateMeDto { /// String? name; + /// User password (deprecated, use change password endpoint) /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/validate_access_token_response_dto.dart b/mobile/openapi/lib/model/validate_access_token_response_dto.dart index 5e36efcfed..16b9d0f925 100644 --- a/mobile/openapi/lib/model/validate_access_token_response_dto.dart +++ b/mobile/openapi/lib/model/validate_access_token_response_dto.dart @@ -16,6 +16,7 @@ class ValidateAccessTokenResponseDto { required this.authStatus, }); + /// Authentication status bool authStatus; @override diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 79ddb9a540..59c3680782 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -17,8 +17,10 @@ class ValidateLibraryDto { this.importPaths = const {}, }); + /// Exclusion patterns (max 128) Set exclusionPatterns; + /// Import paths to validate (max 128) Set importPaths; @override diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 11fbbd74c2..78cc03dc94 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -18,10 +18,13 @@ class ValidateLibraryImportPathResponseDto { this.message, }); + /// Import path String importPath; + /// Is valid bool isValid; + /// Validation message /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/validate_library_response_dto.dart b/mobile/openapi/lib/model/validate_library_response_dto.dart index e0dc2a2d14..37f6ad07d1 100644 --- a/mobile/openapi/lib/model/validate_library_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_response_dto.dart @@ -16,6 +16,7 @@ class ValidateLibraryResponseDto { this.importPaths = const [], }); + /// Validation results for import paths List importPaths; @override diff --git a/mobile/openapi/lib/model/version_check_state_response_dto.dart b/mobile/openapi/lib/model/version_check_state_response_dto.dart index d3f9a6cd95..71075a681c 100644 --- a/mobile/openapi/lib/model/version_check_state_response_dto.dart +++ b/mobile/openapi/lib/model/version_check_state_response_dto.dart @@ -17,8 +17,10 @@ class VersionCheckStateResponseDto { required this.releaseVersion, }); + /// Last check timestamp String? checkedAt; + /// Release version String? releaseVersion; @override diff --git a/mobile/openapi/lib/model/video_codec.dart b/mobile/openapi/lib/model/video_codec.dart index 307b208757..ba6441c8f7 100644 --- a/mobile/openapi/lib/model/video_codec.dart +++ b/mobile/openapi/lib/model/video_codec.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Target video codec class VideoCodec { /// Instantiate a new enum with the provided [value]. const VideoCodec._(this.value); diff --git a/mobile/openapi/lib/model/video_container.dart b/mobile/openapi/lib/model/video_container.dart index b8efc94adc..b1a47c8721 100644 --- a/mobile/openapi/lib/model/video_container.dart +++ b/mobile/openapi/lib/model/video_container.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Accepted containers class VideoContainer { /// Instantiate a new enum with the provided [value]. const VideoContainer._(this.value); diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index cb0c39eae9..9222dd6ba7 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -17,6 +17,7 @@ class WorkflowActionItemDto { required this.pluginActionId, }); + /// Action configuration /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -25,6 +26,7 @@ class WorkflowActionItemDto { /// Object? actionConfig; + /// Plugin action ID String pluginActionId; @override diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 5132623e89..8f77e9cf2b 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -20,14 +20,19 @@ class WorkflowActionResponseDto { required this.workflowId, }); + /// Action configuration Object? actionConfig; + /// Action ID String id; + /// Action order num order; + /// Plugin action ID String pluginActionId; + /// Workflow ID String workflowId; @override diff --git a/mobile/openapi/lib/model/workflow_create_dto.dart b/mobile/openapi/lib/model/workflow_create_dto.dart index c6e44743ac..38665a1912 100644 --- a/mobile/openapi/lib/model/workflow_create_dto.dart +++ b/mobile/openapi/lib/model/workflow_create_dto.dart @@ -21,8 +21,10 @@ class WorkflowCreateDto { required this.triggerType, }); + /// Workflow actions List actions; + /// Workflow description /// /// 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 @@ -31,6 +33,7 @@ class WorkflowCreateDto { /// String? description; + /// Workflow enabled /// /// 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 @@ -39,10 +42,13 @@ class WorkflowCreateDto { /// bool? enabled; + /// Workflow filters List filters; + /// Workflow name String name; + /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index bd8090b05e..52e29c3e93 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -17,6 +17,7 @@ class WorkflowFilterItemDto { required this.pluginFilterId, }); + /// Filter configuration /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -25,6 +26,7 @@ class WorkflowFilterItemDto { /// Object? filterConfig; + /// Plugin filter ID String pluginFilterId; @override diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 94dce27a3f..355378adac 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -20,14 +20,19 @@ class WorkflowFilterResponseDto { required this.workflowId, }); + /// Filter configuration Object? filterConfig; + /// Filter ID String id; + /// Filter order num order; + /// Plugin filter ID String pluginFilterId; + /// Workflow ID String workflowId; @override diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index 5132e7cb73..ae3e6510aa 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -24,23 +24,32 @@ class WorkflowResponseDto { required this.triggerType, }); + /// Workflow actions List actions; + /// Creation date String createdAt; + /// Workflow description String description; + /// Workflow enabled bool enabled; + /// Workflow filters List filters; + /// Workflow ID String id; + /// Workflow name String? name; + /// Owner user ID String ownerId; - WorkflowResponseDtoTriggerTypeEnum triggerType; + /// Workflow trigger type + PluginTriggerType triggerType; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowResponseDto && @@ -105,7 +114,7 @@ class WorkflowResponseDto { id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, - triggerType: WorkflowResponseDtoTriggerTypeEnum.fromJson(json[r'triggerType'])!, + triggerType: PluginTriggerType.fromJson(json[r'triggerType'])!, ); } return null; @@ -165,77 +174,3 @@ class WorkflowResponseDto { }; } - -class WorkflowResponseDtoTriggerTypeEnum { - /// Instantiate a new enum with the provided [value]. - const WorkflowResponseDtoTriggerTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const assetCreate = WorkflowResponseDtoTriggerTypeEnum._(r'AssetCreate'); - static const personRecognized = WorkflowResponseDtoTriggerTypeEnum._(r'PersonRecognized'); - - /// List of all possible values in this [enum][WorkflowResponseDtoTriggerTypeEnum]. - static const values = [ - assetCreate, - personRecognized, - ]; - - static WorkflowResponseDtoTriggerTypeEnum? fromJson(dynamic value) => WorkflowResponseDtoTriggerTypeEnumTypeTransformer().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 = WorkflowResponseDtoTriggerTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [WorkflowResponseDtoTriggerTypeEnum] to String, -/// and [decode] dynamic data back to [WorkflowResponseDtoTriggerTypeEnum]. -class WorkflowResponseDtoTriggerTypeEnumTypeTransformer { - factory WorkflowResponseDtoTriggerTypeEnumTypeTransformer() => _instance ??= const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._(); - - const WorkflowResponseDtoTriggerTypeEnumTypeTransformer._(); - - String encode(WorkflowResponseDtoTriggerTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a WorkflowResponseDtoTriggerTypeEnum. - /// - /// 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. - WorkflowResponseDtoTriggerTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'AssetCreate': return WorkflowResponseDtoTriggerTypeEnum.assetCreate; - case r'PersonRecognized': return WorkflowResponseDtoTriggerTypeEnum.personRecognized; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [WorkflowResponseDtoTriggerTypeEnumTypeTransformer] instance. - static WorkflowResponseDtoTriggerTypeEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index b36a396dc6..9891fff079 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -18,10 +18,13 @@ class WorkflowUpdateDto { this.enabled, this.filters = const [], this.name, + this.triggerType, }); + /// Workflow actions List actions; + /// Workflow description /// /// 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 @@ -30,6 +33,7 @@ class WorkflowUpdateDto { /// String? description; + /// Workflow enabled /// /// 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 @@ -38,8 +42,10 @@ class WorkflowUpdateDto { /// bool? enabled; + /// Workflow filters List filters; + /// Workflow name /// /// 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 @@ -48,13 +54,23 @@ class WorkflowUpdateDto { /// String? name; + /// Workflow trigger type + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginTriggerType? triggerType; + @override bool operator ==(Object other) => identical(this, other) || other is WorkflowUpdateDto && _deepEquality.equals(other.actions, actions) && other.description == description && other.enabled == enabled && _deepEquality.equals(other.filters, filters) && - other.name == name; + other.name == name && + other.triggerType == triggerType; @override int get hashCode => @@ -63,10 +79,11 @@ class WorkflowUpdateDto { (description == null ? 0 : description!.hashCode) + (enabled == null ? 0 : enabled!.hashCode) + (filters.hashCode) + - (name == null ? 0 : name!.hashCode); + (name == null ? 0 : name!.hashCode) + + (triggerType == null ? 0 : triggerType!.hashCode); @override - String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name]'; + String toString() => 'WorkflowUpdateDto[actions=$actions, description=$description, enabled=$enabled, filters=$filters, name=$name, triggerType=$triggerType]'; Map toJson() { final json = {}; @@ -87,6 +104,11 @@ class WorkflowUpdateDto { } else { // json[r'name'] = null; } + if (this.triggerType != null) { + json[r'triggerType'] = this.triggerType; + } else { + // json[r'triggerType'] = null; + } return json; } @@ -104,6 +126,7 @@ class WorkflowUpdateDto { enabled: mapValueOfType(json, r'enabled'), filters: WorkflowFilterItemDto.listFromJson(json[r'filters']), name: mapValueOfType(json, r'name'), + triggerType: PluginTriggerType.fromJson(json[r'triggerType']), ); } return null; diff --git a/mobile/packages/ui/.gitignore b/mobile/packages/ui/.gitignore new file mode 100644 index 0000000000..b84f47ac2c --- /dev/null +++ b/mobile/packages/ui/.gitignore @@ -0,0 +1,15 @@ +# Build artifacts +build/ + +# Platform-specific files are not needed as this is a Flutter UI package +android/ +ios/ + +# Test cache and generated files +.dart_tool/ +.packages +.flutter-plugins +.flutter-plugins-dependencies + +# Fonts copied by build process +fonts/ \ No newline at end of file diff --git a/mobile/packages/ui/lib/immich_ui.dart b/mobile/packages/ui/lib/immich_ui.dart index 2417149f76..c9e510a162 100644 --- a/mobile/packages/ui/lib/immich_ui.dart +++ b/mobile/packages/ui/lib/immich_ui.dart @@ -1,3 +1,11 @@ -export 'src/buttons/close_button.dart'; -export 'src/buttons/icon_button.dart'; +export 'src/components/close_button.dart'; +export 'src/components/form.dart'; +export 'src/components/formatted_text.dart'; +export 'src/components/icon_button.dart'; +export 'src/components/password_input.dart'; +export 'src/components/text_button.dart'; +export 'src/components/text_input.dart'; +export 'src/constants.dart'; +export 'src/theme.dart'; +export 'src/translation.dart'; export 'src/types.dart'; diff --git a/mobile/packages/ui/lib/src/buttons/close_button.dart b/mobile/packages/ui/lib/src/components/close_button.dart similarity index 75% rename from mobile/packages/ui/lib/src/buttons/close_button.dart rename to mobile/packages/ui/lib/src/components/close_button.dart index c8c5d62a12..9308fdaadb 100644 --- a/mobile/packages/ui/lib/src/buttons/close_button.dart +++ b/mobile/packages/ui/lib/src/components/close_button.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:immich_ui/src/buttons/icon_button.dart'; import 'package:immich_ui/src/types.dart'; +import 'icon_button.dart'; + class ImmichCloseButton extends StatelessWidget { - final VoidCallback? onTap; + final VoidCallback? onPressed; final ImmichVariant variant; final ImmichColor color; const ImmichCloseButton({ super.key, - this.onTap, + this.onPressed, this.color = ImmichColor.primary, this.variant = ImmichVariant.ghost, }); @@ -20,6 +21,6 @@ class ImmichCloseButton extends StatelessWidget { icon: Icons.close, color: color, variant: variant, - onTap: onTap ?? () => Navigator.of(context).pop(), + onPressed: onPressed ?? () => Navigator.of(context).pop(), ); } diff --git a/mobile/packages/ui/lib/src/components/form.dart b/mobile/packages/ui/lib/src/components/form.dart new file mode 100644 index 0000000000..9e8c161806 --- /dev/null +++ b/mobile/packages/ui/lib/src/components/form.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:immich_ui/src/internal.dart'; + +class ImmichForm extends StatefulWidget { + final String? submitText; + final IconData? submitIcon; + final FutureOr Function()? onSubmit; + final Widget child; + + const ImmichForm({ + super.key, + this.submitText, + this.submitIcon, + required this.onSubmit, + required this.child, + }); + + @override + State createState() => ImmichFormState(); + + static ImmichFormState of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType<_ImmichFormScope>(); + if (scope == null) { + throw FlutterError( + 'ImmichForm.of() called with a context that does not contain an ImmichForm.\n' + 'No ImmichForm ancestor could be found starting from the context that was passed to ' + 'ImmichForm.of(). This usually happens when the context provided is ' + 'from a widget above the ImmichForm.\n' + 'The context used was:\n' + '$context', + ); + } + return scope._formState; + } +} + +class ImmichFormState extends State { + final _formKey = GlobalKey(); + bool _isLoading = false; + + FutureOr submit() async { + final isValid = _formKey.currentState?.validate() ?? false; + if (!isValid) { + return; + } + + setState(() { + _isLoading = true; + }); + + try { + await widget.onSubmit?.call(); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final submitText = widget.submitText ?? context.translations.submit; + return _ImmichFormScope( + formState: this, + child: Form( + key: _formKey, + child: Column( + spacing: ImmichSpacing.md, + children: [ + widget.child, + ImmichTextButton( + labelText: submitText, + icon: widget.submitIcon, + variant: ImmichVariant.filled, + loading: _isLoading, + onPressed: submit, + disabled: widget.onSubmit == null, + ), + ], + ), + ), + ); + } +} + +class _ImmichFormScope extends InheritedWidget { + const _ImmichFormScope({required super.child, required ImmichFormState formState}) : _formState = formState; + + final ImmichFormState _formState; + + @override + bool updateShouldNotify(_ImmichFormScope oldWidget) => oldWidget._formState != _formState; +} diff --git a/mobile/packages/ui/lib/src/components/formatted_text.dart b/mobile/packages/ui/lib/src/components/formatted_text.dart new file mode 100644 index 0000000000..95e42d834d --- /dev/null +++ b/mobile/packages/ui/lib/src/components/formatted_text.dart @@ -0,0 +1,141 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class FormattedSpan { + final TextStyle? style; + final VoidCallback? onTap; + + const FormattedSpan({this.style, this.onTap}); +} + +/// A widget that renders text with optional HTML-style formatting. +/// +/// Supports the following tags: +/// - `` for bold text +/// - `` or any tag ending with `-link` for tappable links +/// +/// Tags must not be nested. Each tag is matched independently left-to-right. +/// +/// By default, `` renders as [FontWeight.bold] and link tags render with an +/// underline and no tap handler. Provide [spanBuilder] to attach tap callbacks +/// or override styles per tag. +/// +/// Bold-only example (no [spanBuilder] needed): +/// ```dart +/// ImmichFormattedText('Delete {count} items?') +/// ``` +/// +/// Link example: +/// ```dart +/// ImmichFormattedText( +/// 'Refer to docs and other', +/// spanBuilder: (tag) => FormattedSpan( +/// onTap: switch (tag) { +/// 'docs-link' => () => launchUrl(docsUrl), +/// 'other-link' => () => launchUrl(otherUrl), +/// _ => null, +/// }, +/// ), +/// ) +/// ``` +class ImmichFormattedText extends StatefulWidget { + final String text; + final TextStyle? style; + final TextAlign? textAlign; + final TextOverflow? overflow; + final int? maxLines; + final bool? softWrap; + final FormattedSpan Function(String tag)? spanBuilder; + + const ImmichFormattedText( + this.text, { + this.spanBuilder, + super.key, + this.style, + this.textAlign, + this.overflow, + this.maxLines, + this.softWrap, + }); + + @override + State createState() => _ImmichFormattedTextState(); +} + +class _ImmichFormattedTextState extends State { + final _recognizers = []; + + // Matches , , or any *-link tag and its content. + static final _tagPattern = RegExp(r'<(b|link|[\w]+-link)>(.*?)', caseSensitive: false, dotAll: true); + + @override + void dispose() { + _disposeRecognizers(); + super.dispose(); + } + + void _disposeRecognizers() { + for (final recognizer in _recognizers) { + recognizer.dispose(); + } + _recognizers.clear(); + } + + List _buildSpans() { + _disposeRecognizers(); + + final spans = []; + int cursor = 0; + + for (final match in _tagPattern.allMatches(widget.text)) { + if (match.start > cursor) { + spans.add(TextSpan(text: widget.text.substring(cursor, match.start))); + } + + final tag = match.group(1)!.toLowerCase(); + final content = match.group(2)!; + final formattedSpan = (widget.spanBuilder ?? _defaultSpanBuilder)(tag); + final style = formattedSpan.style ?? _defaultTextStyle(tag); + + GestureRecognizer? recognizer; + if (formattedSpan.onTap != null) { + recognizer = TapGestureRecognizer()..onTap = formattedSpan.onTap; + _recognizers.add(recognizer); + } + spans.add(TextSpan(text: content, style: style, recognizer: recognizer)); + + cursor = match.end; + } + + if (cursor < widget.text.length) { + spans.add(TextSpan(text: widget.text.substring(cursor))); + } + + return spans; + } + + FormattedSpan _defaultSpanBuilder(String tag) => switch (tag) { + 'b' => const FormattedSpan(style: TextStyle(fontWeight: FontWeight.bold)), + 'link' => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)), + _ when tag.endsWith('-link') => const FormattedSpan(style: TextStyle(decoration: TextDecoration.underline)), + _ => const FormattedSpan(), + }; + + TextStyle? _defaultTextStyle(String tag) => switch (tag) { + 'b' => const TextStyle(fontWeight: FontWeight.bold), + 'link' => const TextStyle(decoration: TextDecoration.underline), + _ when tag.endsWith('-link') => const TextStyle(decoration: TextDecoration.underline), + _ => null, + }; + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan(style: widget.style, children: _buildSpans()), + textAlign: widget.textAlign, + overflow: widget.overflow, + maxLines: widget.maxLines, + softWrap: widget.softWrap, + ); + } +} diff --git a/mobile/packages/ui/lib/src/buttons/icon_button.dart b/mobile/packages/ui/lib/src/components/icon_button.dart similarity index 60% rename from mobile/packages/ui/lib/src/buttons/icon_button.dart rename to mobile/packages/ui/lib/src/components/icon_button.dart index 5c62ee8eda..dc140b71f9 100644 --- a/mobile/packages/ui/lib/src/buttons/icon_button.dart +++ b/mobile/packages/ui/lib/src/components/icon_button.dart @@ -3,42 +3,48 @@ import 'package:immich_ui/src/types.dart'; class ImmichIconButton extends StatelessWidget { final IconData icon; - final VoidCallback onTap; + final VoidCallback onPressed; final ImmichVariant variant; final ImmichColor color; + final bool disabled; const ImmichIconButton({ super.key, required this.icon, - required this.onTap, + required this.onPressed, this.color = ImmichColor.primary, this.variant = ImmichVariant.filled, + this.disabled = false, }); @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final background = switch (variant) { ImmichVariant.filled => switch (color) { - ImmichColor.primary => Theme.of(context).colorScheme.primary, - ImmichColor.secondary => Theme.of(context).colorScheme.secondary, + ImmichColor.primary => colorScheme.primary, + ImmichColor.secondary => colorScheme.secondary, }, ImmichVariant.ghost => Colors.transparent, }; final foreground = switch (variant) { ImmichVariant.filled => switch (color) { - ImmichColor.primary => Theme.of(context).colorScheme.onPrimary, - ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary, + ImmichColor.primary => colorScheme.onPrimary, + ImmichColor.secondary => colorScheme.onSecondary, }, ImmichVariant.ghost => switch (color) { - ImmichColor.primary => Theme.of(context).colorScheme.primary, - ImmichColor.secondary => Theme.of(context).colorScheme.secondary, + ImmichColor.primary => colorScheme.primary, + ImmichColor.secondary => colorScheme.secondary, }, }; + final effectiveOnPressed = disabled ? null : onPressed; + return IconButton( icon: Icon(icon), - onPressed: onTap, + onPressed: effectiveOnPressed, style: IconButton.styleFrom( backgroundColor: background, foregroundColor: foreground, diff --git a/mobile/packages/ui/lib/src/components/password_input.dart b/mobile/packages/ui/lib/src/components/password_input.dart new file mode 100644 index 0000000000..bd5a149354 --- /dev/null +++ b/mobile/packages/ui/lib/src/components/password_input.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/components/text_input.dart'; +import 'package:immich_ui/src/internal.dart'; + +class ImmichPasswordInput extends StatefulWidget { + final String? label; + final String? hintText; + final TextEditingController? controller; + final FocusNode? focusNode; + final String? Function(String?)? validator; + final void Function(BuildContext, String)? onSubmit; + final TextInputAction? keyboardAction; + + const ImmichPasswordInput({ + super.key, + this.controller, + this.focusNode, + this.label, + this.hintText, + this.validator, + this.onSubmit, + this.keyboardAction, + }); + + @override + State createState() => _ImmichPasswordInputState(); +} + +class _ImmichPasswordInputState extends State { + bool _visible = false; + + void _toggleVisibility() { + setState(() { + _visible = !_visible; + }); + } + + @override + Widget build(BuildContext context) { + return ImmichTextInput( + key: widget.key, + label: widget.label ?? context.translations.password, + hintText: widget.hintText, + controller: widget.controller, + focusNode: widget.focusNode, + validator: widget.validator, + onSubmit: widget.onSubmit, + keyboardAction: widget.keyboardAction, + obscureText: !_visible, + suffixIcon: IconButton( + onPressed: _toggleVisibility, + icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded), + ), + autofillHints: [AutofillHints.password], + keyboardType: TextInputType.text, + ); + } +} diff --git a/mobile/packages/ui/lib/src/components/text_button.dart b/mobile/packages/ui/lib/src/components/text_button.dart new file mode 100644 index 0000000000..6dc677aee2 --- /dev/null +++ b/mobile/packages/ui/lib/src/components/text_button.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/constants.dart'; +import 'package:immich_ui/src/types.dart'; + +class ImmichTextButton extends StatelessWidget { + final String labelText; + final IconData? icon; + final FutureOr Function() onPressed; + final ImmichVariant variant; + final ImmichColor color; + final bool expanded; + final bool loading; + final bool disabled; + + const ImmichTextButton({ + super.key, + required this.labelText, + this.icon, + required this.onPressed, + this.variant = ImmichVariant.filled, + this.color = ImmichColor.primary, + this.expanded = true, + this.loading = false, + this.disabled = false, + }); + + Widget _buildButton(ImmichVariant variant) { + final Widget? effectiveIcon = loading + ? const SizedBox.square( + dimension: ImmichIconSize.md, + child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg), + ) + : icon != null + ? Icon(icon, fontWeight: FontWeight.w600) + : null; + final hasIcon = effectiveIcon != null; + + final label = Text(labelText, style: const TextStyle(fontSize: ImmichTextSize.body, fontWeight: FontWeight.bold)); + final style = ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: ImmichSpacing.md)); + + final effectiveOnPressed = disabled || loading ? null : onPressed; + + switch (variant) { + case ImmichVariant.filled: + if (hasIcon) { + return ElevatedButton.icon( + style: style, + onPressed: effectiveOnPressed, + icon: effectiveIcon, + label: label, + ); + } + + return ElevatedButton( + style: style, + onPressed: effectiveOnPressed, + child: label, + ); + case ImmichVariant.ghost: + if (hasIcon) { + return TextButton.icon( + style: style, + onPressed: effectiveOnPressed, + icon: effectiveIcon, + label: label, + ); + } + + return TextButton( + style: style, + onPressed: effectiveOnPressed, + child: label, + ); + } + } + + @override + Widget build(BuildContext context) { + final button = _buildButton(variant); + if (expanded) { + return SizedBox(width: double.infinity, child: button); + } + return button; + } +} diff --git a/mobile/packages/ui/lib/src/components/text_input.dart b/mobile/packages/ui/lib/src/components/text_input.dart new file mode 100644 index 0000000000..1b3fb91f51 --- /dev/null +++ b/mobile/packages/ui/lib/src/components/text_input.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class ImmichTextInput extends StatefulWidget { + final String label; + final String? hintText; + final TextEditingController? controller; + final FocusNode? focusNode; + final String? Function(String?)? validator; + final void Function(BuildContext, String)? onSubmit; + final TextInputType keyboardType; + final TextInputAction? keyboardAction; + final List? autofillHints; + final Widget? suffixIcon; + final bool obscureText; + final bool autoCorrect; + + const ImmichTextInput({ + super.key, + this.controller, + this.focusNode, + required this.label, + this.hintText, + this.validator, + this.onSubmit, + this.keyboardType = TextInputType.text, + this.keyboardAction, + this.autofillHints, + this.suffixIcon, + this.obscureText = false, + this.autoCorrect = true, + }); + + @override + State createState() => _ImmichTextInputState(); +} + +class _ImmichTextInputState extends State { + late final FocusNode _focusNode; + String? _error; + + @override + void initState() { + super.initState(); + _focusNode = widget.focusNode ?? FocusNode(); + } + + @override + void dispose() { + if (widget.focusNode == null) { + _focusNode.dispose(); + } + super.dispose(); + } + + String? _validateInput(String? value) { + setState(() { + _error = widget.validator?.call(value); + }); + return null; + } + + bool get _hasError => _error != null && _error!.isNotEmpty; + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + + return TextFormField( + controller: widget.controller, + focusNode: _focusNode, + decoration: InputDecoration( + hintText: widget.hintText, + labelText: widget.label, + labelStyle: themeData.inputDecorationTheme.labelStyle?.copyWith( + color: _hasError ? themeData.colorScheme.error : null, + ), + errorText: _error, + suffixIcon: widget.suffixIcon, + ), + obscureText: widget.obscureText, + validator: _validateInput, + keyboardType: widget.keyboardType, + textInputAction: widget.keyboardAction, + autocorrect: widget.autoCorrect, + autofillHints: widget.autofillHints, + onTap: () => setState(() => _error = null), + onTapOutside: (_) => _focusNode.unfocus(), + onFieldSubmitted: (value) => widget.onSubmit?.call(context, value), + ); + } +} diff --git a/mobile/packages/ui/lib/src/constants.dart b/mobile/packages/ui/lib/src/constants.dart new file mode 100644 index 0000000000..96122c9b36 --- /dev/null +++ b/mobile/packages/ui/lib/src/constants.dart @@ -0,0 +1,199 @@ +/// Spacing constants for gaps between widgets +abstract class ImmichSpacing { + const ImmichSpacing._(); + + /// Extra small spacing: 4.0 + static const double xs = 4.0; + + /// Small spacing: 8.0 + static const double sm = 8.0; + + /// Medium spacing (default): 12.0 + static const double md = 12.0; + + /// Large spacing: 16.0 + static const double lg = 16.0; + + /// Extra large spacing: 24.0 + static const double xl = 24.0; + + /// Extra extra large spacing: 32.0 + static const double xxl = 32.0; + + /// Extra extra extra large spacing: 48.0 + static const double xxxl = 48.0; +} + +/// Border radius constants for consistent rounded corners +abstract class ImmichRadius { + const ImmichRadius._(); + + /// No radius: 0.0 + static const double none = 0.0; + + /// Extra small radius: 4.0 + static const double xs = 4.0; + + /// Small radius: 8.0 + static const double sm = 8.0; + + /// Medium radius (default): 12.0 + static const double md = 12.0; + + /// Large radius: 16.0 + static const double lg = 16.0; + + /// Extra large radius: 20.0 + static const double xl = 20.0; + + /// Extra extra large radius: 24.0 + static const double xxl = 24.0; + + /// Full circular radius: infinity + static const double full = double.infinity; +} + +/// Icon size constants for consistent icon sizing +abstract class ImmichIconSize { + const ImmichIconSize._(); + + /// Extra small icon: 16.0 + static const double xs = 16.0; + + /// Small icon: 20.0 + static const double sm = 20.0; + + /// Medium icon (default): 24.0 + static const double md = 24.0; + + /// Large icon: 32.0 + static const double lg = 32.0; + + /// Extra large icon: 40.0 + static const double xl = 40.0; + + /// Extra extra large icon: 48.0 + static const double xxl = 48.0; +} + +/// Animation duration constants for consistent timing +abstract class ImmichDuration { + const ImmichDuration._(); + + /// Extra fast: 100ms + static const Duration extraFast = Duration(milliseconds: 100); + + /// Fast: 150ms + static const Duration fast = Duration(milliseconds: 150); + + /// Normal: 200ms + static const Duration normal = Duration(milliseconds: 200); + + /// Moderate: 300ms + static const Duration moderate = Duration(milliseconds: 300); + + /// Slow: 500ms + static const Duration slow = Duration(milliseconds: 500); + + /// Extra slow: 700ms + static const Duration extraSlow = Duration(milliseconds: 700); +} + +/// Elevation constants for consistent shadows and depth +abstract class ImmichElevation { + const ImmichElevation._(); + + /// No elevation: 0.0 + static const double none = 0.0; + + /// Extra small elevation: 1.0 + static const double xs = 1.0; + + /// Small elevation: 2.0 + static const double sm = 2.0; + + /// Medium elevation: 4.0 + static const double md = 4.0; + + /// Large elevation: 8.0 + static const double lg = 8.0; + + /// Extra large elevation: 12.0 + static const double xl = 12.0; + + /// Extra extra large elevation: 16.0 + static const double xxl = 16.0; +} + +/// Border width constants (similar to Tailwind's border-* scale) +abstract class ImmichBorderWidth { + const ImmichBorderWidth._(); + + /// No border: 0.0 + static const double none = 0.0; + + /// Hairline border: 0.5 + static const double hairline = 0.5; + + /// Default border: 1.0 (border) + static const double base = 1.0; + + /// Medium border: 2.0 (border-2) + static const double md = 2.0; + + /// Large border: 3.0 (border-4) + static const double lg = 3.0; + + /// Extra large border: 4.0 + static const double xl = 4.0; +} + +/// Text size constants with semantic HTML-like naming +/// These follow a type scale for harmonious text hierarchy +abstract class ImmichTextSize { + const ImmichTextSize._(); + + /// Caption text: 10.0 + /// Use for: Tiny labels, legal text, metadata, timestamps + static const double caption = 10.0; + + /// Label text: 12.0 + /// Use for: Form labels, secondary text, helper text + static const double label = 12.0; + + /// Body text: 14.0 (default) + /// Use for: Main body text, paragraphs, default UI text + static const double body = 14.0; + + /// Body emphasized: 16.0 + /// Use for: Emphasized body text, button labels, tabs + static const double bodyLarge = 16.0; + + /// Heading 6: 18.0 (smallest heading) + /// Use for: Subtitles, card titles, section headers + static const double h6 = 18.0; + + /// Heading 5: 20.0 + /// Use for: Small headings, prominent labels + static const double h5 = 20.0; + + /// Heading 4: 24.0 + /// Use for: Page titles, dialog titles + static const double h4 = 24.0; + + /// Heading 3: 30.0 + /// Use for: Section headings, large headings + static const double h3 = 30.0; + + /// Heading 2: 36.0 + /// Use for: Major section headings + static const double h2 = 36.0; + + /// Heading 1: 48.0 (largest heading) + /// Use for: Page hero headings, main titles + static const double h1 = 48.0; + + /// Display text: 60.0 + /// Use for: Hero numbers, splash screens, extra large display + static const double display = 60.0; +} diff --git a/mobile/packages/ui/lib/src/internal.dart b/mobile/packages/ui/lib/src/internal.dart new file mode 100644 index 0000000000..7f503927ff --- /dev/null +++ b/mobile/packages/ui/lib/src/internal.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/translation.dart'; + +extension TranslationHelper on BuildContext { + ImmichTranslations get translations => ImmichTranslationProvider.of(this); +} diff --git a/mobile/packages/ui/lib/src/theme.dart b/mobile/packages/ui/lib/src/theme.dart new file mode 100644 index 0000000000..387723b8ce --- /dev/null +++ b/mobile/packages/ui/lib/src/theme.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/src/constants.dart'; + +class ImmichThemeProvider extends StatelessWidget { + final ColorScheme colorScheme; + final Widget child; + + const ImmichThemeProvider({super.key, required this.colorScheme, required this.child}); + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: colorScheme, + brightness: colorScheme.brightness, + inputDecorationTheme: InputDecorationTheme( + floatingLabelBehavior: FloatingLabelBehavior.always, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.primary), + borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.primary), + borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.error), + borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), + ), + focusedErrorBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.error), + borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)), + ), + labelStyle: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w600), + hintStyle: const TextStyle(fontSize: ImmichTextSize.body), + errorStyle: TextStyle(color: colorScheme.error, fontWeight: FontWeight.w600), + ), + ), + child: child, + ); + } +} diff --git a/mobile/packages/ui/lib/src/translation.dart b/mobile/packages/ui/lib/src/translation.dart new file mode 100644 index 0000000000..cd51f74422 --- /dev/null +++ b/mobile/packages/ui/lib/src/translation.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class ImmichTranslations { + late String submit; + late String password; + + ImmichTranslations({String? submit, String? password}) { + this.submit = submit ?? 'Submit'; + this.password = password ?? 'Password'; + } +} + +class ImmichTranslationProvider extends InheritedWidget { + final ImmichTranslations? translations; + + const ImmichTranslationProvider({ + super.key, + this.translations, + required super.child, + }); + + static ImmichTranslations of(BuildContext context) { + final provider = context.dependOnInheritedWidgetOfExactType(); + return provider?.translations ?? ImmichTranslations(); + } + + @override + bool updateShouldNotify(covariant ImmichTranslationProvider oldWidget) { + return oldWidget.translations != translations; + } +} diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index b9d150f174..697e1debf5 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" characters: dependency: transitive description: @@ -9,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" collection: dependency: transitive description: @@ -17,11 +41,56 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -38,11 +107,67 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" vector_math: dependency: transitive description: @@ -51,5 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" sdks: dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/mobile/packages/ui/pubspec.yaml b/mobile/packages/ui/pubspec.yaml index 47b9a9dd8a..a25dfb6ca4 100644 --- a/mobile/packages/ui/pubspec.yaml +++ b/mobile/packages/ui/pubspec.yaml @@ -8,5 +8,9 @@ dependencies: flutter: sdk: flutter +dev_dependencies: + flutter_test: + sdk: flutter + flutter: uses-material-design: true \ No newline at end of file diff --git a/mobile/packages/ui/showcase/.gitignore b/mobile/packages/ui/showcase/.gitignore new file mode 100644 index 0000000000..b285cd608b --- /dev/null +++ b/mobile/packages/ui/showcase/.gitignore @@ -0,0 +1,11 @@ +# Build artifacts +build/ + +# Test cache and generated files +.dart_tool/ +.packages +.flutter-plugins +.flutter-plugins-dependencies + +# IDE-specific files +.vscode/ \ No newline at end of file diff --git a/mobile/packages/ui/showcase/.metadata b/mobile/packages/ui/showcase/.metadata new file mode 100644 index 0000000000..b95fa4d74e --- /dev/null +++ b/mobile/packages/ui/showcase/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/mobile/packages/ui/showcase/analysis_options.yaml b/mobile/packages/ui/showcase/analysis_options.yaml new file mode 100644 index 0000000000..f9b303465f --- /dev/null +++ b/mobile/packages/ui/showcase/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/mobile/packages/ui/showcase/assets/immich-text-dark.png b/mobile/packages/ui/showcase/assets/immich-text-dark.png new file mode 100644 index 0000000000..215687af8f Binary files /dev/null and b/mobile/packages/ui/showcase/assets/immich-text-dark.png differ diff --git a/mobile/packages/ui/showcase/assets/immich-text-light.png b/mobile/packages/ui/showcase/assets/immich-text-light.png new file mode 100644 index 0000000000..478158d39c Binary files /dev/null and b/mobile/packages/ui/showcase/assets/immich-text-light.png differ diff --git a/mobile/packages/ui/showcase/assets/immich_logo.png b/mobile/packages/ui/showcase/assets/immich_logo.png new file mode 100644 index 0000000000..49fd3ae289 Binary files /dev/null and b/mobile/packages/ui/showcase/assets/immich_logo.png differ diff --git a/mobile/packages/ui/showcase/assets/themes/github_dark.json b/mobile/packages/ui/showcase/assets/themes/github_dark.json new file mode 100644 index 0000000000..bd4801482e --- /dev/null +++ b/mobile/packages/ui/showcase/assets/themes/github_dark.json @@ -0,0 +1,339 @@ +{ + "name": "GitHub Dark", + "settings": [ + { + "settings": { + "foreground": "#e1e4e8", + "background": "#24292e" + } + }, + { + "scope": [ + "comment", + "punctuation.definition.comment", + "string.comment" + ], + "settings": { + "foreground": "#6a737d" + } + }, + { + "scope": [ + "constant", + "entity.name.constant", + "variable.other.constant", + "variable.other.enummember", + "variable.language" + ], + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "entity", + "entity.name" + ], + "settings": { + "foreground": "#b392f0" + } + }, + { + "scope": "variable.parameter.function", + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": "entity.name.tag", + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": "keyword", + "settings": { + "foreground": "#f97583" + } + }, + { + "scope": [ + "storage", + "storage.type" + ], + "settings": { + "foreground": "#f97583" + } + }, + { + "scope": [ + "storage.modifier.package", + "storage.modifier.import", + "storage.type.java" + ], + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": [ + "string", + "punctuation.definition.string", + "string punctuation.section.embedded source" + ], + "settings": { + "foreground": "#9ecbff" + } + }, + { + "scope": "support", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.property-name", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "variable", + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": "variable.other", + "settings": { + "foreground": "#e1e4e8" + } + }, + { + "scope": "invalid.broken", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.deprecated", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.illegal", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "invalid.unimplemented", + "settings": { + "fontStyle": "italic", + "foreground": "#fdaeb7" + } + }, + { + "scope": "message.error", + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": "string variable", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "source.regexp", + "string.regexp" + ], + "settings": { + "foreground": "#dbedff" + } + }, + { + "scope": [ + "string.regexp.character-class", + "string.regexp constant.character.escape", + "string.regexp source.ruby.embedded", + "string.regexp string.regexp.arbitrary-repitition" + ], + "settings": { + "foreground": "#dbedff" + } + }, + { + "scope": "string.regexp constant.character.escape", + "settings": { + "fontStyle": "bold", + "foreground": "#85e89d" + } + }, + { + "scope": "support.constant", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "support.variable", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.module-reference", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": [ + "markup.heading", + "markup.heading entity.name" + ], + "settings": { + "fontStyle": "bold", + "foreground": "#79b8ff" + } + }, + { + "scope": "markup.quote", + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": "markup.italic", + "settings": { + "fontStyle": "italic", + "foreground": "#e1e4e8" + } + }, + { + "scope": "markup.bold", + "settings": { + "fontStyle": "bold", + "foreground": "#e1e4e8" + } + }, + { + "scope": "markup.underline", + "settings": { + "fontStyle": "underline" + } + }, + { + "scope": "markup.inline.raw", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "markup.deleted", + "meta.diff.header.from-file", + "punctuation.definition.deleted" + ], + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": [ + "markup.inserted", + "meta.diff.header.to-file", + "punctuation.definition.inserted" + ], + "settings": { + "foreground": "#85e89d" + } + }, + { + "scope": [ + "markup.changed", + "punctuation.definition.changed" + ], + "settings": { + "foreground": "#ffab70" + } + }, + { + "scope": [ + "markup.ignored", + "markup.untracked" + ], + "settings": { + "foreground": "#2f363d" + } + }, + { + "scope": "meta.diff.range", + "settings": { + "fontStyle": "bold", + "foreground": "#b392f0" + } + }, + { + "scope": "meta.diff.header", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.separator", + "settings": { + "fontStyle": "bold", + "foreground": "#79b8ff" + } + }, + { + "scope": "meta.output", + "settings": { + "foreground": "#79b8ff" + } + }, + { + "scope": [ + "brackethighlighter.tag", + "brackethighlighter.curly", + "brackethighlighter.round", + "brackethighlighter.square", + "brackethighlighter.angle", + "brackethighlighter.quote" + ], + "settings": { + "foreground": "#d1d5da" + } + }, + { + "scope": "brackethighlighter.unmatched", + "settings": { + "foreground": "#fdaeb7" + } + }, + { + "scope": [ + "constant.other.reference.link", + "string.other.link" + ], + "settings": { + "fontStyle": "underline", + "foreground": "#dbedff" + } + } + ] +} diff --git a/mobile/packages/ui/showcase/lib/app_theme.dart b/mobile/packages/ui/showcase/lib/app_theme.dart new file mode 100644 index 0000000000..995bf3c91e --- /dev/null +++ b/mobile/packages/ui/showcase/lib/app_theme.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Light theme colors + static const _primary500 = Color(0xFF4250AF); + static const _primary100 = Color(0xFFD4D6F0); + static const _primary900 = Color(0xFF181E44); + static const _danger500 = Color(0xFFE53E3E); + static const _light50 = Color(0xFFFAFAFA); + static const _light300 = Color(0xFFD4D4D4); + static const _light500 = Color(0xFF737373); + + // Dark theme colors + static const _darkPrimary500 = Color(0xFFACCBFA); + static const _darkPrimary300 = Color(0xFF616D94); + static const _darkDanger500 = Color(0xFFE88080); + static const _darkLight50 = Color(0xFF0A0A0A); + static const _darkLight100 = Color(0xFF171717); + static const _darkLight200 = Color(0xFF262626); + + static ThemeData get lightTheme { + return ThemeData( + colorScheme: const ColorScheme.light( + primary: _primary500, + onPrimary: Colors.white, + primaryContainer: _primary100, + onPrimaryContainer: _primary900, + secondary: _light500, + onSecondary: Colors.white, + error: _danger500, + onError: Colors.white, + surface: _light50, + onSurface: Color(0xFF1A1C1E), + surfaceContainerHighest: Color(0xFFE3E4E8), + outline: Color(0xFFD1D3D9), + outlineVariant: _light300, + ), + useMaterial3: true, + fontFamily: 'GoogleSans', + scaffoldBackgroundColor: _light50, + cardTheme: const CardThemeData( + elevation: 0, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: _light300, width: 1), + ), + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + elevation: 0, + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + foregroundColor: Color(0xFF1A1C1E), + ), + ); + } + + static ThemeData get darkTheme { + return ThemeData( + colorScheme: const ColorScheme.dark( + primary: _darkPrimary500, + onPrimary: Color(0xFF0F1433), + primaryContainer: _darkPrimary300, + onPrimaryContainer: _primary100, + secondary: Color(0xFFC4C6D0), + onSecondary: Color(0xFF2E3042), + error: _darkDanger500, + onError: Color(0xFF0F1433), + surface: _darkLight50, + onSurface: Color(0xFFE3E3E6), + surfaceContainerHighest: _darkLight200, + outline: Color(0xFF8E9099), + outlineVariant: Color(0xFF43464F), + ), + useMaterial3: true, + fontFamily: 'GoogleSans', + scaffoldBackgroundColor: _darkLight50, + cardTheme: const CardThemeData( + elevation: 0, + color: _darkLight100, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: _darkLight200, width: 1), + ), + ), + appBarTheme: const AppBarTheme( + centerTitle: false, + elevation: 0, + backgroundColor: _darkLight50, + surfaceTintColor: Colors.transparent, + foregroundColor: Color(0xFFE3E3E6), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/constants.dart b/mobile/packages/ui/showcase/lib/constants.dart new file mode 100644 index 0000000000..cfca4cfda9 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/constants.dart @@ -0,0 +1,16 @@ +const String appTitle = '@immich/ui'; + +class LayoutConstants { + static const double sidebarWidth = 220.0; + + static const double gridSpacing = 16.0; + static const double gridAspectRatio = 2.5; + + static const double borderRadiusSmall = 6.0; + static const double borderRadiusMedium = 8.0; + static const double borderRadiusLarge = 12.0; + + static const double iconSizeSmall = 16.0; + static const double iconSizeMedium = 18.0; + static const double iconSizeLarge = 20.0; +} diff --git a/mobile/packages/ui/showcase/lib/main.dart b/mobile/packages/ui/showcase/lib/main.dart new file mode 100644 index 0000000000..6cd2df4fe5 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/main.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/app_theme.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/router.dart'; +import 'package:showcase/widgets/example_card.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initializeCodeHighlighter(); + runApp(const ShowcaseApp()); +} + +class ShowcaseApp extends StatefulWidget { + const ShowcaseApp({super.key}); + + @override + State createState() => _ShowcaseAppState(); +} + +class _ShowcaseAppState extends State { + ThemeMode _themeMode = ThemeMode.light; + late final GoRouter _router; + + @override + void initState() { + super.initState(); + _router = AppRouter.createRouter(_toggleTheme); + } + + void _toggleTheme() { + setState(() { + _themeMode = _themeMode == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: appTitle, + themeMode: _themeMode, + routerConfig: _router, + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + debugShowCheckedModeBanner: false, + builder: (context, child) => ImmichThemeProvider( + colorScheme: Theme.of(context).colorScheme, + child: child!, + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart new file mode 100644 index 0000000000..1bae98e0a4 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/close_button_page.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class CloseButtonPage extends StatelessWidget { + const CloseButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.closeButton.name, + child: ComponentExamples( + title: 'ImmichCloseButton', + subtitle: 'Pre-configured close button for dialogs and sheets.', + examples: [ + ExampleCard( + title: 'Default & Custom', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichCloseButton(onPressed: () {}), + ImmichCloseButton( + variant: ImmichVariant.filled, + onPressed: () {}, + ), + ImmichCloseButton( + color: ImmichColor.secondary, + onPressed: () {}, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart new file mode 100644 index 0000000000..7e36ac7537 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_bold_text.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class FormattedTextBoldText extends StatelessWidget { + const FormattedTextBoldText({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText('This is bold text.'); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart new file mode 100644 index 0000000000..3910a5117a --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_links.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class FormattedTextLinks extends StatelessWidget { + const FormattedTextLinks({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText( + 'Read the documentation or visit GitHub.', + spanBuilder: (tag) => FormattedSpan( + onTap: switch (tag) { + 'docs-link' => () => ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Docs link clicked!'))), + 'github-link' => () => ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('GitHub link clicked!'))), + _ => null, + }, + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart new file mode 100644 index 0000000000..3490b1c386 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/examples/formatted_text_mixed_tags.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; + +class FormattedTextMixedContent extends StatelessWidget { + const FormattedTextMixedContent({super.key}); + + @override + Widget build(BuildContext context) { + return ImmichFormattedText( + 'You can use bold text and links together.', + spanBuilder: (tag) => switch (tag) { + 'b' => const FormattedSpan( + style: TextStyle(fontWeight: FontWeight.bold), + ), + _ => FormattedSpan( + onTap: () => ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Link clicked!'))), + ), + }, + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/form_page.dart b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart new file mode 100644 index 0000000000..14567031de --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/form_page.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class FormPage extends StatefulWidget { + const FormPage({super.key}); + + @override + State createState() => _FormPageState(); +} + +class _FormPageState extends State { + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + String _result = ''; + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.form.name, + child: ComponentExamples( + title: 'ImmichForm', + subtitle: + 'Form container with built-in validation and submit handling.', + examples: [ + ExampleCard( + title: 'Login Form', + preview: Column( + children: [ + ImmichForm( + submitText: 'Login', + submitIcon: Icons.login, + onSubmit: () async { + await Future.delayed(const Duration(seconds: 1)); + setState(() { + _result = 'Form submitted!'; + }); + }, + child: Column( + spacing: 10, + children: [ + ImmichTextInput( + label: 'Email', + controller: _emailController, + keyboardType: TextInputType.emailAddress, + validator: (value) => + value?.isEmpty ?? true ? 'Required' : null, + ), + ImmichPasswordInput( + label: 'Password', + controller: _passwordController, + validator: (value) => + value?.isEmpty ?? true ? 'Required' : null, + ), + ], + ), + ), + if (_result.isNotEmpty) ...[ + const SizedBox(height: 16), + Text(_result, style: const TextStyle(color: Colors.green)), + ], + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart b/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart new file mode 100644 index 0000000000..b827e0340b --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/formatted_text_page.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/pages/components/examples/formatted_text_bold_text.dart'; +import 'package:showcase/pages/components/examples/formatted_text_links.dart'; +import 'package:showcase/pages/components/examples/formatted_text_mixed_tags.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class FormattedTextPage extends StatelessWidget { + const FormattedTextPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.formattedText.name, + child: ComponentExamples( + title: 'ImmichFormattedText', + subtitle: 'Render text with HTML formatting (bold, links).', + examples: [ + ExampleCard( + title: 'Bold Text', + preview: const FormattedTextBoldText(), + code: 'formatted_text_bold_text.dart', + ), + ExampleCard( + title: 'Links', + preview: const FormattedTextLinks(), + code: 'formatted_text_links.dart', + ), + ExampleCard( + title: 'Mixed Content', + preview: const FormattedTextMixedContent(), + code: 'formatted_text_mixed_tags.dart', + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart new file mode 100644 index 0000000000..4418b1de4f --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/icon_button_page.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class IconButtonPage extends StatelessWidget { + const IconButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.iconButton.name, + child: ComponentExamples( + title: 'ImmichIconButton', + subtitle: 'Icon-only button with customizable styling.', + examples: [ + ExampleCard( + title: 'Variants & Colors', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichIconButton( + icon: Icons.add, + onPressed: () {}, + variant: ImmichVariant.filled, + ), + ImmichIconButton( + icon: Icons.edit, + onPressed: () {}, + variant: ImmichVariant.ghost, + ), + ImmichIconButton( + icon: Icons.delete, + onPressed: () {}, + color: ImmichColor.secondary, + ), + ImmichIconButton( + icon: Icons.settings, + onPressed: () {}, + disabled: true, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart new file mode 100644 index 0000000000..772dd7882f --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/password_input_page.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class PasswordInputPage extends StatelessWidget { + const PasswordInputPage({super.key}); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.passwordInput.name, + child: ComponentExamples( + title: 'ImmichPasswordInput', + subtitle: 'Password field with visibility toggle.', + examples: [ + ExampleCard( + title: 'Password Input', + preview: ImmichPasswordInput( + label: 'Password', + hintText: 'Enter your password', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart new file mode 100644 index 0000000000..59e5b86294 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/text_button_page.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class TextButtonPage extends StatefulWidget { + const TextButtonPage({super.key}); + + @override + State createState() => _TextButtonPageState(); +} + +class _TextButtonPageState extends State { + bool _isLoading = false; + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.textButton.name, + child: ComponentExamples( + title: 'ImmichTextButton', + subtitle: + 'A versatile button component with multiple variants and color options.', + examples: [ + ExampleCard( + title: 'Variants', + description: + 'Filled and ghost variants for different visual hierarchy', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Filled', + variant: ImmichVariant.filled, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Ghost', + variant: ImmichVariant.ghost, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Colors', + description: 'Primary and secondary color options', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Primary', + color: ImmichColor.primary, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Secondary', + color: ImmichColor.secondary, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'With Icons', + description: 'Add leading icons', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'With Icon', + icon: Icons.add, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Download', + icon: Icons.download, + variant: ImmichVariant.ghost, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Loading State', + description: 'Shows loading indicator during async operations', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImmichTextButton( + onPressed: () async { + setState(() => _isLoading = true); + await Future.delayed(const Duration(seconds: 2)); + if (mounted) setState(() => _isLoading = false); + }, + labelText: _isLoading ? 'Loading...' : 'Click Me', + loading: _isLoading, + expanded: false, + ), + ], + ), + ), + ExampleCard( + title: 'Disabled State', + description: 'Buttons can be disabled', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + ImmichTextButton( + onPressed: () {}, + labelText: 'Disabled', + disabled: true, + expanded: false, + ), + ImmichTextButton( + onPressed: () {}, + labelText: 'Disabled Ghost', + variant: ImmichVariant.ghost, + disabled: true, + expanded: false, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart b/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart new file mode 100644 index 0000000000..5a0bfec6cd --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/components/text_input_page.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class TextInputPage extends StatefulWidget { + const TextInputPage({super.key}); + + @override + State createState() => _TextInputPageState(); +} + +class _TextInputPageState extends State { + final _controller1 = TextEditingController(); + final _controller2 = TextEditingController(); + + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.textInput.name, + child: ComponentExamples( + title: 'ImmichTextInput', + subtitle: 'Text field with validation support.', + examples: [ + ExampleCard( + title: 'Basic Usage', + preview: Column( + children: [ + ImmichTextInput( + label: 'Email', + hintText: 'Enter your email', + controller: _controller1, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 16), + ImmichTextInput( + label: 'Username', + controller: _controller2, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Username is required'; + } + if (value.length < 3) { + return 'Username must be at least 3 characters'; + } + return null; + }, + ), + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller1.dispose(); + _controller2.dispose(); + super.dispose(); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart new file mode 100644 index 0000000000..17de02d80a --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/design_system/constants_page.dart @@ -0,0 +1,396 @@ +import 'package:flutter/material.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/component_examples.dart'; +import 'package:showcase/widgets/example_card.dart'; +import 'package:showcase/widgets/page_title.dart'; + +class ConstantsPage extends StatefulWidget { + const ConstantsPage({super.key}); + + @override + State createState() => _ConstantsPageState(); +} + +class _ConstantsPageState extends State { + @override + Widget build(BuildContext context) { + return PageTitle( + title: AppRoute.constants.name, + child: ComponentExamples( + title: 'Constants', + subtitle: 'Consistent spacing, sizing, and styling constants.', + expand: true, + examples: [ + const ExampleCard( + title: 'Spacing', + description: 'ImmichSpacing (4.0 → 48.0)', + preview: Column( + children: [ + _SpacingBox(label: 'xs', size: ImmichSpacing.xs), + _SpacingBox(label: 'sm', size: ImmichSpacing.sm), + _SpacingBox(label: 'md', size: ImmichSpacing.md), + _SpacingBox(label: 'lg', size: ImmichSpacing.lg), + _SpacingBox(label: 'xl', size: ImmichSpacing.xl), + _SpacingBox(label: 'xxl', size: ImmichSpacing.xxl), + _SpacingBox(label: 'xxxl', size: ImmichSpacing.xxxl), + ], + ), + ), + const ExampleCard( + title: 'Border Radius', + description: 'ImmichRadius (0.0 → 24.0)', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _RadiusBox(label: 'none', radius: ImmichRadius.none), + _RadiusBox(label: 'xs', radius: ImmichRadius.xs), + _RadiusBox(label: 'sm', radius: ImmichRadius.sm), + _RadiusBox(label: 'md', radius: ImmichRadius.md), + _RadiusBox(label: 'lg', radius: ImmichRadius.lg), + _RadiusBox(label: 'xl', radius: ImmichRadius.xl), + _RadiusBox(label: 'xxl', radius: ImmichRadius.xxl), + ], + ), + ), + const ExampleCard( + title: 'Icon Sizes', + description: 'ImmichIconSize (16.0 → 48.0)', + preview: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.start, + children: [ + _IconSizeBox(label: 'xs', size: ImmichIconSize.xs), + _IconSizeBox(label: 'sm', size: ImmichIconSize.sm), + _IconSizeBox(label: 'md', size: ImmichIconSize.md), + _IconSizeBox(label: 'lg', size: ImmichIconSize.lg), + _IconSizeBox(label: 'xl', size: ImmichIconSize.xl), + _IconSizeBox(label: 'xxl', size: ImmichIconSize.xxl), + ], + ), + ), + const ExampleCard( + title: 'Text Sizes', + description: 'ImmichTextSize (10.0 → 60.0)', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Caption', + style: TextStyle(fontSize: ImmichTextSize.caption), + ), + Text('Label', style: TextStyle(fontSize: ImmichTextSize.label)), + Text('Body', style: TextStyle(fontSize: ImmichTextSize.body)), + Text('H6', style: TextStyle(fontSize: ImmichTextSize.h6)), + Text('H5', style: TextStyle(fontSize: ImmichTextSize.h5)), + Text('H4', style: TextStyle(fontSize: ImmichTextSize.h4)), + Text('H3', style: TextStyle(fontSize: ImmichTextSize.h3)), + Text('H2', style: TextStyle(fontSize: ImmichTextSize.h2)), + Text('H1', style: TextStyle(fontSize: ImmichTextSize.h1)), + ], + ), + ), + const ExampleCard( + title: 'Elevation', + description: 'ImmichElevation (0.0 → 16.0)', + preview: Wrap( + spacing: 12, + runSpacing: 12, + children: [ + _ElevationBox(label: 'none', elevation: ImmichElevation.none), + _ElevationBox(label: 'xs', elevation: ImmichElevation.xs), + _ElevationBox(label: 'sm', elevation: ImmichElevation.sm), + _ElevationBox(label: 'md', elevation: ImmichElevation.md), + _ElevationBox(label: 'lg', elevation: ImmichElevation.lg), + _ElevationBox(label: 'xl', elevation: ImmichElevation.xl), + _ElevationBox(label: 'xxl', elevation: ImmichElevation.xxl), + ], + ), + ), + const ExampleCard( + title: 'Border Width', + description: 'ImmichBorderWidth (0.5 → 4.0)', + preview: Column( + children: [ + _BorderBox( + label: 'hairline', + borderWidth: ImmichBorderWidth.hairline, + ), + _BorderBox(label: 'base', borderWidth: ImmichBorderWidth.base), + _BorderBox(label: 'md', borderWidth: ImmichBorderWidth.md), + _BorderBox(label: 'lg', borderWidth: ImmichBorderWidth.lg), + _BorderBox(label: 'xl', borderWidth: ImmichBorderWidth.xl), + ], + ), + ), + const ExampleCard( + title: 'Animation Durations', + description: 'ImmichDuration (100ms → 700ms)', + preview: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + _AnimatedDurationBox( + label: 'Extra Fast', + duration: ImmichDuration.extraFast, + ), + _AnimatedDurationBox( + label: 'Fast', + duration: ImmichDuration.fast, + ), + _AnimatedDurationBox( + label: 'Normal', + duration: ImmichDuration.normal, + ), + _AnimatedDurationBox( + label: 'Slow', + duration: ImmichDuration.slow, + ), + _AnimatedDurationBox( + label: 'Extra Slow', + duration: ImmichDuration.extraSlow, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _SpacingBox extends StatelessWidget { + final String label; + final double size; + + const _SpacingBox({required this.label, required this.size}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 60, + child: Text( + label, + style: const TextStyle(fontFamily: 'GoogleSansCode'), + ), + ), + Container( + width: size, + height: 24, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text('${size.toStringAsFixed(1)}px'), + ], + ), + ); + } +} + +class _RadiusBox extends StatelessWidget { + final String label; + final double radius; + + const _RadiusBox({required this.label, required this.radius}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(radius), + ), + ), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12)), + ], + ); + } +} + +class _IconSizeBox extends StatelessWidget { + final String label; + final double size; + + const _IconSizeBox({required this.label, required this.size}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon(Icons.palette_rounded, size: size), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 12)), + Text( + '${size.toStringAsFixed(0)}px', + style: const TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ); + } +} + +class _ElevationBox extends StatelessWidget { + final String label; + final double elevation; + + const _ElevationBox({required this.label, required this.elevation}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Material( + elevation: elevation, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Container( + width: 60, + height: 60, + alignment: Alignment.center, + child: Text(label, style: const TextStyle(fontSize: 12)), + ), + ), + const SizedBox(height: 4), + Text( + elevation.toStringAsFixed(1), + style: const TextStyle(fontSize: 10), + ), + ], + ); + } +} + +class _BorderBox extends StatelessWidget { + final String label; + final double borderWidth; + + const _BorderBox({required this.label, required this.borderWidth}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + label, + style: const TextStyle(fontFamily: 'GoogleSansCode'), + ), + ), + Expanded( + child: Container( + height: 40, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: borderWidth, + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + ), + ), + const SizedBox(width: 8), + Text('${borderWidth.toStringAsFixed(1)}px'), + ], + ), + ); + } +} + +class _AnimatedDurationBox extends StatefulWidget { + final String label; + final Duration duration; + + const _AnimatedDurationBox({required this.label, required this.duration}); + + @override + State<_AnimatedDurationBox> createState() => _AnimatedDurationBoxState(); +} + +class _AnimatedDurationBoxState extends State<_AnimatedDurationBox> { + bool _atEnd = false; + bool _isAnimating = false; + + void _playAnimation() async { + if (_isAnimating) return; + setState(() => _isAnimating = true); + setState(() => _atEnd = true); + await Future.delayed(widget.duration); + if (!mounted) return; + setState(() => _atEnd = false); + await Future.delayed(widget.duration); + if (!mounted) return; + setState(() => _isAnimating = false); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( + children: [ + SizedBox( + width: 90, + child: Text( + widget.label, + style: const TextStyle(fontFamily: 'GoogleSansCode', fontSize: 12), + ), + ), + Expanded( + child: Container( + height: 32, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(6), + ), + child: AnimatedAlign( + duration: widget.duration, + curve: Curves.easeInOut, + alignment: _atEnd ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + width: 60, + height: 28, + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + '${widget.duration.inMilliseconds}ms', + style: TextStyle( + fontSize: 11, + color: colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: _isAnimating ? null : _playAnimation, + icon: Icon( + Icons.play_arrow_rounded, + color: _isAnimating ? colorScheme.outline : colorScheme.primary, + ), + iconSize: 24, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + ), + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/pages/home_page.dart b/mobile/packages/ui/showcase/lib/pages/home_page.dart new file mode 100644 index 0000000000..de7af6c26b --- /dev/null +++ b/mobile/packages/ui/showcase/lib/pages/home_page.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/routes.dart'; + +class HomePage extends StatelessWidget { + final VoidCallback onThemeToggle; + + const HomePage({super.key, required this.onThemeToggle}); + + @override + Widget build(BuildContext context) { + return Title( + title: appTitle, + color: Theme.of(context).colorScheme.primary, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + children: [ + Text( + appTitle, + style: Theme.of(context).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 12), + Text( + 'A collection of Flutter components that are shared across all Immich projects', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w400, + height: 1.5, + ), + ), + const SizedBox(height: 48), + ...routesByCategory.entries.map((entry) { + if (entry.key == AppRouteCategory.root) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + entry.key.displayName, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 16), + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: LayoutConstants.gridSpacing, + mainAxisSpacing: LayoutConstants.gridSpacing, + childAspectRatio: LayoutConstants.gridAspectRatio, + ), + itemCount: entry.value.length, + itemBuilder: (context, index) { + return _ComponentCard(route: entry.value[index]); + }, + ), + const SizedBox(height: 48), + ], + ); + }), + ], + ), + ); + } +} + +class _ComponentCard extends StatelessWidget { + final AppRoute route; + + const _ComponentCard({required this.route}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => context.go(route.path), + borderRadius: const BorderRadius.all(Radius.circular(LayoutConstants.borderRadiusLarge)), + child: Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(route.icon, size: 32, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 16), + Text( + route.name, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + + const SizedBox(height: 8), + Text( + route.description, + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant, height: 1.4), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/router.dart b/mobile/packages/ui/showcase/lib/router.dart new file mode 100644 index 0000000000..34393da508 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/router.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/pages/components/close_button_page.dart'; +import 'package:showcase/pages/components/form_page.dart'; +import 'package:showcase/pages/components/formatted_text_page.dart'; +import 'package:showcase/pages/components/icon_button_page.dart'; +import 'package:showcase/pages/components/password_input_page.dart'; +import 'package:showcase/pages/components/text_button_page.dart'; +import 'package:showcase/pages/components/text_input_page.dart'; +import 'package:showcase/pages/design_system/constants_page.dart'; +import 'package:showcase/pages/home_page.dart'; +import 'package:showcase/routes.dart'; +import 'package:showcase/widgets/shell_layout.dart'; + +class AppRouter { + static GoRouter createRouter(VoidCallback onThemeToggle) { + return GoRouter( + initialLocation: AppRoute.home.path, + routes: [ + ShellRoute( + builder: (context, state, child) => + ShellLayout(onThemeToggle: onThemeToggle, child: child), + routes: AppRoute.values + .map( + (route) => GoRoute( + path: route.path, + pageBuilder: (context, state) => NoTransitionPage( + key: state.pageKey, + child: switch (route) { + AppRoute.home => HomePage(onThemeToggle: onThemeToggle), + AppRoute.textButton => const TextButtonPage(), + AppRoute.iconButton => const IconButtonPage(), + AppRoute.closeButton => const CloseButtonPage(), + AppRoute.textInput => const TextInputPage(), + AppRoute.passwordInput => const PasswordInputPage(), + AppRoute.form => const FormPage(), + AppRoute.formattedText => const FormattedTextPage(), + AppRoute.constants => const ConstantsPage(), + }, + ), + ), + ) + .toList(), + ), + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/routes.dart b/mobile/packages/ui/showcase/lib/routes.dart new file mode 100644 index 0000000000..4feeeafdb6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/routes.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +enum AppRouteCategory { + root(''), + forms('Forms'), + buttons('Buttons'), + designSystem('Design System'); + + final String displayName; + const AppRouteCategory(this.displayName); +} + +enum AppRoute { + home( + name: 'Home', + description: 'Home page', + path: '/', + category: AppRouteCategory.root, + icon: Icons.home_outlined, + ), + textButton( + name: 'Text Button', + description: 'Versatile button with filled and ghost variants', + path: '/text-button', + category: AppRouteCategory.buttons, + icon: Icons.smart_button_rounded, + ), + iconButton( + name: 'Icon Button', + description: 'Icon-only button with customizable styling', + path: '/icon-button', + category: AppRouteCategory.buttons, + icon: Icons.radio_button_unchecked_rounded, + ), + closeButton( + name: 'Close Button', + description: 'Pre-configured close button for dialogs', + path: '/close-button', + category: AppRouteCategory.buttons, + icon: Icons.close_rounded, + ), + textInput( + name: 'Text Input', + description: 'Text field with validation support', + path: '/text-input', + category: AppRouteCategory.forms, + icon: Icons.text_fields_outlined, + ), + passwordInput( + name: 'Password Input', + description: 'Password field with visibility toggle', + path: '/password-input', + category: AppRouteCategory.forms, + icon: Icons.password_outlined, + ), + form( + name: 'Form', + description: 'Form container with built-in validation', + path: '/form', + category: AppRouteCategory.forms, + icon: Icons.description_outlined, + ), + formattedText( + name: 'Formatted Text', + description: 'Render text with HTML formatting', + path: '/formatted-text', + category: AppRouteCategory.forms, + icon: Icons.code_rounded, + ), + constants( + name: 'Constants', + description: 'Spacing, colors, typography, and more', + path: '/constants', + category: AppRouteCategory.designSystem, + icon: Icons.palette_outlined, + ); + + final String name; + final String description; + final String path; + final AppRouteCategory category; + final IconData icon; + + const AppRoute({ + required this.name, + required this.description, + required this.path, + required this.category, + required this.icon, + }); +} + +final routesByCategory = AppRoute.values + .fold>>({}, (map, route) { + map.putIfAbsent(route.category, () => []).add(route); + return map; + }); diff --git a/mobile/packages/ui/showcase/lib/widgets/component_examples.dart b/mobile/packages/ui/showcase/lib/widgets/component_examples.dart new file mode 100644 index 0000000000..21e6516079 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/component_examples.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class ComponentExamples extends StatelessWidget { + final String title; + final String? subtitle; + final List examples; + final bool expand; + + const ComponentExamples({ + super.key, + required this.title, + this.subtitle, + required this.examples, + this.expand = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 24, 24, 24), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: _PageHeader(title: title, subtitle: subtitle), + ), + const SliverPadding(padding: EdgeInsets.only(top: 24)), + if (expand) + SliverList.builder( + itemCount: examples.length, + itemBuilder: (context, index) => examples[index], + ) + else + SliverLayoutBuilder( + builder: (context, constraints) { + return SliverList.builder( + itemCount: examples.length, + itemBuilder: (context, index) => Align( + alignment: Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: constraints.crossAxisExtent * 0.6, + maxWidth: constraints.crossAxisExtent, + ), + child: IntrinsicWidth(child: examples[index]), + ), + ), + ); + }, + ), + ], + ), + ); + } +} + +class _PageHeader extends StatelessWidget { + final String title; + final String? subtitle; + + const _PageHeader({required this.title, this.subtitle}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of( + context, + ).textTheme.headlineLarge?.copyWith(fontWeight: FontWeight.bold), + ), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/example_card.dart b/mobile/packages/ui/showcase/lib/widgets/example_card.dart new file mode 100644 index 0000000000..fea561afb6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/example_card.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:showcase/constants.dart'; +import 'package:syntax_highlight/syntax_highlight.dart'; + +late final Highlighter _codeHighlighter; + +Future initializeCodeHighlighter() async { + await Highlighter.initialize(['dart']); + final darkTheme = await HighlighterTheme.loadFromAssets([ + 'assets/themes/github_dark.json', + ], const TextStyle(color: Color(0xFFe1e4e8))); + + _codeHighlighter = Highlighter(language: 'dart', theme: darkTheme); +} + +class ExampleCard extends StatefulWidget { + final String title; + final String? description; + final Widget preview; + final String? code; + + const ExampleCard({ + super.key, + required this.title, + this.description, + required this.preview, + this.code, + }); + + @override + State createState() => _ExampleCardState(); +} + +class _ExampleCardState extends State { + bool _showPreview = true; + String? code; + + @override + void initState() { + super.initState(); + if (widget.code != null) { + rootBundle + .loadString('lib/pages/components/examples/${widget.code!}') + .then((value) { + setState(() { + code = value; + }); + }); + } + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 1, + margin: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + if (widget.description != null) + Text( + widget.description!, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (code != null) ...[ + const SizedBox(width: 16), + Row( + children: [ + _ToggleButton( + icon: Icons.visibility_rounded, + label: 'Preview', + isSelected: _showPreview, + onTap: () => setState(() => _showPreview = true), + ), + const SizedBox(width: 8), + _ToggleButton( + icon: Icons.code_rounded, + label: 'Code', + isSelected: !_showPreview, + onTap: () => setState(() => _showPreview = false), + ), + ], + ), + ], + ], + ), + ), + const Divider(height: 1), + if (_showPreview) + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox(width: double.infinity, child: widget.preview), + ) + else + Container( + width: double.infinity, + decoration: const BoxDecoration( + color: Color(0xFF24292e), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular( + LayoutConstants.borderRadiusMedium, + ), + bottomRight: Radius.circular( + LayoutConstants.borderRadiusMedium, + ), + ), + ), + child: _CodeCard(code: code!), + ), + ], + ), + ); + } +} + +class _ToggleButton extends StatelessWidget { + final IconData icon; + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _ToggleButton({ + required this.icon, + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all(Radius.circular(24)), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.7) + : Theme.of(context).colorScheme.primary, + borderRadius: const BorderRadius.all(Radius.circular(24)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 16, + color: Theme.of(context).colorScheme.onPrimary, + ), + const SizedBox(width: 6), + Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ), + ), + ); + } +} + +class _CodeCard extends StatelessWidget { + final String code; + + const _CodeCard({required this.code}); + + @override + Widget build(BuildContext context) { + final lines = code.split('\n'); + final lineNumberColor = Colors.white.withValues(alpha: 0.4); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: List.generate( + lines.length, + (index) => SizedBox( + height: 20, + child: Text( + '${index + 1}', + style: TextStyle( + fontFamily: 'GoogleSansCode', + fontSize: 13, + color: lineNumberColor, + height: 1.5, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + SelectableText.rich( + _codeHighlighter.highlight(code), + style: const TextStyle( + fontFamily: 'GoogleSansCode', + fontSize: 13, + height: 1.54, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/page_title.dart b/mobile/packages/ui/showcase/lib/widgets/page_title.dart new file mode 100644 index 0000000000..eae3bf6ffb --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/page_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PageTitle extends StatelessWidget { + final String title; + final Widget child; + + const PageTitle({super.key, required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Title( + title: '$title | @immich/ui', + color: Theme.of(context).colorScheme.primary, + child: child, + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart new file mode 100644 index 0000000000..8bcb687e75 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/shell_layout.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/widgets/sidebar_navigation.dart'; + +class ShellLayout extends StatelessWidget { + final Widget child; + final VoidCallback onThemeToggle; + + const ShellLayout({ + super.key, + required this.child, + required this.onThemeToggle, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/immich_logo.png', height: 32, width: 32), + const SizedBox(width: 8), + Image.asset( + isDark + ? 'assets/immich-text-dark.png' + : 'assets/immich-text-light.png', + height: 24, + filterQuality: FilterQuality.none, + isAntiAlias: true, + ), + ], + ), + actions: [ + IconButton( + icon: Icon( + isDark ? Icons.light_mode_outlined : Icons.dark_mode_outlined, + size: LayoutConstants.iconSizeLarge, + ), + onPressed: onThemeToggle, + tooltip: 'Toggle theme', + ), + ], + shape: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1), + ), + ), + body: Row( + children: [ + const SidebarNavigation(), + const VerticalDivider(), + Expanded(child: child), + ], + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart b/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart new file mode 100644 index 0000000000..10eba170e6 --- /dev/null +++ b/mobile/packages/ui/showcase/lib/widgets/sidebar_navigation.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:showcase/constants.dart'; +import 'package:showcase/routes.dart'; + +class SidebarNavigation extends StatelessWidget { + const SidebarNavigation({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: LayoutConstants.sidebarWidth, + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface), + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + children: [ + ...routesByCategory.entries.expand((entry) { + final category = entry.key; + final routes = entry.value; + return [ + if (category != AppRouteCategory.root) _CategoryHeader(category), + ...routes.map((route) => _NavItem(route)), + const SizedBox(height: 24), + ]; + }), + ], + ), + ); + } +} + +class _CategoryHeader extends StatelessWidget { + final AppRouteCategory category; + + const _CategoryHeader(this.category); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: Text( + category.displayName, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + ); + } +} + +class _NavItem extends StatelessWidget { + final AppRoute route; + + const _NavItem(this.route); + + @override + Widget build(BuildContext context) { + final currentRoute = GoRouterState.of(context).uri.toString(); + final isSelected = currentRoute == route.path; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + context.go(route.path); + }, + borderRadius: BorderRadius.circular( + LayoutConstants.borderRadiusMedium, + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? (isDark + ? Colors.white.withValues(alpha: 0.1) + : Theme.of( + context, + ).colorScheme.primaryContainer.withValues(alpha: 0.5)) + : Colors.transparent, + borderRadius: BorderRadius.circular( + LayoutConstants.borderRadiusMedium, + ), + ), + child: Row( + children: [ + Icon( + route.icon, + size: 20, + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + route.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/mobile/packages/ui/showcase/pubspec.lock b/mobile/packages/ui/showcase/pubspec.lock new file mode 100644 index 0000000000..c79e6c18c7 --- /dev/null +++ b/mobile/packages/ui/showcase/pubspec.lock @@ -0,0 +1,377 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a + url: "https://pub.dev" + source: hosted + version: "17.0.1" + immich_ui: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.0" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" + url: "https://pub.dev" + source: hosted + version: "0.5.5" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 + url: "https://pub.dev" + source: hosted + version: "0.7.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pixel_snap: + dependency: transitive + description: + name: pixel_snap + sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" + url: "https://pub.dev" + source: hosted + version: "0.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + super_clipboard: + dependency: transitive + description: + name: super_clipboard + sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + syntax_highlight: + dependency: "direct main" + description: + name: syntax_highlight + sha256: "4d3ba40658cadba6ba55d697f29f00b43538ebb6eb4a0ca0e895c568eaced138" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0" diff --git a/mobile/packages/ui/showcase/pubspec.yaml b/mobile/packages/ui/showcase/pubspec.yaml new file mode 100644 index 0000000000..e45ce07e66 --- /dev/null +++ b/mobile/packages/ui/showcase/pubspec.yaml @@ -0,0 +1,47 @@ +name: showcase +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + immich_ui: + path: ../ + go_router: ^17.0.1 + syntax_highlight: ^0.5.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + assets: + - assets/ + - assets/themes/ + - lib/pages/components/examples/ + + fonts: + - family: GoogleSans + fonts: + - asset: ../../../fonts/GoogleSans/GoogleSans-Regular.ttf + - asset: ../../../fonts/GoogleSans/GoogleSans-Italic.ttf + style: italic + - asset: ../../../fonts/GoogleSans/GoogleSans-Medium.ttf + weight: 500 + - asset: ../../../fonts/GoogleSans/GoogleSans-SemiBold.ttf + weight: 600 + - asset: ../../../fonts/GoogleSans/GoogleSans-Bold.ttf + weight: 700 + - family: GoogleSansCode + fonts: + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Regular.ttf + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-Medium.ttf + weight: 500 + - asset: ../../../fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf + weight: 600 \ No newline at end of file diff --git a/mobile/packages/ui/showcase/web/favicon.ico b/mobile/packages/ui/showcase/web/favicon.ico new file mode 100644 index 0000000000..7ec34e9e53 Binary files /dev/null and b/mobile/packages/ui/showcase/web/favicon.ico differ diff --git a/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png b/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000..49fd3ae289 Binary files /dev/null and b/mobile/packages/ui/showcase/web/icons/Icon-maskable-192.png differ diff --git a/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png b/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000..a7220554bc Binary files /dev/null and b/mobile/packages/ui/showcase/web/icons/Icon-maskable-512.png differ diff --git a/mobile/packages/ui/showcase/web/icons/apple-icon-180.png b/mobile/packages/ui/showcase/web/icons/apple-icon-180.png new file mode 100644 index 0000000000..4e642631a3 Binary files /dev/null and b/mobile/packages/ui/showcase/web/icons/apple-icon-180.png differ diff --git a/mobile/packages/ui/showcase/web/index.html b/mobile/packages/ui/showcase/web/index.html new file mode 100644 index 0000000000..abf42ad1fd --- /dev/null +++ b/mobile/packages/ui/showcase/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + @immich/ui + + + + + + diff --git a/mobile/packages/ui/showcase/web/manifest.json b/mobile/packages/ui/showcase/web/manifest.json new file mode 100644 index 0000000000..25b44bd1ae --- /dev/null +++ b/mobile/packages/ui/showcase/web/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "@immich/ui Showcase", + "short_name": "@immich/ui", + "start_url": ".", + "display": "standalone", + "background_color": "#FCFCFD", + "theme_color": "#4250AF", + "description": "Immich UI component library showcase and documentation", + "orientation": "landscape", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/mobile/packages/ui/test/formatted_text_test.dart b/mobile/packages/ui/test/formatted_text_test.dart new file mode 100644 index 0000000000..54ef343727 --- /dev/null +++ b/mobile/packages/ui/test/formatted_text_test.dart @@ -0,0 +1,211 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_ui/src/components/formatted_text.dart'; + +import 'test_utils.dart'; + +/// Text.rich creates a nested structure: root (DefaultTextStyle) -> wrapper (ImmichFormattedText) -> actual children +List _getContentSpans(WidgetTester tester) { + final richText = tester.widget(find.byType(RichText)); + final root = richText.text as TextSpan; + final wrapper = root.children?.firstOrNull; + if (wrapper is TextSpan) return wrapper.children ?? []; + return []; +} + +TextSpan _findSpan(List spans, String text) { + return spans.firstWhere( + (span) => span is TextSpan && span.text == text, + orElse: () => throw StateError('No span found with text: "$text"'), + ) as TextSpan; +} + +String _concatenateText(List spans) { + return spans.whereType().map((s) => s.text ?? '').join(); +} + +void _triggerTap(TextSpan span) { + final recognizer = span.recognizer; + if (recognizer is TapGestureRecognizer) { + recognizer.onTap?.call(); + } +} + +void main() { + group('ImmichFormattedText', () { + testWidgets('renders plain text without HTML tags', (tester) async { + await tester.pumpTestWidget( + const ImmichFormattedText('This is plain text'), + ); + + expect(find.text('This is plain text'), findsOneWidget); + }); + + testWidgets('applies text style properties', (tester) async { + await tester.pumpTestWidget( + const ImmichFormattedText( + 'Test text', + style: TextStyle( + fontSize: 16, + color: Colors.purple, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + + final text = tester.widget(find.byType(Text)); + final richText = text.textSpan as TextSpan; + + expect(richText.style?.fontSize, 16); + expect(richText.style?.color, Colors.purple); + expect(text.textAlign, TextAlign.center); + expect(text.maxLines, 2); + expect(text.overflow, TextOverflow.ellipsis); + }); + + testWidgets('handles text with special characters', (tester) async { + await tester.pumpTestWidget( + const ImmichFormattedText('Text with & < > " \' characters'), + ); + + expect(find.byType(RichText), findsOneWidget); + + final spans = _getContentSpans(tester); + expect(_concatenateText(spans), 'Text with & < > " \' characters'); + }); + + group('bold', () { + testWidgets('renders bold text with tag', (tester) async { + await tester.pumpTestWidget( + const ImmichFormattedText('This is bold text'), + ); + + final spans = _getContentSpans(tester); + final boldSpan = _findSpan(spans, 'bold'); + + expect(boldSpan.style?.fontWeight, FontWeight.bold); + expect(_concatenateText(spans), 'This is bold text'); + }); + }); + + group('link', () { + testWidgets('renders link text with tag', (tester) async { + await tester.pumpTestWidget( + ImmichFormattedText( + 'This is a custom link text', + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'link' => () {}, _ => null }), + ), + ); + + final spans = _getContentSpans(tester); + final linkSpan = _findSpan(spans, 'custom link'); + + expect(linkSpan.style?.decoration, TextDecoration.underline); + expect(linkSpan.recognizer, isA()); + }); + + testWidgets('handles link tap with callback', (tester) async { + var linkTapped = false; + + await tester.pumpTestWidget( + ImmichFormattedText( + 'Tap here', + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'link' => () => linkTapped = true, _ => null }), + ), + ); + + final spans = _getContentSpans(tester); + final linkSpan = _findSpan(spans, 'here'); + expect(linkSpan.recognizer, isA()); + + _triggerTap(linkSpan); + expect(linkTapped, isTrue); + }); + + testWidgets('handles custom prefixed link tags', (tester) async { + await tester.pumpTestWidget( + ImmichFormattedText( + 'Refer to docs and other', + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { + 'docs-link' => () {}, + 'other-link' => () {}, + _ => null, + },), + ), + ); + + final spans = _getContentSpans(tester); + final docsSpan = _findSpan(spans, 'docs'); + final otherSpan = _findSpan(spans, 'other'); + + expect(docsSpan.style?.decoration, TextDecoration.underline); + expect(otherSpan.style?.decoration, TextDecoration.underline); + }); + + testWidgets('applies custom link style', (tester) async { + const customLinkStyle = TextStyle( + color: Colors.red, + decoration: TextDecoration.overline, + ); + + await tester.pumpTestWidget( + ImmichFormattedText( + 'Click here', + spanBuilder: (tag) => FormattedSpan(style: customLinkStyle, onTap: () {}), + ), + ); + + final spans = _getContentSpans(tester); + final linkSpan = _findSpan(spans, 'here'); + + expect(linkSpan.style?.color, Colors.red); + expect(linkSpan.style?.decoration, TextDecoration.overline); + }); + + testWidgets('link without handler renders but is not tappable', (tester) async { + await tester.pumpTestWidget( + ImmichFormattedText( + 'Link without handler: click me', + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { 'other-link' => () {}, _ => null }), + ), + ); + + final spans = _getContentSpans(tester); + final linkSpan = _findSpan(spans, 'click me'); + + expect(linkSpan.style?.decoration, TextDecoration.underline); + expect(linkSpan.recognizer, isNull); + }); + + testWidgets('handles multiple links with different handlers', (tester) async { + var firstLinkTapped = false; + var secondLinkTapped = false; + + await tester.pumpTestWidget( + ImmichFormattedText( + 'Go to docs or help', + spanBuilder: (tag) => FormattedSpan(onTap: switch (tag) { + 'docs-link' => () => firstLinkTapped = true, + 'help-link' => () => secondLinkTapped = true, + _ => null, + },), + ), + ); + + final spans = _getContentSpans(tester); + final docsSpan = _findSpan(spans, 'docs'); + final helpSpan = _findSpan(spans, 'help'); + + _triggerTap(docsSpan); + expect(firstLinkTapped, isTrue); + expect(secondLinkTapped, isFalse); + + _triggerTap(helpSpan); + expect(secondLinkTapped, isTrue); + }); + }); + }); +} diff --git a/mobile/packages/ui/test/test_utils.dart b/mobile/packages/ui/test/test_utils.dart new file mode 100644 index 0000000000..42cc74da87 --- /dev/null +++ b/mobile/packages/ui/test/test_utils.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +extension WidgetTesterExtension on WidgetTester { + /// Pumps a widget wrapped in MaterialApp and Scaffold for testing. + Future pumpTestWidget(Widget widget) { + return pumpWidget(MaterialApp(home: Scaffold(body: widget))); + } +} diff --git a/mobile/pigeon/thumbnail_api.dart b/mobile/pigeon/local_image_api.dart similarity index 68% rename from mobile/pigeon/thumbnail_api.dart rename to mobile/pigeon/local_image_api.dart index 0698e7cdc9..eb538d7b1a 100644 --- a/mobile/pigeon/thumbnail_api.dart +++ b/mobile/pigeon/local_image_api.dart @@ -2,28 +2,29 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/platform/thumbnail_api.g.dart', - swiftOut: 'ios/Runner/Images/Thumbnails.g.swift', + dartOut: 'lib/platform/local_image_api.g.dart', + swiftOut: 'ios/Runner/Images/LocalImages.g.swift', swiftOptions: SwiftOptions(includeErrorClass: false), kotlinOut: - 'android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt', + 'android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt', kotlinOptions: KotlinOptions(package: 'app.alextran.immich.images'), dartOptions: DartOptions(), dartPackageName: 'immich_mobile', ), ) @HostApi() -abstract class ThumbnailApi { +abstract class LocalImageApi { @async - Map requestImage( + Map? requestImage( String assetId, { required int requestId, required int width, required int height, required bool isVideo, + required bool preferEncoded, }); - void cancelImageRequest(int requestId); + void cancelRequest(int requestId); @async Map getThumbhash(String thumbhash); diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index ec28afb008..cd55addd99 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -11,6 +11,15 @@ import 'package:pigeon/pigeon.dart'; dartPackageName: 'immich_mobile', ), ) +enum PlatformAssetPlaybackStyle { + unknown, + image, + video, + imageAnimated, + livePhoto, + videoLooping, +} + class PlatformAsset { final String id; final String name; @@ -31,6 +40,8 @@ class PlatformAsset { final double? latitude; final double? longitude; + final PlatformAssetPlaybackStyle playbackStyle; + const PlatformAsset({ required this.id, required this.name, @@ -45,6 +56,7 @@ class PlatformAsset { this.adjustmentTime, this.latitude, this.longitude, + this.playbackStyle = PlatformAssetPlaybackStyle.unknown, }); } @@ -90,6 +102,14 @@ class HashResult { const HashResult({required this.assetId, this.error, this.hash}); } +class CloudIdResult { + final String assetId; + final String? error; + final String? cloudId; + + const CloudIdResult({required this.assetId, this.error, this.cloudId}); +} + @HostApi() abstract class NativeSyncApi { bool shouldFullSync(); @@ -121,4 +141,7 @@ abstract class NativeSyncApi { @TaskQueue(type: TaskQueueType.serialBackgroundThread) Map> getTrashedAssets(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getCloudIdForAssetIds(List assetIds); } diff --git a/mobile/pigeon/network_api.dart b/mobile/pigeon/network_api.dart new file mode 100644 index 0000000000..68d2f7d8fc --- /dev/null +++ b/mobile/pigeon/network_api.dart @@ -0,0 +1,41 @@ +import 'package:pigeon/pigeon.dart'; + +class ClientCertData { + Uint8List data; + String password; + + ClientCertData(this.data, this.password); +} + +class ClientCertPrompt { + String title; + String message; + String cancel; + String confirm; + + ClientCertPrompt(this.title, this.message, this.cancel, this.confirm); +} + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/network_api.g.dart', + swiftOut: 'ios/Runner/Core/Network.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), + kotlinOut: + 'android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.core', includeErrorClass: true), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +@HostApi() +abstract class NetworkApi { + @async + void addCertificate(ClientCertData clientData); + + @async + ClientCertData selectCertificate(ClientCertPrompt promptText); + + @async + void removeCertificate(); +} diff --git a/mobile/pigeon/remote_image_api.dart b/mobile/pigeon/remote_image_api.dart new file mode 100644 index 0000000000..333f65a225 --- /dev/null +++ b/mobile/pigeon/remote_image_api.dart @@ -0,0 +1,29 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/remote_image_api.g.dart', + swiftOut: 'ios/Runner/Images/RemoteImages.g.swift', + swiftOptions: SwiftOptions(includeErrorClass: false), + kotlinOut: + 'android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.images', includeErrorClass: false), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +@HostApi() +abstract class RemoteImageApi { + @async + Map? requestImage( + String url, { + required Map headers, + required int requestId, + required bool preferEncoded, + }); + + void cancelRequest(int requestId); + + @async + int clearCache(); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3179d71bd1..077544b4f7 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -201,30 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.5" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" cancellation_token: dependency: transitive description: @@ -337,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cronet_http: + dependency: "direct main" + description: + name: cronet_http + sha256: "1fff7f26ac0c4cda97fe2a9aa082494baee4775f167c27ba45f6c8e88571e3ab" + url: "https://pub.dev" + source: hosted + version: "1.7.0" crop_image: dependency: "direct main" description: @@ -369,6 +353,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + cupertino_http: + dependency: "direct main" + description: + name: cupertino_http + sha256: "82cbec60c90bf785a047a9525688b6dacac444e177e1d5a5876963d3c50369e8" + url: "https://pub.dev" + source: hosted + version: "2.4.0" custom_lint: dependency: "direct dev" description: @@ -522,14 +514,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 - url: "https://pub.dev" - source: hosted - version: "8.3.7" file_selector_linux: dependency: transitive description: @@ -936,6 +920,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" image: dependency: transitive description: @@ -1077,6 +1069,14 @@ packages: url: "https://github.com/immich-app/isar" source: git version: "3.1.8" + jni: + dependency: transitive + description: + name: jni + sha256: "8706a77e94c76fe9ec9315e18949cc9479cc03af97085ca9c1077b61323ea12d" + url: "https://pub.dev" + source: hosted + version: "0.15.2" js: dependency: transitive description: @@ -1217,10 +1217,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1270,6 +1270,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64" + url: "https://pub.dev" + source: hosted + version: "9.1.0" octo_image: dependency: "direct main" description: @@ -1902,10 +1910,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" thumbhash: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2552e43d78..0b54dfc53e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 2.4.0+3029 +version: 2.5.6+3037 environment: sdk: '>=3.8.0 <4.0.0' @@ -12,7 +12,6 @@ dependencies: async: ^2.13.0 auto_route: ^9.2.0 background_downloader: ^9.3.0 - cached_network_image: ^3.4.1 cancellation_token_http: ^2.1.0 cast: ^2.1.0 collection: ^1.19.1 @@ -26,7 +25,6 @@ dependencies: dynamic_color: ^1.8.1 easy_localization: ^3.0.8 ffi: ^2.1.4 - file_picker: ^8.0.0+1 flutter: sdk: flutter flutter_cache_manager: ^3.4.1 @@ -86,6 +84,8 @@ dependencies: uuid: ^4.5.1 wakelock_plus: ^1.3.0 worker_manager: ^7.2.7 + cronet_http: ^1.7.0 + cupertino_http: ^2.4.0 dev_dependencies: auto_route_generator: ^9.0.0 @@ -127,24 +127,26 @@ flutter: assets: - assets/ fonts: - - family: Inconsolata + - family: GoogleSans fonts: - - asset: fonts/Inconsolata-Regular.ttf - - family: Overpass - fonts: - - asset: fonts/overpass/Overpass-Regular.ttf + - asset: fonts/GoogleSans/GoogleSans-Regular.ttf weight: 400 - - asset: fonts/overpass/Overpass-Italic.ttf + - asset: fonts/GoogleSans/GoogleSans-Italic.ttf style: italic - - asset: fonts/overpass/Overpass-Medium.ttf + - asset: fonts/GoogleSans/GoogleSans-Medium.ttf weight: 500 - - asset: fonts/overpass/Overpass-SemiBold.ttf + - asset: fonts/GoogleSans/GoogleSans-SemiBold.ttf weight: 600 - - asset: fonts/overpass/Overpass-Bold.ttf + - asset: fonts/GoogleSans/GoogleSans-Bold.ttf weight: 700 - - family: OverpassMono + - family: GoogleSansCode fonts: - - asset: fonts/overpass/OverpassMono.ttf + - asset: fonts/GoogleSansCode/GoogleSansCode-Regular.ttf + weight: 400 + - asset: fonts/GoogleSansCode/GoogleSansCode-Medium.ttf + weight: 500 + - asset: fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf + weight: 600 flutter_launcher_icons: image_path_android: 'assets/immich-logo.png' adaptive_icon_background: '#ffffff' diff --git a/mobile/test/api.mocks.dart b/mobile/test/api.mocks.dart index b0a4e9b8fd..c6a3a90582 100644 --- a/mobile/test/api.mocks.dart +++ b/mobile/test/api.mocks.dart @@ -4,3 +4,5 @@ import 'package:openapi/api.dart'; class MockAssetsApi extends Mock implements AssetsApi {} class MockSyncApi extends Mock implements SyncApi {} + +class MockServerApi extends Mock implements ServerApi {} diff --git a/mobile/test/domain/repositories/sync_stream_repository_test.dart b/mobile/test/domain/repositories/sync_stream_repository_test.dart new file mode 100644 index 0000000000..a26683213c --- /dev/null +++ b/mobile/test/domain/repositories/sync_stream_repository_test.dart @@ -0,0 +1,186 @@ +import 'package:drift/drift.dart' as drift; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; +import 'package:openapi/api.dart'; + +SyncUserV1 _createUser({String id = 'user-1'}) { + return SyncUserV1( + id: id, + name: 'Test User', + email: 'test@test.com', + deletedAt: null, + avatarColor: null, + hasProfileImage: false, + profileChangedAt: DateTime(2024, 1, 1), + ); +} + +SyncAssetV1 _createAsset({ + required String id, + required String checksum, + required String fileName, + String ownerId = 'user-1', + int? width, + int? height, +}) { + return SyncAssetV1( + id: id, + checksum: checksum, + originalFileName: fileName, + type: AssetTypeEnum.IMAGE, + ownerId: ownerId, + isFavorite: false, + fileCreatedAt: DateTime(2024, 1, 1), + fileModifiedAt: DateTime(2024, 1, 1), + localDateTime: DateTime(2024, 1, 1), + visibility: AssetVisibility.timeline, + width: width, + height: height, + deletedAt: null, + duration: null, + libraryId: null, + livePhotoVideoId: null, + stackId: null, + thumbhash: null, + isEdited: false, + ); +} + +SyncAssetExifV1 _createExif({ + required String assetId, + required int width, + required int height, + required String orientation, +}) { + return SyncAssetExifV1( + assetId: assetId, + exifImageWidth: width, + exifImageHeight: height, + orientation: orientation, + city: null, + country: null, + dateTimeOriginal: null, + description: null, + exposureTime: null, + fNumber: null, + fileSizeInByte: null, + focalLength: null, + fps: null, + iso: null, + latitude: null, + lensModel: null, + longitude: null, + make: null, + model: null, + modifyDate: null, + profileDescription: null, + projectionType: null, + rating: null, + state: null, + timeZone: null, + ); +} + +void main() { + late Drift db; + late SyncStreamRepository sut; + + setUp(() async { + db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + sut = SyncStreamRepository(db); + }); + + tearDown(() async { + await db.close(); + }); + + group('SyncStreamRepository - Dimension swapping based on orientation', () { + test('swaps dimensions for asset with rotated orientation', () async { + final flippedOrientations = ['5', '6', '7', '8', '90', '-90']; + + for (final orientation in flippedOrientations) { + final assetId = 'asset-$orientation-degrees'; + + await sut.updateUsersV1([_createUser()]); + + final asset = _createAsset( + id: assetId, + checksum: 'checksum-$orientation', + fileName: 'rotated_$orientation.jpg', + ); + await sut.updateAssetsV1([asset]); + + final exif = _createExif( + assetId: assetId, + width: 1920, + height: 1080, + orientation: orientation, // EXIF orientation value for 90 degrees CW + ); + await sut.updateAssetsExifV1([exif]); + + final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId)); + final result = await query.getSingle(); + + expect(result.width, equals(1080)); + expect(result.height, equals(1920)); + } + }); + + test('does not swap dimensions for asset with normal orientation', () async { + final nonFlippedOrientations = ['1', '2', '3', '4']; + for (final orientation in nonFlippedOrientations) { + final assetId = 'asset-$orientation-degrees'; + + await sut.updateUsersV1([_createUser()]); + + final asset = _createAsset(id: assetId, checksum: 'checksum-$orientation', fileName: 'normal_$orientation.jpg'); + await sut.updateAssetsV1([asset]); + + final exif = _createExif( + assetId: assetId, + width: 1920, + height: 1080, + orientation: orientation, // EXIF orientation value for normal + ); + await sut.updateAssetsExifV1([exif]); + + final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId)); + final result = await query.getSingle(); + + expect(result.width, equals(1920)); + expect(result.height, equals(1080)); + } + }); + + test('does not update dimensions if asset already has width and height', () async { + const assetId = 'asset-with-dimensions'; + const existingWidth = 1920; + const existingHeight = 1080; + const exifWidth = 3840; + const exifHeight = 2160; + + await sut.updateUsersV1([_createUser()]); + + final asset = _createAsset( + id: assetId, + checksum: 'checksum-with-dims', + fileName: 'with_dimensions.jpg', + width: existingWidth, + height: existingHeight, + ); + await sut.updateAssetsV1([asset]); + + final exif = _createExif(assetId: assetId, width: exifWidth, height: exifHeight, orientation: '6'); + await sut.updateAssetsExifV1([exif]); + + // Verify the asset still has original dimensions (not updated from EXIF) + final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId)); + final result = await query.getSingle(); + + expect(result.width, equals(existingWidth), reason: 'Width should remain as originally set'); + expect(result.height, equals(existingHeight), reason: 'Height should remain as originally set'); + }); + }); +} diff --git a/mobile/test/domain/service.mock.dart b/mobile/test/domain/service.mock.dart index 0bab675889..56b4802f88 100644 --- a/mobile/test/domain/service.mock.dart +++ b/mobile/test/domain/service.mock.dart @@ -3,7 +3,7 @@ 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:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreService extends Mock implements StoreService {} @@ -16,5 +16,5 @@ class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockAppSettingsService extends Mock implements AppSettingsService {} -class MockUploadService extends Mock implements UploadService {} +class MockBackgroundUploadService extends Mock implements BackgroundUploadService {} diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index b86819536d..9110a09471 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; @@ -13,38 +14,6 @@ void main() { late DriftRemoteAlbumRepository mockRemoteAlbumRepo; late DriftAlbumApiRepository mockAlbumApiRepo; - setUp(() { - mockRemoteAlbumRepo = MockRemoteAlbumRepository(); - mockAlbumApiRepo = MockDriftAlbumApiRepository(); - sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); - - when(() => mockRemoteAlbumRepo.getNewestAssetTimestamp(any())).thenAnswer((invocation) { - // Simulate a timestamp for the newest asset in the album - final albumID = invocation.positionalArguments[0] as String; - - if (albumID == '1') { - return Future.value(DateTime(2023, 1, 1)); - } else if (albumID == '2') { - return Future.value(DateTime(2023, 2, 1)); - } - - return Future.value(DateTime.fromMillisecondsSinceEpoch(0)); - }); - - when(() => mockRemoteAlbumRepo.getOldestAssetTimestamp(any())).thenAnswer((invocation) { - // Simulate a timestamp for the oldest asset in the album - final albumID = invocation.positionalArguments[0] as String; - - if (albumID == '1') { - return Future.value(DateTime(2019, 1, 1)); - } else if (albumID == '2') { - return Future.value(DateTime(2019, 2, 1)); - } - - return Future.value(DateTime.fromMillisecondsSinceEpoch(0)); - }); - }); - final albumA = RemoteAlbum( id: '1', name: 'Album A', @@ -73,6 +42,21 @@ void main() { isShared: false, ); + setUp(() { + mockRemoteAlbumRepo = MockRemoteAlbumRepository(); + mockAlbumApiRepo = MockDriftAlbumApiRepository(); + + when( + () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.end), + ).thenAnswer((_) async => ['1', '2']); + + when( + () => mockRemoteAlbumRepo.getSortedAlbumIds(any(), aggregation: AssetDateAggregation.start), + ).thenAnswer((_) async => ['1', '2']); + + sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo); + }); + group('sortAlbums', () { test('should sort correctly based on name', () async { final albums = [albumB, albumA]; @@ -85,35 +69,47 @@ void main() { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.created); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on updatedAt', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on assetCount', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on newestAssetTimestamp', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); - expect(result, [albumA, albumB]); + expect(result, [albumB, albumA]); }); test('should sort correctly based on oldestAssetTimestamp', () async { final albums = [albumB, albumA]; final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); - expect(result, [albumB, albumA]); + expect(result, [albumA, albumB]); + }); + + test('should flip order when isReverse is true for all modes', () async { + final albums = [albumB, albumA]; + + for (final mode in AlbumSortMode.values) { + final normal = await sut.sortAlbums(albums, mode, isReverse: false); + final reversed = await sut.sortAlbums(albums, mode, isReverse: true); + + // reversed should be the exact inverse of normal + expect(reversed, normal.reversed.toList(), reason: 'Mode: $mode'); + } }); }); } diff --git a/mobile/test/domain/services/asset.service_test.dart b/mobile/test/domain/services/asset.service_test.dart index ca9defc332..04e49f89f9 100644 --- a/mobile/test/domain/services/asset.service_test.dart +++ b/mobile/test/domain/services/asset.service_test.dart @@ -166,8 +166,8 @@ void main() { expect(result, 1080 / 1920); }); - test('handles various flipped EXIF orientations correctly', () async { - final flippedOrientations = ['5', '6', '7', '8', '90', '-90']; + test('should not flip remote asset dimensions', () async { + final flippedOrientations = ['1', '2', '3', '4', '5', '6', '7', '8', '90', '-90']; for (final orientation in flippedOrientations) { final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); @@ -178,23 +178,7 @@ void main() { final result = await sut.getAspectRatio(remoteAsset); - expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions'); - } - }); - - test('handles various non-flipped EXIF orientations correctly', () async { - final nonFlippedOrientations = ['1', '2', '3', '4']; - - for (final orientation in nonFlippedOrientations) { - final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080); - - final exif = ExifInfo(orientation: orientation); - - when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif); - - final result = await sut.getAspectRatio(remoteAsset); - - expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions'); + expect(result, 1920 / 1080, reason: 'Should not flipped remote asset dimensions for orientation $orientation'); } }); }); diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 3529ecca38..9f36a5635e 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -33,6 +33,7 @@ void main() { registerFallbackValue(LocalAssetStub.image1); registerFallbackValue({}); + when(() => mockAssetRepo.reconcileHashesFromCloudId()).thenAnswer((_) async => {}); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); }); @@ -190,5 +191,4 @@ void main() { verify(() => mockNativeApi.hashAssets([asset2.id], allowNetworkAccess: false)).called(1); }); }); - } diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 92ab01c7e0..df65fa3306 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/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/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; @@ -25,6 +26,7 @@ import '../../repository.mocks.dart'; void main() { late LocalSyncService sut; late DriftLocalAlbumRepository mockLocalAlbumRepository; + late DriftLocalAssetRepository mockLocalAssetRepository; late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepository; late LocalFilesManagerRepository mockLocalFilesManager; late StorageRepository mockStorageRepository; @@ -47,6 +49,7 @@ void main() { setUp(() async { mockLocalAlbumRepository = MockLocalAlbumRepository(); + mockLocalAssetRepository = MockLocalAssetRepository(); mockTrashedLocalAssetRepository = MockTrashedLocalAssetRepository(); mockLocalFilesManager = MockLocalFilesManagerRepository(); mockStorageRepository = MockStorageRepository(); @@ -66,6 +69,7 @@ void main() { sut = LocalSyncService( localAlbumRepository: mockLocalAlbumRepository, + localAssetRepository: mockLocalAssetRepository, trashedLocalAssetRepository: mockTrashedLocalAssetRepository, localFilesManager: mockLocalFilesManager, storageRepository: mockStorageRepository, @@ -127,6 +131,7 @@ void main() { durationInSeconds: 0, orientation: 0, isFavorite: false, + playbackStyle: PlatformAssetPlaybackStyle.image ); final assetsToRestore = [LocalAssetStub.image1]; @@ -153,7 +158,14 @@ void main() { 'album-a': [platformAsset], }); - verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(any())).called(1); + final trashedSnapshot = + verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(captureAny())).captured.single + as Iterable; + expect(trashedSnapshot.length, 1); + final trashedEntry = trashedSnapshot.single; + expect(trashedEntry.albumId, 'album-a'); + expect(trashedEntry.asset.id, platformAsset.id); + expect(trashedEntry.asset.name, platformAsset.name); verify(() => mockTrashedLocalAssetRepository.getToTrash()).called(1); verify(() => mockLocalFilesManager.restoreAssetsFromTrash(any())).called(1); @@ -174,6 +186,10 @@ void main() { await sut.processTrashedAssets({}); + final trashedSnapshot = + verify(() => mockTrashedLocalAssetRepository.processTrashSnapshot(captureAny())).captured.single + as Iterable; + expect(trashedSnapshot, isEmpty); verifyNever(() => mockLocalFilesManager.restoreAssetsFromTrash(any())); verifyNever(() => mockTrashedLocalAssetRepository.applyRestoredAssets(any())); }); @@ -199,6 +215,7 @@ void main() { isFavorite: false, createdAt: 1700000000, updatedAt: 1732000000, + playbackStyle: PlatformAssetPlaybackStyle.image ); final localAsset = platformAsset.toLocalAsset(); diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 109b54a907..a182c6cdca 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -18,13 +18,17 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:openapi/api.dart'; +import '../../api.mocks.dart'; import '../../fixtures/asset.stub.dart'; import '../../fixtures/sync_stream.stub.dart'; import '../../infrastructure/repository.mock.dart'; import '../../mocks/asset_entity.mock.dart'; import '../../repository.mocks.dart'; +import '../../service.mocks.dart'; class _AbortCallbackWrapper { const _AbortCallbackWrapper(); @@ -50,6 +54,9 @@ void main() { late DriftTrashedLocalAssetRepository mockTrashedLocalAssetRepo; late LocalFilesManagerRepository mockLocalFilesManagerRepo; late StorageRepository mockStorageRepo; + late MockApiService mockApi; + late MockServerApi mockServerApi; + late MockSyncMigrationRepository mockSyncMigrationRepo; late Future Function(List, Function(), Function()) handleEventsCallback; late _MockAbortCallbackWrapper mockAbortCallbackWrapper; late _MockAbortCallbackWrapper mockResetCallbackWrapper; @@ -60,6 +67,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); debugDefaultTargetPlatformOverride = TargetPlatform.android; registerFallbackValue(LocalAssetStub.image1); + registerFallbackValue(const SemVer(major: 2, minor: 5, patch: 0)); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); await StoreService.init(storeRepository: DriftStoreRepository(db)); @@ -82,18 +90,35 @@ void main() { mockStorageRepo = MockStorageRepository(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); mockResetCallbackWrapper = _MockAbortCallbackWrapper(); + mockApi = MockApiService(); + mockServerApi = MockServerApi(); + mockSyncMigrationRepo = MockSyncMigrationRepository(); when(() => mockAbortCallbackWrapper()).thenReturn(false); - when(() => mockSyncApiRepo.streamChanges(any())).thenAnswer((invocation) async { + when(() => mockSyncApiRepo.streamChanges(any(), serverVersion: any(named: 'serverVersion'))).thenAnswer(( + invocation, + ) async { handleEventsCallback = invocation.positionalArguments.first; }); - when(() => mockSyncApiRepo.streamChanges(any(), onReset: any(named: 'onReset'))).thenAnswer((invocation) async { + when( + () => mockSyncApiRepo.streamChanges( + any(), + onReset: any(named: 'onReset'), + serverVersion: any(named: 'serverVersion'), + ), + ).thenAnswer((invocation) async { handleEventsCallback = invocation.positionalArguments.first; }); when(() => mockSyncApiRepo.ack(any())).thenAnswer((_) async => {}); + when(() => mockSyncApiRepo.deleteSyncAck(any())).thenAnswer((_) async => {}); + + when(() => mockApi.serverInfoApi).thenReturn(mockServerApi); + when( + () => mockServerApi.getServerVersion(), + ).thenAnswer((_) async => ServerVersionResponseDto(major: 1, minor: 132, patch_: 0)); when(() => mockSyncStreamRepo.updateUsersV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer(successHandler); @@ -127,6 +152,7 @@ void main() { when(() => mockSyncStreamRepo.deletePeopleV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler); + when(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset()).thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, @@ -135,6 +161,8 @@ void main() { trashedLocalAssetRepository: mockTrashedLocalAssetRepo, localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {}); @@ -216,6 +244,8 @@ void main() { localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); await sut.sync(); @@ -255,6 +285,8 @@ void main() { localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); await sut.sync(); @@ -474,11 +506,7 @@ void main() { }); final events = [ - SyncStreamStub.assetModified( - id: 'remote-1', - checksum: 'checksum-trash', - ack: 'asset-remote-1-11', - ), + SyncStreamStub.assetModified(id: 'remote-1', checksum: 'checksum-trash', ack: 'asset-remote-1-11'), ]; await simulateEvents(events); @@ -486,4 +514,75 @@ void main() { verify(() => mockTrashedLocalAssetRepo.applyRestoredAssets(restoredIds)).called(1); }); }); + + group('SyncStreamService - Sync Migration', () { + test('ensure that <2.5.0 migrations run', () async { + await Store.put(StoreKey.syncMigrationStatus, "[]"); + when( + () => mockServerApi.getServerVersion(), + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + + await sut.sync(); + + verifyInOrder([ + () => mockSyncApiRepo.deleteSyncAck([ + SyncEntityType.assetExifV1, + SyncEntityType.partnerAssetExifV1, + SyncEntityType.albumAssetExifCreateV1, + SyncEntityType.albumAssetExifUpdateV1, + ]), + () => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset(), + ]); + + // should only run on server >2.5.0 + verifyNever( + () => mockSyncApiRepo.deleteSyncAck([ + SyncEntityType.assetV1, + SyncEntityType.partnerAssetV1, + SyncEntityType.albumAssetCreateV1, + SyncEntityType.albumAssetUpdateV1, + ]), + ); + }); + test('ensure that >=2.5.0 migrations run', () async { + await Store.put(StoreKey.syncMigrationStatus, "[]"); + when( + () => mockServerApi.getServerVersion(), + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 5, patch_: 0)); + await sut.sync(); + + verifyInOrder([ + () => mockSyncApiRepo.deleteSyncAck([ + SyncEntityType.assetExifV1, + SyncEntityType.partnerAssetExifV1, + SyncEntityType.albumAssetExifCreateV1, + SyncEntityType.albumAssetExifUpdateV1, + ]), + () => mockSyncApiRepo.deleteSyncAck([ + SyncEntityType.assetV1, + SyncEntityType.partnerAssetV1, + SyncEntityType.albumAssetCreateV1, + SyncEntityType.albumAssetUpdateV1, + ]), + ]); + + // v20260128_ResetAssetV1 writes that v20260128_CopyExifWidthHeightToAsset has been completed + verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset()); + }); + + test('ensure that migrations do not re-run', () async { + await Store.put( + StoreKey.syncMigrationStatus, + '["${SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name}"]', + ); + + when( + () => mockServerApi.getServerVersion(), + ).thenAnswer((_) async => ServerVersionResponseDto(major: 2, minor: 4, patch_: 1)); + + await sut.sync(); + + verifyNever(() => mockSyncMigrationRepo.v20260128CopyExifWidthHeightToAsset()); + }); + }); } diff --git a/mobile/test/drift/main/generated/schema.dart b/mobile/test/drift/main/generated/schema.dart index 5e19610574..153697896a 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -17,6 +17,13 @@ import 'schema_v11.dart' as v11; import 'schema_v12.dart' as v12; import 'schema_v13.dart' as v13; import 'schema_v14.dart' as v14; +import 'schema_v15.dart' as v15; +import 'schema_v16.dart' as v16; +import 'schema_v17.dart' as v17; +import 'schema_v18.dart' as v18; +import 'schema_v19.dart' as v19; +import 'schema_v20.dart' as v20; +import 'schema_v21.dart' as v21; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -50,10 +57,46 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v13.DatabaseAtV13(db); case 14: return v14.DatabaseAtV14(db); + case 15: + return v15.DatabaseAtV15(db); + case 16: + return v16.DatabaseAtV16(db); + case 17: + return v17.DatabaseAtV17(db); + case 18: + return v18.DatabaseAtV18(db); + case 19: + return v19.DatabaseAtV19(db); + case 20: + return v20.DatabaseAtV20(db); + case 21: + return v21.DatabaseAtV21(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; + static const versions = const [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ]; } diff --git a/mobile/test/drift/main/generated/schema_v15.dart b/mobile/test/drift/main/generated/schema_v15.dart new file mode 100644 index 0000000000..fa419d7395 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v15.dart @@ -0,0 +1,7913 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV15 extends GeneratedDatabase { + DatabaseAtV15(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 15; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v16.dart b/mobile/test/drift/main/generated/schema_v16.dart new file mode 100644 index 0000000000..0690288d7f --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v16.dart @@ -0,0 +1,8299 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId') + ..write(')')) + .toString(); + } +} + +class StackEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StackEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn primaryAssetId = GeneratedColumn( + 'primary_asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + primaryAssetId, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'stack_entity'; + @override + Set get $primaryKey => {id}; + @override + StackEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StackEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + primaryAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}primary_asset_id'], + )!, + ); + } + + @override + StackEntity createAlias(String alias) { + return StackEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StackEntityData extends DataClass implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + const StackEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['primary_asset_id'] = Variable(primaryAssetId); + return map; + } + + factory StackEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StackEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + primaryAssetId: serializer.fromJson(json['primaryAssetId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'primaryAssetId': serializer.toJson(primaryAssetId), + }; + } + + StackEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) => StackEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + StackEntityData copyWithCompanion(StackEntityCompanion data) { + return StackEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + primaryAssetId: data.primaryAssetId.present + ? data.primaryAssetId.value + : this.primaryAssetId, + ); + } + + @override + String toString() { + return (StringBuffer('StackEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StackEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.primaryAssetId == this.primaryAssetId); +} + +class StackEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value primaryAssetId; + const StackEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.primaryAssetId = const Value.absent(), + }); + StackEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String primaryAssetId, + }) : id = Value(id), + ownerId = Value(ownerId), + primaryAssetId = Value(primaryAssetId); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? primaryAssetId, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (primaryAssetId != null) 'primary_asset_id': primaryAssetId, + }); + } + + StackEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? primaryAssetId, + }) { + return StackEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (primaryAssetId.present) { + map['primary_asset_id'] = Variable(primaryAssetId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StackEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('primaryAssetId: $primaryAssetId') + ..write(')')) + .toString(); + } +} + +class LocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV16 extends GeneratedDatabase { + DatabaseAtV16(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 16; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v17.dart b/mobile/test/drift/main/generated/schema_v17.dart new file mode 100644 index 0000000000..042c069ecd --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v17.dart @@ -0,0 +1,8337 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..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'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV17 extends GeneratedDatabase { + DatabaseAtV17(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 17; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v18.dart b/mobile/test/drift/main/generated/schema_v18.dart new file mode 100644 index 0000000000..c0b1e68894 --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v18.dart @@ -0,0 +1,8342 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..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'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV18 extends GeneratedDatabase { + DatabaseAtV18(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxLatLng, + idxRemoteAssetCloudId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 18; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v19.dart b/mobile/test/drift/main/generated/schema_v19.dart new file mode 100644 index 0000000000..4a8dea806e --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v19.dart @@ -0,0 +1,8397 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..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'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class MemoryEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn data = GeneratedColumn( + 'data', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isSaved = GeneratedColumn( + 'is_saved', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_saved" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn memoryAt = GeneratedColumn( + 'memory_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + late final GeneratedColumn seenAt = GeneratedColumn( + 'seen_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn showAt = GeneratedColumn( + 'show_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn hideAt = GeneratedColumn( + 'hide_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_entity'; + @override + Set get $primaryKey => {id}; + @override + MemoryEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + data: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}data'], + )!, + isSaved: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_saved'], + )!, + memoryAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}memory_at'], + )!, + seenAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}seen_at'], + ), + showAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}show_at'], + ), + hideAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}hide_at'], + ), + ); + } + + @override + MemoryEntity createAlias(String alias) { + return MemoryEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + final int type; + final String data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + const MemoryEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + map['owner_id'] = Variable(ownerId); + map['type'] = Variable(type); + map['data'] = Variable(data); + map['is_saved'] = Variable(isSaved); + map['memory_at'] = Variable(memoryAt); + if (!nullToAbsent || seenAt != null) { + map['seen_at'] = Variable(seenAt); + } + if (!nullToAbsent || showAt != null) { + map['show_at'] = Variable(showAt); + } + if (!nullToAbsent || hideAt != null) { + map['hide_at'] = Variable(hideAt); + } + return map; + } + + factory MemoryEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + deletedAt: serializer.fromJson(json['deletedAt']), + ownerId: serializer.fromJson(json['ownerId']), + type: serializer.fromJson(json['type']), + data: serializer.fromJson(json['data']), + isSaved: serializer.fromJson(json['isSaved']), + memoryAt: serializer.fromJson(json['memoryAt']), + seenAt: serializer.fromJson(json['seenAt']), + showAt: serializer.fromJson(json['showAt']), + hideAt: serializer.fromJson(json['hideAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'deletedAt': serializer.toJson(deletedAt), + 'ownerId': serializer.toJson(ownerId), + 'type': serializer.toJson(type), + 'data': serializer.toJson(data), + 'isSaved': serializer.toJson(isSaved), + 'memoryAt': serializer.toJson(memoryAt), + 'seenAt': serializer.toJson(seenAt), + 'showAt': serializer.toJson(showAt), + 'hideAt': serializer.toJson(hideAt), + }; + } + + MemoryEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + String? ownerId, + int? type, + String? data, + bool? isSaved, + DateTime? memoryAt, + Value seenAt = const Value.absent(), + Value showAt = const Value.absent(), + Value hideAt = const Value.absent(), + }) => MemoryEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt.present ? seenAt.value : this.seenAt, + showAt: showAt.present ? showAt.value : this.showAt, + hideAt: hideAt.present ? hideAt.value : this.hideAt, + ); + MemoryEntityData copyWithCompanion(MemoryEntityCompanion data) { + return MemoryEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + type: data.type.present ? data.type.value : this.type, + data: data.data.present ? data.data.value : this.data, + isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved, + memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt, + seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt, + showAt: data.showAt.present ? data.showAt.value : this.showAt, + hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + deletedAt, + ownerId, + type, + data, + isSaved, + memoryAt, + seenAt, + showAt, + hideAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.deletedAt == this.deletedAt && + other.ownerId == this.ownerId && + other.type == this.type && + other.data == this.data && + other.isSaved == this.isSaved && + other.memoryAt == this.memoryAt && + other.seenAt == this.seenAt && + other.showAt == this.showAt && + other.hideAt == this.hideAt); +} + +class MemoryEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value deletedAt; + final Value ownerId; + final Value type; + final Value data; + final Value isSaved; + final Value memoryAt; + final Value seenAt; + final Value showAt; + final Value hideAt; + const MemoryEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.type = const Value.absent(), + this.data = const Value.absent(), + this.isSaved = const Value.absent(), + this.memoryAt = const Value.absent(), + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }); + MemoryEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.deletedAt = const Value.absent(), + required String ownerId, + required int type, + required String data, + this.isSaved = const Value.absent(), + required DateTime memoryAt, + this.seenAt = const Value.absent(), + this.showAt = const Value.absent(), + this.hideAt = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + type = Value(type), + data = Value(data), + memoryAt = Value(memoryAt); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? deletedAt, + Expression? ownerId, + Expression? type, + Expression? data, + Expression? isSaved, + Expression? memoryAt, + Expression? seenAt, + Expression? showAt, + Expression? hideAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (deletedAt != null) 'deleted_at': deletedAt, + if (ownerId != null) 'owner_id': ownerId, + if (type != null) 'type': type, + if (data != null) 'data': data, + if (isSaved != null) 'is_saved': isSaved, + if (memoryAt != null) 'memory_at': memoryAt, + if (seenAt != null) 'seen_at': seenAt, + if (showAt != null) 'show_at': showAt, + if (hideAt != null) 'hide_at': hideAt, + }); + } + + MemoryEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? ownerId, + Value? type, + Value? data, + Value? isSaved, + Value? memoryAt, + Value? seenAt, + Value? showAt, + Value? hideAt, + }) { + return MemoryEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (data.present) { + map['data'] = Variable(data.value); + } + if (isSaved.present) { + map['is_saved'] = Variable(isSaved.value); + } + if (memoryAt.present) { + map['memory_at'] = Variable(memoryAt.value); + } + if (seenAt.present) { + map['seen_at'] = Variable(seenAt.value); + } + if (showAt.present) { + map['show_at'] = Variable(showAt.value); + } + if (hideAt.present) { + map['hide_at'] = Variable(hideAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('deletedAt: $deletedAt, ') + ..write('ownerId: $ownerId, ') + ..write('type: $type, ') + ..write('data: $data, ') + ..write('isSaved: $isSaved, ') + ..write('memoryAt: $memoryAt, ') + ..write('seenAt: $seenAt, ') + ..write('showAt: $showAt, ') + ..write('hideAt: $hideAt') + ..write(')')) + .toString(); + } +} + +class MemoryAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + MemoryAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn memoryId = GeneratedColumn( + 'memory_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES memory_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, memoryId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'memory_asset_entity'; + @override + Set get $primaryKey => {assetId, memoryId}; + @override + MemoryAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MemoryAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + memoryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}memory_id'], + )!, + ); + } + + @override + MemoryAssetEntity createAlias(String alias) { + return MemoryAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class MemoryAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String memoryId; + const MemoryAssetEntityData({required this.assetId, required this.memoryId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['memory_id'] = Variable(memoryId); + return map; + } + + factory MemoryAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MemoryAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + memoryId: serializer.fromJson(json['memoryId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'memoryId': serializer.toJson(memoryId), + }; + } + + MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) => + MemoryAssetEntityData( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + MemoryAssetEntityData copyWithCompanion(MemoryAssetEntityCompanion data) { + return MemoryAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId, + ); + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, memoryId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MemoryAssetEntityData && + other.assetId == this.assetId && + other.memoryId == this.memoryId); +} + +class MemoryAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value memoryId; + const MemoryAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.memoryId = const Value.absent(), + }); + MemoryAssetEntityCompanion.insert({ + required String assetId, + required String memoryId, + }) : assetId = Value(assetId), + memoryId = Value(memoryId); + static Insertable custom({ + Expression? assetId, + Expression? memoryId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (memoryId != null) 'memory_id': memoryId, + }); + } + + MemoryAssetEntityCompanion copyWith({ + Value? assetId, + Value? memoryId, + }) { + return MemoryAssetEntityCompanion( + assetId: assetId ?? this.assetId, + memoryId: memoryId ?? this.memoryId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (memoryId.present) { + map['memory_id'] = Variable(memoryId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MemoryAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('memoryId: $memoryId') + ..write(')')) + .toString(); + } +} + +class PersonEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PersonEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn faceAssetId = GeneratedColumn( + 'face_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + ); + late final GeneratedColumn isHidden = GeneratedColumn( + 'is_hidden', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_hidden" IN (0, 1))', + ), + ); + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn birthDate = GeneratedColumn( + 'birth_date', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'person_entity'; + @override + Set get $primaryKey => {id}; + @override + PersonEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PersonEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + faceAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}face_asset_id'], + ), + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + isHidden: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_hidden'], + )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + ), + birthDate: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}birth_date'], + ), + ); + } + + @override + PersonEntity createAlias(String alias) { + return PersonEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PersonEntityData extends DataClass + implements Insertable { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String name; + final String? faceAssetId; + final bool isFavorite; + final bool isHidden; + final String? color; + final DateTime? birthDate; + const PersonEntityData({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.name, + this.faceAssetId, + required this.isFavorite, + required this.isHidden, + this.color, + this.birthDate, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + map['name'] = Variable(name); + if (!nullToAbsent || faceAssetId != null) { + map['face_asset_id'] = Variable(faceAssetId); + } + map['is_favorite'] = Variable(isFavorite); + map['is_hidden'] = Variable(isHidden); + if (!nullToAbsent || color != null) { + map['color'] = Variable(color); + } + if (!nullToAbsent || birthDate != null) { + map['birth_date'] = Variable(birthDate); + } + return map; + } + + factory PersonEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PersonEntityData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + name: serializer.fromJson(json['name']), + faceAssetId: serializer.fromJson(json['faceAssetId']), + isFavorite: serializer.fromJson(json['isFavorite']), + isHidden: serializer.fromJson(json['isHidden']), + color: serializer.fromJson(json['color']), + birthDate: serializer.fromJson(json['birthDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'name': serializer.toJson(name), + 'faceAssetId': serializer.toJson(faceAssetId), + 'isFavorite': serializer.toJson(isFavorite), + 'isHidden': serializer.toJson(isHidden), + 'color': serializer.toJson(color), + 'birthDate': serializer.toJson(birthDate), + }; + } + + PersonEntityData copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? name, + Value faceAssetId = const Value.absent(), + bool? isFavorite, + bool? isHidden, + Value color = const Value.absent(), + Value birthDate = const Value.absent(), + }) => PersonEntityData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId.present ? faceAssetId.value : this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color.present ? color.value : this.color, + birthDate: birthDate.present ? birthDate.value : this.birthDate, + ); + PersonEntityData copyWithCompanion(PersonEntityCompanion data) { + return PersonEntityData( + id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + name: data.name.present ? data.name.value : this.name, + faceAssetId: data.faceAssetId.present + ? data.faceAssetId.value + : this.faceAssetId, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + isHidden: data.isHidden.present ? data.isHidden.value : this.isHidden, + color: data.color.present ? data.color.value : this.color, + birthDate: data.birthDate.present ? data.birthDate.value : this.birthDate, + ); + } + + @override + String toString() { + return (StringBuffer('PersonEntityData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + createdAt, + updatedAt, + ownerId, + name, + faceAssetId, + isFavorite, + isHidden, + color, + birthDate, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PersonEntityData && + other.id == this.id && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.name == this.name && + other.faceAssetId == this.faceAssetId && + other.isFavorite == this.isFavorite && + other.isHidden == this.isHidden && + other.color == this.color && + other.birthDate == this.birthDate); +} + +class PersonEntityCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value name; + final Value faceAssetId; + final Value isFavorite; + final Value isHidden; + final Value color; + final Value birthDate; + const PersonEntityCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.name = const Value.absent(), + this.faceAssetId = const Value.absent(), + this.isFavorite = const Value.absent(), + this.isHidden = const Value.absent(), + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }); + PersonEntityCompanion.insert({ + required String id, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + required String name, + this.faceAssetId = const Value.absent(), + required bool isFavorite, + required bool isHidden, + this.color = const Value.absent(), + this.birthDate = const Value.absent(), + }) : id = Value(id), + ownerId = Value(ownerId), + name = Value(name), + isFavorite = Value(isFavorite), + isHidden = Value(isHidden); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? name, + Expression? faceAssetId, + Expression? isFavorite, + Expression? isHidden, + Expression? color, + Expression? birthDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (name != null) 'name': name, + if (faceAssetId != null) 'face_asset_id': faceAssetId, + if (isFavorite != null) 'is_favorite': isFavorite, + if (isHidden != null) 'is_hidden': isHidden, + if (color != null) 'color': color, + if (birthDate != null) 'birth_date': birthDate, + }); + } + + PersonEntityCompanion copyWith({ + Value? id, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? name, + Value? faceAssetId, + Value? isFavorite, + Value? isHidden, + Value? color, + Value? birthDate, + }) { + return PersonEntityCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + name: name ?? this.name, + faceAssetId: faceAssetId ?? this.faceAssetId, + isFavorite: isFavorite ?? this.isFavorite, + isHidden: isHidden ?? this.isHidden, + color: color ?? this.color, + birthDate: birthDate ?? this.birthDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (faceAssetId.present) { + map['face_asset_id'] = Variable(faceAssetId.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (isHidden.present) { + map['is_hidden'] = Variable(isHidden.value); + } + if (color.present) { + map['color'] = Variable(color.value); + } + if (birthDate.present) { + map['birth_date'] = Variable(birthDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PersonEntityCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('name: $name, ') + ..write('faceAssetId: $faceAssetId, ') + ..write('isFavorite: $isFavorite, ') + ..write('isHidden: $isHidden, ') + ..write('color: $color, ') + ..write('birthDate: $birthDate') + ..write(')')) + .toString(); + } +} + +class AssetFaceEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AssetFaceEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn personId = GeneratedColumn( + 'person_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES person_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn imageWidth = GeneratedColumn( + 'image_width', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn imageHeight = GeneratedColumn( + 'image_height', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX1 = GeneratedColumn( + 'bounding_box_x1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY1 = GeneratedColumn( + 'bounding_box_y1', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxX2 = GeneratedColumn( + 'bounding_box_x2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn boundingBoxY2 = GeneratedColumn( + 'bounding_box_y2', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn sourceType = GeneratedColumn( + 'source_type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'asset_face_entity'; + @override + Set get $primaryKey => {id}; + @override + AssetFaceEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AssetFaceEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + personId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}person_id'], + ), + imageWidth: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_width'], + )!, + imageHeight: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}image_height'], + )!, + boundingBoxX1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x1'], + )!, + boundingBoxY1: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y1'], + )!, + boundingBoxX2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_x2'], + )!, + boundingBoxY2: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}bounding_box_y2'], + )!, + sourceType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}source_type'], + )!, + ); + } + + @override + AssetFaceEntity createAlias(String alias) { + return AssetFaceEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AssetFaceEntityData extends DataClass + implements Insertable { + final String id; + final String assetId; + final String? personId; + final int imageWidth; + final int imageHeight; + final int boundingBoxX1; + final int boundingBoxY1; + final int boundingBoxX2; + final int boundingBoxY2; + final String sourceType; + const AssetFaceEntityData({ + required this.id, + required this.assetId, + this.personId, + required this.imageWidth, + required this.imageHeight, + required this.boundingBoxX1, + required this.boundingBoxY1, + required this.boundingBoxX2, + required this.boundingBoxY2, + required this.sourceType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || personId != null) { + map['person_id'] = Variable(personId); + } + map['image_width'] = Variable(imageWidth); + map['image_height'] = Variable(imageHeight); + map['bounding_box_x1'] = Variable(boundingBoxX1); + map['bounding_box_y1'] = Variable(boundingBoxY1); + map['bounding_box_x2'] = Variable(boundingBoxX2); + map['bounding_box_y2'] = Variable(boundingBoxY2); + map['source_type'] = Variable(sourceType); + return map; + } + + factory AssetFaceEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AssetFaceEntityData( + id: serializer.fromJson(json['id']), + assetId: serializer.fromJson(json['assetId']), + personId: serializer.fromJson(json['personId']), + imageWidth: serializer.fromJson(json['imageWidth']), + imageHeight: serializer.fromJson(json['imageHeight']), + boundingBoxX1: serializer.fromJson(json['boundingBoxX1']), + boundingBoxY1: serializer.fromJson(json['boundingBoxY1']), + boundingBoxX2: serializer.fromJson(json['boundingBoxX2']), + boundingBoxY2: serializer.fromJson(json['boundingBoxY2']), + sourceType: serializer.fromJson(json['sourceType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'assetId': serializer.toJson(assetId), + 'personId': serializer.toJson(personId), + 'imageWidth': serializer.toJson(imageWidth), + 'imageHeight': serializer.toJson(imageHeight), + 'boundingBoxX1': serializer.toJson(boundingBoxX1), + 'boundingBoxY1': serializer.toJson(boundingBoxY1), + 'boundingBoxX2': serializer.toJson(boundingBoxX2), + 'boundingBoxY2': serializer.toJson(boundingBoxY2), + 'sourceType': serializer.toJson(sourceType), + }; + } + + AssetFaceEntityData copyWith({ + String? id, + String? assetId, + Value personId = const Value.absent(), + int? imageWidth, + int? imageHeight, + int? boundingBoxX1, + int? boundingBoxY1, + int? boundingBoxX2, + int? boundingBoxY2, + String? sourceType, + }) => AssetFaceEntityData( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId.present ? personId.value : this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + AssetFaceEntityData copyWithCompanion(AssetFaceEntityCompanion data) { + return AssetFaceEntityData( + id: data.id.present ? data.id.value : this.id, + assetId: data.assetId.present ? data.assetId.value : this.assetId, + personId: data.personId.present ? data.personId.value : this.personId, + imageWidth: data.imageWidth.present + ? data.imageWidth.value + : this.imageWidth, + imageHeight: data.imageHeight.present + ? data.imageHeight.value + : this.imageHeight, + boundingBoxX1: data.boundingBoxX1.present + ? data.boundingBoxX1.value + : this.boundingBoxX1, + boundingBoxY1: data.boundingBoxY1.present + ? data.boundingBoxY1.value + : this.boundingBoxY1, + boundingBoxX2: data.boundingBoxX2.present + ? data.boundingBoxX2.value + : this.boundingBoxX2, + boundingBoxY2: data.boundingBoxY2.present + ? data.boundingBoxY2.value + : this.boundingBoxY2, + sourceType: data.sourceType.present + ? data.sourceType.value + : this.sourceType, + ); + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityData(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AssetFaceEntityData && + other.id == this.id && + other.assetId == this.assetId && + other.personId == this.personId && + other.imageWidth == this.imageWidth && + other.imageHeight == this.imageHeight && + other.boundingBoxX1 == this.boundingBoxX1 && + other.boundingBoxY1 == this.boundingBoxY1 && + other.boundingBoxX2 == this.boundingBoxX2 && + other.boundingBoxY2 == this.boundingBoxY2 && + other.sourceType == this.sourceType); +} + +class AssetFaceEntityCompanion extends UpdateCompanion { + final Value id; + final Value assetId; + final Value personId; + final Value imageWidth; + final Value imageHeight; + final Value boundingBoxX1; + final Value boundingBoxY1; + final Value boundingBoxX2; + final Value boundingBoxY2; + final Value sourceType; + const AssetFaceEntityCompanion({ + this.id = const Value.absent(), + this.assetId = const Value.absent(), + this.personId = const Value.absent(), + this.imageWidth = const Value.absent(), + this.imageHeight = const Value.absent(), + this.boundingBoxX1 = const Value.absent(), + this.boundingBoxY1 = const Value.absent(), + this.boundingBoxX2 = const Value.absent(), + this.boundingBoxY2 = const Value.absent(), + this.sourceType = const Value.absent(), + }); + AssetFaceEntityCompanion.insert({ + required String id, + required String assetId, + this.personId = const Value.absent(), + required int imageWidth, + required int imageHeight, + required int boundingBoxX1, + required int boundingBoxY1, + required int boundingBoxX2, + required int boundingBoxY2, + required String sourceType, + }) : id = Value(id), + assetId = Value(assetId), + imageWidth = Value(imageWidth), + imageHeight = Value(imageHeight), + boundingBoxX1 = Value(boundingBoxX1), + boundingBoxY1 = Value(boundingBoxY1), + boundingBoxX2 = Value(boundingBoxX2), + boundingBoxY2 = Value(boundingBoxY2), + sourceType = Value(sourceType); + static Insertable custom({ + Expression? id, + Expression? assetId, + Expression? personId, + Expression? imageWidth, + Expression? imageHeight, + Expression? boundingBoxX1, + Expression? boundingBoxY1, + Expression? boundingBoxX2, + Expression? boundingBoxY2, + Expression? sourceType, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (assetId != null) 'asset_id': assetId, + if (personId != null) 'person_id': personId, + if (imageWidth != null) 'image_width': imageWidth, + if (imageHeight != null) 'image_height': imageHeight, + if (boundingBoxX1 != null) 'bounding_box_x1': boundingBoxX1, + if (boundingBoxY1 != null) 'bounding_box_y1': boundingBoxY1, + if (boundingBoxX2 != null) 'bounding_box_x2': boundingBoxX2, + if (boundingBoxY2 != null) 'bounding_box_y2': boundingBoxY2, + if (sourceType != null) 'source_type': sourceType, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + }) { + return AssetFaceEntityCompanion( + id: id ?? this.id, + assetId: assetId ?? this.assetId, + personId: personId ?? this.personId, + imageWidth: imageWidth ?? this.imageWidth, + imageHeight: imageHeight ?? this.imageHeight, + boundingBoxX1: boundingBoxX1 ?? this.boundingBoxX1, + boundingBoxY1: boundingBoxY1 ?? this.boundingBoxY1, + boundingBoxX2: boundingBoxX2 ?? this.boundingBoxX2, + boundingBoxY2: boundingBoxY2 ?? this.boundingBoxY2, + sourceType: sourceType ?? this.sourceType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (personId.present) { + map['person_id'] = Variable(personId.value); + } + if (imageWidth.present) { + map['image_width'] = Variable(imageWidth.value); + } + if (imageHeight.present) { + map['image_height'] = Variable(imageHeight.value); + } + if (boundingBoxX1.present) { + map['bounding_box_x1'] = Variable(boundingBoxX1.value); + } + if (boundingBoxY1.present) { + map['bounding_box_y1'] = Variable(boundingBoxY1.value); + } + if (boundingBoxX2.present) { + map['bounding_box_x2'] = Variable(boundingBoxX2.value); + } + if (boundingBoxY2.present) { + map['bounding_box_y2'] = Variable(boundingBoxY2.value); + } + if (sourceType.present) { + map['source_type'] = Variable(sourceType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AssetFaceEntityCompanion(') + ..write('id: $id, ') + ..write('assetId: $assetId, ') + ..write('personId: $personId, ') + ..write('imageWidth: $imageWidth, ') + ..write('imageHeight: $imageHeight, ') + ..write('boundingBoxX1: $boundingBoxX1, ') + ..write('boundingBoxY1: $boundingBoxY1, ') + ..write('boundingBoxX2: $boundingBoxX2, ') + ..write('boundingBoxY2: $boundingBoxY2, ') + ..write('sourceType: $sourceType') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV19 extends GeneratedDatabase { + DatabaseAtV19(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAlbumOwnerId = Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetLocalDateTimeDay = Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + late final Index idxRemoteAssetLocalDateTimeMonth = Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 19; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v20.dart b/mobile/test/drift/main/generated/schema_v20.dart new file mode 100644 index 0000000000..8f7b204f7a --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v20.dart @@ -0,0 +1,8471 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..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'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..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, + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ]; + @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'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @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; + final bool isVisible; + final DateTime? deletedAt; + 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, + required this.isVisible, + this.deletedAt, + }); + @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); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + 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']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @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), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + 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, + bool? isVisible, + Value deletedAt = const Value.absent(), + }) => 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, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + 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, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @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('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ); + @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 && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); +} + +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; + final Value isVisible; + final Value deletedAt; + 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(), + this.isVisible = const Value.absent(), + this.deletedAt = 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, + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }) : 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, + Expression? isVisible, + Expression? deletedAt, + }) { + 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, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + Value? isVisible, + Value? deletedAt, + }) { + 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, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @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); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.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('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV20 extends GeneratedDatabase { + DatabaseAtV20(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAlbumOwnerId = Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetLocalDateTimeDay = Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + late final Index idxRemoteAssetLocalDateTimeMonth = Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 20; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/drift/main/generated/schema_v21.dart b/mobile/test/drift/main/generated/schema_v21.dart new file mode 100644 index 0000000000..846eb4aabc --- /dev/null +++ b/mobile/test/drift/main/generated/schema_v21.dart @@ -0,0 +1,8545 @@ +// dart format width=80 +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +import 'package:drift/drift.dart'; + +class UserEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_entity'; + @override + Set get $primaryKey => {id}; + @override + UserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + ); + } + + @override + UserEntity createAlias(String alias) { + return UserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserEntityData extends DataClass implements Insertable { + final String id; + final String name; + final String email; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + const UserEntityData({ + required this.id, + required this.name, + required this.email, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + return map; + } + + factory UserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + }; + } + + UserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + }) => UserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + UserEntityData copyWithCompanion(UserEntityCompanion data) { + return UserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + ); + } + + @override + String toString() { + return (StringBuffer('UserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + hasProfileImage, + profileChangedAt, + avatarColor, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor); +} + +class UserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + const UserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }); + UserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + }); + } + + UserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + }) { + return UserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor') + ..write(')')) + .toString(); + } +} + +class RemoteAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn durationInSeconds = GeneratedColumn( + 'duration_in_seconds', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn checksum = GeneratedColumn( + 'checksum', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isFavorite = GeneratedColumn( + 'is_favorite', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn localDateTime = + GeneratedColumn( + 'local_date_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn thumbHash = GeneratedColumn( + 'thumb_hash', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn livePhotoVideoId = GeneratedColumn( + 'live_photo_video_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn visibility = GeneratedColumn( + 'visibility', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stackId = GeneratedColumn( + 'stack_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn libraryId = GeneratedColumn( + 'library_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn isEdited = GeneratedColumn( + 'is_edited', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_edited" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAssetEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetEntityData( + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}type'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + durationInSeconds: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}duration_in_seconds'], + ), + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + checksum: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}checksum'], + )!, + isFavorite: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_favorite'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + localDateTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}local_date_time'], + ), + thumbHash: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumb_hash'], + ), + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + livePhotoVideoId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}live_photo_video_id'], + ), + visibility: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}visibility'], + )!, + stackId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}stack_id'], + ), + libraryId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}library_id'], + ), + isEdited: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_edited'], + )!, + ); + } + + @override + RemoteAssetEntity createAlias(String alias) { + return RemoteAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends DataClass + implements Insertable { + final String name; + final int type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final String? livePhotoVideoId; + final int visibility; + final String? stackId; + final String? libraryId; + final bool isEdited; + const RemoteAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + this.livePhotoVideoId, + required this.visibility, + this.stackId, + this.libraryId, + required this.isEdited, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['type'] = Variable(type); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = Variable(durationInSeconds); + } + map['id'] = Variable(id); + map['checksum'] = Variable(checksum); + map['is_favorite'] = Variable(isFavorite); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + if (!nullToAbsent || livePhotoVideoId != null) { + map['live_photo_video_id'] = Variable(livePhotoVideoId); + } + map['visibility'] = Variable(visibility); + if (!nullToAbsent || stackId != null) { + map['stack_id'] = Variable(stackId); + } + if (!nullToAbsent || libraryId != null) { + map['library_id'] = Variable(libraryId); + } + map['is_edited'] = Variable(isEdited); + return map; + } + + factory RemoteAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: serializer.fromJson(json['type']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']), + visibility: serializer.fromJson(json['visibility']), + stackId: serializer.fromJson(json['stackId']), + libraryId: serializer.fromJson(json['libraryId']), + isEdited: serializer.fromJson(json['isEdited']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer.toJson(type), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'livePhotoVideoId': serializer.toJson(livePhotoVideoId), + 'visibility': serializer.toJson(visibility), + 'stackId': serializer.toJson(stackId), + 'libraryId': serializer.toJson(libraryId), + 'isEdited': serializer.toJson(isEdited), + }; + } + + RemoteAssetEntityData copyWith({ + String? name, + int? type, + DateTime? createdAt, + DateTime? updatedAt, + Value width = const Value.absent(), + Value height = const Value.absent(), + Value durationInSeconds = const Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + Value localDateTime = const Value.absent(), + Value thumbHash = const Value.absent(), + Value deletedAt = const Value.absent(), + Value livePhotoVideoId = const Value.absent(), + int? visibility, + Value stackId = const Value.absent(), + Value libraryId = const Value.absent(), + bool? isEdited, + }) => RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime.present + ? localDateTime.value + : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + livePhotoVideoId: livePhotoVideoId.present + ? livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId.present ? stackId.value : this.stackId, + libraryId: libraryId.present ? libraryId.value : this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + RemoteAssetEntityData copyWithCompanion(RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: data.isFavorite.present + ? data.isFavorite.value + : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + livePhotoVideoId: data.livePhotoVideoId.present + ? data.livePhotoVideoId.value + : this.livePhotoVideoId, + visibility: data.visibility.present + ? data.visibility.value + : this.visibility, + stackId: data.stackId.present ? data.stackId.value : this.stackId, + libraryId: data.libraryId.present ? data.libraryId.value : this.libraryId, + isEdited: data.isEdited.present ? data.isEdited.value : this.isEdited, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + livePhotoVideoId, + visibility, + stackId, + libraryId, + isEdited, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.livePhotoVideoId == this.livePhotoVideoId && + other.visibility == this.visibility && + other.stackId == this.stackId && + other.libraryId == this.libraryId && + other.isEdited == this.isEdited); +} + +class RemoteAssetEntityCompanion + extends UpdateCompanion { + final Value name; + final Value type; + final Value createdAt; + final Value updatedAt; + final Value width; + final Value height; + final Value durationInSeconds; + final Value id; + final Value checksum; + final Value isFavorite; + final Value ownerId; + final Value localDateTime; + final Value thumbHash; + final Value deletedAt; + final Value livePhotoVideoId; + final Value visibility; + final Value stackId; + final Value libraryId; + final Value isEdited; + const RemoteAssetEntityCompanion({ + this.name = const Value.absent(), + this.type = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + this.id = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.ownerId = const Value.absent(), + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + this.visibility = const Value.absent(), + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required int type, + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.width = const Value.absent(), + this.height = const Value.absent(), + this.durationInSeconds = const Value.absent(), + required String id, + required String checksum, + this.isFavorite = const Value.absent(), + required String ownerId, + this.localDateTime = const Value.absent(), + this.thumbHash = const Value.absent(), + this.deletedAt = const Value.absent(), + this.livePhotoVideoId = const Value.absent(), + required int visibility, + this.stackId = const Value.absent(), + this.libraryId = const Value.absent(), + this.isEdited = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + checksum = Value(checksum), + ownerId = Value(ownerId), + visibility = Value(visibility); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? checksum, + Expression? isFavorite, + Expression? ownerId, + Expression? localDateTime, + Expression? thumbHash, + Expression? deletedAt, + Expression? livePhotoVideoId, + Expression? visibility, + Expression? stackId, + Expression? libraryId, + Expression? isEdited, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId, + if (visibility != null) 'visibility': visibility, + if (stackId != null) 'stack_id': stackId, + if (libraryId != null) 'library_id': libraryId, + if (isEdited != null) 'is_edited': isEdited, + }); + } + + RemoteAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? ownerId, + Value? localDateTime, + Value? thumbHash, + Value? deletedAt, + Value? livePhotoVideoId, + Value? visibility, + Value? stackId, + Value? libraryId, + Value? isEdited, + }) { + return RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, + visibility: visibility ?? this.visibility, + stackId: stackId ?? this.stackId, + libraryId: libraryId ?? this.libraryId, + isEdited: isEdited ?? this.isEdited, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (checksum.present) { + map['checksum'] = Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.value); + } + if (livePhotoVideoId.present) { + map['live_photo_video_id'] = Variable(livePhotoVideoId.value); + } + if (visibility.present) { + map['visibility'] = Variable(visibility.value); + } + if (stackId.present) { + map['stack_id'] = Variable(stackId.value); + } + if (libraryId.present) { + map['library_id'] = Variable(libraryId.value); + } + if (isEdited.present) { + map['is_edited'] = Variable(isEdited.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('livePhotoVideoId: $livePhotoVideoId, ') + ..write('visibility: $visibility, ') + ..write('stackId: $stackId, ') + ..write('libraryId: $libraryId, ') + ..write('isEdited: $isEdited') + ..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'), + ); + late final GeneratedColumn iCloudId = GeneratedColumn( + 'i_cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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 playbackStyle = GeneratedColumn( + 'playback_style', + 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, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ]; + @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'], + )!, + iCloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}i_cloud_id'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @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; + final String? iCloudId; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + final int playbackStyle; + 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, + this.iCloudId, + this.adjustmentTime, + this.latitude, + this.longitude, + required this.playbackStyle, + }); + @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); + if (!nullToAbsent || iCloudId != null) { + map['i_cloud_id'] = Variable(iCloudId); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + map['playback_style'] = Variable(playbackStyle); + 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']), + iCloudId: serializer.fromJson(json['iCloudId']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @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), + 'iCloudId': serializer.toJson(iCloudId), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + 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, + Value iCloudId = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + int? playbackStyle, + }) => 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, + iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + 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, + iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + checksum, + isFavorite, + orientation, + iCloudId, + adjustmentTime, + latitude, + longitude, + playbackStyle, + ); + @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 && + other.iCloudId == this.iCloudId && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.playbackStyle == this.playbackStyle); +} + +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; + final Value iCloudId; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + final Value playbackStyle; + 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = 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(), + this.iCloudId = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.playbackStyle = 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, + Expression? iCloudId, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + Expression? playbackStyle, + }) { + 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, + if (iCloudId != null) 'i_cloud_id': iCloudId, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + LocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? iCloudId, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + Value? playbackStyle, + }) { + 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, + iCloudId: iCloudId ?? this.iCloudId, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @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); + } + if (iCloudId.present) { + map['i_cloud_id'] = Variable(iCloudId.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.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('iCloudId: $iCloudId, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const CustomExpression('\'\''), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn ownerId = GeneratedColumn( + 'owner_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn thumbnailAssetId = GeneratedColumn( + 'thumbnail_asset_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn isActivityEnabled = GeneratedColumn( + 'is_activity_enabled', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn order = GeneratedColumn( + 'order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + Set get $primaryKey => {id}; + @override + RemoteAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + ownerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}owner_id'], + )!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}thumbnail_asset_id'], + ), + isActivityEnabled: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_activity_enabled'], + )!, + order: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}order'], + )!, + ); + } + + @override + RemoteAlbumEntity createAlias(String alias) { + return RemoteAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final int order; + const RemoteAlbumEntityData({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['description'] = Variable(description); + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + map['owner_id'] = Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = Variable(isActivityEnabled); + map['order'] = Variable(order); + return map; + } + + factory RemoteAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: serializer.fromJson(json['order']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson(order), + }; + } + + RemoteAlbumEntityData copyWith({ + String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + Value thumbnailAssetId = const Value.absent(), + bool? isActivityEnabled, + int? order, + }) => RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value createdAt; + final Value updatedAt; + final Value ownerId; + final Value thumbnailAssetId; + final Value isActivityEnabled; + final Value order; + const RemoteAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.ownerId = const Value.absent(), + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + this.order = const Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + required String ownerId, + this.thumbnailAssetId = const Value.absent(), + this.isActivityEnabled = const Value.absent(), + required int order, + }) : id = Value(id), + name = Value(name), + ownerId = Value(ownerId), + order = Value(order); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? createdAt, + Expression? updatedAt, + Expression? ownerId, + Expression? thumbnailAssetId, + Expression? isActivityEnabled, + Expression? order, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + RemoteAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? description, + Value? createdAt, + Value? updatedAt, + Value? ownerId, + Value? thumbnailAssetId, + Value? isActivityEnabled, + Value? order, + }) { + return RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = Variable(order.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} + +class LocalAlbumEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn backupSelection = GeneratedColumn( + 'backup_selection', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn isIosSharedAlbum = GeneratedColumn( + 'is_ios_shared_album', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_ios_shared_album" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn linkedRemoteAlbumId = + GeneratedColumn( + 'linked_remote_album_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE SET NULL', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [ + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + Set get $primaryKey => {id}; + @override + LocalAlbumEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + updatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}updated_at'], + )!, + backupSelection: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}backup_selection'], + )!, + isIosSharedAlbum: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_ios_shared_album'], + )!, + linkedRemoteAlbumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}linked_remote_album_id'], + ), + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumEntity createAlias(String alias) { + return LocalAlbumEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int backupSelection; + final bool isIosSharedAlbum; + final String? linkedRemoteAlbumId; + final bool? marker_; + const LocalAlbumEntityData({ + required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + required this.isIosSharedAlbum, + this.linkedRemoteAlbumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['updated_at'] = Variable(updatedAt); + map['backup_selection'] = Variable(backupSelection); + map['is_ios_shared_album'] = Variable(isIosSharedAlbum); + if (!nullToAbsent || linkedRemoteAlbumId != null) { + map['linked_remote_album_id'] = Variable(linkedRemoteAlbumId); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: serializer.fromJson(json['backupSelection']), + isIosSharedAlbum: serializer.fromJson(json['isIosSharedAlbum']), + linkedRemoteAlbumId: serializer.fromJson( + json['linkedRemoteAlbumId'], + ), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(backupSelection), + 'isIosSharedAlbum': serializer.toJson(isIosSharedAlbum), + 'linkedRemoteAlbumId': serializer.toJson(linkedRemoteAlbumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumEntityData copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? backupSelection, + bool? isIosSharedAlbum, + Value linkedRemoteAlbumId = const Value.absent(), + Value marker_ = const Value.absent(), + }) => LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId.present + ? linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isIosSharedAlbum: data.isIosSharedAlbum.present + ? data.isIosSharedAlbum.value + : this.isIosSharedAlbum, + linkedRemoteAlbumId: data.linkedRemoteAlbumId.present + ? data.linkedRemoteAlbumId.value + : this.linkedRemoteAlbumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + updatedAt, + backupSelection, + isIosSharedAlbum, + linkedRemoteAlbumId, + marker_, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.isIosSharedAlbum == this.isIosSharedAlbum && + other.linkedRemoteAlbumId == this.linkedRemoteAlbumId && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value updatedAt; + final Value backupSelection; + final Value isIosSharedAlbum; + final Value linkedRemoteAlbumId; + final Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.updatedAt = const Value.absent(), + this.backupSelection = const Value.absent(), + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const Value.absent(), + required int backupSelection, + this.isIosSharedAlbum = const Value.absent(), + this.linkedRemoteAlbumId = const Value.absent(), + this.marker_ = const Value.absent(), + }) : id = Value(id), + name = Value(name), + backupSelection = Value(backupSelection); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? updatedAt, + Expression? backupSelection, + Expression? isIosSharedAlbum, + Expression? linkedRemoteAlbumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isIosSharedAlbum != null) 'is_ios_shared_album': isIosSharedAlbum, + if (linkedRemoteAlbumId != null) + 'linked_remote_album_id': linkedRemoteAlbumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumEntityCompanion copyWith({ + Value? id, + Value? name, + Value? updatedAt, + Value? backupSelection, + Value? isIosSharedAlbum, + Value? linkedRemoteAlbumId, + Value? marker_, + }) { + return LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + isIosSharedAlbum: isIosSharedAlbum ?? this.isIosSharedAlbum, + linkedRemoteAlbumId: linkedRemoteAlbumId ?? this.linkedRemoteAlbumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = Variable(backupSelection.value); + } + if (isIosSharedAlbum.present) { + map['is_ios_shared_album'] = Variable(isIosSharedAlbum.value); + } + if (linkedRemoteAlbumId.present) { + map['linked_remote_album_id'] = Variable( + linkedRemoteAlbumId.value, + ); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('isIosSharedAlbum: $isIosSharedAlbum, ') + ..write('linkedRemoteAlbumId: $linkedRemoteAlbumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class LocalAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + LocalAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn marker_ = GeneratedColumn( + 'marker', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("marker" IN (0, 1))', + ), + ); + @override + List get $columns => [assetId, albumId, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + LocalAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + marker_: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}marker'], + ), + ); + } + + @override + LocalAlbumAssetEntity createAlias(String alias) { + return LocalAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + final bool? marker_; + const LocalAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + this.marker_, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + if (!nullToAbsent || marker_ != null) { + map['marker'] = Variable(marker_); + } + return map; + } + + factory LocalAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + 'marker_': serializer.toJson(marker_), + }; + } + + LocalAlbumAssetEntityData copyWith({ + String? assetId, + String? albumId, + Value marker_ = const Value.absent(), + }) => LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumAssetEntityData copyWithCompanion( + LocalAlbumAssetEntityCompanion data, + ) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId && + other.marker_ == this.marker_); +} + +class LocalAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + final Value marker_; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + this.marker_ = const Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + this.marker_ = const Value.absent(), + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + Expression? marker_, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + if (marker_ != null) 'marker': marker_, + }); + } + + LocalAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + Value? marker_, + }) { + return LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (marker_.present) { + map['marker'] = Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} + +class AuthUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + AuthUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn email = GeneratedColumn( + 'email', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + late final GeneratedColumn isAdmin = GeneratedColumn( + 'is_admin', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_admin" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn hasProfileImage = GeneratedColumn( + 'has_profile_image', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("has_profile_image" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn profileChangedAt = + GeneratedColumn( + 'profile_changed_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const CustomExpression('CURRENT_TIMESTAMP'), + ); + late final GeneratedColumn avatarColor = GeneratedColumn( + 'avatar_color', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn quotaSizeInBytes = GeneratedColumn( + 'quota_size_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn quotaUsageInBytes = GeneratedColumn( + 'quota_usage_in_bytes', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn pinCode = GeneratedColumn( + 'pin_code', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'auth_user_entity'; + @override + Set get $primaryKey => {id}; + @override + AuthUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return AuthUserEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + email: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}email'], + )!, + isAdmin: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_admin'], + )!, + hasProfileImage: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}has_profile_image'], + )!, + profileChangedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}profile_changed_at'], + )!, + avatarColor: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}avatar_color'], + )!, + quotaSizeInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_size_in_bytes'], + )!, + quotaUsageInBytes: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}quota_usage_in_bytes'], + )!, + pinCode: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}pin_code'], + ), + ); + } + + @override + AuthUserEntity createAlias(String alias) { + return AuthUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AuthUserEntityData extends DataClass + implements Insertable { + final String id; + final String name; + final String email; + final bool isAdmin; + final bool hasProfileImage; + final DateTime profileChangedAt; + final int avatarColor; + final int quotaSizeInBytes; + final int quotaUsageInBytes; + final String? pinCode; + const AuthUserEntityData({ + required this.id, + required this.name, + required this.email, + required this.isAdmin, + required this.hasProfileImage, + required this.profileChangedAt, + required this.avatarColor, + required this.quotaSizeInBytes, + required this.quotaUsageInBytes, + this.pinCode, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['email'] = Variable(email); + map['is_admin'] = Variable(isAdmin); + map['has_profile_image'] = Variable(hasProfileImage); + map['profile_changed_at'] = Variable(profileChangedAt); + map['avatar_color'] = Variable(avatarColor); + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes); + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes); + if (!nullToAbsent || pinCode != null) { + map['pin_code'] = Variable(pinCode); + } + return map; + } + + factory AuthUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return AuthUserEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + email: serializer.fromJson(json['email']), + isAdmin: serializer.fromJson(json['isAdmin']), + hasProfileImage: serializer.fromJson(json['hasProfileImage']), + profileChangedAt: serializer.fromJson(json['profileChangedAt']), + avatarColor: serializer.fromJson(json['avatarColor']), + quotaSizeInBytes: serializer.fromJson(json['quotaSizeInBytes']), + quotaUsageInBytes: serializer.fromJson(json['quotaUsageInBytes']), + pinCode: serializer.fromJson(json['pinCode']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'email': serializer.toJson(email), + 'isAdmin': serializer.toJson(isAdmin), + 'hasProfileImage': serializer.toJson(hasProfileImage), + 'profileChangedAt': serializer.toJson(profileChangedAt), + 'avatarColor': serializer.toJson(avatarColor), + 'quotaSizeInBytes': serializer.toJson(quotaSizeInBytes), + 'quotaUsageInBytes': serializer.toJson(quotaUsageInBytes), + 'pinCode': serializer.toJson(pinCode), + }; + } + + AuthUserEntityData copyWith({ + String? id, + String? name, + String? email, + bool? isAdmin, + bool? hasProfileImage, + DateTime? profileChangedAt, + int? avatarColor, + int? quotaSizeInBytes, + int? quotaUsageInBytes, + Value pinCode = const Value.absent(), + }) => AuthUserEntityData( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode.present ? pinCode.value : this.pinCode, + ); + AuthUserEntityData copyWithCompanion(AuthUserEntityCompanion data) { + return AuthUserEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + email: data.email.present ? data.email.value : this.email, + isAdmin: data.isAdmin.present ? data.isAdmin.value : this.isAdmin, + hasProfileImage: data.hasProfileImage.present + ? data.hasProfileImage.value + : this.hasProfileImage, + profileChangedAt: data.profileChangedAt.present + ? data.profileChangedAt.value + : this.profileChangedAt, + avatarColor: data.avatarColor.present + ? data.avatarColor.value + : this.avatarColor, + quotaSizeInBytes: data.quotaSizeInBytes.present + ? data.quotaSizeInBytes.value + : this.quotaSizeInBytes, + quotaUsageInBytes: data.quotaUsageInBytes.present + ? data.quotaUsageInBytes.value + : this.quotaUsageInBytes, + pinCode: data.pinCode.present ? data.pinCode.value : this.pinCode, + ); + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + email, + isAdmin, + hasProfileImage, + profileChangedAt, + avatarColor, + quotaSizeInBytes, + quotaUsageInBytes, + pinCode, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is AuthUserEntityData && + other.id == this.id && + other.name == this.name && + other.email == this.email && + other.isAdmin == this.isAdmin && + other.hasProfileImage == this.hasProfileImage && + other.profileChangedAt == this.profileChangedAt && + other.avatarColor == this.avatarColor && + other.quotaSizeInBytes == this.quotaSizeInBytes && + other.quotaUsageInBytes == this.quotaUsageInBytes && + other.pinCode == this.pinCode); +} + +class AuthUserEntityCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value email; + final Value isAdmin; + final Value hasProfileImage; + final Value profileChangedAt; + final Value avatarColor; + final Value quotaSizeInBytes; + final Value quotaUsageInBytes; + final Value pinCode; + const AuthUserEntityCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.email = const Value.absent(), + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + this.avatarColor = const Value.absent(), + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }); + AuthUserEntityCompanion.insert({ + required String id, + required String name, + required String email, + this.isAdmin = const Value.absent(), + this.hasProfileImage = const Value.absent(), + this.profileChangedAt = const Value.absent(), + required int avatarColor, + this.quotaSizeInBytes = const Value.absent(), + this.quotaUsageInBytes = const Value.absent(), + this.pinCode = const Value.absent(), + }) : id = Value(id), + name = Value(name), + email = Value(email), + avatarColor = Value(avatarColor); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? email, + Expression? isAdmin, + Expression? hasProfileImage, + Expression? profileChangedAt, + Expression? avatarColor, + Expression? quotaSizeInBytes, + Expression? quotaUsageInBytes, + Expression? pinCode, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (email != null) 'email': email, + if (isAdmin != null) 'is_admin': isAdmin, + if (hasProfileImage != null) 'has_profile_image': hasProfileImage, + if (profileChangedAt != null) 'profile_changed_at': profileChangedAt, + if (avatarColor != null) 'avatar_color': avatarColor, + if (quotaSizeInBytes != null) 'quota_size_in_bytes': quotaSizeInBytes, + if (quotaUsageInBytes != null) 'quota_usage_in_bytes': quotaUsageInBytes, + if (pinCode != null) 'pin_code': pinCode, + }); + } + + AuthUserEntityCompanion copyWith({ + Value? id, + Value? name, + Value? email, + Value? isAdmin, + Value? hasProfileImage, + Value? profileChangedAt, + Value? avatarColor, + Value? quotaSizeInBytes, + Value? quotaUsageInBytes, + Value? pinCode, + }) { + return AuthUserEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + isAdmin: isAdmin ?? this.isAdmin, + hasProfileImage: hasProfileImage ?? this.hasProfileImage, + profileChangedAt: profileChangedAt ?? this.profileChangedAt, + avatarColor: avatarColor ?? this.avatarColor, + quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes, + quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes, + pinCode: pinCode ?? this.pinCode, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (email.present) { + map['email'] = Variable(email.value); + } + if (isAdmin.present) { + map['is_admin'] = Variable(isAdmin.value); + } + if (hasProfileImage.present) { + map['has_profile_image'] = Variable(hasProfileImage.value); + } + if (profileChangedAt.present) { + map['profile_changed_at'] = Variable(profileChangedAt.value); + } + if (avatarColor.present) { + map['avatar_color'] = Variable(avatarColor.value); + } + if (quotaSizeInBytes.present) { + map['quota_size_in_bytes'] = Variable(quotaSizeInBytes.value); + } + if (quotaUsageInBytes.present) { + map['quota_usage_in_bytes'] = Variable(quotaUsageInBytes.value); + } + if (pinCode.present) { + map['pin_code'] = Variable(pinCode.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AuthUserEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('email: $email, ') + ..write('isAdmin: $isAdmin, ') + ..write('hasProfileImage: $hasProfileImage, ') + ..write('profileChangedAt: $profileChangedAt, ') + ..write('avatarColor: $avatarColor, ') + ..write('quotaSizeInBytes: $quotaSizeInBytes, ') + ..write('quotaUsageInBytes: $quotaUsageInBytes, ') + ..write('pinCode: $pinCode') + ..write(')')) + .toString(); + } +} + +class UserMetadataEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + UserMetadataEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn key = GeneratedColumn( + 'key', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn value = GeneratedColumn( + 'value', + aliasedName, + false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + ); + @override + List get $columns => [userId, key, value]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'user_metadata_entity'; + @override + Set get $primaryKey => {userId, key}; + @override + UserMetadataEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return UserMetadataEntityData( + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + key: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}key'], + )!, + value: attachedDatabase.typeMapping.read( + DriftSqlType.blob, + data['${effectivePrefix}value'], + )!, + ); + } + + @override + UserMetadataEntity createAlias(String alias) { + return UserMetadataEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class UserMetadataEntityData extends DataClass + implements Insertable { + final String userId; + final int key; + final Uint8List value; + const UserMetadataEntityData({ + required this.userId, + required this.key, + required this.value, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['key'] = Variable(key); + map['value'] = Variable(value); + return map; + } + + factory UserMetadataEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return UserMetadataEntityData( + userId: serializer.fromJson(json['userId']), + key: serializer.fromJson(json['key']), + value: serializer.fromJson(json['value']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'key': serializer.toJson(key), + 'value': serializer.toJson(value), + }; + } + + UserMetadataEntityData copyWith({ + String? userId, + int? key, + Uint8List? value, + }) => UserMetadataEntityData( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + UserMetadataEntityData copyWithCompanion(UserMetadataEntityCompanion data) { + return UserMetadataEntityData( + userId: data.userId.present ? data.userId.value : this.userId, + key: data.key.present ? data.key.value : this.key, + value: data.value.present ? data.value.value : this.value, + ); + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityData(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, key, $driftBlobEquality.hash(value)); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is UserMetadataEntityData && + other.userId == this.userId && + other.key == this.key && + $driftBlobEquality.equals(other.value, this.value)); +} + +class UserMetadataEntityCompanion + extends UpdateCompanion { + final Value userId; + final Value key; + final Value value; + const UserMetadataEntityCompanion({ + this.userId = const Value.absent(), + this.key = const Value.absent(), + this.value = const Value.absent(), + }); + UserMetadataEntityCompanion.insert({ + required String userId, + required int key, + required Uint8List value, + }) : userId = Value(userId), + key = Value(key), + value = Value(value); + static Insertable custom({ + Expression? userId, + Expression? key, + Expression? value, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (key != null) 'key': key, + if (value != null) 'value': value, + }); + } + + UserMetadataEntityCompanion copyWith({ + Value? userId, + Value? key, + Value? value, + }) { + return UserMetadataEntityCompanion( + userId: userId ?? this.userId, + key: key ?? this.key, + value: value ?? this.value, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (key.present) { + map['key'] = Variable(key.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UserMetadataEntityCompanion(') + ..write('userId: $userId, ') + ..write('key: $key, ') + ..write('value: $value') + ..write(')')) + .toString(); + } +} + +class PartnerEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + PartnerEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn sharedById = GeneratedColumn( + 'shared_by_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn sharedWithId = GeneratedColumn( + 'shared_with_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn inTimeline = GeneratedColumn( + 'in_timeline', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("in_timeline" IN (0, 1))', + ), + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [sharedById, sharedWithId, inTimeline]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'partner_entity'; + @override + Set get $primaryKey => {sharedById, sharedWithId}; + @override + PartnerEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PartnerEntityData( + sharedById: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_by_id'], + )!, + sharedWithId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}shared_with_id'], + )!, + inTimeline: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}in_timeline'], + )!, + ); + } + + @override + PartnerEntity createAlias(String alias) { + return PartnerEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class PartnerEntityData extends DataClass + implements Insertable { + final String sharedById; + final String sharedWithId; + final bool inTimeline; + const PartnerEntityData({ + required this.sharedById, + required this.sharedWithId, + required this.inTimeline, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['shared_by_id'] = Variable(sharedById); + map['shared_with_id'] = Variable(sharedWithId); + map['in_timeline'] = Variable(inTimeline); + return map; + } + + factory PartnerEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PartnerEntityData( + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), + inTimeline: serializer.fromJson(json['inTimeline']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), + 'inTimeline': serializer.toJson(inTimeline), + }; + } + + PartnerEntityData copyWith({ + String? sharedById, + String? sharedWithId, + bool? inTimeline, + }) => PartnerEntityData( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + PartnerEntityData copyWithCompanion(PartnerEntityCompanion data) { + return PartnerEntityData( + sharedById: data.sharedById.present + ? data.sharedById.value + : this.sharedById, + sharedWithId: data.sharedWithId.present + ? data.sharedWithId.value + : this.sharedWithId, + inTimeline: data.inTimeline.present + ? data.inTimeline.value + : this.inTimeline, + ); + } + + @override + String toString() { + return (StringBuffer('PartnerEntityData(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PartnerEntityData && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && + other.inTimeline == this.inTimeline); +} + +class PartnerEntityCompanion extends UpdateCompanion { + final Value sharedById; + final Value sharedWithId; + final Value inTimeline; + const PartnerEntityCompanion({ + this.sharedById = const Value.absent(), + this.sharedWithId = const Value.absent(), + this.inTimeline = const Value.absent(), + }); + PartnerEntityCompanion.insert({ + required String sharedById, + required String sharedWithId, + this.inTimeline = const Value.absent(), + }) : sharedById = Value(sharedById), + sharedWithId = Value(sharedWithId); + static Insertable custom({ + Expression? sharedById, + Expression? sharedWithId, + Expression? inTimeline, + }) { + return RawValuesInsertable({ + if (sharedById != null) 'shared_by_id': sharedById, + if (sharedWithId != null) 'shared_with_id': sharedWithId, + if (inTimeline != null) 'in_timeline': inTimeline, + }); + } + + PartnerEntityCompanion copyWith({ + Value? sharedById, + Value? sharedWithId, + Value? inTimeline, + }) { + return PartnerEntityCompanion( + sharedById: sharedById ?? this.sharedById, + sharedWithId: sharedWithId ?? this.sharedWithId, + inTimeline: inTimeline ?? this.inTimeline, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (sharedById.present) { + map['shared_by_id'] = Variable(sharedById.value); + } + if (sharedWithId.present) { + map['shared_with_id'] = Variable(sharedWithId.value); + } + if (inTimeline.present) { + map['in_timeline'] = Variable(inTimeline.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PartnerEntityCompanion(') + ..write('sharedById: $sharedById, ') + ..write('sharedWithId: $sharedWithId, ') + ..write('inTimeline: $inTimeline') + ..write(')')) + .toString(); + } +} + +class RemoteExifEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteExifEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn city = GeneratedColumn( + 'city', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn state = GeneratedColumn( + 'state', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn country = GeneratedColumn( + 'country', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn dateTimeOriginal = + GeneratedColumn( + 'date_time_original', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn height = GeneratedColumn( + 'height', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn width = GeneratedColumn( + 'width', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn exposureTime = GeneratedColumn( + 'exposure_time', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn fNumber = GeneratedColumn( + 'f_number', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn focalLength = GeneratedColumn( + 'focal_length', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', + aliasedName, + true, + type: DriftSqlType.double, + requiredDuringInsert: false, + ); + late final GeneratedColumn iso = GeneratedColumn( + 'iso', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn make = GeneratedColumn( + 'make', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn model = GeneratedColumn( + 'model', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn lens = GeneratedColumn( + 'lens', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn orientation = GeneratedColumn( + 'orientation', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn timeZone = GeneratedColumn( + 'time_zone', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn rating = GeneratedColumn( + 'rating', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + late final GeneratedColumn projectionType = GeneratedColumn( + 'projection_type', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteExifEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteExifEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + city: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}city'], + ), + state: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}state'], + ), + country: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}country'], + ), + dateTimeOriginal: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original'], + ), + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + height: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}height'], + ), + width: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}width'], + ), + exposureTime: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}exposure_time'], + ), + fNumber: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}f_number'], + ), + fileSize: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}file_size'], + ), + focalLength: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}focal_length'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + iso: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}iso'], + ), + make: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}make'], + ), + model: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}model'], + ), + lens: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}lens'], + ), + orientation: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}orientation'], + ), + timeZone: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}time_zone'], + ), + rating: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}rating'], + ), + projectionType: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}projection_type'], + ), + ); + } + + @override + RemoteExifEntity createAlias(String alias) { + return RemoteExifEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends DataClass + implements Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final double? fNumber; + final int? fileSize; + final double? focalLength; + final double? latitude; + final double? longitude; + final int? iso; + final String? make; + final String? model; + final String? lens; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData({ + required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.lens, + this.orientation, + this.timeZone, + this.rating, + this.projectionType, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = Variable(model); + } + if (!nullToAbsent || lens != null) { + map['lens'] = Variable(lens); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: serializer.fromJson( + json['dateTimeOriginal'], + ), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + lens: serializer.fromJson(json['lens']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'lens': serializer.toJson(lens), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + RemoteExifEntityData copyWith({ + String? assetId, + Value city = const Value.absent(), + Value state = const Value.absent(), + Value country = const Value.absent(), + Value dateTimeOriginal = const Value.absent(), + Value description = const Value.absent(), + Value height = const Value.absent(), + Value width = const Value.absent(), + Value exposureTime = const Value.absent(), + Value fNumber = const Value.absent(), + Value fileSize = const Value.absent(), + Value focalLength = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value iso = const Value.absent(), + Value make = const Value.absent(), + Value model = const Value.absent(), + Value lens = const Value.absent(), + Value orientation = const Value.absent(), + Value timeZone = const Value.absent(), + Value rating = const Value.absent(), + Value projectionType = const Value.absent(), + }) => RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + lens: lens.present ? lens.value : this.lens, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: projectionType.present + ? projectionType.value + : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: data.description.present + ? data.description.value + : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: data.focalLength.present + ? data.focalLength.value + : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + lens: data.lens.present ? data.lens.value : this.lens, + orientation: data.orientation.present + ? data.orientation.value + : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + lens, + orientation, + timeZone, + rating, + projectionType, + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.lens == this.lens && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion extends UpdateCompanion { + final Value assetId; + final Value city; + final Value state; + final Value country; + final Value dateTimeOriginal; + final Value description; + final Value height; + final Value width; + final Value exposureTime; + final Value fNumber; + final Value fileSize; + final Value focalLength; + final Value latitude; + final Value longitude; + final Value iso; + final Value make; + final Value model; + final Value lens; + final Value orientation; + final Value timeZone; + final Value rating; + final Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const Value.absent(), + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const Value.absent(), + this.state = const Value.absent(), + this.country = const Value.absent(), + this.dateTimeOriginal = const Value.absent(), + this.description = const Value.absent(), + this.height = const Value.absent(), + this.width = const Value.absent(), + this.exposureTime = const Value.absent(), + this.fNumber = const Value.absent(), + this.fileSize = const Value.absent(), + this.focalLength = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.iso = const Value.absent(), + this.make = const Value.absent(), + this.model = const Value.absent(), + this.lens = const Value.absent(), + this.orientation = const Value.absent(), + this.timeZone = const Value.absent(), + this.rating = const Value.absent(), + this.projectionType = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? city, + Expression? state, + Expression? country, + Expression? dateTimeOriginal, + Expression? description, + Expression? height, + Expression? width, + Expression? exposureTime, + Expression? fNumber, + Expression? fileSize, + Expression? focalLength, + Expression? latitude, + Expression? longitude, + Expression? iso, + Expression? make, + Expression? model, + Expression? lens, + Expression? orientation, + Expression? timeZone, + Expression? rating, + Expression? projectionType, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (lens != null) 'lens': lens, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + RemoteExifEntityCompanion copyWith({ + Value? assetId, + Value? city, + Value? state, + Value? country, + Value? dateTimeOriginal, + Value? description, + Value? height, + Value? width, + Value? exposureTime, + Value? fNumber, + Value? fileSize, + Value? focalLength, + Value? latitude, + Value? longitude, + Value? iso, + Value? make, + Value? model, + Value? lens, + Value? orientation, + Value? timeZone, + Value? rating, + Value? projectionType, + }) { + return RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + lens: lens ?? this.lens, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (city.present) { + map['city'] = Variable(city.value); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (country.present) { + map['country'] = Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (width.present) { + map['width'] = Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (iso.present) { + map['iso'] = Variable(iso.value); + } + if (make.present) { + map['make'] = Variable(make.value); + } + if (model.present) { + map['model'] = Variable(model.value); + } + if (lens.present) { + map['lens'] = Variable(lens.value); + } + if (orientation.present) { + map['orientation'] = Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('lens: $lens, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumAssetEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn assetId = GeneratedColumn( + 'asset_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + Set get $primaryKey => {assetId, albumId}; + @override + RemoteAlbumAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + ); + } + + @override + RemoteAlbumAssetEntity createAlias(String alias) { + return RemoteAlbumAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends DataClass + implements Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData({ + required this.assetId, + required this.albumId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + map['album_id'] = Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + RemoteAlbumAssetEntityCompanion data, + ) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const Value.absent(), + this.albumId = const Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = Value(assetId), + albumId = Value(albumId); + static Insertable custom({ + Expression? assetId, + Expression? albumId, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + RemoteAlbumAssetEntityCompanion copyWith({ + Value? assetId, + Value? albumId, + }) { + return RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} + +class RemoteAlbumUserEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAlbumUserEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn albumId = GeneratedColumn( + 'album_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE', + ), + ); + late final GeneratedColumn role = GeneratedColumn( + 'role', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_user_entity'; + @override + Set get $primaryKey => {albumId, userId}; + @override + RemoteAlbumUserEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAlbumUserEntityData( + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_id'], + )!, + userId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}user_id'], + )!, + role: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}role'], + )!, + ); + } + + @override + RemoteAlbumUserEntity createAlias(String alias) { + return RemoteAlbumUserEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumUserEntityData extends DataClass + implements Insertable { + final String albumId; + final String userId; + final int role; + const RemoteAlbumUserEntityData({ + required this.albumId, + required this.userId, + required this.role, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = Variable(albumId); + map['user_id'] = Variable(userId); + map['role'] = Variable(role); + return map; + } + + factory RemoteAlbumUserEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: serializer.fromJson(json['role']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer.toJson(role), + }; + } + + RemoteAlbumUserEntityData copyWith({ + String? albumId, + String? userId, + int? role, + }) => RemoteAlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + RemoteAlbumUserEntityData copyWithCompanion( + RemoteAlbumUserEntityCompanion data, + ) { + return RemoteAlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class RemoteAlbumUserEntityCompanion + extends UpdateCompanion { + final Value albumId; + final Value userId; + final Value role; + const RemoteAlbumUserEntityCompanion({ + this.albumId = const Value.absent(), + this.userId = const Value.absent(), + this.role = const Value.absent(), + }); + RemoteAlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required int role, + }) : albumId = Value(albumId), + userId = Value(userId), + role = Value(role); + static Insertable custom({ + Expression? albumId, + Expression? userId, + Expression? role, + }) { + return RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + RemoteAlbumUserEntityCompanion copyWith({ + Value? albumId, + Value? userId, + Value? role, + }) { + return RemoteAlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (role.present) { + map['role'] = Variable(role.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} + +class RemoteAssetCloudIdEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + RemoteAssetCloudIdEntity(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 cloudId = GeneratedColumn( + 'cloud_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + late final GeneratedColumn adjustmentTime = + GeneratedColumn( + 'adjustment_time', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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, + ); + @override + List get $columns => [ + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_cloud_id_entity'; + @override + Set get $primaryKey => {assetId}; + @override + RemoteAssetCloudIdEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RemoteAssetCloudIdEntityData( + assetId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}asset_id'], + )!, + cloudId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}cloud_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + ), + adjustmentTime: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}adjustment_time'], + ), + latitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}latitude'], + ), + longitude: attachedDatabase.typeMapping.read( + DriftSqlType.double, + data['${effectivePrefix}longitude'], + ), + ); + } + + @override + RemoteAssetCloudIdEntity createAlias(String alias) { + return RemoteAssetCloudIdEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetCloudIdEntityData extends DataClass + implements Insertable { + final String assetId; + final String? cloudId; + final DateTime? createdAt; + final DateTime? adjustmentTime; + final double? latitude; + final double? longitude; + const RemoteAssetCloudIdEntityData({ + required this.assetId, + this.cloudId, + this.createdAt, + this.adjustmentTime, + this.latitude, + this.longitude, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = Variable(assetId); + if (!nullToAbsent || cloudId != null) { + map['cloud_id'] = Variable(cloudId); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || adjustmentTime != null) { + map['adjustment_time'] = Variable(adjustmentTime); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = Variable(longitude); + } + return map; + } + + factory RemoteAssetCloudIdEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RemoteAssetCloudIdEntityData( + assetId: serializer.fromJson(json['assetId']), + cloudId: serializer.fromJson(json['cloudId']), + createdAt: serializer.fromJson(json['createdAt']), + adjustmentTime: serializer.fromJson(json['adjustmentTime']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'cloudId': serializer.toJson(cloudId), + 'createdAt': serializer.toJson(createdAt), + 'adjustmentTime': serializer.toJson(adjustmentTime), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + }; + } + + RemoteAssetCloudIdEntityData copyWith({ + String? assetId, + Value cloudId = const Value.absent(), + Value createdAt = const Value.absent(), + Value adjustmentTime = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + }) => RemoteAssetCloudIdEntityData( + assetId: assetId ?? this.assetId, + cloudId: cloudId.present ? cloudId.value : this.cloudId, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + adjustmentTime: adjustmentTime.present + ? adjustmentTime.value + : this.adjustmentTime, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + ); + RemoteAssetCloudIdEntityData copyWithCompanion( + RemoteAssetCloudIdEntityCompanion data, + ) { + return RemoteAssetCloudIdEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + adjustmentTime: data.adjustmentTime.present + ? data.adjustmentTime.value + : this.adjustmentTime, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityData(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + assetId, + cloudId, + createdAt, + adjustmentTime, + latitude, + longitude, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RemoteAssetCloudIdEntityData && + other.assetId == this.assetId && + other.cloudId == this.cloudId && + other.createdAt == this.createdAt && + other.adjustmentTime == this.adjustmentTime && + other.latitude == this.latitude && + other.longitude == this.longitude); +} + +class RemoteAssetCloudIdEntityCompanion + extends UpdateCompanion { + final Value assetId; + final Value cloudId; + final Value createdAt; + final Value adjustmentTime; + final Value latitude; + final Value longitude; + const RemoteAssetCloudIdEntityCompanion({ + this.assetId = const Value.absent(), + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }); + RemoteAssetCloudIdEntityCompanion.insert({ + required String assetId, + this.cloudId = const Value.absent(), + this.createdAt = const Value.absent(), + this.adjustmentTime = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + }) : assetId = Value(assetId); + static Insertable custom({ + Expression? assetId, + Expression? cloudId, + Expression? createdAt, + Expression? adjustmentTime, + Expression? latitude, + Expression? longitude, + }) { + return RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (cloudId != null) 'cloud_id': cloudId, + if (createdAt != null) 'created_at': createdAt, + if (adjustmentTime != null) 'adjustment_time': adjustmentTime, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + }); + } + + RemoteAssetCloudIdEntityCompanion copyWith({ + Value? assetId, + Value? cloudId, + Value? createdAt, + Value? adjustmentTime, + Value? latitude, + Value? longitude, + }) { + return RemoteAssetCloudIdEntityCompanion( + assetId: assetId ?? this.assetId, + cloudId: cloudId ?? this.cloudId, + createdAt: createdAt ?? this.createdAt, + adjustmentTime: adjustmentTime ?? this.adjustmentTime, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = Variable(assetId.value); + } + if (cloudId.present) { + map['cloud_id'] = Variable(cloudId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (adjustmentTime.present) { + map['adjustment_time'] = Variable(adjustmentTime.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetCloudIdEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('cloudId: $cloudId, ') + ..write('createdAt: $createdAt, ') + ..write('adjustmentTime: $adjustmentTime, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude') + ..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, + ); + late final GeneratedColumn isVisible = GeneratedColumn( + 'is_visible', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_visible" IN (0, 1))', + ), + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn deletedAt = GeneratedColumn( + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + @override + List get $columns => [ + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ]; + @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'], + )!, + isVisible: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_visible'], + )!, + deletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}deleted_at'], + ), + ); + } + + @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; + final bool isVisible; + final DateTime? deletedAt; + 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, + required this.isVisible, + this.deletedAt, + }); + @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); + map['is_visible'] = Variable(isVisible); + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = Variable(deletedAt); + } + 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']), + isVisible: serializer.fromJson(json['isVisible']), + deletedAt: serializer.fromJson(json['deletedAt']), + ); + } + @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), + 'isVisible': serializer.toJson(isVisible), + 'deletedAt': serializer.toJson(deletedAt), + }; + } + + 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, + bool? isVisible, + Value deletedAt = const Value.absent(), + }) => 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, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + ); + 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, + isVisible: data.isVisible.present ? data.isVisible.value : this.isVisible, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + ); + } + + @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('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + assetId, + personId, + imageWidth, + imageHeight, + boundingBoxX1, + boundingBoxY1, + boundingBoxX2, + boundingBoxY2, + sourceType, + isVisible, + deletedAt, + ); + @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 && + other.isVisible == this.isVisible && + other.deletedAt == this.deletedAt); +} + +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; + final Value isVisible; + final Value deletedAt; + 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(), + this.isVisible = const Value.absent(), + this.deletedAt = 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, + this.isVisible = const Value.absent(), + this.deletedAt = const Value.absent(), + }) : 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, + Expression? isVisible, + Expression? deletedAt, + }) { + 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, + if (isVisible != null) 'is_visible': isVisible, + if (deletedAt != null) 'deleted_at': deletedAt, + }); + } + + AssetFaceEntityCompanion copyWith({ + Value? id, + Value? assetId, + Value? personId, + Value? imageWidth, + Value? imageHeight, + Value? boundingBoxX1, + Value? boundingBoxY1, + Value? boundingBoxX2, + Value? boundingBoxY2, + Value? sourceType, + Value? isVisible, + Value? deletedAt, + }) { + 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, + isVisible: isVisible ?? this.isVisible, + deletedAt: deletedAt ?? this.deletedAt, + ); + } + + @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); + } + if (isVisible.present) { + map['is_visible'] = Variable(isVisible.value); + } + if (deletedAt.present) { + map['deleted_at'] = Variable(deletedAt.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('isVisible: $isVisible, ') + ..write('deletedAt: $deletedAt') + ..write(')')) + .toString(); + } +} + +class StoreEntity extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + StoreEntity(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn stringValue = GeneratedColumn( + 'string_value', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + late final GeneratedColumn intValue = GeneratedColumn( + 'int_value', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, stringValue, intValue]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'store_entity'; + @override + Set get $primaryKey => {id}; + @override + StoreEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StoreEntityData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + stringValue: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}string_value'], + ), + intValue: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}int_value'], + ), + ); + } + + @override + StoreEntity createAlias(String alias) { + return StoreEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class StoreEntityData extends DataClass implements Insertable { + final int id; + final String? stringValue; + final int? intValue; + const StoreEntityData({required this.id, this.stringValue, this.intValue}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || stringValue != null) { + map['string_value'] = Variable(stringValue); + } + if (!nullToAbsent || intValue != null) { + map['int_value'] = Variable(intValue); + } + return map; + } + + factory StoreEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StoreEntityData( + id: serializer.fromJson(json['id']), + stringValue: serializer.fromJson(json['stringValue']), + intValue: serializer.fromJson(json['intValue']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'stringValue': serializer.toJson(stringValue), + 'intValue': serializer.toJson(intValue), + }; + } + + StoreEntityData copyWith({ + int? id, + Value stringValue = const Value.absent(), + Value intValue = const Value.absent(), + }) => StoreEntityData( + id: id ?? this.id, + stringValue: stringValue.present ? stringValue.value : this.stringValue, + intValue: intValue.present ? intValue.value : this.intValue, + ); + StoreEntityData copyWithCompanion(StoreEntityCompanion data) { + return StoreEntityData( + id: data.id.present ? data.id.value : this.id, + stringValue: data.stringValue.present + ? data.stringValue.value + : this.stringValue, + intValue: data.intValue.present ? data.intValue.value : this.intValue, + ); + } + + @override + String toString() { + return (StringBuffer('StoreEntityData(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, stringValue, intValue); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StoreEntityData && + other.id == this.id && + other.stringValue == this.stringValue && + other.intValue == this.intValue); +} + +class StoreEntityCompanion extends UpdateCompanion { + final Value id; + final Value stringValue; + final Value intValue; + const StoreEntityCompanion({ + this.id = const Value.absent(), + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }); + StoreEntityCompanion.insert({ + required int id, + this.stringValue = const Value.absent(), + this.intValue = const Value.absent(), + }) : id = Value(id); + static Insertable custom({ + Expression? id, + Expression? stringValue, + Expression? intValue, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (stringValue != null) 'string_value': stringValue, + if (intValue != null) 'int_value': intValue, + }); + } + + StoreEntityCompanion copyWith({ + Value? id, + Value? stringValue, + Value? intValue, + }) { + return StoreEntityCompanion( + id: id ?? this.id, + stringValue: stringValue ?? this.stringValue, + intValue: intValue ?? this.intValue, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (stringValue.present) { + map['string_value'] = Variable(stringValue.value); + } + if (intValue.present) { + map['int_value'] = Variable(intValue.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StoreEntityCompanion(') + ..write('id: $id, ') + ..write('stringValue: $stringValue, ') + ..write('intValue: $intValue') + ..write(')')) + .toString(); + } +} + +class TrashedLocalAssetEntity extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TrashedLocalAssetEntity(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 albumId = GeneratedColumn( + 'album_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'), + ); + late final GeneratedColumn source = GeneratedColumn( + 'source', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + ); + late final GeneratedColumn playbackStyle = GeneratedColumn( + 'playback_style', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const CustomExpression('0'), + ); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'trashed_local_asset_entity'; + @override + Set get $primaryKey => {id, albumId}; + @override + TrashedLocalAssetEntityData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TrashedLocalAssetEntityData( + 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'], + )!, + albumId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}album_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'], + )!, + source: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}source'], + )!, + playbackStyle: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}playback_style'], + )!, + ); + } + + @override + TrashedLocalAssetEntity createAlias(String alias) { + return TrashedLocalAssetEntity(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class TrashedLocalAssetEntityData 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 albumId; + final String? checksum; + final bool isFavorite; + final int orientation; + final int source; + final int playbackStyle; + const TrashedLocalAssetEntityData({ + required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.id, + required this.albumId, + this.checksum, + required this.isFavorite, + required this.orientation, + required this.source, + required this.playbackStyle, + }); + @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['album_id'] = Variable(albumId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = Variable(checksum); + } + map['is_favorite'] = Variable(isFavorite); + map['orientation'] = Variable(orientation); + map['source'] = Variable(source); + map['playback_style'] = Variable(playbackStyle); + return map; + } + + factory TrashedLocalAssetEntityData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TrashedLocalAssetEntityData( + 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']), + albumId: serializer.fromJson(json['albumId']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + orientation: serializer.fromJson(json['orientation']), + source: serializer.fromJson(json['source']), + playbackStyle: serializer.fromJson(json['playbackStyle']), + ); + } + @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), + 'albumId': serializer.toJson(albumId), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'orientation': serializer.toJson(orientation), + 'source': serializer.toJson(source), + 'playbackStyle': serializer.toJson(playbackStyle), + }; + } + + TrashedLocalAssetEntityData 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? albumId, + Value checksum = const Value.absent(), + bool? isFavorite, + int? orientation, + int? source, + int? playbackStyle, + }) => TrashedLocalAssetEntityData( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + TrashedLocalAssetEntityData copyWithCompanion( + TrashedLocalAssetEntityCompanion data, + ) { + return TrashedLocalAssetEntityData( + 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, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + 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, + source: data.source.present ? data.source.value : this.source, + playbackStyle: data.playbackStyle.present + ? data.playbackStyle.value + : this.playbackStyle, + ); + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityData(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + id, + albumId, + checksum, + isFavorite, + orientation, + source, + playbackStyle, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TrashedLocalAssetEntityData && + 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.albumId == this.albumId && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.orientation == this.orientation && + other.source == this.source && + other.playbackStyle == this.playbackStyle); +} + +class TrashedLocalAssetEntityCompanion + 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 albumId; + final Value checksum; + final Value isFavorite; + final Value orientation; + final Value source; + final Value playbackStyle; + const TrashedLocalAssetEntityCompanion({ + 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.albumId = const Value.absent(), + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + this.source = const Value.absent(), + this.playbackStyle = const Value.absent(), + }); + TrashedLocalAssetEntityCompanion.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 albumId, + this.checksum = const Value.absent(), + this.isFavorite = const Value.absent(), + this.orientation = const Value.absent(), + required int source, + this.playbackStyle = const Value.absent(), + }) : name = Value(name), + type = Value(type), + id = Value(id), + albumId = Value(albumId), + source = Value(source); + static Insertable custom({ + Expression? name, + Expression? type, + Expression? createdAt, + Expression? updatedAt, + Expression? width, + Expression? height, + Expression? durationInSeconds, + Expression? id, + Expression? albumId, + Expression? checksum, + Expression? isFavorite, + Expression? orientation, + Expression? source, + Expression? playbackStyle, + }) { + 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 (albumId != null) 'album_id': albumId, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, + if (playbackStyle != null) 'playback_style': playbackStyle, + }); + } + + TrashedLocalAssetEntityCompanion copyWith({ + Value? name, + Value? type, + Value? createdAt, + Value? updatedAt, + Value? width, + Value? height, + Value? durationInSeconds, + Value? id, + Value? albumId, + Value? checksum, + Value? isFavorite, + Value? orientation, + Value? source, + Value? playbackStyle, + }) { + return TrashedLocalAssetEntityCompanion( + 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, + albumId: albumId ?? this.albumId, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + orientation: orientation ?? this.orientation, + source: source ?? this.source, + playbackStyle: playbackStyle ?? this.playbackStyle, + ); + } + + @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 (albumId.present) { + map['album_id'] = Variable(albumId.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); + } + if (source.present) { + map['source'] = Variable(source.value); + } + if (playbackStyle.present) { + map['playback_style'] = Variable(playbackStyle.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TrashedLocalAssetEntityCompanion(') + ..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('albumId: $albumId, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('orientation: $orientation, ') + ..write('source: $source, ') + ..write('playbackStyle: $playbackStyle') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV21 extends GeneratedDatabase { + DatabaseAtV21(QueryExecutor e) : super(e); + late final UserEntity userEntity = UserEntity(this); + late final RemoteAssetEntity remoteAssetEntity = RemoteAssetEntity(this); + late final StackEntity stackEntity = StackEntity(this); + late final LocalAssetEntity localAssetEntity = LocalAssetEntity(this); + late final RemoteAlbumEntity remoteAlbumEntity = RemoteAlbumEntity(this); + late final LocalAlbumEntity localAlbumEntity = LocalAlbumEntity(this); + late final LocalAlbumAssetEntity localAlbumAssetEntity = + LocalAlbumAssetEntity(this); + late final Index idxLocalAlbumAssetAlbumAsset = Index( + 'idx_local_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAlbumOwnerId = Index( + 'idx_remote_album_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)', + ); + late final Index idxLocalAssetChecksum = Index( + 'idx_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)', + ); + late final Index idxLocalAssetCloudId = Index( + 'idx_local_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)', + ); + late final Index idxStackPrimaryAssetId = Index( + 'idx_stack_primary_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)', + ); + late final Index idxRemoteAssetOwnerChecksum = Index( + 'idx_remote_asset_owner_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)', + ); + late final Index uQRemoteAssetsOwnerChecksum = Index( + 'UQ_remote_assets_owner_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)', + ); + late final Index uQRemoteAssetsOwnerLibraryChecksum = Index( + 'UQ_remote_assets_owner_library_checksum', + 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)', + ); + late final Index idxRemoteAssetChecksum = Index( + 'idx_remote_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)', + ); + late final Index idxRemoteAssetStackId = Index( + 'idx_remote_asset_stack_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)', + ); + late final Index idxRemoteAssetLocalDateTimeDay = Index( + 'idx_remote_asset_local_date_time_day', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))', + ); + late final Index idxRemoteAssetLocalDateTimeMonth = Index( + 'idx_remote_asset_local_date_time_month', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))', + ); + late final AuthUserEntity authUserEntity = AuthUserEntity(this); + late final UserMetadataEntity userMetadataEntity = UserMetadataEntity(this); + late final PartnerEntity partnerEntity = PartnerEntity(this); + late final RemoteExifEntity remoteExifEntity = RemoteExifEntity(this); + late final RemoteAlbumAssetEntity remoteAlbumAssetEntity = + RemoteAlbumAssetEntity(this); + late final RemoteAlbumUserEntity remoteAlbumUserEntity = + RemoteAlbumUserEntity(this); + late final RemoteAssetCloudIdEntity remoteAssetCloudIdEntity = + RemoteAssetCloudIdEntity(this); + late final MemoryEntity memoryEntity = MemoryEntity(this); + late final MemoryAssetEntity memoryAssetEntity = MemoryAssetEntity(this); + late final PersonEntity personEntity = PersonEntity(this); + late final AssetFaceEntity assetFaceEntity = AssetFaceEntity(this); + late final StoreEntity storeEntity = StoreEntity(this); + late final TrashedLocalAssetEntity trashedLocalAssetEntity = + TrashedLocalAssetEntity(this); + late final Index idxPartnerSharedWithId = Index( + 'idx_partner_shared_with_id', + 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)', + ); + late final Index idxLatLng = Index( + 'idx_lat_lng', + 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)', + ); + late final Index idxRemoteAlbumAssetAlbumAsset = Index( + 'idx_remote_album_asset_album_asset', + 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)', + ); + late final Index idxRemoteAssetCloudId = Index( + 'idx_remote_asset_cloud_id', + 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)', + ); + late final Index idxPersonOwnerId = Index( + 'idx_person_owner_id', + 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)', + ); + late final Index idxAssetFacePersonId = Index( + 'idx_asset_face_person_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)', + ); + late final Index idxAssetFaceAssetId = Index( + 'idx_asset_face_asset_id', + 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)', + ); + late final Index idxTrashedLocalAssetChecksum = Index( + 'idx_trashed_local_asset_checksum', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)', + ); + late final Index idxTrashedLocalAssetAlbum = Index( + 'idx_trashed_local_asset_album', + 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)', + ); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + userEntity, + remoteAssetEntity, + stackEntity, + localAssetEntity, + remoteAlbumEntity, + localAlbumEntity, + localAlbumAssetEntity, + idxLocalAlbumAssetAlbumAsset, + idxRemoteAlbumOwnerId, + idxLocalAssetChecksum, + idxLocalAssetCloudId, + idxStackPrimaryAssetId, + idxRemoteAssetOwnerChecksum, + uQRemoteAssetsOwnerChecksum, + uQRemoteAssetsOwnerLibraryChecksum, + idxRemoteAssetChecksum, + idxRemoteAssetStackId, + idxRemoteAssetLocalDateTimeDay, + idxRemoteAssetLocalDateTimeMonth, + authUserEntity, + userMetadataEntity, + partnerEntity, + remoteExifEntity, + remoteAlbumAssetEntity, + remoteAlbumUserEntity, + remoteAssetCloudIdEntity, + memoryEntity, + memoryAssetEntity, + personEntity, + assetFaceEntity, + storeEntity, + trashedLocalAssetEntity, + idxPartnerSharedWithId, + idxLatLng, + idxRemoteAlbumAssetAlbumAsset, + idxRemoteAssetCloudId, + idxPersonOwnerId, + idxAssetFacePersonId, + idxAssetFaceAssetId, + idxTrashedLocalAssetChecksum, + idxTrashedLocalAssetAlbum, + ]; + @override + int get schemaVersion => 21; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); +} diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 8d92011999..90a7f11737 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -64,6 +64,8 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2025), updatedAt: DateTime(2025, 2), + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, ); static final image2 = LocalAsset( @@ -72,5 +74,7 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2000), updatedAt: DateTime(20021), + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, ); } diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index 523984f966..c2254c0a03 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -94,25 +94,11 @@ abstract final class SyncStreamStub { required String ack, DateTime? trashedAt, }) { - return _assetV1( - id: id, - checksum: checksum, - deletedAt: trashedAt ?? DateTime(2025, 1, 1), - ack: ack, - ); + return _assetV1(id: id, checksum: checksum, deletedAt: trashedAt ?? DateTime(2025, 1, 1), ack: ack); } - static SyncEvent assetModified({ - required String id, - required String checksum, - required String ack, - }) { - return _assetV1( - id: id, - checksum: checksum, - deletedAt: null, - ack: ack, - ); + static SyncEvent assetModified({required String id, required String checksum, required String ack}) { + return _assetV1(id: id, checksum: checksum, deletedAt: null, ack: ack); } static SyncEvent _assetV1({ @@ -140,6 +126,9 @@ abstract final class SyncStreamStub { thumbhash: null, type: AssetTypeEnum.IMAGE, visibility: AssetVisibility.timeline, + width: null, + height: null, + isEdited: false, ), ack: ack, ); diff --git a/mobile/test/infrastructure/repositories/backup_repository_test.dart b/mobile/test/infrastructure/repositories/backup_repository_test.dart new file mode 100644 index 0000000000..c042685779 --- /dev/null +++ b/mobile/test/infrastructure/repositories/backup_repository_test.dart @@ -0,0 +1,244 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; + +import '../../medium/repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftBackupRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftBackupRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getAllCounts', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns zeros when no albums exist', () async { + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('returns zeros when no selected albums exist', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.none); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('counts asset in selected album as total and remainder', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + expect(result.processing, 0); + }); + + test('backed up asset reduces remainder', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: userId); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 0); + expect(result.processing, 0); + }); + + test('asset with null checksum is counted as processing', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + expect(result.processing, 1); + }); + + test('asset in excluded album is not counted even if also in selected album', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final excludedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 0); + expect(result.remainder, 0); + }); + + test('counts assets across multiple selected albums without duplicates', () async { + final album1 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final album2 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + // Same asset in two selected albums + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: asset.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + }); + + test('backed up asset for different user is still counted as remainder', () async { + final otherUser = await ctx.newUser(); + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: otherUser.id); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 1); + expect(result.remainder, 1); + }); + + test('mixed assets produce correct combined counts', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + + // backed up + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local1.id); + + // not backed up, has checksum + final local2 = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local2.id); + + // processing (null checksum) + final local3 = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: local3.id); + + final result = await sut.getAllCounts(userId); + expect(result.total, 3); + expect(result.remainder, 2); // local2 + local3 + expect(result.processing, 1); // local3 + }); + }); + + group('getCandidates', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns empty list when no selected albums exist', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.none); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('returns asset in selected album that is not backed up', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + + test('excludes asset already backed up for the same user', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: userId); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('includes asset backed up for a different user', () async { + final otherUser = await ctx.newUser(); + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final remote = await ctx.newRemoteAsset(ownerId: otherUser.id); + final local = await ctx.newLocalAsset(checksum: remote.checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, local.id); + }); + + test('excludes asset in excluded album even if also in selected album', () async { + final selectedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final excludedAlbum = await ctx.newLocalAlbum(backupSelection: BackupSelection.excluded); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: selectedAlbum.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('excludes asset with null checksum when onlyHashed is true', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result, isEmpty); + }); + + test('includes asset with null checksum when onlyHashed is false', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(checksumOption: const Option.none()); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getCandidates(userId, onlyHashed: false); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + + test('returns assets ordered by createdAt descending', () async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset1 = await ctx.newLocalAsset(createdAt: DateTime(2024, 1, 1)); + final asset2 = await ctx.newLocalAsset(createdAt: DateTime(2024, 3, 1)); + final asset3 = await ctx.newLocalAsset(createdAt: DateTime(2024, 2, 1)); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset1.id); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset2.id); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: asset3.id); + + final result = await sut.getCandidates(userId); + expect(result.map((a) => a.id).toList(), [asset2.id, asset3.id, asset1.id]); + }); + + test('does not return duplicate when asset is in multiple selected albums', () async { + final album1 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final album2 = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final asset = await ctx.newLocalAsset(); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: asset.id); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: asset.id); + + final result = await sut.getCandidates(userId); + expect(result.length, 1); + expect(result.first.id, asset.id); + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart new file mode 100644 index 0000000000..88f8d00e03 --- /dev/null +++ b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart @@ -0,0 +1,570 @@ +import 'package:flutter_test/flutter_test.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/utils/option.dart'; + +import '../../medium/repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftLocalAssetRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftLocalAssetRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getRemovalCandidates', () { + final cutoffDate = DateTime(2024, 1, 1); + final beforeCutoff = DateTime(2023, 12, 31); + final afterCutoff = DateTime(2024, 1, 2); + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns only assets that match all criteria', () async { + final otherUser = await ctx.newUser(); + + // Asset 1: Should be included - backed up, before cutoff, correct owner, not deleted, not favorite + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final includedAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + // Asset 2: Should NOT be included - not backed up (no remote asset) + await ctx.newLocalAsset(createdAt: beforeCutoff); + + // Asset 3: Should NOT be included - after cutoff date + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); + + // Asset 4: Should NOT be included - different owner + final otherRemoteAsset = await ctx.newRemoteAsset(ownerId: otherUser.id); + await ctx.newLocalAsset(checksum: otherRemoteAsset.checksum, createdAt: beforeCutoff); + + // Asset 5: Should NOT be included - remote asset is deleted + final deletedAsset = await ctx.newRemoteAsset(ownerId: userId, deletedAt: DateTime(2024, 1, 1)); + await ctx.newLocalAsset(checksum: deletedAsset.checksum, createdAt: beforeCutoff); + + // Asset 6: Should NOT be included - is favorite (when keepFavorites=true) + final favoriteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: favoriteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets.length, 1); + expect(result.assets.first.id, includedAsset.id); + }); + + test('includes favorites when keepFavorites is false', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final favoriteAsset = await ctx.newLocalAsset( + checksum: remoteAsset.checksum, + createdAt: beforeCutoff, + isFavorite: true, + ); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); + expect(result.assets.length, 1); + expect(result.assets.first.id, favoriteAsset.id); + expect(result.assets.first.isFavorite, true); + }); + + test('excludes asset when both local and remote are favorites', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('excludes asset when only local is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff, isFavorite: true); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('excludes asset when only remote is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId, isFavorite: true); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets, isEmpty); + }); + + test('includes asset when neither local nor remote is favorite', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + expect(result.assets.length, 1); + expect(result.assets.first.id, localAsset.id); + }); + + test('keepMediaType photosOnly returns only videos for deletion', () async { + final photoAsset = await ctx.newRemoteAsset(ownerId: userId); + // Photo - should be kept + await ctx.newLocalAsset(checksum: photoAsset.checksum, createdAt: beforeCutoff); + + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + // Video - should be deleted + final videoLocalAsset = await ctx.newLocalAsset( + checksum: videoRemoteAsset.checksum, + createdAt: beforeCutoff, + type: AssetType.video, + ); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly); + expect(result.assets.length, 1); + expect(result.assets.first.id, videoLocalAsset.id); + expect(result.assets.first.type, AssetType.video); + }); + + test('keepMediaType videosOnly returns only photos for deletion', () async { + // Photo - should be deleted + final photoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final photoAsset = await ctx.newLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); + + // Video - should be kept + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: videoRemoteAsset.checksum, createdAt: beforeCutoff, type: AssetType.video); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.videosOnly); + expect(result.assets.length, 1); + expect(result.assets.first.id, photoAsset.id); + expect(result.assets.first.type, AssetType.image); + }); + + test('returns both photos and videos with keepMediaType.all', () async { + // Photo + final photoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final photoAsset = await ctx.newLocalAsset(checksum: photoRemoteAsset.checksum, createdAt: beforeCutoff); + + // Video + final videoRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final videoAsset = await ctx.newLocalAsset( + checksum: videoRemoteAsset.checksum, + createdAt: beforeCutoff, + type: AssetType.video, + ); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.none); + expect(result.assets.length, 2); + final ids = result.assets.map((a) => a.id).toSet(); + expect(ids, containsAll([photoAsset.id, videoAsset.id])); + }); + + test('excludes assets in iOS shared albums', () async { + // Regular album + final regularAlbum = await ctx.newLocalAlbum(); + + // iOS shared album + final sharedAlbum = await ctx.newLocalAlbum(isIosSharedAlbum: true); + + // Asset in regular album (should be included) + final regularRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final regularAsset = await ctx.newLocalAsset(checksum: regularRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: regularAsset.id); + + // Asset in iOS shared album (should be excluded) + final sharedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final sharedAsset = await ctx.newLocalAsset(checksum: sharedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: sharedAlbum.id, assetId: sharedAsset.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets.length, 1); + expect(result.assets.first.id, regularAsset.id); + }); + + test('includes assets at exact cutoff date', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: cutoffDate); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets.length, 1); + expect(result.assets.first.id, localAsset.id); + }); + + test('returns empty list when no assets match criteria', () async { + // Only assets after cutoff + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: afterCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets, isEmpty); + }); + + test('handles multiple assets with same checksum', () async { + // Two local assets with same checksum (edge case, but should handle it) + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets.length, 2); + expect(result.assets.map((a) => a.checksum).toSet(), equals({remoteAsset.checksum})); + }); + + test('includes assets not in any album', () async { + // Asset not in any album should be included + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets.length, 1); + expect(result.assets.first.id, localAsset.id); + }); + + test('excludes asset that is in both regular and iOS shared album', () async { + // Regular album + final regularAlbum = await ctx.newLocalAlbum(); + + // iOS shared album + final sharedAlbum = await ctx.newLocalAlbum(isIosSharedAlbum: true); + + // Asset in BOTH albums - should be excluded because it's in an iOS shared album + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: localAsset.id); + await ctx.newLocalAlbumAsset(albumId: sharedAlbum.id, assetId: localAsset.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets, isEmpty); + }); + + test('excludes assets with null checksum (not backed up)', () async { + // Asset with null checksum cannot be matched to remote asset + await ctx.newLocalAsset(checksumOption: const Option.none()); + + final result = await sut.getRemovalCandidates(userId, cutoffDate); + expect(result.assets, isEmpty); + }); + + test('excludes assets in user-excluded albums', () async { + // Create two regular albums + final includeAlbum = await ctx.newLocalAlbum(); + final excludeAlbum = await ctx.newLocalAlbum(); + + // Asset in included album - should be included + final includedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final includedAsset = await ctx.newLocalAsset(checksum: includedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: includeAlbum.id, assetId: includedAsset.id); + + // Asset in excluded album - should NOT be included + final excludedRemoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final excludedAsset = await ctx.newLocalAsset(checksum: excludedRemoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: excludeAlbum.id, assetId: excludedAsset.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludeAlbum.id}); + + expect(result.assets.length, 1); + expect(result.assets.first.id, includedAsset.id); + }); + + test('excludes assets that are in any of multiple excluded albums', () async { + // Create multiple albums + final album1 = await ctx.newLocalAlbum(); + final album2 = await ctx.newLocalAlbum(); + final album3 = await ctx.newLocalAlbum(); + + // Asset in album-1 (excluded) - should NOT be included + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: local1.id); + + // Asset in album-2 (excluded) - should NOT be included + final remote2 = await ctx.newRemoteAsset(ownerId: userId); + final local2 = await ctx.newLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album2.id, assetId: local2.id); + + // Asset in album-3 (not excluded) - should be included + final remote3 = await ctx.newRemoteAsset(ownerId: userId); + final local3 = await ctx.newLocalAsset(checksum: remote3.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album3.id, assetId: local3.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {album1.id, album2.id}); + expect(result.assets.length, 1); + expect(result.assets.first.id, local3.id); + }); + + test('excludes asset that is in both excluded and non-excluded album', () async { + final includedAlbum = await ctx.newLocalAlbum(); + final excludedAlbum = await ctx.newLocalAlbum(); + + // Asset in BOTH albums - should be excluded because it's in an excluded album + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final localAsset = await ctx.newLocalAsset(checksum: remoteAsset.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: includedAlbum.id, assetId: localAsset.id); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: localAsset.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludedAlbum.id}); + expect(result.assets, isEmpty); + }); + + test('includes all assets when excludedAlbumIds is empty', () async { + final album1 = await ctx.newLocalAlbum(); + + final remote1 = await ctx.newRemoteAsset(ownerId: userId); + final local1 = await ctx.newLocalAsset(checksum: remote1.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: album1.id, assetId: local1.id); + + final remote2 = await ctx.newRemoteAsset(ownerId: userId); + await ctx.newLocalAsset(checksum: remote2.checksum, createdAt: beforeCutoff); + + // Empty excludedAlbumIds should include all eligible assets + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); + expect(result.assets.length, 2); + }); + + test('excludes asset not in any album when album is excluded', () async { + final excludedAlbum = await ctx.newLocalAlbum(); + + // Asset NOT in any album - should be included + final noAlbumRemote = await ctx.newRemoteAsset(ownerId: userId); + final noAlbumAsset = await ctx.newLocalAsset(checksum: noAlbumRemote.checksum, createdAt: beforeCutoff); + + // Asset in excluded album - should NOT be included + final excludedRemote = await ctx.newRemoteAsset(ownerId: userId); + final excludedAsset = await ctx.newLocalAsset(checksum: excludedRemote.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: excludedAsset.id); + + final result = await sut.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {excludedAlbum.id}); + expect(result.assets.length, 1); + expect(result.assets.first.id, noAlbumAsset.id); + }); + + test('combines excludedAlbumIds with keepMediaType correctly', () async { + final excludedAlbum = await ctx.newLocalAlbum(); + final regularAlbum = await ctx.newLocalAlbum(); + + // Photo in excluded album - should NOT be included (album excluded) + final photoExcludedRemote = await ctx.newRemoteAsset(ownerId: userId); + final photoExcludedAsset = await ctx.newLocalAsset( + checksum: photoExcludedRemote.checksum, + createdAt: beforeCutoff, + ); + await ctx.newLocalAlbumAsset(albumId: excludedAlbum.id, assetId: photoExcludedAsset.id); + + // Video in regular album - should be included (keepMediaType photosOnly = delete videos) + final videoRemote = await ctx.newRemoteAsset(ownerId: userId); + final videoAsset = await ctx.newLocalAsset( + checksum: videoRemote.checksum, + createdAt: beforeCutoff, + type: AssetType.video, + ); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: videoAsset.id); + + // Photo in regular album - should NOT be included (keepMediaType photosOnly = keep photos) + final photoRegularRemote = await ctx.newRemoteAsset(ownerId: userId); + final photoRegularAsset = await ctx.newLocalAsset(checksum: photoRegularRemote.checksum, createdAt: beforeCutoff); + await ctx.newLocalAlbumAsset(albumId: regularAlbum.id, assetId: photoRegularAsset.id); + + final result = await sut.getRemovalCandidates( + userId, + cutoffDate, + keepMediaType: AssetKeepType.photosOnly, + keepAlbumIds: {excludedAlbum.id}, + ); + + expect(result.assets.length, 1); + expect(result.assets.first.id, videoAsset.id); + }); + }); + + group('reconcileHashesFromCloudId', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('updates local asset checksum when all metadata matches', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); + }); + + test('does not update when local asset already has checksum', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final remoteCloudAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + + final localAsset = await ctx.newLocalAsset( + checksum: 'existing', + iCloudId: remoteCloudAsset.cloudId, + createdAt: remoteCloudAsset.createdAt, + adjustmentTime: remoteCloudAsset.adjustmentTime, + latitude: remoteCloudAsset.latitude, + longitude: remoteCloudAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, 'existing'); + }); + + test('does not update when adjustment_time does not match', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, adjustmentTime: DateTime(2024, 1, 12)); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: DateTime(2026, 1, 12), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('does not update when latitude does not match', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, latitude: const Option.none()); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: 40.7128, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('does not update when longitude does not match', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: (-74.006).toOption()); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: 0.0, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('does not update when createdAt does not match', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, createdAt: DateTime(2024, 1, 5)); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: DateTime(2024, 6, 1), + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('does not update when iCloudId is null', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: null, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('does not update when cloudId does not match iCloudId', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: 'different-cloud-id', + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('handles partial null metadata fields matching correctly', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId( + id: remoteAsset.id, + adjustmentTimeOption: const Option.none(), + ); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTimeOption: const Option.none(), + latitude: cloudIdAsset.latitude, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, remoteAsset.checksum); + }); + + test('does not update when one has null and other has value', () async { + final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id); + final localAsset = await ctx.newLocalAsset( + checksumOption: const Option.none(), + iCloudId: cloudIdAsset.cloudId, + createdAt: cloudIdAsset.createdAt, + adjustmentTime: cloudIdAsset.adjustmentTime, + latitude: null, + longitude: cloudIdAsset.longitude, + ); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + + test('handles no matching assets gracefully', () async { + final localAsset = await ctx.newLocalAsset(checksumOption: const Option.none(), iCloudId: 'cloud-no-match'); + + await sut.reconcileHashesFromCloudId(); + final updated = await sut.getById(localAsset.id); + expect(updated?.checksum, isNull); + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart new file mode 100644 index 0000000000..a25a9d92a7 --- /dev/null +++ b/mobile/test/infrastructure/repositories/merged_asset_drift_test.dart @@ -0,0 +1,51 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.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/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +void main() { + late Drift db; + + setUp(() { + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + }); + + tearDown(() async { + await db.close(); + }); + + test('mergedBucket falls back to createdAt when localDateTime is null', () async { + const userId = 'user-1'; + final createdAt = DateTime(2024, 1, 1, 12); + + await db + .into(db.userEntity) + .insert(UserEntityCompanion.insert(id: userId, email: 'user-1@test.dev', name: 'User 1')); + + await db + .into(db.remoteAssetEntity) + .insert( + RemoteAssetEntityCompanion.insert( + id: 'asset-1', + name: 'asset-1.jpg', + type: AssetType.image, + checksum: 'checksum-1', + ownerId: userId, + visibility: AssetVisibility.timeline, + createdAt: Value(createdAt), + updatedAt: Value(createdAt), + localDateTime: const Value(null), + ), + ); + + final buckets = await db.mergedAssetDrift.mergedBucket(groupBy: GroupAssetsBy.day.index, userIds: [userId]).get(); + + expect(buckets, hasLength(1)); + expect(buckets.single.assetCount, 1); + expect(buckets.single.bucketDate, isNotEmpty); + }); +} diff --git a/mobile/test/infrastructure/repositories/remote_album_repository_test.dart b/mobile/test/infrastructure/repositories/remote_album_repository_test.dart new file mode 100644 index 0000000000..1bc797f6e1 --- /dev/null +++ b/mobile/test/infrastructure/repositories/remote_album_repository_test.dart @@ -0,0 +1,210 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; + +import '../../medium/repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftRemoteAlbumRepository sut; + + setUp(() async { + ctx = MediumRepositoryContext(); + sut = DriftRemoteAlbumRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getSortedAlbumIds', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + }); + + test('returns empty list when albumIds is empty', () async { + final result = await sut.getSortedAlbumIds([], aggregation: AssetDateAggregation.start); + expect(result, isEmpty); + }); + + test('returns single album when only one album exists', () async { + final album = await ctx.newRemoteAlbum(ownerId: userId); + final asset = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album.id, assetId: asset.id); + + final result = await sut.getSortedAlbumIds([album.id], aggregation: AssetDateAggregation.start); + expect(result, [album.id]); + }); + + test('sorts albums by start date (MIN) ascending', () async { + // Album 1: Assets from Jan 10 to Jan 20 (start: Jan 10) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + + // Album 2: Assets from Jan 5 to Jan 15 (start: Jan 5) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset4.id); + + // Album 3: Assets from Jan 25 to Jan 30 (start: Jan 25) + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 30)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset5.id); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset6.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.start); + + // Expected order: album2 (Jan 5), album1 (Jan 10), album3 (Jan 25) + expect(result, [album2.id, album1.id, album3.id]); + }); + + test('sorts albums by end date (MAX) ascending', () async { + // Album 1: Assets from Jan 10 to Jan 20 (end: Jan 20) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + + // Album 2: Assets from Jan 5 to Jan 15 (end: Jan 15) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset4.id); + + // Album 3: Assets from Jan 25 to Jan 30 (end: Jan 30) + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 30)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset5.id); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset6.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.end); + + // Expected order: album2 (Jan 15), album1 (Jan 20), album3 (Jan 30) + expect(result, [album2.id, album1.id, album3.id]); + }); + + test('handles albums with single asset', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + expect(result, [album2.id, album1.id]); + }); + + test('only returns requested album IDs in the result', () async { + // Create 3 albums + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset3.id); + + // Only request album1 and album3 + final result = await sut.getSortedAlbumIds([album1.id, album3.id], aggregation: AssetDateAggregation.start); + + // Should only return album1 and album3, not album2 + expect(result, [album1.id, album3.id]); + }); + + test('handles albums with same date correctly', () async { + final sameDate = DateTime(2024, 1, 10); + + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: sameDate); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: sameDate); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // Both albums have the same date, so both should be returned + expect(result, hasLength(2)); + expect(result, containsAll([album1.id, album2.id])); + }); + + test('handles albums across different years', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2023, 12, 25)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset2.id); + + final album3 = await ctx.newRemoteAlbum(ownerId: userId); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2025, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album3.id, assetId: asset3.id); + + final result = await sut.getSortedAlbumIds([ + album1.id, + album2.id, + album3.id, + ], aggregation: AssetDateAggregation.start); + + expect(result, [album1.id, album2.id, album3.id]); + }); + + test('handles album with multiple assets correctly', () async { + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + // Album 1 has 5 assets from Jan 5 to Jan 25 + final asset1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + final asset2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + final asset3 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 15)); + final asset4 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + final asset5 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 25)); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset1.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset2.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset3.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset4.id); + await ctx.insertRemoteAlbumAsset(albumId: album1.id, assetId: asset5.id); + + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final asset6 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 1)); + await ctx.insertRemoteAlbumAsset(albumId: album2.id, assetId: asset6.id); + + final resultStart = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // album2 (Jan 1) should come before album1 (Jan 5) + expect(resultStart, [album2.id, album1.id]); + + final resultEnd = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.end); + + // album2 (Jan 1) should come before album1 (Jan 25) + expect(resultEnd, [album2.id, album1.id]); + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 660b8206bb..62aae4c0da 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -72,8 +73,14 @@ void main() { Future streamChanges( Future Function(List, Function() abort, Function() reset) onDataCallback, + SemVer serverVersion, ) { - return sut.streamChanges(onDataCallback, batchSize: testBatchSize, httpClient: mockHttpClient); + return sut.streamChanges( + onDataCallback, + batchSize: testBatchSize, + httpClient: mockHttpClient, + serverVersion: serverVersion, + ); } test('streamChanges stops processing stream when abort is called', () async { @@ -94,7 +101,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); // Give the stream subscription time to start (longer delay to account for mock delay) await Future.delayed(const Duration(milliseconds: 50)); @@ -145,7 +152,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -197,7 +204,7 @@ void main() { } } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -244,7 +251,7 @@ void main() { onDataCallCount++; } - final streamChangesFuture = streamChanges(onDataCallback); + final streamChangesFuture = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); await Future.delayed(const Duration(milliseconds: 50)); @@ -271,7 +278,7 @@ void main() { onDataCallCount++; } - final future = streamChanges(onDataCallback); + final future = streamChanges(onDataCallback, const SemVer(major: 2, minor: 5, patch: 0)); errorBodyController.add(utf8.encode('{"error":"Unauthorized"}')); await errorBodyController.close(); diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index aac384c29e..2d4af5b308 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor 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_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/user.repository.dart'; @@ -46,6 +47,8 @@ class MockDriftBackupRepository extends Mock implements DriftBackupRepository {} class MockUploadRepository extends Mock implements UploadRepository {} +class MockSyncMigrationRepository extends Mock implements SyncMigrationRepository {} + // API Repos class MockUserApiRepository extends Mock implements UserApiRepository {} diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart new file mode 100644 index 0000000000..2c4758400c --- /dev/null +++ b/mobile/test/medium/repository_context.dart @@ -0,0 +1,246 @@ +import 'dart:math'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.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/user.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/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_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:uuid/uuid.dart'; + +class MediumRepositoryContext { + final Drift db; + final Random _random = Random(); + + MediumRepositoryContext() : db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + + Future dispose() async { + await db.close(); + } + + static Value _resolveUndefined(T? plain, Option? option, T fallback) { + if (plain != null) { + return Value(plain); + } + + return _resolveOption(option, fallback); + } + + static Value _resolveOption(Option? option, T fallback) { + if (option != null) { + return option.fold(Value.new, Value.absent); + } + + return Value(fallback); + } + + Future newUser({ + String? id, + String? email, + AvatarColor? avatarColor, + DateTime? profileChangedAt, + bool? hasProfileImage, + }) async { + id = id ?? const Uuid().v4(); + return await db + .into(db.userEntity) + .insertReturning( + UserEntityCompanion( + id: Value(id), + email: Value(email ?? '$id@test.com'), + name: Value(email ?? 'user_$id'), + avatarColor: Value(avatarColor ?? AvatarColor.values[_random.nextInt(AvatarColor.values.length)]), + profileChangedAt: Value(profileChangedAt ?? DateTime.now()), + hasProfileImage: Value(hasProfileImage ?? false), + ), + ); + } + + Future newRemoteAsset({ + String? id, + String? checksum, + String? ownerId, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + AssetType? type, + AssetVisibility? visibility, + int? durationInSeconds, + int? width, + int? height, + bool? isFavorite, + bool? isEdited, + String? livePhotoVideoId, + String? stackId, + String? thumbHash, + String? libraryId, + }) async { + id = id ?? const Uuid().v4(); + createdAt = createdAt ?? DateTime.now(); + return db + .into(db.remoteAssetEntity) + .insertReturning( + RemoteAssetEntityCompanion( + id: Value(id), + name: Value('remote_$id.jpg'), + checksum: Value(checksum ?? const Uuid().v4()), + type: Value(type ?? AssetType.image), + createdAt: Value(createdAt), + updatedAt: Value(updatedAt ?? DateTime.now()), + ownerId: Value(ownerId ?? const Uuid().v4()), + visibility: Value(visibility ?? AssetVisibility.timeline), + deletedAt: Value(deletedAt), + durationInSeconds: Value(durationInSeconds ?? 0), + width: Value(width ?? _random.nextInt(1000)), + height: Value(height ?? _random.nextInt(1000)), + isFavorite: Value(isFavorite ?? false), + isEdited: Value(isEdited ?? false), + livePhotoVideoId: Value(livePhotoVideoId), + stackId: Value(stackId), + localDateTime: Value(createdAt.toLocal()), + thumbHash: Value(thumbHash ?? const Uuid().v4()), + libraryId: Value(libraryId ?? const Uuid().v4()), + ), + ); + } + + Future newRemoteAssetCloudId({ + String? id, + String? cloudId, + DateTime? createdAt, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + Option? latitude, + Option? longitude, + }) { + return db + .into(db.remoteAssetCloudIdEntity) + .insertReturning( + RemoteAssetCloudIdEntityCompanion( + assetId: Value(id ?? const Uuid().v4()), + cloudId: Value(cloudId ?? const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: _resolveOption(latitude, _random.nextDouble() * 180 - 90), + longitude: _resolveOption(longitude, _random.nextDouble() * 360 - 180), + ), + ); + } + + Future newRemoteAlbum({ + String? id, + String? name, + String? ownerId, + DateTime? createdAt, + DateTime? updatedAt, + String? description, + bool? isActivityEnabled, + AlbumAssetOrder? order, + String? thumbnailAssetId, + }) async { + id = id ?? const Uuid().v4(); + return db + .into(db.remoteAlbumEntity) + .insertReturning( + RemoteAlbumEntityCompanion( + id: Value(id), + name: Value(name ?? 'remote_album_$id'), + ownerId: Value(ownerId ?? const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + updatedAt: Value(updatedAt ?? DateTime.now()), + description: Value(description ?? 'Description for album $id'), + isActivityEnabled: Value(isActivityEnabled ?? false), + order: Value(order ?? AlbumAssetOrder.asc), + thumbnailAssetId: Value(thumbnailAssetId), + ), + ); + } + + Future insertRemoteAlbumAsset({required String albumId, required String assetId}) { + return db + .into(db.remoteAlbumAssetEntity) + .insert(RemoteAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); + } + + Future newLocalAsset({ + String? id, + String? name, + String? checksum, + Option? checksumOption, + DateTime? createdAt, + AssetType? type, + bool? isFavorite, + String? iCloudId, + DateTime? adjustmentTime, + Option? adjustmentTimeOption, + double? latitude, + double? longitude, + int? width, + int? height, + int? durationInSeconds, + int? orientation, + DateTime? updatedAt, + }) async { + id = id ?? const Uuid().v4(); + return db + .into(db.localAssetEntity) + .insertReturning( + LocalAssetEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_$id.jpg'), + height: Value(height ?? _random.nextInt(1000)), + width: Value(width ?? _random.nextInt(1000)), + durationInSeconds: Value(durationInSeconds ?? 0), + orientation: Value(orientation ?? 0), + updatedAt: Value(updatedAt ?? DateTime.now()), + checksum: _resolveUndefined(checksum, checksumOption, const Uuid().v4()), + createdAt: Value(createdAt ?? DateTime.now()), + type: Value(type ?? AssetType.image), + isFavorite: Value(isFavorite ?? false), + iCloudId: Value(iCloudId ?? const Uuid().v4()), + adjustmentTime: _resolveUndefined(adjustmentTime, adjustmentTimeOption, DateTime.now()), + latitude: Value(latitude ?? _random.nextDouble() * 180 - 90), + longitude: Value(longitude ?? _random.nextDouble() * 360 - 180), + ), + ); + } + + Future newLocalAlbum({ + String? id, + String? name, + DateTime? updatedAt, + BackupSelection? backupSelection, + bool? isIosSharedAlbum, + String? linkedRemoteAlbumId, + }) { + id = id ?? const Uuid().v4(); + return db + .into(db.localAlbumEntity) + .insertReturning( + LocalAlbumEntityCompanion( + id: Value(id), + name: Value(name ?? 'local_album_$id'), + updatedAt: Value(updatedAt ?? DateTime.now()), + backupSelection: Value(backupSelection ?? BackupSelection.none), + isIosSharedAlbum: Value(isIosSharedAlbum ?? false), + linkedRemoteAlbumId: Value(linkedRemoteAlbumId), + ), + ); + } + + Future newLocalAlbumAsset({required String albumId, required String assetId}) { + return db + .into(db.localAlbumAssetEntity) + .insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId)); + } +} diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index b956c4bfb9..a577b0544f 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -45,5 +45,17 @@ void main() { addDefault(value, keys, defaultValue); expect(value['alpha']['beta'], 'gamma'); }); + + test('addDefault with null', () { + dynamic value = jsonDecode(""" +{ + "download": { + "archiveSize": 4294967296, + "includeEmbeddedVideos": false + } +} +"""); + expect(value['download']['unknownKey'], isNull); + }); }); } diff --git a/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart b/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart new file mode 100644 index 0000000000..1706b4d307 --- /dev/null +++ b/mobile/test/presentation/widgets/remote_album/drift_album_option_widget_test.dart @@ -0,0 +1,500 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; + +import '../../../widget_tester_extensions.dart'; + +void main() { + group('DriftRemoteAlbumOption', () { + testWidgets('shows kebab menu icon button', (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + expect(find.byIcon(Icons.more_vert_rounded), findsOneWidget); + }); + + testWidgets('opens menu when icon button is tapped', (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + }); + + testWidgets('shows edit album option when onEditAlbum is provided', + (tester) async { + bool editCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () => editCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + + await tester.tap(find.byIcon(Icons.edit)); + await tester.pumpAndSettle(); + + expect(editCalled, isTrue); + }); + + testWidgets('hides edit album option when onEditAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsNothing); + }); + + testWidgets('shows add photos option when onAddPhotos is provided', + (tester) async { + bool addPhotosCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () => addPhotosCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + + await tester.tap(find.byIcon(Icons.add_a_photo)); + await tester.pumpAndSettle(); + + expect(addPhotosCalled, isTrue); + }); + + testWidgets('hides add photos option when onAddPhotos is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_a_photo), findsNothing); + }); + + testWidgets('shows add users option when onAddUsers is provided', + (tester) async { + bool addUsersCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddUsers: () => addUsersCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.group_add), findsOneWidget); + + await tester.tap(find.byIcon(Icons.group_add)); + await tester.pumpAndSettle(); + + expect(addUsersCalled, isTrue); + }); + + testWidgets('hides add users option when onAddUsers is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.group_add), findsNothing); + }); + + testWidgets('shows leave album option when onLeaveAlbum is provided', + (tester) async { + bool leaveAlbumCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onLeaveAlbum: () => leaveAlbumCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + + await tester.tap(find.byIcon(Icons.person_remove_rounded)); + await tester.pumpAndSettle(); + + expect(leaveAlbumCalled, isTrue); + }); + + testWidgets('hides leave album option when onLeaveAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + }); + + testWidgets( + 'shows toggle album order option when onToggleAlbumOrder is provided', + (tester) async { + bool toggleOrderCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onToggleAlbumOrder: () => toggleOrderCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + + await tester.tap(find.byIcon(Icons.swap_vert_rounded)); + await tester.pumpAndSettle(); + + expect(toggleOrderCalled, isTrue); + }); + + testWidgets('hides toggle album order option when onToggleAlbumOrder is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + }); + + testWidgets( + 'shows create shared link option when onCreateSharedLink is provided', + (tester) async { + bool createSharedLinkCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onCreateSharedLink: () => createSharedLinkCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.link), findsOneWidget); + + await tester.tap(find.byIcon(Icons.link)); + await tester.pumpAndSettle(); + + expect(createSharedLinkCalled, isTrue); + }); + + testWidgets('hides create shared link option when onCreateSharedLink is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.link), findsNothing); + }); + + testWidgets('shows options option when onShowOptions is provided', + (tester) async { + bool showOptionsCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onShowOptions: () => showOptionsCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.settings), findsOneWidget); + + await tester.tap(find.byIcon(Icons.settings)); + await tester.pumpAndSettle(); + + expect(showOptionsCalled, isTrue); + }); + + testWidgets('hides options option when onShowOptions is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.settings), findsNothing); + }); + + testWidgets('shows delete album option when onDeleteAlbum is provided', + (tester) async { + bool deleteAlbumCalled = false; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onDeleteAlbum: () => deleteAlbumCalled = true, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.delete), findsOneWidget); + + await tester.tap(find.byIcon(Icons.delete)); + await tester.pumpAndSettle(); + + expect(deleteAlbumCalled, isTrue); + }); + + testWidgets('hides delete album option when onDeleteAlbum is null', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('shows divider before delete album option', (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byType(Divider), findsOneWidget); + }); + + testWidgets('shows all options when all callbacks are provided', + (tester) async { + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onAddPhotos: () {}, + onAddUsers: () {}, + onLeaveAlbum: () {}, + onToggleAlbumOrder: () {}, + onCreateSharedLink: () {}, + onShowOptions: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsOneWidget); + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + expect(find.byIcon(Icons.group_add), findsOneWidget); + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + expect(find.byIcon(Icons.link), findsOneWidget); + expect(find.byIcon(Icons.settings), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + expect(find.byType(Divider), findsOneWidget); + }); + + testWidgets('shows no options when all callbacks are null', (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.add_a_photo), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.settings), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('uses custom icon color when provided', (tester) async { + const customColor = Colors.red; + + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption( + iconColor: customColor, + ), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.color, equals(customColor)); + }); + + testWidgets('uses default white color when iconColor is null', + (tester) async { + await tester.pumpConsumerWidget( + const DriftRemoteAlbumOption(), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.color, equals(Colors.white)); + }); + + testWidgets('applies icon shadows when provided', (tester) async { + final shadows = [ + const Shadow(offset: Offset(0, 2), blurRadius: 5, color: Colors.black), + ]; + + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + iconShadows: shadows, + ), + ); + + final iconButton = tester.widget(find.byType(IconButton)); + final icon = iconButton.icon as Icon; + + expect(icon.shadows, equals(shadows)); + }); + + group('owner vs non-owner scenarios', () { + testWidgets('owner sees all management options', (tester) async { + // Simulating owner scenario - all callbacks provided + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onEditAlbum: () {}, + onAddPhotos: () {}, + onAddUsers: () {}, + onToggleAlbumOrder: () {}, + onCreateSharedLink: () {}, + onShowOptions: () {}, + onDeleteAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Owner should see all management options + expect(find.byIcon(Icons.edit), findsOneWidget); + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + expect(find.byIcon(Icons.group_add), findsOneWidget); + expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget); + expect(find.byIcon(Icons.link), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + // Owner should NOT see leave album + expect(find.byIcon(Icons.person_remove_rounded), findsNothing); + }); + + testWidgets('non-owner with editor role sees limited options', + (tester) async { + // Simulating non-owner with editor role - can add photos, show options, leave + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onAddPhotos: () {}, + onShowOptions: () {}, + onLeaveAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Editor can add photos + expect(find.byIcon(Icons.add_a_photo), findsOneWidget); + // Can see options + expect(find.byIcon(Icons.settings), findsOneWidget); + // Can leave album + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + // Cannot see owner-only options + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + + testWidgets('non-owner viewer sees minimal options', (tester) async { + // Simulating viewer - can only show options and leave + await tester.pumpConsumerWidget( + DriftRemoteAlbumOption( + onShowOptions: () {}, + onLeaveAlbum: () {}, + ), + ); + + await tester.tap(find.byIcon(Icons.more_vert_rounded)); + await tester.pumpAndSettle(); + + // Can see options + expect(find.byIcon(Icons.settings), findsOneWidget); + // Can leave album + expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget); + // Cannot see any other options + expect(find.byIcon(Icons.edit), findsNothing); + expect(find.byIcon(Icons.add_a_photo), findsNothing); + expect(find.byIcon(Icons.group_add), findsNothing); + expect(find.byIcon(Icons.swap_vert_rounded), findsNothing); + expect(find.byIcon(Icons.link), findsNothing); + expect(find.byIcon(Icons.delete), findsNothing); + }); + }); + }); +} diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart new file mode 100644 index 0000000000..87263c9ae7 --- /dev/null +++ b/mobile/test/services/action.service_test.dart @@ -0,0 +1,118 @@ +import 'package:drift/drift.dart' as drift; +import 'package:drift/native.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_mobile/services/action.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../infrastructure/repository.mock.dart'; +import '../repository.mocks.dart'; + +class MockDownloadRepository extends Mock implements DownloadRepository {} + +void main() { + late ActionService sut; + + late MockAssetApiRepository assetApiRepository; + late MockRemoteAssetRepository remoteAssetRepository; + late MockDriftLocalAssetRepository localAssetRepository; + late MockDriftAlbumApiRepository albumApiRepository; + late MockRemoteAlbumRepository remoteAlbumRepository; + late MockTrashedLocalAssetRepository trashedLocalAssetRepository; + late MockAssetMediaRepository assetMediaRepository; + late MockDownloadRepository downloadRepository; + + late Drift db; + + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + }); + + tearDownAll(() async { + debugDefaultTargetPlatformOverride = null; + await Store.clear(); + await db.close(); + }); + + setUp(() { + assetApiRepository = MockAssetApiRepository(); + remoteAssetRepository = MockRemoteAssetRepository(); + localAssetRepository = MockDriftLocalAssetRepository(); + albumApiRepository = MockDriftAlbumApiRepository(); + remoteAlbumRepository = MockRemoteAlbumRepository(); + trashedLocalAssetRepository = MockTrashedLocalAssetRepository(); + assetMediaRepository = MockAssetMediaRepository(); + downloadRepository = MockDownloadRepository(); + + sut = ActionService( + assetApiRepository, + remoteAssetRepository, + localAssetRepository, + albumApiRepository, + remoteAlbumRepository, + trashedLocalAssetRepository, + assetMediaRepository, + downloadRepository, + ); + }); + + tearDown(() async { + await Store.clear(); + }); + + group('ActionService.deleteLocal', () { + test('routes deleted ids to trashed repository when Android trash handling is enabled', () async { + await Store.put(StoreKey.manageLocalMediaAndroid, true); + const ids = ['a', 'b']; + + when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); + when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {}); + + final result = await sut.deleteLocal(ids); + + expect(result, ids.length); + verify(() => assetMediaRepository.deleteAll(ids)).called(1); + verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1); + verifyNever(() => localAssetRepository.delete(any())); + }); + + test('deletes locally when Android trash handling is disabled', () async { + await Store.put(StoreKey.manageLocalMediaAndroid, false); + const ids = ['c']; + + when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); + when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {}); + + final result = await sut.deleteLocal(ids); + + expect(result, ids.length); + verify(() => assetMediaRepository.deleteAll(ids)).called(1); + verify(() => localAssetRepository.delete(ids)).called(1); + verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); + }); + + test('short-circuits when nothing was deleted', () async { + await Store.put(StoreKey.manageLocalMediaAndroid, true); + const ids = ['x']; + + when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => []); + + final result = await sut.deleteLocal(ids); + + expect(result, 0); + verify(() => assetMediaRepository.deleteAll(ids)).called(1); + verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); + verifyNever(() => localAssetRepository.delete(any())); + }); + }); +} diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 1bad780ca7..7c7de3cd0e 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -21,7 +21,6 @@ void main() { late MockApiService apiService; late MockNetworkService networkService; late MockBackgroundSyncManager backgroundSyncManager; - late MockUploadService uploadService; late MockAppSettingService appSettingsService; late Isar db; @@ -31,7 +30,6 @@ void main() { apiService = MockApiService(); networkService = MockNetworkService(); backgroundSyncManager = MockBackgroundSyncManager(); - uploadService = MockUploadService(); appSettingsService = MockAppSettingService(); sut = AuthService( @@ -118,7 +116,6 @@ void main() { when(() => authApiRepository.logout()).thenAnswer((_) async => {}); when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null)); - when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1)); when( () => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false), ).thenAnswer((_) => Future.value(null)); @@ -133,7 +130,6 @@ void main() { when(() => authApiRepository.logout()).thenThrow(Exception('Server error')); when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {}); when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null)); - when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1)); when( () => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false), ).thenAnswer((_) => Future.value(null)); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart new file mode 100644 index 0000000000..585ffcb499 --- /dev/null +++ b/mobile/test/services/background_upload.service_test.dart @@ -0,0 +1,355 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.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/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/background_upload.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../domain/service.mock.dart'; +import '../fixtures/asset.stub.dart'; +import '../infrastructure/repository.mock.dart'; +import '../mocks/asset_entity.mock.dart'; +import '../repository.mocks.dart'; + +void main() { + late BackgroundUploadService sut; + late MockUploadRepository mockUploadRepository; + late MockStorageRepository mockStorageRepository; + late MockDriftLocalAssetRepository mockLocalAssetRepository; + late MockDriftBackupRepository mockBackupRepository; + late MockAppSettingsService mockAppSettingsService; + late MockAssetMediaRepository mockAssetMediaRepository; + late Drift db; + + setUpAll(() async { + registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos); + + TestWidgetsFlutterBinding.ensureInitialized(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('plugins.flutter.io/path_provider'), + (MethodCall methodCall) async => 'test', + ); + db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db)); + + await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); + await Store.put(StoreKey.deviceId, 'test-device-id'); + }); + + setUp(() { + mockUploadRepository = MockUploadRepository(); + mockStorageRepository = MockStorageRepository(); + mockLocalAssetRepository = MockDriftLocalAssetRepository(); + mockBackupRepository = MockDriftBackupRepository(); + mockAppSettingsService = MockAppSettingsService(); + mockAssetMediaRepository = MockAssetMediaRepository(); + + when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false); + when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false); + + sut = BackgroundUploadService( + mockUploadRepository, + mockStorageRepository, + mockLocalAssetRepository, + mockBackupRepository, + mockAppSettingsService, + mockAssetMediaRepository, + ); + + mockUploadRepository.onUploadStatus = (_) {}; + mockUploadRepository.onTaskProgress = (_) {}; + }); + + tearDown(() { + sut.dispose(); + }); + + group('getUploadTask', () { + test('should call getOriginalFilename from AssetMediaRepository for regular photo', () async { + final asset = LocalAssetStub.image1; + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/file.jpg'); + + when(() => mockEntity.isLivePhoto).thenReturn(false); + when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg'); + + final task = await sut.getUploadTask(asset); + + expect(task, isNotNull); + expect(task!.fields['filename'], equals('OriginalPhoto.jpg')); + verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); + }); + + test('should call getOriginalFilename when original filename is null', () async { + final asset = LocalAssetStub.image2; + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/file.jpg'); + + when(() => mockEntity.isLivePhoto).thenReturn(false); + when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); + + final task = await sut.getUploadTask(asset); + + expect(task, isNotNull); + expect(task!.fields['filename'], equals(asset.name)); + verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); + }); + + test('should call getOriginalFilename for live photo', () async { + final asset = LocalAssetStub.image1; + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/file.mov'); + + when(() => mockEntity.isLivePhoto).thenReturn(true); + when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => mockFile); + when( + () => mockAssetMediaRepository.getOriginalFilename(asset.id), + ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); + + final task = await sut.getUploadTask(asset); + expect(task, isNotNull); + // For live photos, extension should be changed to match the video file + expect(task!.fields['filename'], equals('OriginalLivePhoto.mov')); + verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); + }); + }); + + group('getLivePhotoUploadTask', () { + test('should call getOriginalFilename for live photo upload task', () async { + final asset = LocalAssetStub.image1; + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/livephoto.heic'); + + when(() => mockEntity.isLivePhoto).thenReturn(true); + when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when( + () => mockAssetMediaRepository.getOriginalFilename(asset.id), + ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); + + final task = await sut.getLivePhotoUploadTask(asset, 'video-id-123'); + + expect(task, isNotNull); + expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC')); + expect(task.fields['livePhotoVideoId'], equals('video-id-123')); + verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); + }); + + test('should call getOriginalFilename when original filename is null', () async { + final asset = LocalAssetStub.image2; + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/fallback.heic'); + + when(() => mockEntity.isLivePhoto).thenReturn(true); + when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); + when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); + + final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456'); + expect(task, isNotNull); + // Should fall back to asset.name when original filename is null + expect(task!.fields['filename'], equals(asset.name)); + verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); + }); + }); + + group('Server Info - cloudId and eTag metadata', () { + test('should include cloudId and eTag metadata on iOS when server version is 2.4+', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + final sutWithV24 = BackgroundUploadService( + mockUploadRepository, + mockStorageRepository, + mockLocalAssetRepository, + mockBackupRepository, + mockAppSettingsService, + mockAssetMediaRepository, + ); + addTearDown(() => sutWithV24.dispose()); + + final assetWithCloudId = LocalAsset( + id: 'test-asset-id', + name: 'test.jpg', + type: AssetType.image, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 2), + cloudId: 'cloud-id-123', + latitude: 37.7749, + longitude: -122.4194, + adjustmentTime: DateTime(2026, 1, 2), + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, + ); + + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/test.jpg'); + + when(() => mockEntity.isLivePhoto).thenReturn(false); + when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg'); + + final task = await sutWithV24.getUploadTask(assetWithCloudId); + + expect(task, isNotNull); + expect(task!.fields.containsKey('metadata'), isTrue); + + final metadata = jsonDecode(task.fields['metadata']!) as List; + expect(metadata, hasLength(1)); + expect(metadata[0]['key'], equals('mobile-app')); + expect(metadata[0]['value']['iCloudId'], equals('cloud-id-123')); + expect(metadata[0]['value']['createdAt'], isNotNull); + expect(metadata[0]['value']['adjustmentTime'], isNotNull); + expect(metadata[0]['value']['latitude'], isNotNull); + expect(metadata[0]['value']['longitude'], isNotNull); + }); + + test('should NOT include metadata on Android regardless of server version', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + final sutAndroid = BackgroundUploadService( + mockUploadRepository, + mockStorageRepository, + mockLocalAssetRepository, + mockBackupRepository, + mockAppSettingsService, + mockAssetMediaRepository, + ); + addTearDown(() => sutAndroid.dispose()); + + final assetWithCloudId = LocalAsset( + id: 'test-asset-id', + name: 'test.jpg', + type: AssetType.image, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 2), + cloudId: 'cloud-id-123', + latitude: 37.7749, + longitude: -122.4194, + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, + ); + + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/test.jpg'); + + when(() => mockEntity.isLivePhoto).thenReturn(false); + when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when(() => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id)).thenAnswer((_) async => 'test.jpg'); + + final task = await sutAndroid.getUploadTask(assetWithCloudId); + + expect(task, isNotNull); + expect(task!.fields.containsKey('metadata'), isFalse); + }); + + test('should NOT include metadata when cloudId is null even on iOS with server 2.4+', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + final sutWithV24 = BackgroundUploadService( + mockUploadRepository, + mockStorageRepository, + mockLocalAssetRepository, + mockBackupRepository, + mockAppSettingsService, + mockAssetMediaRepository, + ); + addTearDown(() => sutWithV24.dispose()); + + final assetWithoutCloudId = LocalAsset( + id: 'test-asset-id', + name: 'test.jpg', + type: AssetType.image, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 2), + cloudId: null, // No cloudId + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, + ); + + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/test.jpg'); + + when(() => mockEntity.isLivePhoto).thenReturn(false); + when(() => mockStorageRepository.getAssetEntityForAsset(assetWithoutCloudId)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(assetWithoutCloudId.id)).thenAnswer((_) async => mockFile); + when( + () => mockAssetMediaRepository.getOriginalFilename(assetWithoutCloudId.id), + ).thenAnswer((_) async => 'test.jpg'); + + final task = await sutWithV24.getUploadTask(assetWithoutCloudId); + + expect(task, isNotNull); + expect(task!.fields.containsKey('metadata'), isFalse); + }); + + test('should include metadata for live photos with cloudId on iOS 2.4+', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + + final sutWithV24 = BackgroundUploadService( + mockUploadRepository, + mockStorageRepository, + mockLocalAssetRepository, + mockBackupRepository, + mockAppSettingsService, + mockAssetMediaRepository, + ); + addTearDown(() => sutWithV24.dispose()); + + final assetWithCloudId = LocalAsset( + id: 'test-livephoto-id', + name: 'livephoto.heic', + type: AssetType.image, + createdAt: DateTime(2025, 1, 1), + updatedAt: DateTime(2025, 1, 2), + cloudId: 'cloud-id-livephoto', + latitude: 37.7749, + longitude: -122.4194, + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, + ); + + final mockEntity = MockAssetEntity(); + final mockFile = File('/path/to/livephoto.heic'); + + when(() => mockEntity.isLivePhoto).thenReturn(true); + when(() => mockStorageRepository.getAssetEntityForAsset(assetWithCloudId)).thenAnswer((_) async => mockEntity); + when(() => mockStorageRepository.getFileForAsset(assetWithCloudId.id)).thenAnswer((_) async => mockFile); + when( + () => mockAssetMediaRepository.getOriginalFilename(assetWithCloudId.id), + ).thenAnswer((_) async => 'livephoto.heic'); + + final task = await sutWithV24.getLivePhotoUploadTask(assetWithCloudId, 'video-123'); + + expect(task, isNotNull); + expect(task!.fields.containsKey('metadata'), isTrue); + expect(task.fields['livePhotoVideoId'], equals('video-123')); + + final metadata = jsonDecode(task.fields['metadata']!) as List; + expect(metadata, hasLength(1)); + expect(metadata[0]['key'], equals('mobile-app')); + expect(metadata[0]['value']['iCloudId'], equals('cloud-id-livephoto')); + }); + }); +} diff --git a/mobile/test/services/cleanup.service_test.dart b/mobile/test/services/cleanup.service_test.dart new file mode 100644 index 0000000000..2038941ecb --- /dev/null +++ b/mobile/test/services/cleanup.service_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../infrastructure/repository.mock.dart'; +import '../repository.mocks.dart'; + +void main() { + late CleanupService sut; + + late MockDriftLocalAssetRepository localAssetRepository; + late MockAssetMediaRepository assetMediaRepository; + + setUp(() { + localAssetRepository = MockDriftLocalAssetRepository(); + assetMediaRepository = MockAssetMediaRepository(); + sut = CleanupService(localAssetRepository, assetMediaRepository); + }); + + group('CleanupService.deleteLocalAssets', () { + test('returns 0 and does nothing for empty input', () async { + final result = await sut.deleteLocalAssets([]); + + expect(result, 0); + verifyNever(() => assetMediaRepository.deleteAll(any())); + verifyNever(() => localAssetRepository.delete(any())); + }); + + test('deletes in a single batch when under limit', () async { + final ids = List.generate(999, (i) => 'asset-$i'); + + when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async { + return (invocation.positionalArguments.first as List).toList(); + }); + when(() => localAssetRepository.delete(any())).thenAnswer((_) async {}); + + final result = await sut.deleteLocalAssets(ids); + + expect(result, ids.length); + verify(() => assetMediaRepository.deleteAll(ids)).called(1); + verify(() => localAssetRepository.delete(ids)).called(1); + }); + + test('deletes in platform-specific batches when over limit', () async { + final batchSize = CurrentPlatform.isAndroid ? 2000 : 10000; + final ids = List.generate(batchSize * 2 + 501, (i) => 'asset-$i'); + final capturedBatches = >[]; + + when(() => assetMediaRepository.deleteAll(any())).thenAnswer((invocation) async { + final batch = (invocation.positionalArguments.first as List).toList(); + capturedBatches.add(batch); + return batch; + }); + when(() => localAssetRepository.delete(any())).thenAnswer((_) async {}); + + final result = await sut.deleteLocalAssets(ids); + + expect(result, ids.length); + expect(capturedBatches.length, 3); + expect(capturedBatches[0].length, batchSize); + expect(capturedBatches[1].length, batchSize); + expect(capturedBatches[2].length, 501); + expect(capturedBatches[0].first, 'asset-0'); + expect(capturedBatches[0].last, 'asset-${batchSize - 1}'); + expect(capturedBatches[1].first, 'asset-$batchSize'); + expect(capturedBatches[1].last, 'asset-${batchSize * 2 - 1}'); + expect(capturedBatches[2].first, 'asset-${batchSize * 2}'); + expect(capturedBatches[2].last, 'asset-${batchSize * 2 + 500}'); + verify(() => localAssetRepository.delete(any())).called(3); + }); + }); +} diff --git a/mobile/test/services/upload.service_test.dart b/mobile/test/services/upload.service_test.dart deleted file mode 100644 index d33126782f..0000000000 --- a/mobile/test/services/upload.service_test.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:io'; - -import 'package:drift/drift.dart' hide isNull, isNotNull; -import 'package:drift/native.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/services/upload.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../domain/service.mock.dart'; -import '../fixtures/asset.stub.dart'; -import '../infrastructure/repository.mock.dart'; -import '../repository.mocks.dart'; -import '../mocks/asset_entity.mock.dart'; - -void main() { - late UploadService sut; - late MockUploadRepository mockUploadRepository; - late MockDriftBackupRepository mockBackupRepository; - late MockStorageRepository mockStorageRepository; - late MockDriftLocalAssetRepository mockLocalAssetRepository; - late MockAppSettingsService mockAppSettingsService; - late MockAssetMediaRepository mockAssetMediaRepository; - late Drift db; - - setUpAll(() async { - registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos); - - TestWidgetsFlutterBinding.ensureInitialized(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - const MethodChannel('plugins.flutter.io/path_provider'), - (MethodCall methodCall) async => 'test', - ); - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); - - await Store.put(StoreKey.serverEndpoint, 'http://test-server.com'); - await Store.put(StoreKey.deviceId, 'test-device-id'); - }); - - setUp(() { - mockUploadRepository = MockUploadRepository(); - mockBackupRepository = MockDriftBackupRepository(); - mockStorageRepository = MockStorageRepository(); - mockLocalAssetRepository = MockDriftLocalAssetRepository(); - mockAppSettingsService = MockAppSettingsService(); - mockAssetMediaRepository = MockAssetMediaRepository(); - - when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false); - when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false); - - sut = UploadService( - mockUploadRepository, - mockBackupRepository, - mockStorageRepository, - mockLocalAssetRepository, - mockAppSettingsService, - mockAssetMediaRepository, - ); - - mockUploadRepository.onUploadStatus = (_) {}; - mockUploadRepository.onTaskProgress = (_) {}; - }); - - tearDown(() { - sut.dispose(); - }); - - group('getUploadTask', () { - test('should call getOriginalFilename from AssetMediaRepository for regular photo', () async { - final asset = LocalAssetStub.image1; - final mockEntity = MockAssetEntity(); - final mockFile = File('/path/to/file.jpg'); - - when(() => mockEntity.isLivePhoto).thenReturn(false); - when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'OriginalPhoto.jpg'); - - final task = await sut.getUploadTask(asset); - - expect(task, isNotNull); - expect(task!.fields['filename'], equals('OriginalPhoto.jpg')); - verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); - }); - - test('should call getOriginalFilename when original filename is null', () async { - final asset = LocalAssetStub.image2; - final mockEntity = MockAssetEntity(); - final mockFile = File('/path/to/file.jpg'); - - when(() => mockEntity.isLivePhoto).thenReturn(false); - when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); - - final task = await sut.getUploadTask(asset); - - expect(task, isNotNull); - expect(task!.fields['filename'], equals(asset.name)); - verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); - }); - - test('should call getOriginalFilename for live photo', () async { - final asset = LocalAssetStub.image1; - final mockEntity = MockAssetEntity(); - final mockFile = File('/path/to/file.mov'); - - when(() => mockEntity.isLivePhoto).thenReturn(true); - when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => mockFile); - when( - () => mockAssetMediaRepository.getOriginalFilename(asset.id), - ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); - - final task = await sut.getUploadTask(asset); - expect(task, isNotNull); - // For live photos, extension should be changed to match the video file - expect(task!.fields['filename'], equals('OriginalLivePhoto.mov')); - verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); - }); - }); - - group('getLivePhotoUploadTask', () { - test('should call getOriginalFilename for live photo upload task', () async { - final asset = LocalAssetStub.image1; - final mockEntity = MockAssetEntity(); - final mockFile = File('/path/to/livephoto.heic'); - - when(() => mockEntity.isLivePhoto).thenReturn(true); - when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when( - () => mockAssetMediaRepository.getOriginalFilename(asset.id), - ).thenAnswer((_) async => 'OriginalLivePhoto.HEIC'); - - final task = await sut.getLivePhotoUploadTask(asset, 'video-id-123'); - - expect(task, isNotNull); - expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC')); - expect(task.fields['livePhotoVideoId'], equals('video-id-123')); - verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); - }); - - test('should call getOriginalFilename when original filename is null', () async { - final asset = LocalAssetStub.image2; - final mockEntity = MockAssetEntity(); - final mockFile = File('/path/to/fallback.heic'); - - when(() => mockEntity.isLivePhoto).thenReturn(true); - when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity); - when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile); - when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => null); - - final task = await sut.getLivePhotoUploadTask(asset, 'video-id-456'); - expect(task, isNotNull); - // Should fall back to asset.name when original filename is null - expect(task!.fields['filename'], equals(asset.name)); - verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1); - }); - }); -} diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 498607e3d2..30d4e2e6d4 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -131,6 +131,7 @@ abstract final class TestUtils { isFavorite: false, width: width, height: height, + isEdited: false, ); } @@ -154,6 +155,8 @@ abstract final class TestUtils { width: width, height: height, orientation: orientation, + playbackStyle: domain.AssetPlaybackStyle.image, + isEdited: false, ); } } diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 19ad7166c6..50e73e5b5e 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -27,6 +27,8 @@ class MediumFactory { type: type ?? AssetType.image, createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, ); } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index d93d59d3c7..b5540f9dc7 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -23,6 +23,8 @@ LocalAsset createLocalAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + playbackStyle: AssetPlaybackStyle.image, + isEdited: false, ); } @@ -45,6 +47,7 @@ RemoteAsset createRemoteAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: false, ); } @@ -635,6 +638,185 @@ void main() { }); }); + group('setProfilePicture button', () { + test('should show when owner, not locked, and asset is RemoteAsset', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.setProfilePicture.shouldShow(context), isTrue); + }); + + test('should not show when not owner', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: false, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.setProfilePicture.shouldShow(context), isFalse); + }); + + test('should not show when in locked view', () { + final remoteAsset = createRemoteAsset(); + final context = ActionButtonContext( + asset: remoteAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: true, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.setProfilePicture.shouldShow(context), isFalse); + }); + + test('should not show when asset is not RemoteAsset', () { + final localAsset = createLocalAsset(); + final context = ActionButtonContext( + asset: localAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + + expect(ActionButtonType.setProfilePicture.shouldShow(context), isFalse); + }); + }); + + group('setAlbumCover button', () { + test('should show when owner, not locked, has album, and selectedCount is 1', () { + final album = createRemoteAlbum(); + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 1, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isTrue); + }); + + test('should not show when not owner', () { + final album = createRemoteAlbum(); + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: false, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 1, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + }); + + test('should not show when in locked view', () { + final album = createRemoteAlbum(); + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: true, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 1, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + }); + + test('should not show when no current album', () { + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: null, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 1, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + }); + + test('should not show when selectedCount is not 1', () { + final album = createRemoteAlbum(); + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 0, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + }); + + test('should not show when selectedCount is greater than 1', () { + final album = createRemoteAlbum(); + final context = ActionButtonContext( + asset: mergedAsset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + selectedCount: 2, + ); + + expect(ActionButtonType.setAlbumCover.shouldShow(context), isFalse); + }); + }); + group('likeActivity button', () { test('should show when not locked, has album, activity enabled, and shared', () { final album = createRemoteAlbum(isActivityEnabled: true, isShared: true); @@ -844,6 +1026,21 @@ void main() { ); final widget = buttonType.buildButton(contextWithAlbum); expect(widget, isA()); + } else if (buttonType == ActionButtonType.setAlbumCover) { + final album = createRemoteAlbum(); + final contextWithAlbum = ActionButtonContext( + asset: asset, + isOwner: true, + isArchived: false, + isTrashEnabled: true, + isInLockedView: false, + currentAlbum: album, + advancedTroubleshooting: false, + isStacked: false, + source: ActionSource.timeline, + ); + final widget = buttonType.buildButton(contextWithAlbum); + expect(widget, isA()); } else if (buttonType == ActionButtonType.unstack) { final album = createRemoteAlbum(); final contextWithAlbum = ActionButtonContext( diff --git a/mobile/test/utils/option_test.dart b/mobile/test/utils/option_test.dart new file mode 100644 index 0000000000..4fa44a3865 --- /dev/null +++ b/mobile/test/utils/option_test.dart @@ -0,0 +1,116 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/option.dart'; + +void main() { + group('Option', () { + group('constructors', () { + test('Option.some creates a Some instance', () { + const option = Option.some(42); + expect(option, isA>()); + expect((option as Some).value, 42); + }); + + test('Option.none creates a None instance', () { + const option = Option.none(); + expect(option, isA>()); + }); + + test('Option.fromNullable returns Some for non-null value', () { + final option = Option.fromNullable('hello'); + expect(option, isA>()); + expect((option as Some).value, 'hello'); + }); + + test('Option.fromNullable returns None for null value', () { + final option = Option.fromNullable(null); + expect(option, isA()); + }); + }); + + group('isSome / isNone', () { + test('Some.isSome is true', () { + expect(const Option.some(1).isSome, isTrue); + }); + + test('Some.isNone is false', () { + expect(const Option.some(1).isNone, isFalse); + }); + + test('None.isSome is false', () { + expect(const Option.none().isSome, isFalse); + }); + + test('None.isNone is true', () { + expect(const Option.none().isNone, isTrue); + }); + }); + + group('unwrapOrNull', () { + test('returns value for Some', () { + expect(const Option.some('hi').unwrapOrNull, 'hi'); + }); + + test('returns null for None', () { + expect(const Option.none().unwrapOrNull, isNull); + }); + }); + + group('fold', () { + test('calls onSome with value for Some', () { + final result = const Option.some('world').fold((v) => 'some: $v', () => 'none'); + expect(result, 'some: world'); + }); + + test('calls onNone for None', () { + final result = const Option.none().fold((v) => 'some: $v', () => 'none'); + expect(result, 'none'); + }); + }); + + group('equality', () { + test('Some equals Some with same value', () { + expect(const Option.some(1) == const Option.some(1), isTrue); + }); + + test('Some does not equal Some with different value', () { + expect(const Option.some(1) == const Option.some(2), isFalse); + }); + + test('None equals None of same type', () { + expect(const Option.none() == const Option.none(), isTrue); + }); + + test('None does not equal None of different type', () { + expect(const Option.none() == (const Option.none() as Object), isFalse); + }); + + test('Some does not equal None', () { + expect(const Option.some(0) == const Option.none(), isFalse); + }); + }); + + group('hashCode', () { + test('Some hashCode equals value hashCode', () { + expect(const Option.some('abc').hashCode, 'abc'.hashCode); + }); + + test('None hashCode is 0', () { + expect(const Option.none().hashCode, 0); + }); + }); + }); + + group('ObjectOptionExtension', () { + test('non-null value.toOption() returns Some', () { + final option = 'hello'.toOption(); + expect(option, isA>()); + expect((option as Some).value, 'hello'); + }); + + test('null value.toOption() returns None', () { + const String? value = null; + final option = value.toOption(); + expect(option, isA>()); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 43292089d7..522063185f 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -27,7 +27,7 @@ function dart { } function typescript { - pnpm dlx oazapfts --optimistic --argumentStyle=object --useEnumType immich-openapi-specs.json typescript-sdk/src/fetch-client.ts + pnpm dlx oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas immich-openapi-specs.json typescript-sdk/src/fetch-client.ts pnpm --filter @immich/sdk install --frozen-lockfile pnpm --filter @immich/sdk build } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b6df615433..9a8cace4db 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10,6 +10,7 @@ "name": "albumId", "required": true, "in": "query", + "description": "Album ID", "schema": { "format": "uuid", "type": "string" @@ -19,6 +20,7 @@ "name": "assetId", "required": false, "in": "query", + "description": "Asset ID (if activity is for an asset)", "schema": { "format": "uuid", "type": "string" @@ -28,6 +30,7 @@ "name": "level", "required": false, "in": "query", + "description": "Filter by activity level", "schema": { "$ref": "#/components/schemas/ReactionLevel" } @@ -36,6 +39,7 @@ "name": "type", "required": false, "in": "query", + "description": "Filter by activity type", "schema": { "$ref": "#/components/schemas/ReactionType" } @@ -44,6 +48,7 @@ "name": "userId", "required": false, "in": "query", + "description": "Filter by user ID", "schema": { "format": "uuid", "type": "string" @@ -165,6 +170,7 @@ "name": "albumId", "required": true, "in": "query", + "description": "Album ID", "schema": { "format": "uuid", "type": "string" @@ -174,6 +180,7 @@ "name": "assetId", "required": false, "in": "query", + "description": "Asset ID (if activity is for an asset)", "schema": { "format": "uuid", "type": "string" @@ -322,6 +329,237 @@ "x-immich-state": "Stable" } }, + "/admin/database-backups": { + "delete": { + "description": "Delete a backup by its filename", + "operationId": "deleteDatabaseBackup", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseBackupDeleteDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete database backup", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.delete", + "x-immich-state": "Alpha" + }, + "get": { + "description": "Get the list of the successful and failed backups", + "operationId": "listDatabaseBackups", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseBackupListResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List database backups", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "maintenance", + "x-immich-state": "Alpha" + } + }, + "/admin/database-backups/start-restore": { + "post": { + "description": "Put Immich into maintenance mode to restore a backup (Immich must not be configured)", + "operationId": "startDatabaseRestoreFlow", + "parameters": [], + "responses": { + "201": { + "description": "" + } + }, + "summary": "Start database backup restore flow", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, + "/admin/database-backups/upload": { + "post": { + "description": "Uploads .sql/.sql.gz file to restore backup from", + "operationId": "uploadDatabaseBackup", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/DatabaseBackupUploadDto" + } + } + }, + "description": "Backup Upload", + "required": true + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Upload database backup", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.upload", + "x-immich-state": "Alpha" + } + }, + "/admin/database-backups/{filename}": { + "get": { + "description": "Downloads the database backup file", + "operationId": "downloadDatabaseBackup", + "parameters": [ + { + "name": "filename", + "required": true, + "in": "path", + "schema": { + "format": "string", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Download database backup", + "tags": [ + "Database Backups (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "backup.download", + "x-immich-state": "Alpha" + } + }, "/admin/maintenance": { "post": { "description": "Put Immich into or take it out of maintenance mode", @@ -372,6 +610,53 @@ "x-immich-state": "Alpha" } }, + "/admin/maintenance/detect-install": { + "get": { + "description": "Collect integrity checks and other heuristics about local data.", + "operationId": "detectPriorInstall", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceDetectInstallResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Detect existing install", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-permission": "maintenance", + "x-immich-state": "Alpha" + } + }, "/admin/maintenance/login": { "post": { "description": "Login with maintenance token or cookie to receive current information and perform further actions.", @@ -416,6 +701,40 @@ "x-immich-state": "Alpha" } }, + "/admin/maintenance/status": { + "get": { + "description": "Fetch information about the currently running maintenance action.", + "operationId": "getMaintenanceStatus", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceStatusResponseDto" + } + } + }, + "description": "" + } + }, + "summary": "Get maintenance mode status", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, "/admin/notifications": { "post": { "description": "Create a new notification for a specific user.", @@ -614,6 +933,7 @@ "name": "id", "required": false, "in": "query", + "description": "User ID filter", "schema": { "format": "uuid", "type": "string" @@ -623,6 +943,7 @@ "name": "withDeleted", "required": false, "in": "query", + "description": "Include deleted users", "schema": { "type": "boolean" } @@ -1208,6 +1529,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -1216,6 +1538,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Filter by trash status", "schema": { "type": "boolean" } @@ -1224,6 +1547,7 @@ "name": "visibility", "required": false, "in": "query", + "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -1284,7 +1608,7 @@ "name": "assetId", "required": false, "in": "query", - "description": "Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums", + "description": "Filter albums containing this asset ID (ignores shared parameter)", "schema": { "format": "uuid", "type": "string" @@ -1294,6 +1618,7 @@ "name": "shared", "required": false, "in": "query", + "description": "Filter by shared status: true = only shared, false = not shared, undefined = all owned albums", "schema": { "type": "boolean" } @@ -1617,6 +1942,7 @@ "name": "withoutAssets", "required": false, "in": "query", + "description": "Exclude assets from response", "schema": { "type": "boolean" } @@ -2528,6 +2854,16 @@ "required": true }, "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetMediaResponseDto" + } + } + }, + "description": "Asset is a duplicate" + }, "201": { "content": { "application/json": { @@ -2536,7 +2872,7 @@ } } }, - "description": "" + "description": "Asset uploaded successfully" } }, "security": [ @@ -2746,6 +3082,7 @@ "name": "deviceId", "required": true, "in": "path", + "description": "Device ID", "schema": { "type": "string" } @@ -2851,6 +3188,7 @@ "state": "Stable" } ], + "x-immich-permission": "asset.upload", "x-immich-state": "Stable" } }, @@ -2903,9 +3241,116 @@ "state": "Stable" } ], + "x-immich-permission": "job.create", "x-immich-state": "Stable" } }, + "/assets/metadata": { + "delete": { + "description": "Delete metadata key-value pairs for multiple assets.", + "operationId": "deleteBulkAssetMetadata", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetMetadataBulkDeleteDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete asset metadata", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-permission": "asset.update", + "x-immich-state": "Beta" + }, + "put": { + "description": "Upsert metadata key-value pairs for multiple assets.", + "operationId": "updateBulkAssetMetadata", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetMetadataBulkUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetMetadataBulkResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Upsert asset metadata", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-permission": "asset.update", + "x-immich-state": "Beta" + } + }, "/assets/random": { "get": { "deprecated": true, @@ -2916,6 +3361,7 @@ "name": "count", "required": false, "in": "query", + "description": "Number of random assets to return", "schema": { "minimum": 1, "type": "number" @@ -2977,6 +3423,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -2985,6 +3432,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Filter by trash status", "schema": { "type": "boolean" } @@ -2993,6 +3441,7 @@ "name": "visibility", "required": false, "in": "query", + "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -3187,6 +3636,173 @@ "x-immich-state": "Stable" } }, + "/assets/{id}/edits": { + "delete": { + "description": "Removes all edit actions (crop, rotate, mirror) associated with the specified asset.", + "operationId": "removeAssetEdits", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Remove edits from an existing asset", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-permission": "asset.edit.delete", + "x-immich-state": "Beta" + }, + "get": { + "description": "Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.", + "operationId": "getAssetEdits", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetEditsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve edits for an existing asset", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-permission": "asset.edit.get", + "x-immich-state": "Beta" + }, + "put": { + "description": "Apply a series of edit actions (crop, rotate, mirror) to the specified asset.", + "operationId": "editAsset", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetEditsCreateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetEditsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Apply edits to an existing asset", + "tags": [ + "Assets" + ], + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-permission": "asset.edit.create", + "x-immich-state": "Beta" + } + }, "/assets/{id}/metadata": { "get": { "description": "Retrieve all metadata key-value pairs associated with the specified asset.", @@ -3330,6 +3946,7 @@ "name": "id", "required": true, "in": "path", + "description": "Asset ID", "schema": { "format": "uuid", "type": "string" @@ -3339,8 +3956,9 @@ "name": "key", "required": true, "in": "path", + "description": "Metadata key", "schema": { - "$ref": "#/components/schemas/AssetMetadataKey" + "type": "string" } } ], @@ -3389,6 +4007,7 @@ "name": "id", "required": true, "in": "path", + "description": "Asset ID", "schema": { "format": "uuid", "type": "string" @@ -3398,8 +4017,9 @@ "name": "key", "required": true, "in": "path", + "description": "Metadata key", "schema": { - "$ref": "#/components/schemas/AssetMetadataKey" + "type": "string" } } ], @@ -3516,6 +4136,16 @@ "description": "Downloads the original file of the specified asset.", "operationId": "downloadAsset", "parameters": [ + { + "name": "edited", + "required": false, + "in": "query", + "description": "Return edited asset if available", + "schema": { + "default": false, + "type": "boolean" + } + }, { "name": "id", "required": true, @@ -3637,7 +4267,7 @@ } } }, - "description": "" + "description": "Asset replaced successfully" } }, "security": [ @@ -3673,9 +4303,19 @@ }, "/assets/{id}/thumbnail": { "get": { - "description": "Retrieve the thumbnail image for the specified asset.", + "description": "Retrieve the thumbnail image for the specified asset. Viewing the fullsize thumbnail might redirect to downloadAsset, which requires a different permission.", "operationId": "viewAsset", "parameters": [ + { + "name": "edited", + "required": false, + "in": "query", + "description": "Return edited asset if available", + "schema": { + "default": false, + "type": "boolean" + } + }, { "name": "id", "required": true, @@ -3697,6 +4337,7 @@ "name": "size", "required": false, "in": "query", + "description": "Asset media size", "schema": { "$ref": "#/components/schemas/AssetMediaSize" } @@ -4415,7 +5056,22 @@ "summary": "Retrieve auth status", "tags": [ "Authentication" - ] + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" } }, "/auth/validateToken": { @@ -4493,7 +5149,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetIdsDto" + "$ref": "#/components/schemas/DownloadArchiveDto" } } }, @@ -4788,6 +5444,7 @@ "name": "id", "required": true, "in": "query", + "description": "Face ID", "schema": { "format": "uuid", "type": "string" @@ -5143,6 +5800,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -5698,6 +6356,7 @@ "name": "fileCreatedAfter", "required": false, "in": "query", + "description": "Filter assets created after this date", "schema": { "format": "date-time", "type": "string" @@ -5707,6 +6366,7 @@ "name": "fileCreatedBefore", "required": false, "in": "query", + "description": "Filter assets created before this date", "schema": { "format": "date-time", "type": "string" @@ -5716,6 +6376,7 @@ "name": "isArchived", "required": false, "in": "query", + "description": "Filter by archived status", "schema": { "type": "boolean" } @@ -5724,6 +6385,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -5732,6 +6394,7 @@ "name": "withPartners", "required": false, "in": "query", + "description": "Include partner assets", "schema": { "type": "boolean" } @@ -5740,6 +6403,7 @@ "name": "withSharedAlbums", "required": false, "in": "query", + "description": "Include shared album assets", "schema": { "type": "boolean" } @@ -5789,6 +6453,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.read", "x-immich-state": "Stable" } }, @@ -5801,6 +6466,7 @@ "name": "lat", "required": true, "in": "query", + "description": "Latitude (-90 to 90)", "schema": { "format": "double", "type": "number" @@ -5810,6 +6476,7 @@ "name": "lon", "required": true, "in": "query", + "description": "Longitude (-180 to 180)", "schema": { "format": "double", "type": "number" @@ -5860,6 +6527,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.search", "x-immich-state": "Stable" } }, @@ -5872,6 +6540,7 @@ "name": "for", "required": false, "in": "query", + "description": "Filter by date", "schema": { "format": "date-time", "type": "string" @@ -5881,6 +6550,7 @@ "name": "isSaved", "required": false, "in": "query", + "description": "Filter by saved status", "schema": { "type": "boolean" } @@ -5889,6 +6559,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Include trashed memories", "schema": { "type": "boolean" } @@ -5897,6 +6568,7 @@ "name": "order", "required": false, "in": "query", + "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -5915,6 +6587,7 @@ "name": "type", "required": false, "in": "query", + "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6035,6 +6708,7 @@ "name": "for", "required": false, "in": "query", + "description": "Filter by date", "schema": { "format": "date-time", "type": "string" @@ -6044,6 +6718,7 @@ "name": "isSaved", "required": false, "in": "query", + "description": "Filter by saved status", "schema": { "type": "boolean" } @@ -6052,6 +6727,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Include trashed memories", "schema": { "type": "boolean" } @@ -6060,6 +6736,7 @@ "name": "order", "required": false, "in": "query", + "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6078,6 +6755,7 @@ "name": "type", "required": false, "in": "query", + "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6511,6 +7189,7 @@ "name": "id", "required": false, "in": "query", + "description": "Filter by notification ID", "schema": { "format": "uuid", "type": "string" @@ -6520,6 +7199,7 @@ "name": "level", "required": false, "in": "query", + "description": "Filter by notification level", "schema": { "$ref": "#/components/schemas/NotificationLevel" } @@ -6528,6 +7208,7 @@ "name": "type", "required": false, "in": "query", + "description": "Filter by notification type", "schema": { "$ref": "#/components/schemas/NotificationType" } @@ -6536,6 +7217,7 @@ "name": "unread", "required": false, "in": "query", + "description": "Filter by unread status", "schema": { "type": "boolean" } @@ -7063,6 +7745,7 @@ "name": "direction", "required": true, "in": "query", + "description": "Partner direction", "schema": { "$ref": "#/components/schemas/PartnerDirection" } @@ -7412,6 +8095,7 @@ "name": "closestAssetId", "required": false, "in": "query", + "description": "Closest asset ID for similarity search", "schema": { "format": "uuid", "type": "string" @@ -7421,6 +8105,7 @@ "name": "closestPersonId", "required": false, "in": "query", + "description": "Closest person ID for similarity search", "schema": { "format": "uuid", "type": "string" @@ -7453,6 +8138,7 @@ "name": "withHidden", "required": false, "in": "query", + "description": "Include hidden people", "schema": { "type": "boolean" } @@ -8117,6 +8803,55 @@ "x-immich-state": "Alpha" } }, + "/plugins/triggers": { + "get": { + "description": "Retrieve a list of all available plugin triggers.", + "operationId": "getPluginTriggers", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PluginTriggerResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all plugin triggers", + "tags": [ + "Plugins" + ], + "x-immich-history": [ + { + "version": "v2.3.0", + "state": "Added" + }, + { + "version": "v2.3.0", + "state": "Alpha" + } + ], + "x-immich-permission": "plugin.read", + "x-immich-state": "Alpha" + } + }, "/plugins/{id}": { "get": { "description": "Retrieve information about a specific plugin by its ID.", @@ -8232,6 +8967,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8286,6 +9022,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8352,6 +9089,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8409,6 +9147,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8417,6 +9156,7 @@ "name": "status", "required": false, "in": "query", + "description": "Filter jobs by status", "schema": { "type": "array", "items": { @@ -8585,6 +9325,7 @@ "name": "albumIds", "required": false, "in": "query", + "description": "Filter by album IDs", "schema": { "type": "array", "items": { @@ -8597,6 +9338,7 @@ "name": "city", "required": false, "in": "query", + "description": "Filter by city name", "schema": { "nullable": true, "type": "string" @@ -8606,6 +9348,7 @@ "name": "country", "required": false, "in": "query", + "description": "Filter by country name", "schema": { "nullable": true, "type": "string" @@ -8615,6 +9358,7 @@ "name": "createdAfter", "required": false, "in": "query", + "description": "Filter by creation date (after)", "schema": { "format": "date-time", "type": "string" @@ -8624,6 +9368,7 @@ "name": "createdBefore", "required": false, "in": "query", + "description": "Filter by creation date (before)", "schema": { "format": "date-time", "type": "string" @@ -8633,6 +9378,7 @@ "name": "deviceId", "required": false, "in": "query", + "description": "Device ID to filter by", "schema": { "type": "string" } @@ -8641,6 +9387,7 @@ "name": "isEncoded", "required": false, "in": "query", + "description": "Filter by encoded status", "schema": { "type": "boolean" } @@ -8649,6 +9396,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -8657,6 +9405,7 @@ "name": "isMotion", "required": false, "in": "query", + "description": "Filter by motion photo status", "schema": { "type": "boolean" } @@ -8665,6 +9414,7 @@ "name": "isNotInAlbum", "required": false, "in": "query", + "description": "Filter assets not in any album", "schema": { "type": "boolean" } @@ -8673,6 +9423,7 @@ "name": "isOffline", "required": false, "in": "query", + "description": "Filter by offline status", "schema": { "type": "boolean" } @@ -8681,6 +9432,7 @@ "name": "lensModel", "required": false, "in": "query", + "description": "Filter by lens model", "schema": { "nullable": true, "type": "string" @@ -8690,6 +9442,7 @@ "name": "libraryId", "required": false, "in": "query", + "description": "Library ID to filter by", "schema": { "format": "uuid", "nullable": true, @@ -8700,6 +9453,7 @@ "name": "make", "required": false, "in": "query", + "description": "Filter by camera make", "schema": { "type": "string" } @@ -8708,6 +9462,7 @@ "name": "minFileSize", "required": false, "in": "query", + "description": "Minimum file size in bytes", "schema": { "minimum": 0, "type": "integer" @@ -8717,6 +9472,7 @@ "name": "model", "required": false, "in": "query", + "description": "Filter by camera model", "schema": { "nullable": true, "type": "string" @@ -8726,6 +9482,7 @@ "name": "ocr", "required": false, "in": "query", + "description": "Filter by OCR text content", "schema": { "type": "string" } @@ -8734,6 +9491,7 @@ "name": "personIds", "required": false, "in": "query", + "description": "Filter by person IDs", "schema": { "type": "array", "items": { @@ -8746,9 +9504,27 @@ "name": "rating", "required": false, "in": "query", + "description": "Filter by rating [1-5], or null for unrated", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable", "schema": { "minimum": -1, "maximum": 5, + "nullable": true, "type": "number" } }, @@ -8756,6 +9532,7 @@ "name": "size", "required": false, "in": "query", + "description": "Number of results to return", "schema": { "minimum": 1, "maximum": 1000, @@ -8766,6 +9543,7 @@ "name": "state", "required": false, "in": "query", + "description": "Filter by state/province name", "schema": { "nullable": true, "type": "string" @@ -8775,6 +9553,7 @@ "name": "tagIds", "required": false, "in": "query", + "description": "Filter by tag IDs", "schema": { "nullable": true, "type": "array", @@ -8788,6 +9567,7 @@ "name": "takenAfter", "required": false, "in": "query", + "description": "Filter by taken date (after)", "schema": { "format": "date-time", "type": "string" @@ -8797,6 +9577,7 @@ "name": "takenBefore", "required": false, "in": "query", + "description": "Filter by taken date (before)", "schema": { "format": "date-time", "type": "string" @@ -8806,6 +9587,7 @@ "name": "trashedAfter", "required": false, "in": "query", + "description": "Filter by trash date (after)", "schema": { "format": "date-time", "type": "string" @@ -8815,6 +9597,7 @@ "name": "trashedBefore", "required": false, "in": "query", + "description": "Filter by trash date (before)", "schema": { "format": "date-time", "type": "string" @@ -8824,6 +9607,7 @@ "name": "type", "required": false, "in": "query", + "description": "Asset type filter", "schema": { "$ref": "#/components/schemas/AssetTypeEnum" } @@ -8832,6 +9616,7 @@ "name": "updatedAfter", "required": false, "in": "query", + "description": "Filter by update date (after)", "schema": { "format": "date-time", "type": "string" @@ -8841,6 +9626,7 @@ "name": "updatedBefore", "required": false, "in": "query", + "description": "Filter by update date (before)", "schema": { "format": "date-time", "type": "string" @@ -8850,6 +9636,7 @@ "name": "visibility", "required": false, "in": "query", + "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -8858,6 +9645,7 @@ "name": "withDeleted", "required": false, "in": "query", + "description": "Include deleted assets", "schema": { "type": "boolean" } @@ -8866,6 +9654,7 @@ "name": "withExif", "required": false, "in": "query", + "description": "Include EXIF data in response", "schema": { "type": "boolean" } @@ -8988,6 +9777,7 @@ "name": "name", "required": true, "in": "query", + "description": "Person name to search for", "schema": { "type": "string" } @@ -8996,6 +9786,7 @@ "name": "withHidden", "required": false, "in": "query", + "description": "Include hidden people", "schema": { "type": "boolean" } @@ -9058,6 +9849,7 @@ "name": "name", "required": true, "in": "query", + "description": "Place name to search for", "schema": { "type": "string" } @@ -9303,6 +10095,7 @@ "name": "country", "required": false, "in": "query", + "description": "Filter by country", "schema": { "type": "string" } @@ -9311,6 +10104,7 @@ "name": "includeNull", "required": false, "in": "query", + "description": "Include null values in suggestions", "x-immich-history": [ { "version": "v1.111.0", @@ -9330,6 +10124,7 @@ "name": "lensModel", "required": false, "in": "query", + "description": "Filter by lens model", "schema": { "type": "string" } @@ -9338,6 +10133,7 @@ "name": "make", "required": false, "in": "query", + "description": "Filter by camera make", "schema": { "type": "string" } @@ -9346,6 +10142,7 @@ "name": "model", "required": false, "in": "query", + "description": "Filter by camera model", "schema": { "type": "string" } @@ -9354,6 +10151,7 @@ "name": "state", "required": false, "in": "query", + "description": "Filter by state/province", "schema": { "type": "string" } @@ -9362,6 +10160,7 @@ "name": "type", "required": true, "in": "query", + "description": "Suggestion type", "schema": { "$ref": "#/components/schemas/SearchSuggestionType" } @@ -10425,6 +11224,23 @@ "name": "albumId", "required": false, "in": "query", + "description": "Filter by album ID", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Filter by shared link ID", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + } + ], "schema": { "format": "uuid", "type": "string" @@ -10537,6 +11353,78 @@ "x-immich-state": "Stable" } }, + "/shared-links/login": { + "post": { + "description": "Login to a password protected shared link", + "operationId": "sharedLinkLogin", + "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkLoginDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Shared link login", + "tags": [ + "Shared links" + ], + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Beta" + } + ], + "x-immich-state": "Beta" + } + }, "/shared-links/me": { "get": { "description": "Retrieve the current shared link associated with authentication method.", @@ -10554,6 +11442,7 @@ "name": "password", "required": false, "in": "query", + "description": "Link password", "schema": { "example": "password", "type": "string" @@ -10571,6 +11460,7 @@ "name": "token", "required": false, "in": "query", + "description": "Access token", "schema": { "type": "string" } @@ -11033,6 +11923,7 @@ "name": "primaryAssetId", "required": false, "in": "query", + "description": "Filter by primary asset ID", "schema": { "format": "uuid", "type": "string" @@ -12698,6 +13589,16 @@ "type": "string" } }, + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box coordinates as west,south,east,north (WGS84)", + "schema": { + "example": "11.075683,49.416711,11.117589,49.454875", + "type": "string" + } + }, { "name": "isFavorite", "required": false, @@ -12874,6 +13775,16 @@ "type": "string" } }, + { + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box coordinates as west,south,east,north (WGS84)", + "schema": { + "example": "11.075683,49.416711,11.117589,49.454875", + "type": "string" + } + }, { "name": "isFavorite", "required": false, @@ -14036,6 +14947,7 @@ "state": "Stable" } ], + "x-immich-permission": "folder.read", "x-immich-state": "Stable" } }, @@ -14088,6 +15000,7 @@ "state": "Stable" } ], + "x-immich-permission": "folder.read", "x-immich-state": "Stable" } }, @@ -14365,7 +15278,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "2.4.0", + "version": "2.5.6", "contact": {} }, "tags": [ @@ -14393,6 +15306,10 @@ "name": "Authentication (admin)", "description": "Administrative endpoints related to authentication." }, + { + "name": "Database Backups (admin)", + "description": "Manage backups of the Immich database." + }, { "name": "Deprecated", "description": "Deprecated endpoints that are planned for removal in the next major release." @@ -14542,9 +15459,11 @@ "APIKeyCreateDto": { "properties": { "name": { + "description": "API key name", "type": "string" }, "permissions": { + "description": "List of permissions", "items": { "$ref": "#/components/schemas/Permission" }, @@ -14563,6 +15482,7 @@ "$ref": "#/components/schemas/APIKeyResponseDto" }, "secret": { + "description": "API key secret (only shown once)", "type": "string" } }, @@ -14575,22 +15495,27 @@ "APIKeyResponseDto": { "properties": { "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "id": { + "description": "API key ID", "type": "string" }, "name": { + "description": "API key name", "type": "string" }, "permissions": { + "description": "List of permissions", "items": { "$ref": "#/components/schemas/Permission" }, "type": "array" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -14607,9 +15532,11 @@ "APIKeyUpdateDto": { "properties": { "name": { + "description": "API key name", "type": "string" }, "permissions": { + "description": "List of permissions", "items": { "$ref": "#/components/schemas/Permission" }, @@ -14622,14 +15549,17 @@ "ActivityCreateDto": { "properties": { "albumId": { + "description": "Album ID", "format": "uuid", "type": "string" }, "assetId": { + "description": "Asset ID (if activity is for an asset)", "format": "uuid", "type": "string" }, "comment": { + "description": "Comment text (required if type is comment)", "type": "string" }, "type": { @@ -14637,7 +15567,8 @@ { "$ref": "#/components/schemas/ReactionType" } - ] + ], + "description": "Activity type (like or comment)" } }, "required": [ @@ -14649,18 +15580,22 @@ "ActivityResponseDto": { "properties": { "assetId": { + "description": "Asset ID (if activity is for an asset)", "nullable": true, "type": "string" }, "comment": { + "description": "Comment text (for comment activities)", "nullable": true, "type": "string" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "id": { + "description": "Activity ID", "type": "string" }, "type": { @@ -14668,7 +15603,8 @@ { "$ref": "#/components/schemas/ReactionType" } - ] + ], + "description": "Activity type" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -14686,9 +15622,11 @@ "ActivityStatisticsResponseDto": { "properties": { "comments": { + "description": "Number of comments", "type": "integer" }, "likes": { + "description": "Number of likes", "type": "integer" } }, @@ -14701,6 +15639,7 @@ "AddUsersDto": { "properties": { "albumUsers": { + "description": "Album users to add", "items": { "$ref": "#/components/schemas/AlbumUserAddDto" }, @@ -14716,6 +15655,7 @@ "AdminOnboardingUpdateDto": { "properties": { "isOnboarded": { + "description": "Is admin onboarded", "type": "boolean" } }, @@ -14727,9 +15667,11 @@ "AlbumResponseDto": { "properties": { "albumName": { + "description": "Album name", "type": "string" }, "albumThumbnailAssetId": { + "description": "Thumbnail asset ID", "nullable": true, "type": "string" }, @@ -14740,6 +15682,7 @@ "type": "array" }, "assetCount": { + "description": "Number of assets", "type": "integer" }, "assets": { @@ -14755,26 +15698,33 @@ "type": "array" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "description": { + "description": "Album description", "type": "string" }, "endDate": { + "description": "End date (latest asset)", "format": "date-time", "type": "string" }, "hasSharedLink": { + "description": "Has shared link", "type": "boolean" }, "id": { + "description": "Album ID", "type": "string" }, "isActivityEnabled": { + "description": "Activity feed enabled", "type": "boolean" }, "lastModifiedAssetTimestamp": { + "description": "Last modified asset timestamp", "format": "date-time", "type": "string" }, @@ -14783,22 +15733,27 @@ { "$ref": "#/components/schemas/AssetOrder" } - ] + ], + "description": "Asset sort order" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" }, "ownerId": { + "description": "Owner user ID", "type": "string" }, "shared": { + "description": "Is shared album", "type": "boolean" }, "startDate": { + "description": "Start date (earliest asset)", "format": "date-time", "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -14824,12 +15779,15 @@ "AlbumStatisticsResponseDto": { "properties": { "notShared": { + "description": "Number of non-shared albums", "type": "integer" }, "owned": { + "description": "Number of owned albums", "type": "integer" }, "shared": { + "description": "Number of shared albums", "type": "integer" } }, @@ -14848,9 +15806,11 @@ "$ref": "#/components/schemas/AlbumUserRole" } ], - "default": "editor" + "default": "editor", + "description": "Album user role" }, "userId": { + "description": "User ID", "format": "uuid", "type": "string" } @@ -14867,9 +15827,11 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "userId": { + "description": "User ID", "format": "uuid", "type": "string" } @@ -14887,7 +15849,8 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -14900,6 +15863,7 @@ "type": "object" }, "AlbumUserRole": { + "description": "Album user role", "enum": [ "editor", "viewer" @@ -14909,6 +15873,7 @@ "AlbumsAddAssetsDto": { "properties": { "albumIds": { + "description": "Album IDs", "items": { "format": "uuid", "type": "string" @@ -14916,6 +15881,7 @@ "type": "array" }, "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -14936,9 +15902,11 @@ { "$ref": "#/components/schemas/BulkIdErrorReason" } - ] + ], + "description": "Error reason" }, "success": { + "description": "Operation success", "type": "boolean" } }, @@ -14955,7 +15923,8 @@ "$ref": "#/components/schemas/AssetOrder" } ], - "default": "desc" + "default": "desc", + "description": "Default asset order for albums" } }, "required": [ @@ -14964,13 +15933,15 @@ "type": "object" }, "AlbumsUpdate": { + "description": "Album preferences", "properties": { "defaultAssetOrder": { "allOf": [ { "$ref": "#/components/schemas/AssetOrder" } - ] + ], + "description": "Default asset order for albums" } }, "type": "object" @@ -14978,9 +15949,11 @@ "AssetBulkDeleteDto": { "properties": { "force": { + "description": "Force delete even if in use", "type": "boolean" }, "ids": { + "description": "IDs to process", "items": { "format": "uuid", "type": "string" @@ -14996,19 +15969,24 @@ "AssetBulkUpdateDto": { "properties": { "dateTimeOriginal": { + "description": "Original date and time", "type": "string" }, "dateTimeRelative": { + "description": "Relative time offset in seconds", "type": "number" }, "description": { + "description": "Asset description", "type": "string" }, "duplicateId": { + "description": "Duplicate ID", "nullable": true, "type": "string" }, "ids": { + "description": "Asset IDs to update", "items": { "format": "uuid", "type": "string" @@ -15016,20 +15994,42 @@ "type": "array" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "latitude": { + "description": "Latitude coordinate", "type": "number" }, "longitude": { + "description": "Longitude coordinate", "type": "number" }, "rating": { + "description": "Rating in range [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "timeZone": { + "description": "Time zone (IANA timezone)", "type": "string" }, "visibility": { @@ -15037,7 +16037,8 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "required": [ @@ -15048,6 +16049,7 @@ "AssetBulkUploadCheckDto": { "properties": { "assets": { + "description": "Assets to check", "items": { "$ref": "#/components/schemas/AssetBulkUploadCheckItem" }, @@ -15062,10 +16064,11 @@ "AssetBulkUploadCheckItem": { "properties": { "checksum": { - "description": "base64 or hex encoded sha1 hash", + "description": "Base64 or hex encoded SHA1 hash", "type": "string" }, "id": { + "description": "Asset ID", "type": "string" } }, @@ -15078,6 +16081,7 @@ "AssetBulkUploadCheckResponseDto": { "properties": { "results": { + "description": "Upload check results", "items": { "$ref": "#/components/schemas/AssetBulkUploadCheckResult" }, @@ -15092,6 +16096,7 @@ "AssetBulkUploadCheckResult": { "properties": { "action": { + "description": "Upload action", "enum": [ "accept", "reject" @@ -15099,15 +16104,19 @@ "type": "string" }, "assetId": { + "description": "Existing asset ID if duplicate", "type": "string" }, "id": { + "description": "Asset ID", "type": "string" }, "isTrashed": { + "description": "Whether existing asset is trashed", "type": "boolean" }, "reason": { + "description": "Rejection reason if rejected", "enum": [ "duplicate", "unsupported-format" @@ -15125,29 +16134,36 @@ "properties": { "albums": { "default": true, + "description": "Copy album associations", "type": "boolean" }, "favorite": { "default": true, + "description": "Copy favorite status", "type": "boolean" }, "sharedLinks": { "default": true, + "description": "Copy shared links", "type": "boolean" }, "sidecar": { "default": true, + "description": "Copy sidecar file", "type": "boolean" }, "sourceId": { + "description": "Source asset ID", "format": "uuid", "type": "string" }, "stack": { "default": true, + "description": "Copy stack association", "type": "boolean" }, "targetId": { + "description": "Target asset ID", "format": "uuid", "type": "string" } @@ -15161,10 +16177,12 @@ "AssetDeltaSyncDto": { "properties": { "updatedAfter": { + "description": "Sync assets updated after this date", "format": "date-time", "type": "string" }, "userIds": { + "description": "User IDs to sync", "items": { "format": "uuid", "type": "string" @@ -15181,15 +16199,18 @@ "AssetDeltaSyncResponseDto": { "properties": { "deleted": { + "description": "Deleted asset IDs", "items": { "type": "string" }, "type": "array" }, "needsFullSync": { + "description": "Whether full sync is needed", "type": "boolean" }, "upserted": { + "description": "Upserted assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -15203,32 +16224,153 @@ ], "type": "object" }, + "AssetEditAction": { + "description": "Type of edit action to perform", + "enum": [ + "crop", + "rotate", + "mirror" + ], + "type": "string" + }, + "AssetEditActionItemDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" + } + }, + "required": [ + "action", + "parameters" + ], + "type": "object" + }, + "AssetEditActionItemResponseDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" + } + }, + "required": [ + "action", + "id", + "parameters" + ], + "type": "object" + }, + "AssetEditsCreateDto": { + "properties": { + "edits": { + "description": "List of edit actions to apply (crop, rotate, or mirror)", + "items": { + "$ref": "#/components/schemas/AssetEditActionItemDto" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "edits" + ], + "type": "object" + }, + "AssetEditsResponseDto": { + "properties": { + "assetId": { + "description": "Asset ID these edits belong to", + "format": "uuid", + "type": "string" + }, + "edits": { + "description": "List of edit actions applied to the asset", + "items": { + "$ref": "#/components/schemas/AssetEditActionItemResponseDto" + }, + "type": "array" + } + }, + "required": [ + "assetId", + "edits" + ], + "type": "object" + }, "AssetFaceCreateDto": { "properties": { "assetId": { + "description": "Asset ID", "format": "uuid", "type": "string" }, "height": { + "description": "Face bounding box height", "type": "integer" }, "imageHeight": { + "description": "Image height in pixels", "type": "integer" }, "imageWidth": { + "description": "Image width in pixels", "type": "integer" }, "personId": { + "description": "Person ID", "format": "uuid", "type": "string" }, "width": { + "description": "Face bounding box width", "type": "integer" }, "x": { + "description": "Face bounding box X coordinate", "type": "integer" }, "y": { + "description": "Face bounding box Y coordinate", "type": "integer" } }, @@ -15247,6 +16389,7 @@ "AssetFaceDeleteDto": { "properties": { "force": { + "description": "Force delete even if person has other faces", "type": "boolean" } }, @@ -15258,25 +16401,32 @@ "AssetFaceResponseDto": { "properties": { "boundingBoxX1": { + "description": "Bounding box X1 coordinate", "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2 coordinate", "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1 coordinate", "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2 coordinate", "type": "integer" }, "id": { + "description": "Face ID", "format": "uuid", "type": "string" }, "imageHeight": { + "description": "Image height in pixels", "type": "integer" }, "imageWidth": { + "description": "Image width in pixels", "type": "integer" }, "person": { @@ -15285,6 +16435,7 @@ "$ref": "#/components/schemas/PersonResponseDto" } ], + "description": "Person associated with face", "nullable": true }, "sourceType": { @@ -15292,7 +16443,8 @@ { "$ref": "#/components/schemas/SourceType" } - ] + ], + "description": "Face detection source type" } }, "required": [ @@ -15310,6 +16462,7 @@ "AssetFaceUpdateDto": { "properties": { "data": { + "description": "Face update items", "items": { "$ref": "#/components/schemas/AssetFaceUpdateItem" }, @@ -15324,10 +16477,12 @@ "AssetFaceUpdateItem": { "properties": { "assetId": { + "description": "Asset ID", "format": "uuid", "type": "string" }, "personId": { + "description": "Person ID", "format": "uuid", "type": "string" } @@ -15341,25 +16496,32 @@ "AssetFaceWithoutPersonResponseDto": { "properties": { "boundingBoxX1": { + "description": "Bounding box X1 coordinate", "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2 coordinate", "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1 coordinate", "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2 coordinate", "type": "integer" }, "id": { + "description": "Face ID", "format": "uuid", "type": "string" }, "imageHeight": { + "description": "Image height in pixels", "type": "integer" }, "imageWidth": { + "description": "Image width in pixels", "type": "integer" }, "sourceType": { @@ -15367,7 +16529,8 @@ { "$ref": "#/components/schemas/SourceType" } - ] + ], + "description": "Face detection source type" } }, "required": [ @@ -15384,18 +16547,22 @@ "AssetFullSyncDto": { "properties": { "lastId": { + "description": "Last asset ID (pagination)", "format": "uuid", "type": "string" }, "limit": { + "description": "Maximum number of assets to return", "minimum": 1, "type": "integer" }, "updatedUntil": { + "description": "Sync assets updated until this date", "format": "date-time", "type": "string" }, "userId": { + "description": "Filter by user ID", "format": "uuid", "type": "string" } @@ -15409,6 +16576,7 @@ "AssetIdsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -15424,9 +16592,11 @@ "AssetIdsResponseDto": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "error": { + "description": "Error reason if failed", "enum": [ "duplicate", "no_permission", @@ -15435,6 +16605,7 @@ "type": "string" }, "success": { + "description": "Whether operation succeeded", "type": "boolean" } }, @@ -15445,6 +16616,7 @@ "type": "object" }, "AssetJobName": { + "description": "Job name", "enum": [ "refresh-faces", "refresh-metadata", @@ -15456,6 +16628,7 @@ "AssetJobsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -15467,7 +16640,8 @@ { "$ref": "#/components/schemas/AssetJobName" } - ] + ], + "description": "Job name" } }, "required": [ @@ -15479,43 +16653,54 @@ "AssetMediaCreateDto": { "properties": { "assetData": { + "description": "Asset file data", "format": "binary", "type": "string" }, "deviceAssetId": { + "description": "Device asset ID", "type": "string" }, "deviceId": { + "description": "Device ID", "type": "string" }, "duration": { + "description": "Duration (for videos)", "type": "string" }, "fileCreatedAt": { + "description": "File creation date", "format": "date-time", "type": "string" }, "fileModifiedAt": { + "description": "File modification date", "format": "date-time", "type": "string" }, "filename": { + "description": "Filename", "type": "string" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "livePhotoVideoId": { + "description": "Live photo video ID", "format": "uuid", "type": "string" }, "metadata": { + "description": "Asset metadata items", "items": { "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" }, "type": "array" }, "sidecarData": { + "description": "Sidecar file data", "format": "binary", "type": "string" }, @@ -15524,7 +16709,8 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "required": [ @@ -15532,35 +16718,41 @@ "deviceAssetId", "deviceId", "fileCreatedAt", - "fileModifiedAt", - "metadata" + "fileModifiedAt" ], "type": "object" }, "AssetMediaReplaceDto": { "properties": { "assetData": { + "description": "Asset file data", "format": "binary", "type": "string" }, "deviceAssetId": { + "description": "Device asset ID", "type": "string" }, "deviceId": { + "description": "Device ID", "type": "string" }, "duration": { + "description": "Duration (for videos)", "type": "string" }, "fileCreatedAt": { + "description": "File creation date", "format": "date-time", "type": "string" }, "fileModifiedAt": { + "description": "File modification date", "format": "date-time", "type": "string" }, "filename": { + "description": "Filename", "type": "string" } }, @@ -15576,6 +16768,7 @@ "AssetMediaResponseDto": { "properties": { "id": { + "description": "Asset media ID", "type": "string" }, "status": { @@ -15583,7 +16776,8 @@ { "$ref": "#/components/schemas/AssetMediaStatus" } - ] + ], + "description": "Upload status" } }, "required": [ @@ -15594,6 +16788,7 @@ }, "AssetMediaSize": { "enum": [ + "original", "fullsize", "preview", "thumbnail" @@ -15601,6 +16796,7 @@ "type": "string" }, "AssetMediaStatus": { + "description": "Upload status", "enum": [ "created", "replaced", @@ -15608,26 +16804,118 @@ ], "type": "string" }, - "AssetMetadataKey": { - "enum": [ - "mobile-app" - ], - "type": "string" - }, - "AssetMetadataResponseDto": { + "AssetMetadataBulkDeleteDto": { "properties": { + "items": { + "description": "Metadata items to delete", + "items": { + "$ref": "#/components/schemas/AssetMetadataBulkDeleteItemDto" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "AssetMetadataBulkDeleteItemDto": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "type": "string" + }, "key": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMetadataKey" - } - ] + "description": "Metadata key", + "type": "string" + } + }, + "required": [ + "assetId", + "key" + ], + "type": "object" + }, + "AssetMetadataBulkResponseDto": { + "properties": { + "assetId": { + "description": "Asset ID", + "type": "string" + }, + "key": { + "description": "Metadata key", + "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" }, "value": { + "description": "Metadata value (object)", + "type": "object" + } + }, + "required": [ + "assetId", + "key", + "updatedAt", + "value" + ], + "type": "object" + }, + "AssetMetadataBulkUpsertDto": { + "properties": { + "items": { + "description": "Metadata items to upsert", + "items": { + "$ref": "#/components/schemas/AssetMetadataBulkUpsertItemDto" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + }, + "AssetMetadataBulkUpsertItemDto": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "type": "string" + }, + "key": { + "description": "Metadata key", + "type": "string" + }, + "value": { + "description": "Metadata value (object)", + "type": "object" + } + }, + "required": [ + "assetId", + "key", + "value" + ], + "type": "object" + }, + "AssetMetadataResponseDto": { + "properties": { + "key": { + "description": "Metadata key", + "type": "string" + }, + "updatedAt": { + "description": "Last update date", + "format": "date-time", + "type": "string" + }, + "value": { + "description": "Metadata value (object)", "type": "object" } }, @@ -15641,6 +16929,7 @@ "AssetMetadataUpsertDto": { "properties": { "items": { + "description": "Metadata items to upsert", "items": { "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" }, @@ -15655,13 +16944,11 @@ "AssetMetadataUpsertItemDto": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMetadataKey" - } - ] + "description": "Metadata key", + "type": "string" }, "value": { + "description": "Metadata value (object)", "type": "object" } }, @@ -15754,6 +17041,7 @@ "type": "object" }, "AssetOrder": { + "description": "Asset sort order", "enum": [ "asc", "desc" @@ -15763,7 +17051,7 @@ "AssetResponseDto": { "properties": { "checksum": { - "description": "base64 encoded sha1 hash", + "description": "Base64 encoded SHA1 hash", "type": "string" }, "createdAt": { @@ -15773,16 +17061,20 @@ "type": "string" }, "deviceAssetId": { + "description": "Device asset ID", "type": "string" }, "deviceId": { + "description": "Device ID", "type": "string" }, "duplicateId": { + "description": "Duplicate group ID", "nullable": true, "type": "string" }, "duration": { + "description": "Video duration (for videos)", "type": "string" }, "exifInfo": { @@ -15801,25 +17093,53 @@ "type": "string" }, "hasMetadata": { + "description": "Whether asset has metadata", "type": "boolean" }, + "height": { + "description": "Asset height", + "nullable": true, + "type": "number" + }, "id": { + "description": "Asset ID", "type": "string" }, "isArchived": { + "description": "Is archived", "type": "boolean" }, + "isEdited": { + "description": "Is edited", + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-state": "Beta" + }, "isFavorite": { + "description": "Is favorite", "type": "boolean" }, "isOffline": { + "description": "Is offline", "type": "boolean" }, "isTrashed": { + "description": "Is trashed", "type": "boolean" }, "libraryId": { "deprecated": true, + "description": "Library ID", + "format": "uuid", "nullable": true, "type": "string", "x-immich-history": [ @@ -15835,6 +17155,7 @@ "x-immich-state": "Deprecated" }, "livePhotoVideoId": { + "description": "Live photo video ID", "nullable": true, "type": "string" }, @@ -15845,18 +17166,22 @@ "type": "string" }, "originalFileName": { + "description": "Original file name", "type": "string" }, "originalMimeType": { + "description": "Original MIME type", "type": "string" }, "originalPath": { + "description": "Original file path", "type": "string" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" }, "ownerId": { + "description": "Owner user ID", "type": "string" }, "people": { @@ -15867,6 +17192,7 @@ }, "resized": { "deprecated": true, + "description": "Is resized", "type": "boolean", "x-immich-history": [ { @@ -15895,6 +17221,7 @@ "type": "array" }, "thumbhash": { + "description": "Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.", "nullable": true, "type": "string" }, @@ -15903,7 +17230,8 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type" }, "unassignedFaces": { "items": { @@ -15922,7 +17250,13 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" + }, + "width": { + "description": "Asset width", + "nullable": true, + "type": "number" } }, "required": [ @@ -15934,8 +17268,10 @@ "fileCreatedAt", "fileModifiedAt", "hasMetadata", + "height", "id", "isArchived", + "isEdited", "isFavorite", "isOffline", "isTrashed", @@ -15946,19 +17282,23 @@ "thumbhash", "type", "updatedAt", - "visibility" + "visibility", + "width" ], "type": "object" }, "AssetStackResponseDto": { "properties": { "assetCount": { + "description": "Number of assets in stack", "type": "integer" }, "id": { + "description": "Stack ID", "type": "string" }, "primaryAssetId": { + "description": "Primary asset ID", "type": "string" } }, @@ -15972,12 +17312,15 @@ "AssetStatsResponseDto": { "properties": { "images": { + "description": "Number of images", "type": "integer" }, "total": { + "description": "Total number of assets", "type": "integer" }, "videos": { + "description": "Number of videos", "type": "integer" } }, @@ -15989,6 +17332,7 @@ "type": "object" }, "AssetTypeEnum": { + "description": "Asset type", "enum": [ "IMAGE", "VIDEO", @@ -15998,6 +17342,7 @@ "type": "string" }, "AssetVisibility": { + "description": "Asset visibility", "enum": [ "archive", "timeline", @@ -16007,6 +17352,7 @@ "type": "string" }, "AudioCodec": { + "description": "Target audio codec", "enum": [ "mp3", "aac", @@ -16018,18 +17364,23 @@ "AuthStatusResponseDto": { "properties": { "expiresAt": { + "description": "Session expiration date", "type": "string" }, "isElevated": { + "description": "Is elevated session", "type": "boolean" }, "password": { + "description": "Has password set", "type": "boolean" }, "pinCode": { + "description": "Has PIN code set", "type": "boolean" }, "pinExpiresAt": { + "description": "PIN expiration date", "type": "string" } }, @@ -16047,12 +17398,14 @@ { "$ref": "#/components/schemas/UserAvatarColor" } - ] + ], + "description": "Avatar color" } }, "type": "object" }, "BulkIdErrorReason": { + "description": "Error reason", "enum": [ "duplicate", "no_permission", @@ -16064,6 +17417,7 @@ "BulkIdResponseDto": { "properties": { "error": { + "description": "Error reason if failed", "enum": [ "duplicate", "no_permission", @@ -16073,9 +17427,11 @@ "type": "string" }, "id": { + "description": "ID", "type": "string" }, "success": { + "description": "Whether operation succeeded", "type": "boolean" } }, @@ -16088,6 +17444,7 @@ "BulkIdsDto": { "properties": { "ids": { + "description": "IDs to process", "items": { "format": "uuid", "type": "string" @@ -16103,9 +17460,11 @@ "CLIPConfig": { "properties": { "enabled": { + "description": "Whether the task is enabled", "type": "boolean" }, "modelName": { + "description": "Name of the model to use", "type": "string" } }, @@ -16116,6 +17475,7 @@ "type": "object" }, "CQMode": { + "description": "CQ mode", "enum": [ "auto", "cqp", @@ -16127,6 +17487,7 @@ "properties": { "gCastEnabled": { "default": false, + "description": "Whether Google Cast is enabled", "type": "boolean" } }, @@ -16138,6 +17499,7 @@ "CastUpdate": { "properties": { "gCastEnabled": { + "description": "Whether Google Cast is enabled", "type": "boolean" } }, @@ -16147,14 +17509,17 @@ "properties": { "invalidateSessions": { "default": false, + "description": "Invalidate all other sessions", "type": "boolean" }, "newPassword": { + "description": "New password (min 8 characters)", "example": "password", "minLength": 8, "type": "string" }, "password": { + "description": "Current password", "example": "password", "type": "string" } @@ -16168,6 +17533,7 @@ "CheckExistingAssetsDto": { "properties": { "deviceAssetIds": { + "description": "Device asset IDs to check", "items": { "type": "string" }, @@ -16175,6 +17541,7 @@ "type": "array" }, "deviceId": { + "description": "Device ID", "type": "string" } }, @@ -16187,6 +17554,7 @@ "CheckExistingAssetsResponseDto": { "properties": { "existingIds": { + "description": "Existing asset IDs", "items": { "type": "string" }, @@ -16199,6 +17567,7 @@ "type": "object" }, "Colorspace": { + "description": "Colorspace", "enum": [ "srgb", "p3" @@ -16208,9 +17577,11 @@ "ContributorCountResponseDto": { "properties": { "assetCount": { + "description": "Number of assets contributed", "type": "integer" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -16223,15 +17594,18 @@ "CreateAlbumDto": { "properties": { "albumName": { + "description": "Album name", "type": "string" }, "albumUsers": { + "description": "Album users", "items": { "$ref": "#/components/schemas/AlbumUserCreateDto" }, "type": "array" }, "assetIds": { + "description": "Initial asset IDs", "items": { "format": "uuid", "type": "string" @@ -16239,6 +17613,7 @@ "type": "array" }, "description": { + "description": "Album description", "type": "string" } }, @@ -16250,6 +17625,7 @@ "CreateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -16258,6 +17634,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths (max 128)", "items": { "type": "string" }, @@ -16266,9 +17643,11 @@ "uniqueItems": true }, "name": { + "description": "Library name", "type": "string" }, "ownerId": { + "description": "Owner user ID", "format": "uuid", "type": "string" } @@ -16281,6 +17660,7 @@ "CreateProfileImageDto": { "properties": { "file": { + "description": "Profile image file", "format": "binary", "type": "string" } @@ -16293,13 +17673,16 @@ "CreateProfileImageResponseDto": { "properties": { "profileChangedAt": { + "description": "Profile image change date", "format": "date-time", "type": "string" }, "profileImagePath": { + "description": "Profile image file path", "type": "string" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -16310,15 +17693,49 @@ ], "type": "object" }, + "CropParameters": { + "properties": { + "height": { + "description": "Height of the crop", + "minimum": 1, + "type": "number" + }, + "width": { + "description": "Width of the crop", + "minimum": 1, + "type": "number" + }, + "x": { + "description": "Top-Left X coordinate of crop", + "minimum": 0, + "type": "number" + }, + "y": { + "description": "Top-Left Y coordinate of crop", + "minimum": 0, + "type": "number" + } + }, + "required": [ + "height", + "width", + "x", + "y" + ], + "type": "object" + }, "DatabaseBackupConfig": { "properties": { "cronExpression": { + "description": "Cron expression", "type": "string" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "keepLastAmount": { + "description": "Keep last amount", "minimum": 1, "type": "number" } @@ -16330,15 +17747,89 @@ ], "type": "object" }, + "DatabaseBackupDeleteDto": { + "properties": { + "backups": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "backups" + ], + "type": "object" + }, + "DatabaseBackupDto": { + "properties": { + "filename": { + "type": "string" + }, + "filesize": { + "type": "number" + } + }, + "required": [ + "filename", + "filesize" + ], + "type": "object" + }, + "DatabaseBackupListResponseDto": { + "properties": { + "backups": { + "items": { + "$ref": "#/components/schemas/DatabaseBackupDto" + }, + "type": "array" + } + }, + "required": [ + "backups" + ], + "type": "object" + }, + "DatabaseBackupUploadDto": { + "properties": { + "file": { + "format": "binary", + "type": "string" + } + }, + "type": "object" + }, + "DownloadArchiveDto": { + "properties": { + "assetIds": { + "description": "Asset IDs", + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "edited": { + "description": "Download edited asset if available", + "type": "boolean" + } + }, + "required": [ + "assetIds" + ], + "type": "object" + }, "DownloadArchiveInfo": { "properties": { "assetIds": { + "description": "Asset IDs in this archive", "items": { "type": "string" }, "type": "array" }, "size": { + "description": "Archive size in bytes", "type": "integer" } }, @@ -16351,14 +17842,17 @@ "DownloadInfoDto": { "properties": { "albumId": { + "description": "Album ID to download", "format": "uuid", "type": "string" }, "archiveSize": { + "description": "Archive size limit in bytes", "minimum": 1, "type": "integer" }, "assetIds": { + "description": "Asset IDs to download", "items": { "format": "uuid", "type": "string" @@ -16366,6 +17860,7 @@ "type": "array" }, "userId": { + "description": "User ID to download assets from", "format": "uuid", "type": "string" } @@ -16375,10 +17870,12 @@ "DownloadResponse": { "properties": { "archiveSize": { + "description": "Maximum archive size in bytes", "type": "integer" }, "includeEmbeddedVideos": { "default": false, + "description": "Whether to include embedded videos in downloads", "type": "boolean" } }, @@ -16391,12 +17888,14 @@ "DownloadResponseDto": { "properties": { "archives": { + "description": "Archive information", "items": { "$ref": "#/components/schemas/DownloadArchiveInfo" }, "type": "array" }, "totalSize": { + "description": "Total size in bytes", "type": "integer" } }, @@ -16409,10 +17908,12 @@ "DownloadUpdate": { "properties": { "archiveSize": { + "description": "Maximum archive size in bytes", "minimum": 1, "type": "integer" }, "includeEmbeddedVideos": { + "description": "Whether to include embedded videos in downloads", "type": "boolean" } }, @@ -16421,9 +17922,11 @@ "DuplicateDetectionConfig": { "properties": { "enabled": { + "description": "Whether the task is enabled", "type": "boolean" }, "maxDistance": { + "description": "Maximum distance threshold for duplicate detection", "format": "double", "maximum": 0.1, "minimum": 0.001, @@ -16439,12 +17942,14 @@ "DuplicateResponseDto": { "properties": { "assets": { + "description": "Duplicate assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" }, "duplicateId": { + "description": "Duplicate group ID", "type": "string" } }, @@ -16457,12 +17962,15 @@ "EmailNotificationsResponse": { "properties": { "albumInvite": { + "description": "Whether to receive email notifications for album invites", "type": "boolean" }, "albumUpdate": { + "description": "Whether to receive email notifications for album updates", "type": "boolean" }, "enabled": { + "description": "Whether email notifications are enabled", "type": "boolean" } }, @@ -16476,12 +17984,15 @@ "EmailNotificationsUpdate": { "properties": { "albumInvite": { + "description": "Whether to receive email notifications for album invites", "type": "boolean" }, "albumUpdate": { + "description": "Whether to receive email notifications for album updates", "type": "boolean" }, "enabled": { + "description": "Whether email notifications are enabled", "type": "boolean" } }, @@ -16491,114 +18002,136 @@ "properties": { "city": { "default": null, + "description": "City name", "nullable": true, "type": "string" }, "country": { "default": null, + "description": "Country name", "nullable": true, "type": "string" }, "dateTimeOriginal": { "default": null, + "description": "Original date/time", "format": "date-time", "nullable": true, "type": "string" }, "description": { "default": null, + "description": "Image description", "nullable": true, "type": "string" }, "exifImageHeight": { "default": null, + "description": "Image height in pixels", "nullable": true, "type": "number" }, "exifImageWidth": { "default": null, + "description": "Image width in pixels", "nullable": true, "type": "number" }, "exposureTime": { "default": null, + "description": "Exposure time", "nullable": true, "type": "string" }, "fNumber": { "default": null, + "description": "F-number (aperture)", "nullable": true, "type": "number" }, "fileSizeInByte": { "default": null, + "description": "File size in bytes", "format": "int64", "nullable": true, "type": "integer" }, "focalLength": { "default": null, + "description": "Focal length in mm", "nullable": true, "type": "number" }, "iso": { "default": null, + "description": "ISO sensitivity", "nullable": true, "type": "number" }, "latitude": { "default": null, + "description": "GPS latitude", "nullable": true, "type": "number" }, "lensModel": { "default": null, + "description": "Lens model", "nullable": true, "type": "string" }, "longitude": { "default": null, + "description": "GPS longitude", "nullable": true, "type": "number" }, "make": { "default": null, + "description": "Camera make", "nullable": true, "type": "string" }, "model": { "default": null, + "description": "Camera model", "nullable": true, "type": "string" }, "modifyDate": { "default": null, + "description": "Modification date/time", "format": "date-time", "nullable": true, "type": "string" }, "orientation": { "default": null, + "description": "Image orientation", "nullable": true, "type": "string" }, "projectionType": { "default": null, + "description": "Projection type", "nullable": true, "type": "string" }, "rating": { "default": null, + "description": "Rating", "nullable": true, "type": "number" }, "state": { "default": null, + "description": "State/province name", "nullable": true, "type": "string" }, "timeZone": { "default": null, + "description": "Time zone", "nullable": true, "type": "string" } @@ -16608,6 +18141,7 @@ "FaceDto": { "properties": { "id": { + "description": "Face ID", "format": "uuid", "type": "string" } @@ -16620,25 +18154,30 @@ "FacialRecognitionConfig": { "properties": { "enabled": { + "description": "Whether the task is enabled", "type": "boolean" }, "maxDistance": { + "description": "Maximum distance threshold for face recognition", "format": "double", "maximum": 2, "minimum": 0.1, "type": "number" }, "minFaces": { + "description": "Minimum number of faces required for recognition", "minimum": 1, "type": "integer" }, "minScore": { + "description": "Minimum confidence score for face detection", "format": "double", "maximum": 1, "minimum": 0.1, "type": "number" }, "modelName": { + "description": "Name of the model to use", "type": "string" } }, @@ -16655,10 +18194,12 @@ "properties": { "enabled": { "default": false, + "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { "default": false, + "description": "Whether folders appear in web sidebar", "type": "boolean" } }, @@ -16671,15 +18212,18 @@ "FoldersUpdate": { "properties": { "enabled": { + "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether folders appear in web sidebar", "type": "boolean" } }, "type": "object" }, "ImageFormat": { + "description": "Image format", "enum": [ "jpeg", "webp" @@ -16693,7 +18237,8 @@ { "$ref": "#/components/schemas/ManualJobName" } - ] + ], + "description": "Job name" } }, "required": [ @@ -16702,6 +18247,7 @@ "type": "object" }, "JobName": { + "description": "Job name", "enum": [ "AssetDelete", "AssetDeleteCheck", @@ -16709,6 +18255,7 @@ "AssetDetectFaces", "AssetDetectDuplicatesQueueAll", "AssetDetectDuplicates", + "AssetEditThumbnailGeneration", "AssetEncodeVideoQueueAll", "AssetEncodeVideo", "AssetEmptyTrash", @@ -16764,6 +18311,7 @@ "JobSettingsDto": { "properties": { "concurrency": { + "description": "Concurrency", "minimum": 1, "type": "integer" } @@ -16776,39 +18324,48 @@ "LibraryResponseDto": { "properties": { "assetCount": { + "description": "Number of assets", "type": "integer" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "exclusionPatterns": { + "description": "Exclusion patterns", "items": { "type": "string" }, "type": "array" }, "id": { + "description": "Library ID", "type": "string" }, "importPaths": { + "description": "Import paths", "items": { "type": "string" }, "type": "array" }, "name": { + "description": "Library name", "type": "string" }, "ownerId": { + "description": "Owner user ID", "type": "string" }, "refreshedAt": { + "description": "Last refresh date", "format": "date-time", "nullable": true, "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -16830,19 +18387,23 @@ "properties": { "photos": { "default": 0, + "description": "Number of photos", "type": "integer" }, "total": { "default": 0, + "description": "Total number of assets", "type": "integer" }, "usage": { "default": 0, + "description": "Storage usage in bytes", "format": "int64", "type": "integer" }, "videos": { "default": 0, + "description": "Number of videos", "type": "integer" } }, @@ -16857,9 +18418,11 @@ "LicenseKeyDto": { "properties": { "activationKey": { + "description": "Activation key", "type": "string" }, "licenseKey": { + "description": "License key (format: IM(SV|CL)(-XXXX){8})", "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", "type": "string" } @@ -16873,13 +18436,16 @@ "LicenseResponseDto": { "properties": { "activatedAt": { + "description": "Activation date", "format": "date-time", "type": "string" }, "activationKey": { + "description": "Activation key", "type": "string" }, "licenseKey": { + "description": "License key (format: IM(SV|CL)(-XXXX){8})", "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", "type": "string" } @@ -16905,11 +18471,13 @@ "LoginCredentialDto": { "properties": { "email": { + "description": "User email", "example": "testuser@email.com", "format": "email", "type": "string" }, "password": { + "description": "User password", "example": "password", "type": "string" } @@ -16923,27 +18491,35 @@ "LoginResponseDto": { "properties": { "accessToken": { + "description": "Access token", "type": "string" }, "isAdmin": { + "description": "Is admin user", "type": "boolean" }, "isOnboarded": { + "description": "Is onboarded", "type": "boolean" }, "name": { + "description": "User name", "type": "string" }, "profileImagePath": { + "description": "Profile image path", "type": "string" }, "shouldChangePassword": { + "description": "Should change password", "type": "boolean" }, "userEmail": { + "description": "User email", "type": "string" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -16962,9 +18538,11 @@ "LogoutResponseDto": { "properties": { "redirectUri": { + "description": "Redirect URI", "type": "string" }, "successful": { + "description": "Logout successful", "type": "boolean" } }, @@ -16977,6 +18555,7 @@ "MachineLearningAvailabilityChecksDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "interval": { @@ -16994,15 +18573,19 @@ "type": "object" }, "MaintenanceAction": { + "description": "Maintenance action", "enum": [ "start", - "end" + "end", + "select_database_restore", + "restore_database" ], "type": "string" }, "MaintenanceAuthDto": { "properties": { "username": { + "description": "Maintenance username", "type": "string" } }, @@ -17011,15 +18594,91 @@ ], "type": "object" }, + "MaintenanceDetectInstallResponseDto": { + "properties": { + "storage": { + "items": { + "$ref": "#/components/schemas/MaintenanceDetectInstallStorageFolderDto" + }, + "type": "array" + } + }, + "required": [ + "storage" + ], + "type": "object" + }, + "MaintenanceDetectInstallStorageFolderDto": { + "properties": { + "files": { + "description": "Number of files in the folder", + "type": "number" + }, + "folder": { + "allOf": [ + { + "$ref": "#/components/schemas/StorageFolder" + } + ], + "description": "Storage folder" + }, + "readable": { + "description": "Whether the folder is readable", + "type": "boolean" + }, + "writable": { + "description": "Whether the folder is writable", + "type": "boolean" + } + }, + "required": [ + "files", + "folder", + "readable", + "writable" + ], + "type": "object" + }, "MaintenanceLoginDto": { "properties": { "token": { + "description": "Maintenance token", "type": "string" } }, "type": "object" }, + "MaintenanceStatusResponseDto": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/MaintenanceAction" + } + ], + "description": "Maintenance action" + }, + "active": { + "type": "boolean" + }, + "error": { + "type": "string" + }, + "progress": { + "type": "number" + }, + "task": { + "type": "string" + } + }, + "required": [ + "action", + "active" + ], + "type": "object" + }, "ManualJobName": { + "description": "Job name", "enum": [ "person-cleanup", "tag-cleanup", @@ -17033,25 +18692,31 @@ "MapMarkerResponseDto": { "properties": { "city": { + "description": "City name", "nullable": true, "type": "string" }, "country": { + "description": "Country name", "nullable": true, "type": "string" }, "id": { + "description": "Asset ID", "type": "string" }, "lat": { + "description": "Latitude", "format": "double", "type": "number" }, "lon": { + "description": "Longitude", "format": "double", "type": "number" }, "state": { + "description": "State/Province name", "nullable": true, "type": "string" } @@ -17069,14 +18734,17 @@ "MapReverseGeocodeResponseDto": { "properties": { "city": { + "description": "City name", "nullable": true, "type": "string" }, "country": { + "description": "Country name", "nullable": true, "type": "string" }, "state": { + "description": "State/Province name", "nullable": true, "type": "string" } @@ -17092,10 +18760,12 @@ "properties": { "duration": { "default": 5, + "description": "Memory duration in seconds", "type": "integer" }, "enabled": { "default": true, + "description": "Whether memories are enabled", "type": "boolean" } }, @@ -17108,10 +18778,12 @@ "MemoriesUpdate": { "properties": { "duration": { + "description": "Memory duration in seconds", "minimum": 1, "type": "integer" }, "enabled": { + "description": "Whether memories are enabled", "type": "boolean" } }, @@ -17120,6 +18792,7 @@ "MemoryCreateDto": { "properties": { "assetIds": { + "description": "Asset IDs to associate with memory", "items": { "format": "uuid", "type": "string" @@ -17129,23 +18802,59 @@ "data": { "$ref": "#/components/schemas/OnThisDayDto" }, + "hideAt": { + "description": "Date when memory should be hidden", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "isSaved": { + "description": "Is memory saved", "type": "boolean" }, "memoryAt": { + "description": "Memory date", "format": "date-time", "type": "string" }, "seenAt": { + "description": "Date when memory was seen", "format": "date-time", "type": "string" }, + "showAt": { + "description": "Date when memory should be shown", + "format": "date-time", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + }, "type": { "allOf": [ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" } }, "required": [ @@ -17164,6 +18873,7 @@ "type": "array" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, @@ -17171,31 +18881,39 @@ "$ref": "#/components/schemas/OnThisDayDto" }, "deletedAt": { + "description": "Deletion date", "format": "date-time", "type": "string" }, "hideAt": { + "description": "Date when memory should be hidden", "format": "date-time", "type": "string" }, "id": { + "description": "Memory ID", "type": "string" }, "isSaved": { + "description": "Is memory saved", "type": "boolean" }, "memoryAt": { + "description": "Memory date", "format": "date-time", "type": "string" }, "ownerId": { + "description": "Owner user ID", "type": "string" }, "seenAt": { + "description": "Date when memory was seen", "format": "date-time", "type": "string" }, "showAt": { + "description": "Date when memory should be shown", "format": "date-time", "type": "string" }, @@ -17204,9 +18922,11 @@ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -17235,6 +18955,7 @@ "MemoryStatisticsResponseDto": { "properties": { "total": { + "description": "Total number of memories", "type": "integer" } }, @@ -17252,13 +18973,16 @@ "MemoryUpdateDto": { "properties": { "isSaved": { + "description": "Is memory saved", "type": "boolean" }, "memoryAt": { + "description": "Memory date", "format": "date-time", "type": "string" }, "seenAt": { + "description": "Date when memory was seen", "format": "date-time", "type": "string" } @@ -17268,6 +18992,7 @@ "MergePersonDto": { "properties": { "ids": { + "description": "Person IDs to merge", "items": { "format": "uuid", "type": "string" @@ -17283,6 +19008,7 @@ "MetadataSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -17290,72 +19016,92 @@ "type": "array" }, "checksum": { + "description": "Filter by file checksum", "type": "string" }, "city": { + "description": "Filter by city name", "nullable": true, "type": "string" }, "country": { + "description": "Filter by country name", "nullable": true, "type": "string" }, "createdAfter": { + "description": "Filter by creation date (after)", "format": "date-time", "type": "string" }, "createdBefore": { + "description": "Filter by creation date (before)", "format": "date-time", "type": "string" }, "description": { + "description": "Filter by description text", "type": "string" }, "deviceAssetId": { + "description": "Filter by device asset ID", "type": "string" }, "deviceId": { + "description": "Device ID to filter by", "type": "string" }, "encodedVideoPath": { + "description": "Filter by encoded video file path", "type": "string" }, "id": { + "description": "Filter by asset ID", "format": "uuid", "type": "string" }, "isEncoded": { + "description": "Filter by encoded status", "type": "boolean" }, "isFavorite": { + "description": "Filter by favorite status", "type": "boolean" }, "isMotion": { + "description": "Filter by motion photo status", "type": "boolean" }, "isNotInAlbum": { + "description": "Filter assets not in any album", "type": "boolean" }, "isOffline": { + "description": "Filter by offline status", "type": "boolean" }, "lensModel": { + "description": "Filter by lens model", "nullable": true, "type": "string" }, "libraryId": { + "description": "Library ID to filter by", "format": "uuid", "nullable": true, "type": "string" }, "make": { + "description": "Filter by camera make", "type": "string" }, "model": { + "description": "Filter by camera model", "nullable": true, "type": "string" }, "ocr": { + "description": "Filter by OCR text content", "type": "string" }, "order": { @@ -17364,19 +19110,24 @@ "$ref": "#/components/schemas/AssetOrder" } ], - "default": "desc" + "default": "desc", + "description": "Sort order" }, "originalFileName": { + "description": "Filter by original file name", "type": "string" }, "originalPath": { + "description": "Filter by original file path", "type": "string" }, "page": { + "description": "Page number", "minimum": 1, "type": "number" }, "personIds": { + "description": "Filter by person IDs", "items": { "format": "uuid", "type": "string" @@ -17384,23 +19135,45 @@ "type": "array" }, "previewPath": { + "description": "Filter by preview file path", "type": "string" }, "rating": { + "description": "Filter by rating [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "size": { + "description": "Number of results to return", "maximum": 1000, "minimum": 1, "type": "number" }, "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, "tagIds": { + "description": "Filter by tag IDs", "items": { "format": "uuid", "type": "string" @@ -17409,21 +19182,26 @@ "type": "array" }, "takenAfter": { + "description": "Filter by taken date (after)", "format": "date-time", "type": "string" }, "takenBefore": { + "description": "Filter by taken date (before)", "format": "date-time", "type": "string" }, "thumbnailPath": { + "description": "Filter by thumbnail file path", "type": "string" }, "trashedAfter": { + "description": "Filter by trash date (after)", "format": "date-time", "type": "string" }, "trashedBefore": { + "description": "Filter by trash date (before)", "format": "date-time", "type": "string" }, @@ -17432,13 +19210,16 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type filter" }, "updatedAfter": { + "description": "Filter by update date (after)", "format": "date-time", "type": "string" }, "updatedBefore": { + "description": "Filter by update date (before)", "format": "date-time", "type": "string" }, @@ -17447,29 +19228,60 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Filter by visibility" }, "withDeleted": { + "description": "Include deleted assets", "type": "boolean" }, "withExif": { + "description": "Include EXIF data in response", "type": "boolean" }, "withPeople": { + "description": "Include assets with people", "type": "boolean" }, "withStacked": { + "description": "Include stacked assets", "type": "boolean" } }, "type": "object" }, + "MirrorAxis": { + "description": "Axis to mirror along", + "enum": [ + "horizontal", + "vertical" + ], + "type": "string" + }, + "MirrorParameters": { + "properties": { + "axis": { + "allOf": [ + { + "$ref": "#/components/schemas/MirrorAxis" + } + ], + "description": "Axis to mirror along" + } + }, + "required": [ + "axis" + ], + "type": "object" + }, "NotificationCreateDto": { "properties": { "data": { + "description": "Additional notification data", "type": "object" }, "description": { + "description": "Notification description", "nullable": true, "type": "string" }, @@ -17478,14 +19290,17 @@ { "$ref": "#/components/schemas/NotificationLevel" } - ] + ], + "description": "Notification level" }, "readAt": { + "description": "Date when notification was read", "format": "date-time", "nullable": true, "type": "string" }, "title": { + "description": "Notification title", "type": "string" }, "type": { @@ -17493,9 +19308,11 @@ { "$ref": "#/components/schemas/NotificationType" } - ] + ], + "description": "Notification type" }, "userId": { + "description": "User ID to send notification to", "format": "uuid", "type": "string" } @@ -17509,10 +19326,12 @@ "NotificationDeleteAllDto": { "properties": { "ids": { + "description": "Notification IDs to delete", "items": { "format": "uuid", "type": "string" }, + "minItems": 1, "type": "array" } }, @@ -17524,16 +19343,20 @@ "NotificationDto": { "properties": { "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "data": { + "description": "Additional notification data", "type": "object" }, "description": { + "description": "Notification description", "type": "string" }, "id": { + "description": "Notification ID", "type": "string" }, "level": { @@ -17541,13 +19364,16 @@ { "$ref": "#/components/schemas/NotificationLevel" } - ] + ], + "description": "Notification level" }, "readAt": { + "description": "Date when notification was read", "format": "date-time", "type": "string" }, "title": { + "description": "Notification title", "type": "string" }, "type": { @@ -17555,7 +19381,8 @@ { "$ref": "#/components/schemas/NotificationType" } - ] + ], + "description": "Notification type" } }, "required": [ @@ -17590,13 +19417,16 @@ "NotificationUpdateAllDto": { "properties": { "ids": { + "description": "Notification IDs to update", "items": { "format": "uuid", "type": "string" }, + "minItems": 1, "type": "array" }, "readAt": { + "description": "Date when notifications were read", "format": "date-time", "nullable": true, "type": "string" @@ -17610,6 +19440,7 @@ "NotificationUpdateDto": { "properties": { "readAt": { + "description": "Date when notification was read", "format": "date-time", "nullable": true, "type": "string" @@ -17620,6 +19451,7 @@ "OAuthAuthorizeResponseDto": { "properties": { "url": { + "description": "OAuth authorization URL", "type": "string" } }, @@ -17631,12 +19463,15 @@ "OAuthCallbackDto": { "properties": { "codeVerifier": { + "description": "OAuth code verifier (PKCE)", "type": "string" }, "state": { + "description": "OAuth state parameter", "type": "string" }, "url": { + "description": "OAuth callback URL", "type": "string" } }, @@ -17648,12 +19483,15 @@ "OAuthConfigDto": { "properties": { "codeChallenge": { + "description": "OAuth code challenge (PKCE)", "type": "string" }, "redirectUri": { + "description": "OAuth redirect URI", "type": "string" }, "state": { + "description": "OAuth state parameter", "type": "string" } }, @@ -17663,6 +19501,7 @@ "type": "object" }, "OAuthTokenEndpointAuthMethod": { + "description": "Token endpoint auth method", "enum": [ "client_secret_post", "client_secret_basic" @@ -17672,25 +19511,30 @@ "OcrConfig": { "properties": { "enabled": { + "description": "Whether the task is enabled", "type": "boolean" }, "maxResolution": { + "description": "Maximum resolution for OCR processing", "minimum": 1, "type": "integer" }, "minDetectionScore": { + "description": "Minimum confidence score for text detection", "format": "double", "maximum": 1, "minimum": 0.1, "type": "number" }, "minRecognitionScore": { + "description": "Minimum confidence score for text recognition", "format": "double", "maximum": 1, "minimum": 0.1, "type": "number" }, "modelName": { + "description": "Name of the model to use", "type": "string" } }, @@ -17706,6 +19550,7 @@ "OnThisDayDto": { "properties": { "year": { + "description": "Year for on this day memory", "minimum": 1, "type": "number" } @@ -17718,6 +19563,7 @@ "OnboardingDto": { "properties": { "isOnboarded": { + "description": "Is user onboarded", "type": "boolean" } }, @@ -17729,6 +19575,7 @@ "OnboardingResponseDto": { "properties": { "isOnboarded": { + "description": "Is user onboarded", "type": "boolean" } }, @@ -17740,6 +19587,7 @@ "PartnerCreateDto": { "properties": { "sharedWithId": { + "description": "User ID to share with", "format": "uuid", "type": "string" } @@ -17763,25 +19611,32 @@ { "$ref": "#/components/schemas/UserAvatarColor" } - ] + ], + "description": "Avatar color" }, "email": { + "description": "User email", "type": "string" }, "id": { + "description": "User ID", "type": "string" }, "inTimeline": { + "description": "Show in timeline", "type": "boolean" }, "name": { + "description": "User name", "type": "string" }, "profileChangedAt": { + "description": "Profile change date", "format": "date-time", "type": "string" }, "profileImagePath": { + "description": "Profile image path", "type": "string" } }, @@ -17798,6 +19653,7 @@ "PartnerUpdateDto": { "properties": { "inTimeline": { + "description": "Show partner assets in timeline", "type": "boolean" } }, @@ -17810,10 +19666,12 @@ "properties": { "enabled": { "default": true, + "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { "default": false, + "description": "Whether people appear in web sidebar", "type": "boolean" } }, @@ -17826,6 +19684,7 @@ "PeopleResponseDto": { "properties": { "hasNextPage": { + "description": "Whether there are more pages", "type": "boolean", "x-immich-history": [ { @@ -17840,15 +19699,18 @@ "x-immich-state": "Stable" }, "hidden": { + "description": "Number of hidden people", "type": "integer" }, "people": { + "description": "List of people", "items": { "$ref": "#/components/schemas/PersonResponseDto" }, "type": "array" }, "total": { + "description": "Total number of people", "type": "integer" } }, @@ -17862,9 +19724,11 @@ "PeopleUpdate": { "properties": { "enabled": { + "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether people appear in web sidebar", "type": "boolean" } }, @@ -17873,6 +19737,7 @@ "PeopleUpdateDto": { "properties": { "people": { + "description": "People to update", "items": { "$ref": "#/components/schemas/PeopleUpdateItem" }, @@ -17887,33 +19752,35 @@ "PeopleUpdateItem": { "properties": { "birthDate": { - "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", + "description": "Person date of birth", "format": "date", "nullable": true, "type": "string" }, "color": { + "description": "Person color (hex)", "nullable": true, "type": "string" }, "featureFaceAssetId": { - "description": "Asset is used to get the feature face thumbnail.", + "description": "Asset ID used for feature face thumbnail", "format": "uuid", "type": "string" }, "id": { - "description": "Person id.", + "description": "Person ID", "type": "string" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "isHidden": { - "description": "Person visibility", + "description": "Person visibility (hidden)", "type": "boolean" }, "name": { - "description": "Person name.", + "description": "Person name", "type": "string" } }, @@ -17923,6 +19790,7 @@ "type": "object" }, "Permission": { + "description": "List of permissions", "enum": [ "all", "activity.create", @@ -17944,6 +19812,10 @@ "asset.upload", "asset.replace", "asset.copy", + "asset.derive", + "asset.edit.get", + "asset.edit.create", + "asset.edit.delete", "album.create", "album.read", "album.update", @@ -17959,12 +19831,17 @@ "auth.changePassword", "authDevice.delete", "archive.read", + "backup.list", + "backup.download", + "backup.upload", + "backup.delete", "duplicate.read", "duplicate.delete", "face.create", "face.read", "face.update", "face.delete", + "folder.read", "job.create", "job.read", "library.create", @@ -17975,6 +19852,8 @@ "timeline.read", "timeline.download", "maintenance", + "map.read", + "map.search", "memory.create", "memory.read", "memory.update", @@ -18075,24 +19954,26 @@ "PersonCreateDto": { "properties": { "birthDate": { - "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", + "description": "Person date of birth", "format": "date", "nullable": true, "type": "string" }, "color": { + "description": "Person color (hex)", "nullable": true, "type": "string" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "isHidden": { - "description": "Person visibility", + "description": "Person visibility (hidden)", "type": "boolean" }, "name": { - "description": "Person name.", + "description": "Person name", "type": "string" } }, @@ -18101,11 +19982,13 @@ "PersonResponseDto": { "properties": { "birthDate": { + "description": "Person date of birth", "format": "date", "nullable": true, "type": "string" }, "color": { + "description": "Person color (hex)", "type": "string", "x-immich-history": [ { @@ -18120,9 +20003,11 @@ "x-immich-state": "Stable" }, "id": { + "description": "Person ID", "type": "string" }, "isFavorite": { + "description": "Is favorite", "type": "boolean", "x-immich-history": [ { @@ -18137,15 +20022,19 @@ "x-immich-state": "Stable" }, "isHidden": { + "description": "Is hidden", "type": "boolean" }, "name": { + "description": "Person name", "type": "string" }, "thumbnailPath": { + "description": "Thumbnail path", "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string", "x-immich-history": [ @@ -18173,6 +20062,7 @@ "PersonStatisticsResponseDto": { "properties": { "assets": { + "description": "Number of assets", "type": "integer" } }, @@ -18184,29 +20074,31 @@ "PersonUpdateDto": { "properties": { "birthDate": { - "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", + "description": "Person date of birth", "format": "date", "nullable": true, "type": "string" }, "color": { + "description": "Person color (hex)", "nullable": true, "type": "string" }, "featureFaceAssetId": { - "description": "Asset is used to get the feature face thumbnail.", + "description": "Asset ID used for feature face thumbnail", "format": "uuid", "type": "string" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "isHidden": { - "description": "Person visibility", + "description": "Person visibility (hidden)", "type": "boolean" }, "name": { - "description": "Person name.", + "description": "Person name", "type": "string" } }, @@ -18215,11 +20107,13 @@ "PersonWithFacesResponseDto": { "properties": { "birthDate": { + "description": "Person date of birth", "format": "date", "nullable": true, "type": "string" }, "color": { + "description": "Person color (hex)", "type": "string", "x-immich-history": [ { @@ -18234,15 +20128,18 @@ "x-immich-state": "Stable" }, "faces": { + "description": "Face detections", "items": { "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" }, "type": "array" }, "id": { + "description": "Person ID", "type": "string" }, "isFavorite": { + "description": "Is favorite", "type": "boolean", "x-immich-history": [ { @@ -18257,15 +20154,19 @@ "x-immich-state": "Stable" }, "isHidden": { + "description": "Is hidden", "type": "boolean" }, "name": { + "description": "Person name", "type": "string" }, "thumbnailPath": { + "description": "Thumbnail path", "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string", "x-immich-history": [ @@ -18294,13 +20195,16 @@ "PinCodeChangeDto": { "properties": { "newPinCode": { + "description": "New PIN code (4-6 digits)", "example": "123456", "type": "string" }, "password": { + "description": "User password (required if PIN code is not provided)", "type": "string" }, "pinCode": { + "description": "New PIN code (4-6 digits)", "example": "123456", "type": "string" } @@ -18313,9 +20217,11 @@ "PinCodeResetDto": { "properties": { "password": { + "description": "User password (required if PIN code is not provided)", "type": "string" }, "pinCode": { + "description": "New PIN code (4-6 digits)", "example": "123456", "type": "string" } @@ -18325,6 +20231,7 @@ "PinCodeSetupDto": { "properties": { "pinCode": { + "description": "PIN code (4-6 digits)", "example": "123456", "type": "string" } @@ -18337,18 +20244,23 @@ "PlacesResponseDto": { "properties": { "admin1name": { + "description": "Administrative level 1 name (state/province)", "type": "string" }, "admin2name": { + "description": "Administrative level 2 name (county/district)", "type": "string" }, "latitude": { + "description": "Latitude coordinate", "type": "number" }, "longitude": { + "description": "Longitude coordinate", "type": "number" }, "name": { + "description": "Place name", "type": "string" } }, @@ -18362,28 +20274,35 @@ "PluginActionResponseDto": { "properties": { "description": { + "description": "Action description", "type": "string" }, "id": { + "description": "Action ID", "type": "string" }, "methodName": { + "description": "Method name", "type": "string" }, "pluginId": { + "description": "Plugin ID", "type": "string" }, "schema": { + "description": "Action schema", "nullable": true, "type": "object" }, "supportedContexts": { + "description": "Supported contexts", "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, "title": { + "description": "Action title", "type": "string" } }, @@ -18398,7 +20317,8 @@ ], "type": "object" }, - "PluginContext": { + "PluginContextType": { + "description": "Context type", "enum": [ "asset", "album", @@ -18409,28 +20329,35 @@ "PluginFilterResponseDto": { "properties": { "description": { + "description": "Filter description", "type": "string" }, "id": { + "description": "Filter ID", "type": "string" }, "methodName": { + "description": "Method name", "type": "string" }, "pluginId": { + "description": "Plugin ID", "type": "string" }, "schema": { + "description": "Filter schema", "nullable": true, "type": "object" }, "supportedContexts": { + "description": "Supported contexts", "items": { - "$ref": "#/components/schemas/PluginContext" + "$ref": "#/components/schemas/PluginContextType" }, "type": "array" }, "title": { + "description": "Filter title", "type": "string" } }, @@ -18448,39 +20375,49 @@ "PluginResponseDto": { "properties": { "actions": { + "description": "Plugin actions", "items": { "$ref": "#/components/schemas/PluginActionResponseDto" }, "type": "array" }, "author": { + "description": "Plugin author", "type": "string" }, "createdAt": { + "description": "Creation date", "type": "string" }, "description": { + "description": "Plugin description", "type": "string" }, "filters": { + "description": "Plugin filters", "items": { "$ref": "#/components/schemas/PluginFilterResponseDto" }, "type": "array" }, "id": { + "description": "Plugin ID", "type": "string" }, "name": { + "description": "Plugin name", "type": "string" }, "title": { + "description": "Plugin title", "type": "string" }, "updatedAt": { + "description": "Last update date", "type": "string" }, "version": { + "description": "Plugin version", "type": "string" } }, @@ -18498,7 +20435,33 @@ ], "type": "object" }, + "PluginTriggerResponseDto": { + "properties": { + "contextType": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginContextType" + } + ], + "description": "Context type" + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } + ], + "description": "Trigger type" + } + }, + "required": [ + "contextType", + "type" + ], + "type": "object" + }, "PluginTriggerType": { + "description": "Trigger type", "enum": [ "AssetCreate", "PersonRecognized" @@ -18508,9 +20471,11 @@ "PurchaseResponse": { "properties": { "hideBuyButtonUntil": { + "description": "Date until which to hide buy button", "type": "string" }, "showSupportBadge": { + "description": "Whether to show support badge", "type": "boolean" } }, @@ -18523,15 +20488,18 @@ "PurchaseUpdate": { "properties": { "hideBuyButtonUntil": { + "description": "Date until which to hide buy button", "type": "string" }, "showSupportBadge": { + "description": "Whether to show support badge", "type": "boolean" } }, "type": "object" }, "QueueCommand": { + "description": "Queue command to execute", "enum": [ "start", "pause", @@ -18548,9 +20516,11 @@ { "$ref": "#/components/schemas/QueueCommand" } - ] + ], + "description": "Queue command to execute" }, "force": { + "description": "Force the command execution (if applicable)", "type": "boolean" } }, @@ -18582,9 +20552,11 @@ "QueueJobResponseDto": { "properties": { "data": { + "description": "Job data payload", "type": "object" }, "id": { + "description": "Job ID", "type": "string" }, "name": { @@ -18592,9 +20564,11 @@ { "$ref": "#/components/schemas/JobName" } - ] + ], + "description": "Job name" }, "timestamp": { + "description": "Job creation timestamp", "type": "integer" } }, @@ -18634,13 +20608,15 @@ "notifications", "backupDatabase", "ocr", - "workflow" + "workflow", + "editor" ], "type": "string" }, "QueueResponseDto": { "properties": { "isPaused": { + "description": "Whether the queue is paused", "type": "boolean" }, "name": { @@ -18648,7 +20624,8 @@ { "$ref": "#/components/schemas/QueueName" } - ] + ], + "description": "Queue name" }, "statistics": { "$ref": "#/components/schemas/QueueStatisticsDto" @@ -18679,21 +20656,27 @@ "QueueStatisticsDto": { "properties": { "active": { + "description": "Number of active jobs", "type": "integer" }, "completed": { + "description": "Number of completed jobs", "type": "integer" }, "delayed": { + "description": "Number of delayed jobs", "type": "integer" }, "failed": { + "description": "Number of failed jobs", "type": "integer" }, "paused": { + "description": "Number of paused jobs", "type": "integer" }, "waiting": { + "description": "Number of waiting jobs", "type": "integer" } }, @@ -18710,9 +20693,11 @@ "QueueStatusLegacyDto": { "properties": { "isActive": { + "description": "Whether the queue is currently active (has running jobs)", "type": "boolean" }, "isPaused": { + "description": "Whether the queue is paused", "type": "boolean" } }, @@ -18725,6 +20710,7 @@ "QueueUpdateDto": { "properties": { "isPaused": { + "description": "Whether to pause the queue", "type": "boolean" } }, @@ -18741,6 +20727,9 @@ "duplicateDetection": { "$ref": "#/components/schemas/QueueResponseLegacyDto" }, + "editor": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, "faceDetection": { "$ref": "#/components/schemas/QueueResponseLegacyDto" }, @@ -18788,6 +20777,7 @@ "backgroundTask", "backupDatabase", "duplicateDetection", + "editor", "faceDetection", "facialRecognition", "library", @@ -18808,6 +20798,7 @@ "RandomSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -18815,59 +20806,75 @@ "type": "array" }, "city": { + "description": "Filter by city name", "nullable": true, "type": "string" }, "country": { + "description": "Filter by country name", "nullable": true, "type": "string" }, "createdAfter": { + "description": "Filter by creation date (after)", "format": "date-time", "type": "string" }, "createdBefore": { + "description": "Filter by creation date (before)", "format": "date-time", "type": "string" }, "deviceId": { + "description": "Device ID to filter by", "type": "string" }, "isEncoded": { + "description": "Filter by encoded status", "type": "boolean" }, "isFavorite": { + "description": "Filter by favorite status", "type": "boolean" }, "isMotion": { + "description": "Filter by motion photo status", "type": "boolean" }, "isNotInAlbum": { + "description": "Filter assets not in any album", "type": "boolean" }, "isOffline": { + "description": "Filter by offline status", "type": "boolean" }, "lensModel": { + "description": "Filter by lens model", "nullable": true, "type": "string" }, "libraryId": { + "description": "Library ID to filter by", "format": "uuid", "nullable": true, "type": "string" }, "make": { + "description": "Filter by camera make", "type": "string" }, "model": { + "description": "Filter by camera model", "nullable": true, "type": "string" }, "ocr": { + "description": "Filter by OCR text content", "type": "string" }, "personIds": { + "description": "Filter by person IDs", "items": { "format": "uuid", "type": "string" @@ -18875,20 +20882,41 @@ "type": "array" }, "rating": { + "description": "Filter by rating [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "size": { + "description": "Number of results to return", "maximum": 1000, "minimum": 1, "type": "number" }, "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, "tagIds": { + "description": "Filter by tag IDs", "items": { "format": "uuid", "type": "string" @@ -18897,18 +20925,22 @@ "type": "array" }, "takenAfter": { + "description": "Filter by taken date (after)", "format": "date-time", "type": "string" }, "takenBefore": { + "description": "Filter by taken date (before)", "format": "date-time", "type": "string" }, "trashedAfter": { + "description": "Filter by trash date (after)", "format": "date-time", "type": "string" }, "trashedBefore": { + "description": "Filter by trash date (before)", "format": "date-time", "type": "string" }, @@ -18917,13 +20949,16 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type filter" }, "updatedAfter": { + "description": "Filter by update date (after)", "format": "date-time", "type": "string" }, "updatedBefore": { + "description": "Filter by update date (before)", "format": "date-time", "type": "string" }, @@ -18932,18 +20967,23 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Filter by visibility" }, "withDeleted": { + "description": "Include deleted assets", "type": "boolean" }, "withExif": { + "description": "Include EXIF data in response", "type": "boolean" }, "withPeople": { + "description": "Include assets with people", "type": "boolean" }, "withStacked": { + "description": "Include stacked assets", "type": "boolean" } }, @@ -18953,6 +20993,7 @@ "properties": { "enabled": { "default": false, + "description": "Whether ratings are enabled", "type": "boolean" } }, @@ -18964,6 +21005,7 @@ "RatingsUpdate": { "properties": { "enabled": { + "description": "Whether ratings are enabled", "type": "boolean" } }, @@ -18986,10 +21028,12 @@ "ReverseGeocodingStateResponseDto": { "properties": { "lastImportFileName": { + "description": "Last import file name", "nullable": true, "type": "string" }, "lastUpdate": { + "description": "Last update timestamp", "nullable": true, "type": "string" } @@ -19000,9 +21044,22 @@ ], "type": "object" }, + "RotateParameters": { + "properties": { + "angle": { + "description": "Rotation angle in degrees", + "type": "number" + } + }, + "required": [ + "angle" + ], + "type": "object" + }, "SearchAlbumResponseDto": { "properties": { "count": { + "description": "Number of albums in this page", "type": "integer" }, "facets": { @@ -19018,6 +21075,7 @@ "type": "array" }, "total": { + "description": "Total number of matching albums", "type": "integer" } }, @@ -19032,6 +21090,7 @@ "SearchAssetResponseDto": { "properties": { "count": { + "description": "Number of assets in this page", "type": "integer" }, "facets": { @@ -19047,10 +21106,12 @@ "type": "array" }, "nextPage": { + "description": "Next page token", "nullable": true, "type": "string" }, "total": { + "description": "Total number of matching assets", "type": "integer" } }, @@ -19069,6 +21130,7 @@ "$ref": "#/components/schemas/AssetResponseDto" }, "value": { + "description": "Explore value", "type": "string" } }, @@ -19081,6 +21143,7 @@ "SearchExploreResponseDto": { "properties": { "fieldName": { + "description": "Explore field name", "type": "string" }, "items": { @@ -19099,9 +21162,11 @@ "SearchFacetCountResponseDto": { "properties": { "count": { + "description": "Number of assets with this facet value", "type": "integer" }, "value": { + "description": "Facet value", "type": "string" } }, @@ -19114,12 +21179,14 @@ "SearchFacetResponseDto": { "properties": { "counts": { + "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, "type": "array" }, "fieldName": { + "description": "Facet field name", "type": "string" } }, @@ -19147,6 +21214,7 @@ "SearchStatisticsResponseDto": { "properties": { "total": { + "description": "Total number of matching assets", "type": "integer" } }, @@ -19169,66 +21237,87 @@ "ServerAboutResponseDto": { "properties": { "build": { + "description": "Build identifier", "type": "string" }, "buildImage": { + "description": "Build image name", "type": "string" }, "buildImageUrl": { + "description": "Build image URL", "type": "string" }, "buildUrl": { + "description": "Build URL", "type": "string" }, "exiftool": { + "description": "ExifTool version", "type": "string" }, "ffmpeg": { + "description": "FFmpeg version", "type": "string" }, "imagemagick": { + "description": "ImageMagick version", "type": "string" }, "libvips": { + "description": "libvips version", "type": "string" }, "licensed": { + "description": "Whether the server is licensed", "type": "boolean" }, "nodejs": { + "description": "Node.js version", "type": "string" }, "repository": { + "description": "Repository name", "type": "string" }, "repositoryUrl": { + "description": "Repository URL", "type": "string" }, "sourceCommit": { + "description": "Source commit hash", "type": "string" }, "sourceRef": { + "description": "Source reference (branch/tag)", "type": "string" }, "sourceUrl": { + "description": "Source URL", "type": "string" }, "thirdPartyBugFeatureUrl": { + "description": "Third-party bug/feature URL", "type": "string" }, "thirdPartyDocumentationUrl": { + "description": "Third-party documentation URL", "type": "string" }, "thirdPartySourceUrl": { + "description": "Third-party source URL", "type": "string" }, "thirdPartySupportUrl": { + "description": "Third-party support URL", "type": "string" }, "version": { + "description": "Server version", "type": "string" }, "versionUrl": { + "description": "URL to version information", "type": "string" } }, @@ -19242,15 +21331,19 @@ "ServerApkLinksDto": { "properties": { "arm64v8a": { + "description": "APK download link for ARM64 v8a architecture", "type": "string" }, "armeabiv7a": { + "description": "APK download link for ARM EABI v7a architecture", "type": "string" }, "universal": { + "description": "APK download link for universal architecture", "type": "string" }, "x86_64": { + "description": "APK download link for x86_64 architecture", "type": "string" } }, @@ -19265,36 +21358,47 @@ "ServerConfigDto": { "properties": { "externalDomain": { + "description": "External domain URL", "type": "string" }, "isInitialized": { + "description": "Whether the server has been initialized", "type": "boolean" }, "isOnboarded": { + "description": "Whether the admin has completed onboarding", "type": "boolean" }, "loginPageMessage": { + "description": "Login page message", "type": "string" }, "maintenanceMode": { + "description": "Whether maintenance mode is active", "type": "boolean" }, "mapDarkStyleUrl": { + "description": "Map dark style URL", "type": "string" }, "mapLightStyleUrl": { + "description": "Map light style URL", "type": "string" }, "oauthButtonText": { + "description": "OAuth button text", "type": "string" }, "publicUsers": { + "description": "Whether public user registration is enabled", "type": "boolean" }, "trashDays": { + "description": "Number of days before trashed assets are permanently deleted", "type": "integer" }, "userDeleteDelay": { + "description": "Delay in days before deleted users are permanently removed", "type": "integer" } }, @@ -19316,48 +21420,63 @@ "ServerFeaturesDto": { "properties": { "configFile": { + "description": "Whether config file is available", "type": "boolean" }, "duplicateDetection": { + "description": "Whether duplicate detection is enabled", "type": "boolean" }, "email": { + "description": "Whether email notifications are enabled", "type": "boolean" }, "facialRecognition": { + "description": "Whether facial recognition is enabled", "type": "boolean" }, "importFaces": { + "description": "Whether face import is enabled", "type": "boolean" }, "map": { + "description": "Whether map feature is enabled", "type": "boolean" }, "oauth": { + "description": "Whether OAuth is enabled", "type": "boolean" }, "oauthAutoLaunch": { + "description": "Whether OAuth auto-launch is enabled", "type": "boolean" }, "ocr": { + "description": "Whether OCR is enabled", "type": "boolean" }, "passwordLogin": { + "description": "Whether password login is enabled", "type": "boolean" }, "reverseGeocoding": { + "description": "Whether reverse geocoding is enabled", "type": "boolean" }, "search": { + "description": "Whether search is enabled", "type": "boolean" }, "sidecar": { + "description": "Whether sidecar files are supported", "type": "boolean" }, "smartSearch": { + "description": "Whether smart search is enabled", "type": "boolean" }, "trash": { + "description": "Whether trash feature is enabled", "type": "boolean" } }, @@ -19383,18 +21502,21 @@ "ServerMediaTypesResponseDto": { "properties": { "image": { + "description": "Supported image MIME types", "items": { "type": "string" }, "type": "array" }, "sidecar": { + "description": "Supported sidecar MIME types", "items": { "type": "string" }, "type": "array" }, "video": { + "description": "Supported video MIME types", "items": { "type": "string" }, @@ -19425,10 +21547,12 @@ "properties": { "photos": { "default": 0, + "description": "Total number of photos", "type": "integer" }, "usage": { "default": 0, + "description": "Total storage usage in bytes", "format": "int64", "type": "integer" }, @@ -19451,16 +21575,19 @@ }, "usagePhotos": { "default": 0, + "description": "Storage usage for photos in bytes", "format": "int64", "type": "integer" }, "usageVideos": { "default": 0, + "description": "Storage usage for videos in bytes", "format": "int64", "type": "integer" }, "videos": { "default": 0, + "description": "Total number of videos", "type": "integer" } }, @@ -19477,27 +21604,34 @@ "ServerStorageResponseDto": { "properties": { "diskAvailable": { + "description": "Available disk space (human-readable format)", "type": "string" }, "diskAvailableRaw": { + "description": "Available disk space in bytes", "format": "int64", "type": "integer" }, "diskSize": { + "description": "Total disk size (human-readable format)", "type": "string" }, "diskSizeRaw": { + "description": "Total disk size in bytes", "format": "int64", "type": "integer" }, "diskUsagePercentage": { + "description": "Disk usage percentage (0-100)", "format": "double", "type": "number" }, "diskUse": { + "description": "Used disk space (human-readable format)", "type": "string" }, "diskUseRaw": { + "description": "Used disk space in bytes", "format": "int64", "type": "integer" } @@ -19516,6 +21650,7 @@ "ServerThemeDto": { "properties": { "customCss": { + "description": "Custom CSS for theming", "type": "string" } }, @@ -19527,13 +21662,16 @@ "ServerVersionHistoryResponseDto": { "properties": { "createdAt": { + "description": "When this version was first seen", "format": "date-time", "type": "string" }, "id": { + "description": "Version history entry ID", "type": "string" }, "version": { + "description": "Version string", "type": "string" } }, @@ -19547,12 +21685,15 @@ "ServerVersionResponseDto": { "properties": { "major": { + "description": "Major version number", "type": "integer" }, "minor": { + "description": "Minor version number", "type": "integer" }, "patch": { + "description": "Patch version number", "type": "integer" } }, @@ -19566,13 +21707,15 @@ "SessionCreateDto": { "properties": { "deviceOS": { + "description": "Device OS", "type": "string" }, "deviceType": { + "description": "Device type", "type": "string" }, "duration": { - "description": "session duration, in seconds", + "description": "Session duration in seconds", "minimum": 1, "type": "number" } @@ -19582,34 +21725,44 @@ "SessionCreateResponseDto": { "properties": { "appVersion": { + "description": "App version", "nullable": true, "type": "string" }, "createdAt": { + "description": "Creation date", "type": "string" }, "current": { + "description": "Is current session", "type": "boolean" }, "deviceOS": { + "description": "Device OS", "type": "string" }, "deviceType": { + "description": "Device type", "type": "string" }, "expiresAt": { + "description": "Expiration date", "type": "string" }, "id": { + "description": "Session ID", "type": "string" }, "isPendingSyncReset": { + "description": "Is pending sync reset", "type": "boolean" }, "token": { + "description": "Session token", "type": "string" }, "updatedAt": { + "description": "Last update date", "type": "string" } }, @@ -19629,31 +21782,40 @@ "SessionResponseDto": { "properties": { "appVersion": { + "description": "App version", "nullable": true, "type": "string" }, "createdAt": { + "description": "Creation date", "type": "string" }, "current": { + "description": "Is current session", "type": "boolean" }, "deviceOS": { + "description": "Device OS", "type": "string" }, "deviceType": { + "description": "Device type", "type": "string" }, "expiresAt": { + "description": "Expiration date", "type": "string" }, "id": { + "description": "Session ID", "type": "string" }, "isPendingSyncReset": { + "description": "Is pending sync reset", "type": "boolean" }, "updatedAt": { + "description": "Last update date", "type": "string" } }, @@ -19672,9 +21834,11 @@ "SessionUnlockDto": { "properties": { "password": { + "description": "User password (required if PIN code is not provided)", "type": "string" }, "pinCode": { + "description": "New PIN code (4-6 digits)", "example": "123456", "type": "string" } @@ -19684,6 +21848,7 @@ "SessionUpdateDto": { "properties": { "isPendingSyncReset": { + "description": "Reset pending sync state", "type": "boolean" } }, @@ -19696,7 +21861,12 @@ { "$ref": "#/components/schemas/MaintenanceAction" } - ] + ], + "description": "Maintenance action" + }, + "restoreBackupFilename": { + "description": "Restore backup filename", + "type": "string" } }, "required": [ @@ -19707,17 +21877,21 @@ "SharedLinkCreateDto": { "properties": { "albumId": { + "description": "Album ID (for album sharing)", "format": "uuid", "type": "string" }, "allowDownload": { "default": true, + "description": "Allow downloads", "type": "boolean" }, "allowUpload": { + "description": "Allow uploads", "type": "boolean" }, "assetIds": { + "description": "Asset IDs (for individual assets)", "items": { "format": "uuid", "type": "string" @@ -19725,24 +21899,29 @@ "type": "array" }, "description": { + "description": "Link description", "nullable": true, "type": "string" }, "expiresAt": { "default": null, + "description": "Expiration date", "format": "date-time", "nullable": true, "type": "string" }, "password": { + "description": "Link password", "nullable": true, "type": "string" }, "showMetadata": { "default": true, + "description": "Show metadata", "type": "boolean" }, "slug": { + "description": "Custom URL slug", "nullable": true, "type": "string" }, @@ -19751,7 +21930,8 @@ { "$ref": "#/components/schemas/SharedLinkType" } - ] + ], + "description": "Shared link type" } }, "required": [ @@ -19762,47 +21942,69 @@ "SharedLinkEditDto": { "properties": { "allowDownload": { + "description": "Allow downloads", "type": "boolean" }, "allowUpload": { + "description": "Allow uploads", "type": "boolean" }, "changeExpiryTime": { - "description": "Few clients cannot send null to set the expiryTime to never.\nSetting this flag and not sending expiryAt is considered as null instead.\nClients that can send null values can ignore this.", + "description": "Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.", "type": "boolean" }, "description": { + "description": "Link description", "nullable": true, "type": "string" }, "expiresAt": { + "description": "Expiration date", "format": "date-time", "nullable": true, "type": "string" }, "password": { + "description": "Link password", "nullable": true, "type": "string" }, "showMetadata": { + "description": "Show metadata", "type": "boolean" }, "slug": { + "description": "Custom URL slug", "nullable": true, "type": "string" } }, "type": "object" }, + "SharedLinkLoginDto": { + "properties": { + "password": { + "description": "Shared link password", + "example": "password", + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, "SharedLinkResponseDto": { "properties": { "album": { "$ref": "#/components/schemas/AlbumResponseDto" }, "allowDownload": { + "description": "Allow downloads", "type": "boolean" }, "allowUpload": { + "description": "Allow uploads", "type": "boolean" }, "assets": { @@ -19812,47 +22014,74 @@ "type": "array" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "description": { + "description": "Link description", "nullable": true, "type": "string" }, "expiresAt": { + "description": "Expiration date", "format": "date-time", "nullable": true, "type": "string" }, "id": { + "description": "Shared link ID", "type": "string" }, "key": { + "description": "Encryption key (base64url)", "type": "string" }, "password": { + "description": "Has password", "nullable": true, "type": "string" }, "showMetadata": { + "description": "Show metadata", "type": "boolean" }, "slug": { + "description": "Custom URL slug", "nullable": true, "type": "string" }, "token": { + "deprecated": true, + "description": "Access token", "nullable": true, - "type": "string" + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Deprecated" + } + ], + "x-immich-state": "Deprecated" }, "type": { "allOf": [ { "$ref": "#/components/schemas/SharedLinkType" } - ] + ], + "description": "Shared link type" }, "userId": { + "description": "Owner user ID", "type": "string" } }, @@ -19874,6 +22103,7 @@ "type": "object" }, "SharedLinkType": { + "description": "Shared link type", "enum": [ "ALBUM", "INDIVIDUAL" @@ -19884,10 +22114,12 @@ "properties": { "enabled": { "default": true, + "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { "default": false, + "description": "Whether shared links appear in web sidebar", "type": "boolean" } }, @@ -19900,9 +22132,11 @@ "SharedLinksUpdate": { "properties": { "enabled": { + "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether shared links appear in web sidebar", "type": "boolean" } }, @@ -19911,15 +22145,18 @@ "SignUpDto": { "properties": { "email": { + "description": "User email", "example": "testuser@email.com", "format": "email", "type": "string" }, "name": { + "description": "User name", "example": "Admin", "type": "string" }, "password": { + "description": "User password", "example": "password", "type": "string" } @@ -19934,6 +22171,7 @@ "SmartSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -19941,66 +22179,84 @@ "type": "array" }, "city": { + "description": "Filter by city name", "nullable": true, "type": "string" }, "country": { + "description": "Filter by country name", "nullable": true, "type": "string" }, "createdAfter": { + "description": "Filter by creation date (after)", "format": "date-time", "type": "string" }, "createdBefore": { + "description": "Filter by creation date (before)", "format": "date-time", "type": "string" }, "deviceId": { + "description": "Device ID to filter by", "type": "string" }, "isEncoded": { + "description": "Filter by encoded status", "type": "boolean" }, "isFavorite": { + "description": "Filter by favorite status", "type": "boolean" }, "isMotion": { + "description": "Filter by motion photo status", "type": "boolean" }, "isNotInAlbum": { + "description": "Filter assets not in any album", "type": "boolean" }, "isOffline": { + "description": "Filter by offline status", "type": "boolean" }, "language": { + "description": "Search language code", "type": "string" }, "lensModel": { + "description": "Filter by lens model", "nullable": true, "type": "string" }, "libraryId": { + "description": "Library ID to filter by", "format": "uuid", "nullable": true, "type": "string" }, "make": { + "description": "Filter by camera make", "type": "string" }, "model": { + "description": "Filter by camera model", "nullable": true, "type": "string" }, "ocr": { + "description": "Filter by OCR text content", "type": "string" }, "page": { + "description": "Page number", "minimum": 1, "type": "number" }, "personIds": { + "description": "Filter by person IDs", "items": { "format": "uuid", "type": "string" @@ -20008,27 +22264,50 @@ "type": "array" }, "query": { + "description": "Natural language search query", "type": "string" }, "queryAssetId": { + "description": "Asset ID to use as search reference", "format": "uuid", "type": "string" }, "rating": { + "description": "Filter by rating [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "size": { + "description": "Number of results to return", "maximum": 1000, "minimum": 1, "type": "number" }, "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, "tagIds": { + "description": "Filter by tag IDs", "items": { "format": "uuid", "type": "string" @@ -20037,18 +22316,22 @@ "type": "array" }, "takenAfter": { + "description": "Filter by taken date (after)", "format": "date-time", "type": "string" }, "takenBefore": { + "description": "Filter by taken date (before)", "format": "date-time", "type": "string" }, "trashedAfter": { + "description": "Filter by trash date (after)", "format": "date-time", "type": "string" }, "trashedBefore": { + "description": "Filter by trash date (before)", "format": "date-time", "type": "string" }, @@ -20057,13 +22340,16 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type filter" }, "updatedAfter": { + "description": "Filter by update date (after)", "format": "date-time", "type": "string" }, "updatedBefore": { + "description": "Filter by update date (before)", "format": "date-time", "type": "string" }, @@ -20072,18 +22358,22 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Filter by visibility" }, "withDeleted": { + "description": "Include deleted assets", "type": "boolean" }, "withExif": { + "description": "Include EXIF data in response", "type": "boolean" } }, "type": "object" }, "SourceType": { + "description": "Face detection source type", "enum": [ "machine-learning", "exif", @@ -20094,7 +22384,7 @@ "StackCreateDto": { "properties": { "assetIds": { - "description": "first asset becomes the primary", + "description": "Asset IDs (first becomes primary, min 2)", "items": { "format": "uuid", "type": "string" @@ -20111,15 +22401,18 @@ "StackResponseDto": { "properties": { "assets": { + "description": "Stack assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" }, "id": { + "description": "Stack ID", "type": "string" }, "primaryAssetId": { + "description": "Primary asset ID", "type": "string" } }, @@ -20133,6 +22426,7 @@ "StackUpdateDto": { "properties": { "primaryAssetId": { + "description": "Primary asset ID", "format": "uuid", "type": "string" } @@ -20142,6 +22436,7 @@ "StatisticsSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -20149,62 +22444,79 @@ "type": "array" }, "city": { + "description": "Filter by city name", "nullable": true, "type": "string" }, "country": { + "description": "Filter by country name", "nullable": true, "type": "string" }, "createdAfter": { + "description": "Filter by creation date (after)", "format": "date-time", "type": "string" }, "createdBefore": { + "description": "Filter by creation date (before)", "format": "date-time", "type": "string" }, "description": { + "description": "Filter by description text", "type": "string" }, "deviceId": { + "description": "Device ID to filter by", "type": "string" }, "isEncoded": { + "description": "Filter by encoded status", "type": "boolean" }, "isFavorite": { + "description": "Filter by favorite status", "type": "boolean" }, "isMotion": { + "description": "Filter by motion photo status", "type": "boolean" }, "isNotInAlbum": { + "description": "Filter assets not in any album", "type": "boolean" }, "isOffline": { + "description": "Filter by offline status", "type": "boolean" }, "lensModel": { + "description": "Filter by lens model", "nullable": true, "type": "string" }, "libraryId": { + "description": "Library ID to filter by", "format": "uuid", "nullable": true, "type": "string" }, "make": { + "description": "Filter by camera make", "type": "string" }, "model": { + "description": "Filter by camera model", "nullable": true, "type": "string" }, "ocr": { + "description": "Filter by OCR text content", "type": "string" }, "personIds": { + "description": "Filter by person IDs", "items": { "format": "uuid", "type": "string" @@ -20212,15 +22524,35 @@ "type": "array" }, "rating": { + "description": "Filter by rating [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, "tagIds": { + "description": "Filter by tag IDs", "items": { "format": "uuid", "type": "string" @@ -20229,18 +22561,22 @@ "type": "array" }, "takenAfter": { + "description": "Filter by taken date (after)", "format": "date-time", "type": "string" }, "takenBefore": { + "description": "Filter by taken date (before)", "format": "date-time", "type": "string" }, "trashedAfter": { + "description": "Filter by trash date (after)", "format": "date-time", "type": "string" }, "trashedBefore": { + "description": "Filter by trash date (before)", "format": "date-time", "type": "string" }, @@ -20249,13 +22585,16 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type filter" }, "updatedAfter": { + "description": "Filter by update date (after)", "format": "date-time", "type": "string" }, "updatedBefore": { + "description": "Filter by update date (before)", "format": "date-time", "type": "string" }, @@ -20264,14 +22603,28 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Filter by visibility" } }, "type": "object" }, + "StorageFolder": { + "description": "Storage folder", + "enum": [ + "encoded-video", + "library", + "upload", + "profile", + "thumbs", + "backups" + ], + "type": "string" + }, "SyncAckDeleteDto": { "properties": { "types": { + "description": "Sync entity types to delete acks for", "items": { "$ref": "#/components/schemas/SyncEntityType" }, @@ -20283,6 +22636,7 @@ "SyncAckDto": { "properties": { "ack": { + "description": "Acknowledgment ID", "type": "string" }, "type": { @@ -20290,7 +22644,8 @@ { "$ref": "#/components/schemas/SyncEntityType" } - ] + ], + "description": "Sync entity type" } }, "required": [ @@ -20302,6 +22657,7 @@ "SyncAckSetDto": { "properties": { "acks": { + "description": "Acknowledgment IDs (max 1000)", "items": { "type": "string" }, @@ -20321,6 +22677,7 @@ "SyncAlbumDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" } }, @@ -20332,9 +22689,11 @@ "SyncAlbumToAssetDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20347,9 +22706,11 @@ "SyncAlbumToAssetV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20362,9 +22723,11 @@ "SyncAlbumUserDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -20377,6 +22740,7 @@ "SyncAlbumUserV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "role": { @@ -20384,9 +22748,11 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -20400,19 +22766,24 @@ "SyncAlbumV1": { "properties": { "createdAt": { + "description": "Created at", "format": "date-time", "type": "string" }, "description": { + "description": "Album description", "type": "string" }, "id": { + "description": "Album ID", "type": "string" }, "isActivityEnabled": { + "description": "Is activity enabled", "type": "boolean" }, "name": { + "description": "Album name", "type": "string" }, "order": { @@ -20423,13 +22794,16 @@ ] }, "ownerId": { + "description": "Owner ID", "type": "string" }, "thumbnailAssetId": { + "description": "Thumbnail asset ID", "nullable": true, "type": "string" }, "updatedAt": { + "description": "Updated at", "format": "date-time", "type": "string" } @@ -20450,6 +22824,7 @@ "SyncAssetDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20458,111 +22833,178 @@ ], "type": "object" }, - "SyncAssetExifV1": { + "SyncAssetEditDeleteV1": { "properties": { + "editId": { + "type": "string" + } + }, + "required": [ + "editId" + ], + "type": "object" + }, + "SyncAssetEditV1": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ] + }, "assetId": { "type": "string" }, + "id": { + "type": "string" + }, + "parameters": { + "type": "object" + }, + "sequence": { + "type": "integer" + } + }, + "required": [ + "action", + "assetId", + "id", + "parameters", + "sequence" + ], + "type": "object" + }, + "SyncAssetExifV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "type": "string" + }, "city": { + "description": "City", "nullable": true, "type": "string" }, "country": { + "description": "Country", "nullable": true, "type": "string" }, "dateTimeOriginal": { + "description": "Date time original", "format": "date-time", "nullable": true, "type": "string" }, "description": { + "description": "Description", "nullable": true, "type": "string" }, "exifImageHeight": { + "description": "Exif image height", "nullable": true, "type": "integer" }, "exifImageWidth": { + "description": "Exif image width", "nullable": true, "type": "integer" }, "exposureTime": { + "description": "Exposure time", "nullable": true, "type": "string" }, "fNumber": { + "description": "F number", "format": "double", "nullable": true, "type": "number" }, "fileSizeInByte": { + "description": "File size in byte", "nullable": true, "type": "integer" }, "focalLength": { + "description": "Focal length", "format": "double", "nullable": true, "type": "number" }, "fps": { + "description": "FPS", "format": "double", "nullable": true, "type": "number" }, "iso": { + "description": "ISO", "nullable": true, "type": "integer" }, "latitude": { + "description": "Latitude", "format": "double", "nullable": true, "type": "number" }, "lensModel": { + "description": "Lens model", "nullable": true, "type": "string" }, "longitude": { + "description": "Longitude", "format": "double", "nullable": true, "type": "number" }, "make": { + "description": "Make", "nullable": true, "type": "string" }, "model": { + "description": "Model", "nullable": true, "type": "string" }, "modifyDate": { + "description": "Modify date", "format": "date-time", "nullable": true, "type": "string" }, "orientation": { + "description": "Orientation", "nullable": true, "type": "string" }, "profileDescription": { + "description": "Profile description", "nullable": true, "type": "string" }, "projectionType": { + "description": "Projection type", "nullable": true, "type": "string" }, "rating": { + "description": "Rating", "nullable": true, "type": "integer" }, "state": { + "description": "State", "nullable": true, "type": "string" }, "timeZone": { + "description": "Time zone", "nullable": true, "type": "string" } @@ -20599,6 +23041,7 @@ "SyncAssetFaceDeleteV1": { "properties": { "assetFaceId": { + "description": "Asset face ID", "type": "string" } }, @@ -20610,6 +23053,7 @@ "SyncAssetFaceV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "boundingBoxX1": { @@ -20625,6 +23069,7 @@ "type": "integer" }, "id": { + "description": "Asset face ID", "type": "string" }, "imageHeight": { @@ -20634,10 +23079,12 @@ "type": "integer" }, "personId": { + "description": "Person ID", "nullable": true, "type": "string" }, "sourceType": { + "description": "Source type", "type": "string" } }, @@ -20655,17 +23102,79 @@ ], "type": "object" }, + "SyncAssetFaceV2": { + "properties": { + "assetId": { + "description": "Asset ID", + "type": "string" + }, + "boundingBoxX1": { + "type": "integer" + }, + "boundingBoxX2": { + "type": "integer" + }, + "boundingBoxY1": { + "type": "integer" + }, + "boundingBoxY2": { + "type": "integer" + }, + "deletedAt": { + "description": "Face deleted at", + "format": "date-time", + "nullable": true, + "type": "string" + }, + "id": { + "description": "Asset face ID", + "type": "string" + }, + "imageHeight": { + "type": "integer" + }, + "imageWidth": { + "type": "integer" + }, + "isVisible": { + "description": "Is the face visible in the asset", + "type": "boolean" + }, + "personId": { + "description": "Person ID", + "nullable": true, + "type": "string" + }, + "sourceType": { + "description": "Source type", + "type": "string" + } + }, + "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "deletedAt", + "id", + "imageHeight", + "imageWidth", + "isVisible", + "personId", + "sourceType" + ], + "type": "object" + }, "SyncAssetMetadataDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "key": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMetadataKey" - } - ] + "description": "Key", + "type": "string" } }, "required": [ @@ -20677,16 +23186,15 @@ "SyncAssetMetadataV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "key": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMetadataKey" - } - ] + "description": "Key", + "type": "string" }, "value": { + "description": "Value", "type": "object" } }, @@ -20700,57 +23208,80 @@ "SyncAssetV1": { "properties": { "checksum": { + "description": "Checksum", "type": "string" }, "deletedAt": { + "description": "Deleted at", "format": "date-time", "nullable": true, "type": "string" }, "duration": { + "description": "Duration", "nullable": true, "type": "string" }, "fileCreatedAt": { + "description": "File created at", "format": "date-time", "nullable": true, "type": "string" }, "fileModifiedAt": { + "description": "File modified at", "format": "date-time", "nullable": true, "type": "string" }, + "height": { + "description": "Asset height", + "nullable": true, + "type": "integer" + }, "id": { + "description": "Asset ID", "type": "string" }, + "isEdited": { + "description": "Is edited", + "type": "boolean" + }, "isFavorite": { + "description": "Is favorite", "type": "boolean" }, "libraryId": { + "description": "Library ID", "nullable": true, "type": "string" }, "livePhotoVideoId": { + "description": "Live photo video ID", "nullable": true, "type": "string" }, "localDateTime": { + "description": "Local date time", "format": "date-time", "nullable": true, "type": "string" }, "originalFileName": { + "description": "Original file name", "type": "string" }, "ownerId": { + "description": "Owner ID", "type": "string" }, "stackId": { + "description": "Stack ID", "nullable": true, "type": "string" }, "thumbhash": { + "description": "Thumbhash", "nullable": true, "type": "string" }, @@ -20759,14 +23290,21 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type" }, "visibility": { "allOf": [ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" + }, + "width": { + "description": "Asset width", + "nullable": true, + "type": "integer" } }, "required": [ @@ -20775,7 +23313,9 @@ "duration", "fileCreatedAt", "fileModifiedAt", + "height", "id", + "isEdited", "isFavorite", "libraryId", "livePhotoVideoId", @@ -20785,7 +23325,8 @@ "stackId", "thumbhash", "type", - "visibility" + "visibility", + "width" ], "type": "object" }, @@ -20797,36 +23338,46 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], + "description": "User avatar color", "nullable": true }, "deletedAt": { + "description": "User deleted at", "format": "date-time", "nullable": true, "type": "string" }, "email": { + "description": "User email", "type": "string" }, "hasProfileImage": { + "description": "User has profile image", "type": "boolean" }, "id": { + "description": "User ID", "type": "string" }, "isAdmin": { + "description": "User is admin", "type": "boolean" }, "name": { + "description": "User name", "type": "string" }, "oauthId": { + "description": "User OAuth ID", "type": "string" }, "pinCode": { + "description": "User pin code", "nullable": true, "type": "string" }, "profileChangedAt": { + "description": "User profile changed at", "format": "date-time", "type": "string" }, @@ -20838,6 +23389,7 @@ "type": "integer" }, "storageLabel": { + "description": "User storage label", "nullable": true, "type": "string" } @@ -20864,6 +23416,7 @@ "type": "object" }, "SyncEntityType": { + "description": "Sync entity type", "enum": [ "AuthUserV1", "UserV1", @@ -20871,6 +23424,8 @@ "AssetV1", "AssetDeleteV1", "AssetExifV1", + "AssetEditV1", + "AssetEditDeleteV1", "AssetMetadataV1", "AssetMetadataDeleteV1", "PartnerV1", @@ -20906,6 +23461,7 @@ "PersonV1", "PersonDeleteV1", "AssetFaceV1", + "AssetFaceV2", "AssetFaceDeleteV1", "UserMetadataV1", "UserMetadataDeleteV1", @@ -20918,9 +23474,11 @@ "SyncMemoryAssetDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -20933,9 +23491,11 @@ "SyncMemoryAssetV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -20948,6 +23508,7 @@ "SyncMemoryDeleteV1": { "properties": { "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -20959,41 +23520,51 @@ "SyncMemoryV1": { "properties": { "createdAt": { + "description": "Created at", "format": "date-time", "type": "string" }, "data": { + "description": "Data", "type": "object" }, "deletedAt": { + "description": "Deleted at", "format": "date-time", "nullable": true, "type": "string" }, "hideAt": { + "description": "Hide at", "format": "date-time", "nullable": true, "type": "string" }, "id": { + "description": "Memory ID", "type": "string" }, "isSaved": { + "description": "Is saved", "type": "boolean" }, "memoryAt": { + "description": "Memory at", "format": "date-time", "type": "string" }, "ownerId": { + "description": "Owner ID", "type": "string" }, "seenAt": { + "description": "Seen at", "format": "date-time", "nullable": true, "type": "string" }, "showAt": { + "description": "Show at", "format": "date-time", "nullable": true, "type": "string" @@ -21003,9 +23574,11 @@ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" }, "updatedAt": { + "description": "Updated at", "format": "date-time", "type": "string" } @@ -21029,9 +23602,11 @@ "SyncPartnerDeleteV1": { "properties": { "sharedById": { + "description": "Shared by ID", "type": "string" }, "sharedWithId": { + "description": "Shared with ID", "type": "string" } }, @@ -21044,12 +23619,15 @@ "SyncPartnerV1": { "properties": { "inTimeline": { + "description": "In timeline", "type": "boolean" }, "sharedById": { + "description": "Shared by ID", "type": "string" }, "sharedWithId": { + "description": "Shared with ID", "type": "string" } }, @@ -21063,6 +23641,7 @@ "SyncPersonDeleteV1": { "properties": { "personId": { + "description": "Person ID", "type": "string" } }, @@ -21074,38 +23653,48 @@ "SyncPersonV1": { "properties": { "birthDate": { + "description": "Birth date", "format": "date-time", "nullable": true, "type": "string" }, "color": { + "description": "Color", "nullable": true, "type": "string" }, "createdAt": { + "description": "Created at", "format": "date-time", "type": "string" }, "faceAssetId": { + "description": "Face asset ID", "nullable": true, "type": "string" }, "id": { + "description": "Person ID", "type": "string" }, "isFavorite": { + "description": "Is favorite", "type": "boolean" }, "isHidden": { + "description": "Is hidden", "type": "boolean" }, "name": { + "description": "Person name", "type": "string" }, "ownerId": { + "description": "Owner ID", "type": "string" }, "updatedAt": { + "description": "Updated at", "format": "date-time", "type": "string" } @@ -21125,6 +23714,7 @@ "type": "object" }, "SyncRequestType": { + "description": "Sync request types", "enum": [ "AlbumsV1", "AlbumUsersV1", @@ -21133,6 +23723,7 @@ "AlbumAssetExifsV1", "AssetsV1", "AssetExifsV1", + "AssetEditsV1", "AssetMetadataV1", "AuthUsersV1", "MemoriesV1", @@ -21145,6 +23736,7 @@ "UsersV1", "PeopleV1", "AssetFacesV1", + "AssetFacesV2", "UserMetadataV1" ], "type": "string" @@ -21156,6 +23748,7 @@ "SyncStackDeleteV1": { "properties": { "stackId": { + "description": "Stack ID", "type": "string" } }, @@ -21167,19 +23760,24 @@ "SyncStackV1": { "properties": { "createdAt": { + "description": "Created at", "format": "date-time", "type": "string" }, "id": { + "description": "Stack ID", "type": "string" }, "ownerId": { + "description": "Owner ID", "type": "string" }, "primaryAssetId": { + "description": "Primary asset ID", "type": "string" }, "updatedAt": { + "description": "Updated at", "format": "date-time", "type": "string" } @@ -21196,9 +23794,11 @@ "SyncStreamDto": { "properties": { "reset": { + "description": "Reset sync state", "type": "boolean" }, "types": { + "description": "Sync request types", "items": { "$ref": "#/components/schemas/SyncRequestType" }, @@ -21213,6 +23813,7 @@ "SyncUserDeleteV1": { "properties": { "userId": { + "description": "User ID", "type": "string" } }, @@ -21228,9 +23829,11 @@ { "$ref": "#/components/schemas/UserMetadataKey" } - ] + ], + "description": "User metadata key" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -21247,12 +23850,15 @@ { "$ref": "#/components/schemas/UserMetadataKey" } - ] + ], + "description": "User metadata key" }, "userId": { + "description": "User ID", "type": "string" }, "value": { + "description": "User metadata value", "type": "object" } }, @@ -21271,26 +23877,33 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], + "description": "User avatar color", "nullable": true }, "deletedAt": { + "description": "User deleted at", "format": "date-time", "nullable": true, "type": "string" }, "email": { + "description": "User email", "type": "string" }, "hasProfileImage": { + "description": "User has profile image", "type": "boolean" }, "id": { + "description": "User ID", "type": "string" }, "name": { + "description": "User name", "type": "string" }, "profileChangedAt": { + "description": "User profile changed at", "format": "date-time", "type": "string" } @@ -21415,30 +24028,36 @@ { "$ref": "#/components/schemas/TranscodeHWAccel" } - ] + ], + "description": "Transcode hardware acceleration" }, "accelDecode": { + "description": "Accelerated decode", "type": "boolean" }, "acceptedAudioCodecs": { + "description": "Accepted audio codecs", "items": { "$ref": "#/components/schemas/AudioCodec" }, "type": "array" }, "acceptedContainers": { + "description": "Accepted containers", "items": { "$ref": "#/components/schemas/VideoContainer" }, "type": "array" }, "acceptedVideoCodecs": { + "description": "Accepted video codecs", "items": { "$ref": "#/components/schemas/VideoCodec" }, "type": "array" }, "bframes": { + "description": "B-frames", "maximum": 16, "minimum": -1, "type": "integer" @@ -21448,27 +24067,34 @@ { "$ref": "#/components/schemas/CQMode" } - ] + ], + "description": "CQ mode" }, "crf": { + "description": "CRF", "maximum": 51, "minimum": 0, "type": "integer" }, "gopSize": { + "description": "GOP size", "minimum": 0, "type": "integer" }, "maxBitrate": { + "description": "Max bitrate", "type": "string" }, "preferredHwDevice": { + "description": "Preferred hardware device", "type": "string" }, "preset": { + "description": "Preset", "type": "string" }, "refs": { + "description": "References", "maximum": 6, "minimum": 0, "type": "integer" @@ -21478,9 +24104,11 @@ { "$ref": "#/components/schemas/AudioCodec" } - ] + ], + "description": "Target audio codec" }, "targetResolution": { + "description": "Target resolution", "type": "string" }, "targetVideoCodec": { @@ -21488,12 +24116,15 @@ { "$ref": "#/components/schemas/VideoCodec" } - ] + ], + "description": "Target video codec" }, "temporalAQ": { + "description": "Temporal AQ", "type": "boolean" }, "threads": { + "description": "Threads", "minimum": 0, "type": "integer" }, @@ -21502,16 +24133,19 @@ { "$ref": "#/components/schemas/ToneMapping" } - ] + ], + "description": "Tone mapping" }, "transcode": { "allOf": [ { "$ref": "#/components/schemas/TranscodePolicy" } - ] + ], + "description": "Transcode policy" }, "twoPass": { + "description": "Two pass", "type": "boolean" } }, @@ -21543,6 +24177,7 @@ "SystemConfigFacesDto": { "properties": { "import": { + "description": "Import", "type": "boolean" } }, @@ -21554,6 +24189,7 @@ "SystemConfigGeneratedFullsizeImageDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "format": { @@ -21561,9 +24197,16 @@ { "$ref": "#/components/schemas/ImageFormat" } - ] + ], + "description": "Image format" + }, + "progressive": { + "default": false, + "description": "Progressive", + "type": "boolean" }, "quality": { + "description": "Quality", "maximum": 100, "minimum": 1, "type": "integer" @@ -21583,14 +24226,21 @@ { "$ref": "#/components/schemas/ImageFormat" } - ] + ], + "description": "Image format" + }, + "progressive": { + "default": false, + "type": "boolean" }, "quality": { + "description": "Quality", "maximum": 100, "minimum": 1, "type": "integer" }, "size": { + "description": "Size", "minimum": 1, "type": "integer" } @@ -21609,9 +24259,11 @@ { "$ref": "#/components/schemas/Colorspace" } - ] + ], + "description": "Colorspace" }, "extractEmbedded": { + "description": "Extract embedded", "type": "boolean" }, "fullsize": { @@ -21638,6 +24290,9 @@ "backgroundTask": { "$ref": "#/components/schemas/JobSettingsDto" }, + "editor": { + "$ref": "#/components/schemas/JobSettingsDto" + }, "faceDetection": { "$ref": "#/components/schemas/JobSettingsDto" }, @@ -21677,6 +24332,7 @@ }, "required": [ "backgroundTask", + "editor", "faceDetection", "library", "metadataExtraction", @@ -21713,6 +24369,7 @@ "type": "string" }, "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21725,6 +24382,7 @@ "SystemConfigLibraryWatchDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21736,6 +24394,7 @@ "SystemConfigLoggingDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "level": { @@ -21764,6 +24423,7 @@ "$ref": "#/components/schemas/DuplicateDetectionConfig" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "facialRecognition": { @@ -21800,6 +24460,7 @@ "type": "string" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "lightStyle": { @@ -21828,6 +24489,7 @@ "SystemConfigNewVersionCheckDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21839,21 +24501,26 @@ "SystemConfigNightlyTasksDto": { "properties": { "clusterNewFaces": { + "description": "Cluster new faces", "type": "boolean" }, "databaseCleanup": { + "description": "Database cleanup", "type": "boolean" }, "generateMemories": { + "description": "Generate memories", "type": "boolean" }, "missingThumbnails": { + "description": "Missing thumbnails", "type": "boolean" }, "startTime": { "type": "string" }, "syncQuotaUsage": { + "description": "Sync quota usage", "type": "boolean" } }, @@ -21881,58 +24548,74 @@ "SystemConfigOAuthDto": { "properties": { "autoLaunch": { + "description": "Auto launch", "type": "boolean" }, "autoRegister": { + "description": "Auto register", "type": "boolean" }, "buttonText": { + "description": "Button text", "type": "string" }, "clientId": { + "description": "Client ID", "type": "string" }, "clientSecret": { + "description": "Client secret", "type": "string" }, "defaultStorageQuota": { + "description": "Default storage quota", "format": "int64", "minimum": 0, "nullable": true, "type": "integer" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "issuerUrl": { + "description": "Issuer URL", "type": "string" }, "mobileOverrideEnabled": { + "description": "Mobile override enabled", "type": "boolean" }, "mobileRedirectUri": { + "description": "Mobile redirect URI", "format": "uri", "type": "string" }, "profileSigningAlgorithm": { + "description": "Profile signing algorithm", "type": "string" }, "roleClaim": { + "description": "Role claim", "type": "string" }, "scope": { + "description": "Scope", "type": "string" }, "signingAlgorithm": { "type": "string" }, "storageLabelClaim": { + "description": "Storage label claim", "type": "string" }, "storageQuotaClaim": { + "description": "Storage quota claim", "type": "string" }, "timeout": { + "description": "Timeout", "minimum": 1, "type": "integer" }, @@ -21941,7 +24624,8 @@ { "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } - ] + ], + "description": "Token endpoint auth method" } }, "required": [ @@ -21969,6 +24653,7 @@ "SystemConfigPasswordLoginDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21980,6 +24665,7 @@ "SystemConfigReverseGeocodingDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21991,13 +24677,16 @@ "SystemConfigServerDto": { "properties": { "externalDomain": { + "description": "External domain", "format": "uri", "type": "string" }, "loginPageMessage": { + "description": "Login page message", "type": "string" }, "publicUsers": { + "description": "Public users", "type": "boolean" } }, @@ -22011,12 +24700,15 @@ "SystemConfigSmtpDto": { "properties": { "enabled": { + "description": "Whether SMTP email notifications are enabled", "type": "boolean" }, "from": { + "description": "Email address to send from", "type": "string" }, "replyTo": { + "description": "Email address for replies", "type": "string" }, "transport": { @@ -22034,23 +24726,29 @@ "SystemConfigSmtpTransportDto": { "properties": { "host": { + "description": "SMTP server hostname", "type": "string" }, "ignoreCert": { + "description": "Whether to ignore SSL certificate errors", "type": "boolean" }, "password": { + "description": "SMTP password", "type": "string" }, "port": { + "description": "SMTP server port", "maximum": 65535, "minimum": 0, "type": "number" }, "secure": { + "description": "Whether to use secure connection (TLS/SSL)", "type": "boolean" }, "username": { + "description": "SMTP username", "type": "string" } }, @@ -22067,12 +24765,15 @@ "SystemConfigStorageTemplateDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "hashVerificationEnabled": { + "description": "Hash verification enabled", "type": "boolean" }, "template": { + "description": "Template", "type": "string" } }, @@ -22105,48 +24806,56 @@ "SystemConfigTemplateStorageOptionDto": { "properties": { "dayOptions": { + "description": "Available day format options for storage template", "items": { "type": "string" }, "type": "array" }, "hourOptions": { + "description": "Available hour format options for storage template", "items": { "type": "string" }, "type": "array" }, "minuteOptions": { + "description": "Available minute format options for storage template", "items": { "type": "string" }, "type": "array" }, "monthOptions": { + "description": "Available month format options for storage template", "items": { "type": "string" }, "type": "array" }, "presetOptions": { + "description": "Available preset template options", "items": { "type": "string" }, "type": "array" }, "secondOptions": { + "description": "Available second format options for storage template", "items": { "type": "string" }, "type": "array" }, "weekOptions": { + "description": "Available week format options for storage template", "items": { "type": "string" }, "type": "array" }, "yearOptions": { + "description": "Available year format options for storage template", "items": { "type": "string" }, @@ -22179,6 +24888,7 @@ "SystemConfigThemeDto": { "properties": { "customCss": { + "description": "Custom CSS for theming", "type": "string" } }, @@ -22190,10 +24900,12 @@ "SystemConfigTrashDto": { "properties": { "days": { + "description": "Days", "minimum": 0, "type": "integer" }, "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -22206,6 +24918,7 @@ "SystemConfigUserDto": { "properties": { "deleteDelay": { + "description": "Delete delay", "minimum": 1, "type": "integer" } @@ -22218,6 +24931,7 @@ "TagBulkAssetsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -22225,6 +24939,7 @@ "type": "array" }, "tagIds": { + "description": "Tag IDs", "items": { "format": "uuid", "type": "string" @@ -22241,6 +24956,7 @@ "TagBulkAssetsResponseDto": { "properties": { "count": { + "description": "Number of assets tagged", "type": "integer" } }, @@ -22252,13 +24968,16 @@ "TagCreateDto": { "properties": { "color": { + "description": "Tag color (hex)", "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", "type": "string" }, "name": { + "description": "Tag name", "type": "string" }, "parentId": { + "description": "Parent tag ID", "format": "uuid", "nullable": true, "type": "string" @@ -22272,26 +24991,33 @@ "TagResponseDto": { "properties": { "color": { + "description": "Tag color (hex)", "type": "string" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "id": { + "description": "Tag ID", "type": "string" }, "name": { + "description": "Tag name", "type": "string" }, "parentId": { + "description": "Parent tag ID", "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" }, "value": { + "description": "Tag value (full path)", "type": "string" } }, @@ -22307,6 +25033,7 @@ "TagUpdateDto": { "properties": { "color": { + "description": "Tag color (hex)", "nullable": true, "type": "string" } @@ -22316,6 +25043,7 @@ "TagUpsertDto": { "properties": { "tags": { + "description": "Tag names to upsert", "items": { "type": "string" }, @@ -22331,10 +25059,12 @@ "properties": { "enabled": { "default": true, + "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { "default": true, + "description": "Whether tags appear in web sidebar", "type": "boolean" } }, @@ -22347,9 +25077,11 @@ "TagsUpdate": { "properties": { "enabled": { + "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether tags appear in web sidebar", "type": "boolean" } }, @@ -22358,6 +25090,7 @@ "TemplateDto": { "properties": { "template": { + "description": "Template name", "type": "string" } }, @@ -22369,9 +25102,11 @@ "TemplateResponseDto": { "properties": { "html": { + "description": "Template HTML content", "type": "string" }, "name": { + "description": "Template name", "type": "string" } }, @@ -22384,6 +25119,7 @@ "TestEmailResponseDto": { "properties": { "messageId": { + "description": "Email message ID", "type": "string" } }, @@ -22419,7 +25155,7 @@ "type": "array" }, "fileCreatedAt": { - "description": "Array of file creation timestamps in UTC (ISO 8601 format, without timezone)", + "description": "Array of file creation timestamps in UTC", "items": { "type": "string" }, @@ -22574,6 +25310,7 @@ "type": "object" }, "ToneMapping": { + "description": "Tone mapping", "enum": [ "hable", "mobius", @@ -22583,6 +25320,7 @@ "type": "string" }, "TranscodeHWAccel": { + "description": "Transcode hardware acceleration", "enum": [ "nvenc", "qsv", @@ -22593,6 +25331,7 @@ "type": "string" }, "TranscodePolicy": { + "description": "Transcode policy", "enum": [ "all", "optimal", @@ -22605,6 +25344,7 @@ "TrashResponseDto": { "properties": { "count": { + "description": "Number of items in trash", "type": "integer" } }, @@ -22616,16 +25356,20 @@ "UpdateAlbumDto": { "properties": { "albumName": { + "description": "Album name", "type": "string" }, "albumThumbnailAssetId": { + "description": "Album thumbnail asset ID", "format": "uuid", "type": "string" }, "description": { + "description": "Album description", "type": "string" }, "isActivityEnabled": { + "description": "Enable activity feed", "type": "boolean" }, "order": { @@ -22633,7 +25377,8 @@ { "$ref": "#/components/schemas/AssetOrder" } - ] + ], + "description": "Asset sort order" } }, "type": "object" @@ -22645,7 +25390,8 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" } }, "required": [ @@ -22656,36 +25402,61 @@ "UpdateAssetDto": { "properties": { "dateTimeOriginal": { + "description": "Original date and time", "type": "string" }, "description": { + "description": "Asset description", "type": "string" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "latitude": { + "description": "Latitude coordinate", "type": "number" }, "livePhotoVideoId": { + "description": "Live photo video ID", "format": "uuid", "nullable": true, "type": "string" }, "longitude": { + "description": "Longitude coordinate", "type": "number" }, "rating": { + "description": "Rating in range [1-5], or null for unrated", "maximum": 5, "minimum": -1, - "type": "number" + "nullable": true, + "type": "number", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + } + ], + "x-immich-state": "Stable" }, "visibility": { "allOf": [ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "type": "object" @@ -22693,6 +25464,7 @@ "UpdateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -22701,6 +25473,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths (max 128)", "items": { "type": "string" }, @@ -22709,6 +25482,7 @@ "uniqueItems": true }, "name": { + "description": "Library name", "type": "string" } }, @@ -22717,32 +25491,40 @@ "UsageByUserDto": { "properties": { "photos": { + "description": "Number of photos", "type": "integer" }, "quotaSizeInBytes": { + "description": "User quota size in bytes (null if unlimited)", "format": "int64", "nullable": true, "type": "integer" }, "usage": { + "description": "Total storage usage in bytes", "format": "int64", "type": "integer" }, "usagePhotos": { + "description": "Storage usage for photos in bytes", "format": "int64", "type": "integer" }, "usageVideos": { + "description": "Storage usage for videos in bytes", "format": "int64", "type": "integer" }, "userId": { + "description": "User ID", "type": "string" }, "userName": { + "description": "User name", "type": "string" }, "videos": { + "description": "Number of videos", "type": "integer" } }, @@ -22766,34 +25548,49 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], + "description": "Avatar color", "nullable": true }, "email": { + "description": "User email", "format": "email", "type": "string" }, "isAdmin": { + "description": "Grant admin privileges", "type": "boolean" }, "name": { + "description": "User name", "type": "string" }, "notify": { + "description": "Send notification email", "type": "boolean" }, "password": { + "description": "User password", + "type": "string" + }, + "pinCode": { + "description": "PIN code", + "example": "123456", + "nullable": true, "type": "string" }, "quotaSizeInBytes": { + "description": "Storage quota in bytes", "format": "int64", "minimum": 0, "nullable": true, "type": "integer" }, "shouldChangePassword": { + "description": "Require password change on next login", "type": "boolean" }, "storageLabel": { + "description": "Storage label", "nullable": true, "type": "string" } @@ -22808,6 +25605,7 @@ "UserAdminDeleteDto": { "properties": { "force": { + "description": "Force delete even if user has assets", "type": "boolean" } }, @@ -22820,24 +25618,30 @@ { "$ref": "#/components/schemas/UserAvatarColor" } - ] + ], + "description": "Avatar color" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, "deletedAt": { + "description": "Deletion date", "format": "date-time", "nullable": true, "type": "string" }, "email": { + "description": "User email", "type": "string" }, "id": { + "description": "User ID", "type": "string" }, "isAdmin": { + "description": "Is admin user", "type": "boolean" }, "license": { @@ -22846,32 +25650,40 @@ "$ref": "#/components/schemas/UserLicense" } ], + "description": "User license", "nullable": true }, "name": { + "description": "User name", "type": "string" }, "oauthId": { + "description": "OAuth ID", "type": "string" }, "profileChangedAt": { + "description": "Profile change date", "format": "date-time", "type": "string" }, "profileImagePath": { + "description": "Profile image path", "type": "string" }, "quotaSizeInBytes": { + "description": "Storage quota in bytes", "format": "int64", "nullable": true, "type": "integer" }, "quotaUsageInBytes": { + "description": "Storage usage in bytes", "format": "int64", "nullable": true, "type": "integer" }, "shouldChangePassword": { + "description": "Require password change on next login", "type": "boolean" }, "status": { @@ -22879,13 +25691,16 @@ { "$ref": "#/components/schemas/UserStatus" } - ] + ], + "description": "User status" }, "storageLabel": { + "description": "Storage label", "nullable": true, "type": "string" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -22919,36 +25734,45 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], + "description": "Avatar color", "nullable": true }, "email": { + "description": "User email", "format": "email", "type": "string" }, "isAdmin": { + "description": "Grant admin privileges", "type": "boolean" }, "name": { + "description": "User name", "type": "string" }, "password": { + "description": "User password", "type": "string" }, "pinCode": { + "description": "PIN code", "example": "123456", "nullable": true, "type": "string" }, "quotaSizeInBytes": { + "description": "Storage quota in bytes", "format": "int64", "minimum": 0, "nullable": true, "type": "integer" }, "shouldChangePassword": { + "description": "Require password change on next login", "type": "boolean" }, "storageLabel": { + "description": "Storage label", "nullable": true, "type": "string" } @@ -22956,6 +25780,7 @@ "type": "object" }, "UserAvatarColor": { + "description": "Avatar color", "enum": [ "primary", "pink", @@ -22973,13 +25798,16 @@ "UserLicense": { "properties": { "activatedAt": { + "description": "Activation date", "format": "date-time", "type": "string" }, "activationKey": { + "description": "Activation key", "type": "string" }, "licenseKey": { + "description": "License key", "type": "string" } }, @@ -22991,6 +25819,7 @@ "type": "object" }, "UserMetadataKey": { + "description": "User metadata key", "enum": [ "preferences", "license", @@ -23097,22 +25926,28 @@ { "$ref": "#/components/schemas/UserAvatarColor" } - ] + ], + "description": "Avatar color" }, "email": { + "description": "User email", "type": "string" }, "id": { + "description": "User ID", "type": "string" }, "name": { + "description": "User name", "type": "string" }, "profileChangedAt": { + "description": "Profile change date", "format": "date-time", "type": "string" }, "profileImagePath": { + "description": "Profile image path", "type": "string" } }, @@ -23127,6 +25962,7 @@ "type": "object" }, "UserStatus": { + "description": "User status", "enum": [ "active", "removing", @@ -23142,16 +25978,20 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], + "description": "Avatar color", "nullable": true }, "email": { + "description": "User email", "format": "email", "type": "string" }, "name": { + "description": "User name", "type": "string" }, "password": { + "description": "User password (deprecated, use change password endpoint)", "type": "string" } }, @@ -23160,6 +26000,7 @@ "ValidateAccessTokenResponseDto": { "properties": { "authStatus": { + "description": "Authentication status", "type": "boolean" } }, @@ -23171,6 +26012,7 @@ "ValidateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -23179,6 +26021,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths to validate (max 128)", "items": { "type": "string" }, @@ -23192,13 +26035,16 @@ "ValidateLibraryImportPathResponseDto": { "properties": { "importPath": { + "description": "Import path", "type": "string" }, "isValid": { "default": false, + "description": "Is valid", "type": "boolean" }, "message": { + "description": "Validation message", "type": "string" } }, @@ -23211,6 +26057,7 @@ "ValidateLibraryResponseDto": { "properties": { "importPaths": { + "description": "Validation results for import paths", "items": { "$ref": "#/components/schemas/ValidateLibraryImportPathResponseDto" }, @@ -23222,10 +26069,12 @@ "VersionCheckStateResponseDto": { "properties": { "checkedAt": { + "description": "Last check timestamp", "nullable": true, "type": "string" }, "releaseVersion": { + "description": "Release version", "nullable": true, "type": "string" } @@ -23237,6 +26086,7 @@ "type": "object" }, "VideoCodec": { + "description": "Target video codec", "enum": [ "h264", "hevc", @@ -23246,6 +26096,7 @@ "type": "string" }, "VideoContainer": { + "description": "Accepted containers", "enum": [ "mov", "mp4", @@ -23257,9 +26108,11 @@ "WorkflowActionItemDto": { "properties": { "actionConfig": { + "description": "Action configuration", "type": "object" }, "pluginActionId": { + "description": "Plugin action ID", "format": "uuid", "type": "string" } @@ -23272,19 +26125,24 @@ "WorkflowActionResponseDto": { "properties": { "actionConfig": { + "description": "Action configuration", "nullable": true, "type": "object" }, "id": { + "description": "Action ID", "type": "string" }, "order": { + "description": "Action order", "type": "number" }, "pluginActionId": { + "description": "Plugin action ID", "type": "string" }, "workflowId": { + "description": "Workflow ID", "type": "string" } }, @@ -23300,24 +26158,29 @@ "WorkflowCreateDto": { "properties": { "actions": { + "description": "Workflow actions", "items": { "$ref": "#/components/schemas/WorkflowActionItemDto" }, "type": "array" }, "description": { + "description": "Workflow description", "type": "string" }, "enabled": { + "description": "Workflow enabled", "type": "boolean" }, "filters": { + "description": "Workflow filters", "items": { "$ref": "#/components/schemas/WorkflowFilterItemDto" }, "type": "array" }, "name": { + "description": "Workflow name", "type": "string" }, "triggerType": { @@ -23325,7 +26188,8 @@ { "$ref": "#/components/schemas/PluginTriggerType" } - ] + ], + "description": "Workflow trigger type" } }, "required": [ @@ -23339,9 +26203,11 @@ "WorkflowFilterItemDto": { "properties": { "filterConfig": { + "description": "Filter configuration", "type": "object" }, "pluginFilterId": { + "description": "Plugin filter ID", "format": "uuid", "type": "string" } @@ -23354,19 +26220,24 @@ "WorkflowFilterResponseDto": { "properties": { "filterConfig": { + "description": "Filter configuration", "nullable": true, "type": "object" }, "id": { + "description": "Filter ID", "type": "string" }, "order": { + "description": "Filter order", "type": "number" }, "pluginFilterId": { + "description": "Plugin filter ID", "type": "string" }, "workflowId": { + "description": "Workflow ID", "type": "string" } }, @@ -23382,42 +26253,51 @@ "WorkflowResponseDto": { "properties": { "actions": { + "description": "Workflow actions", "items": { "$ref": "#/components/schemas/WorkflowActionResponseDto" }, "type": "array" }, "createdAt": { + "description": "Creation date", "type": "string" }, "description": { + "description": "Workflow description", "type": "string" }, "enabled": { + "description": "Workflow enabled", "type": "boolean" }, "filters": { + "description": "Workflow filters", "items": { "$ref": "#/components/schemas/WorkflowFilterResponseDto" }, "type": "array" }, "id": { + "description": "Workflow ID", "type": "string" }, "name": { + "description": "Workflow name", "nullable": true, "type": "string" }, "ownerId": { + "description": "Owner user ID", "type": "string" }, "triggerType": { - "enum": [ - "AssetCreate", - "PersonRecognized" + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } ], - "type": "string" + "description": "Workflow trigger type" } }, "required": [ @@ -23436,25 +26316,38 @@ "WorkflowUpdateDto": { "properties": { "actions": { + "description": "Workflow actions", "items": { "$ref": "#/components/schemas/WorkflowActionItemDto" }, "type": "array" }, "description": { + "description": "Workflow description", "type": "string" }, "enabled": { + "description": "Workflow enabled", "type": "boolean" }, "filters": { + "description": "Workflow filters", "items": { "$ref": "#/components/schemas/WorkflowFilterItemDto" }, "type": "array" }, "name": { + "description": "Workflow name", "type": "string" + }, + "triggerType": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginTriggerType" + } + ], + "description": "Workflow trigger type" } }, "type": "object" diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 7fd8b5fe58..8f057df6cc 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "2.4.0", + "version": "2.5.6", "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": "^24.10.3", + "@types/node": "^24.10.13", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index bb3b07a1f7..7c6a2ceb9d 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 2.4.0 + * 2.5.6 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -8,179 +8,324 @@ import * as Oazapfts from "@oazapfts/runtime"; import * as QS from "@oazapfts/runtime/query"; export const defaults: Oazapfts.Defaults = { headers: {}, - baseUrl: "/api", + baseUrl: "/api" }; const oazapfts = Oazapfts.runtime(defaults); export const servers = { server1: "/api" }; export type UserResponseDto = { + /** Avatar color */ avatarColor: UserAvatarColor; + /** User email */ email: string; + /** User ID */ id: string; + /** User name */ name: string; + /** Profile change date */ profileChangedAt: string; + /** Profile image path */ profileImagePath: string; }; export type ActivityResponseDto = { + /** Asset ID (if activity is for an asset) */ assetId: string | null; + /** Comment text (for comment activities) */ comment?: string | null; + /** Creation date */ createdAt: string; + /** Activity ID */ id: string; + /** Activity type */ "type": ReactionType; user: UserResponseDto; }; export type ActivityCreateDto = { + /** Album ID */ albumId: string; + /** Asset ID (if activity is for an asset) */ assetId?: string; + /** Comment text (required if type is comment) */ comment?: string; + /** Activity type (like or comment) */ "type": ReactionType; }; export type ActivityStatisticsResponseDto = { + /** Number of comments */ comments: number; + /** Number of likes */ likes: number; }; +export type DatabaseBackupDeleteDto = { + backups: string[]; +}; +export type DatabaseBackupDto = { + filename: string; + filesize: number; +}; +export type DatabaseBackupListResponseDto = { + backups: DatabaseBackupDto[]; +}; +export type DatabaseBackupUploadDto = { + file?: Blob; +}; export type SetMaintenanceModeDto = { + /** Maintenance action */ action: MaintenanceAction; + /** Restore backup filename */ + restoreBackupFilename?: string; +}; +export type MaintenanceDetectInstallStorageFolderDto = { + /** Number of files in the folder */ + files: number; + /** Storage folder */ + folder: StorageFolder; + /** Whether the folder is readable */ + readable: boolean; + /** Whether the folder is writable */ + writable: boolean; +}; +export type MaintenanceDetectInstallResponseDto = { + storage: MaintenanceDetectInstallStorageFolderDto[]; }; export type MaintenanceLoginDto = { + /** Maintenance token */ token?: string; }; export type MaintenanceAuthDto = { + /** Maintenance username */ username: string; }; +export type MaintenanceStatusResponseDto = { + /** Maintenance action */ + action: MaintenanceAction; + active: boolean; + error?: string; + progress?: number; + task?: string; +}; export type NotificationCreateDto = { + /** Additional notification data */ data?: object; + /** Notification description */ description?: string | null; + /** Notification level */ level?: NotificationLevel; + /** Date when notification was read */ readAt?: string | null; + /** Notification title */ title: string; + /** Notification type */ "type"?: NotificationType; + /** User ID to send notification to */ userId: string; }; export type NotificationDto = { + /** Creation date */ createdAt: string; + /** Additional notification data */ data?: object; + /** Notification description */ description?: string; + /** Notification ID */ id: string; + /** Notification level */ level: NotificationLevel; + /** Date when notification was read */ readAt?: string; + /** Notification title */ title: string; + /** Notification type */ "type": NotificationType; }; export type TemplateDto = { + /** Template name */ template: string; }; export type TemplateResponseDto = { + /** Template HTML content */ html: string; + /** Template name */ name: string; }; export type SystemConfigSmtpTransportDto = { + /** SMTP server hostname */ host: string; + /** Whether to ignore SSL certificate errors */ ignoreCert: boolean; + /** SMTP password */ password: string; + /** SMTP server port */ port: number; + /** Whether to use secure connection (TLS/SSL) */ secure: boolean; + /** SMTP username */ username: string; }; export type SystemConfigSmtpDto = { + /** Whether SMTP email notifications are enabled */ enabled: boolean; + /** Email address to send from */ "from": string; + /** Email address for replies */ replyTo: string; transport: SystemConfigSmtpTransportDto; }; export type TestEmailResponseDto = { + /** Email message ID */ messageId: string; }; export type UserLicense = { + /** Activation date */ activatedAt: string; + /** Activation key */ activationKey: string; + /** License key */ licenseKey: string; }; export type UserAdminResponseDto = { + /** Avatar color */ avatarColor: UserAvatarColor; + /** Creation date */ createdAt: string; + /** Deletion date */ deletedAt: string | null; + /** User email */ email: string; + /** User ID */ id: string; + /** Is admin user */ isAdmin: boolean; + /** User license */ license: (UserLicense) | null; + /** User name */ name: string; + /** OAuth ID */ oauthId: string; + /** Profile change date */ profileChangedAt: string; + /** Profile image path */ profileImagePath: string; + /** Storage quota in bytes */ quotaSizeInBytes: number | null; + /** Storage usage in bytes */ quotaUsageInBytes: number | null; + /** Require password change on next login */ shouldChangePassword: boolean; + /** User status */ status: UserStatus; + /** Storage label */ storageLabel: string | null; + /** Last update date */ updatedAt: string; }; export type UserAdminCreateDto = { + /** Avatar color */ avatarColor?: (UserAvatarColor) | null; + /** User email */ email: string; + /** Grant admin privileges */ isAdmin?: boolean; + /** User name */ name: string; + /** Send notification email */ notify?: boolean; + /** User password */ password: string; + /** PIN code */ + pinCode?: string | null; + /** Storage quota in bytes */ quotaSizeInBytes?: number | null; + /** Require password change on next login */ shouldChangePassword?: boolean; + /** Storage label */ storageLabel?: string | null; }; export type UserAdminDeleteDto = { + /** Force delete even if user has assets */ force?: boolean; }; export type UserAdminUpdateDto = { + /** Avatar color */ avatarColor?: (UserAvatarColor) | null; + /** User email */ email?: string; + /** Grant admin privileges */ isAdmin?: boolean; + /** User name */ name?: string; + /** User password */ password?: string; + /** PIN code */ pinCode?: string | null; + /** Storage quota in bytes */ quotaSizeInBytes?: number | null; + /** Require password change on next login */ shouldChangePassword?: boolean; + /** Storage label */ storageLabel?: string | null; }; export type AlbumsResponse = { + /** Default asset order for albums */ defaultAssetOrder: AssetOrder; }; export type CastResponse = { + /** Whether Google Cast is enabled */ gCastEnabled: boolean; }; export type DownloadResponse = { + /** Maximum archive size in bytes */ archiveSize: number; + /** Whether to include embedded videos in downloads */ includeEmbeddedVideos: boolean; }; export type EmailNotificationsResponse = { + /** Whether to receive email notifications for album invites */ albumInvite: boolean; + /** Whether to receive email notifications for album updates */ albumUpdate: boolean; + /** Whether email notifications are enabled */ enabled: boolean; }; export type FoldersResponse = { + /** Whether folders are enabled */ enabled: boolean; + /** Whether folders appear in web sidebar */ sidebarWeb: boolean; }; export type MemoriesResponse = { + /** Memory duration in seconds */ duration: number; + /** Whether memories are enabled */ enabled: boolean; }; export type PeopleResponse = { + /** Whether people are enabled */ enabled: boolean; + /** Whether people appear in web sidebar */ sidebarWeb: boolean; }; export type PurchaseResponse = { + /** Date until which to hide buy button */ hideBuyButtonUntil: string; + /** Whether to show support badge */ showSupportBadge: boolean; }; export type RatingsResponse = { + /** Whether ratings are enabled */ enabled: boolean; }; export type SharedLinksResponse = { + /** Whether shared links are enabled */ enabled: boolean; + /** Whether shared links appear in web sidebar */ sidebarWeb: boolean; }; export type TagsResponse = { + /** Whether tags are enabled */ enabled: boolean; + /** Whether tags appear in web sidebar */ sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { @@ -197,48 +342,69 @@ export type UserPreferencesResponseDto = { tags: TagsResponse; }; export type AlbumsUpdate = { + /** Default asset order for albums */ defaultAssetOrder?: AssetOrder; }; export type AvatarUpdate = { + /** Avatar color */ color?: UserAvatarColor; }; export type CastUpdate = { + /** Whether Google Cast is enabled */ gCastEnabled?: boolean; }; export type DownloadUpdate = { + /** Maximum archive size in bytes */ archiveSize?: number; + /** Whether to include embedded videos in downloads */ includeEmbeddedVideos?: boolean; }; export type EmailNotificationsUpdate = { + /** Whether to receive email notifications for album invites */ albumInvite?: boolean; + /** Whether to receive email notifications for album updates */ albumUpdate?: boolean; + /** Whether email notifications are enabled */ enabled?: boolean; }; export type FoldersUpdate = { + /** Whether folders are enabled */ enabled?: boolean; + /** Whether folders appear in web sidebar */ sidebarWeb?: boolean; }; export type MemoriesUpdate = { + /** Memory duration in seconds */ duration?: number; + /** Whether memories are enabled */ enabled?: boolean; }; export type PeopleUpdate = { + /** Whether people are enabled */ enabled?: boolean; + /** Whether people appear in web sidebar */ sidebarWeb?: boolean; }; export type PurchaseUpdate = { + /** Date until which to hide buy button */ hideBuyButtonUntil?: string; + /** Whether to show support badge */ showSupportBadge?: boolean; }; export type RatingsUpdate = { + /** Whether ratings are enabled */ enabled?: boolean; }; export type SharedLinksUpdate = { + /** Whether shared links are enabled */ enabled?: boolean; + /** Whether shared links appear in web sidebar */ sidebarWeb?: boolean; }; export type TagsUpdate = { + /** Whether tags are enabled */ enabled?: boolean; + /** Whether tags appear in web sidebar */ sidebarWeb?: boolean; }; export type UserPreferencesUpdateDto = { @@ -256,309 +422,584 @@ export type UserPreferencesUpdateDto = { tags?: TagsUpdate; }; export type SessionResponseDto = { + /** App version */ appVersion: string | null; + /** Creation date */ createdAt: string; + /** Is current session */ current: boolean; + /** Device OS */ deviceOS: string; + /** Device type */ deviceType: string; + /** Expiration date */ expiresAt?: string; + /** Session ID */ id: string; + /** Is pending sync reset */ isPendingSyncReset: boolean; + /** Last update date */ updatedAt: string; }; export type AssetStatsResponseDto = { + /** Number of images */ images: number; + /** Total number of assets */ total: number; + /** Number of videos */ videos: number; }; export type AlbumUserResponseDto = { + /** Album user role */ role: AlbumUserRole; user: UserResponseDto; }; export type ExifResponseDto = { + /** City name */ city?: string | null; + /** Country name */ country?: string | null; + /** Original date/time */ dateTimeOriginal?: string | null; + /** Image description */ description?: string | null; + /** Image height in pixels */ exifImageHeight?: number | null; + /** Image width in pixels */ exifImageWidth?: number | null; + /** Exposure time */ exposureTime?: string | null; + /** F-number (aperture) */ fNumber?: number | null; + /** File size in bytes */ fileSizeInByte?: number | null; + /** Focal length in mm */ focalLength?: number | null; + /** ISO sensitivity */ iso?: number | null; + /** GPS latitude */ latitude?: number | null; + /** Lens model */ lensModel?: string | null; + /** GPS longitude */ longitude?: number | null; + /** Camera make */ make?: string | null; + /** Camera model */ model?: string | null; + /** Modification date/time */ modifyDate?: string | null; + /** Image orientation */ orientation?: string | null; + /** Projection type */ projectionType?: string | null; + /** Rating */ rating?: number | null; + /** State/province name */ state?: string | null; + /** Time zone */ timeZone?: string | null; }; export type AssetFaceWithoutPersonResponseDto = { + /** Bounding box X1 coordinate */ boundingBoxX1: number; + /** Bounding box X2 coordinate */ boundingBoxX2: number; + /** Bounding box Y1 coordinate */ boundingBoxY1: number; + /** Bounding box Y2 coordinate */ boundingBoxY2: number; + /** Face ID */ id: string; + /** Image height in pixels */ imageHeight: number; + /** Image width in pixels */ imageWidth: number; + /** Face detection source type */ sourceType?: SourceType; }; export type PersonWithFacesResponseDto = { + /** Person date of birth */ birthDate: string | null; + /** Person color (hex) */ color?: string; + /** Face detections */ faces: AssetFaceWithoutPersonResponseDto[]; + /** Person ID */ id: string; + /** Is favorite */ isFavorite?: boolean; + /** Is hidden */ isHidden: boolean; + /** Person name */ name: string; + /** Thumbnail path */ thumbnailPath: string; + /** Last update date */ updatedAt?: string; }; export type AssetStackResponseDto = { + /** Number of assets in stack */ assetCount: number; + /** Stack ID */ id: string; + /** Primary asset ID */ primaryAssetId: string; }; export type TagResponseDto = { + /** Tag color (hex) */ color?: string; + /** Creation date */ createdAt: string; + /** Tag ID */ id: string; + /** Tag name */ name: string; + /** Parent tag ID */ parentId?: string; + /** Last update date */ updatedAt: string; + /** Tag value (full path) */ value: string; }; export type AssetResponseDto = { - /** base64 encoded sha1 hash */ + /** Base64 encoded SHA1 hash */ checksum: string; /** The UTC timestamp when the asset was originally uploaded to Immich. */ createdAt: string; + /** Device asset ID */ deviceAssetId: string; + /** Device ID */ deviceId: string; + /** Duplicate group ID */ duplicateId?: string | null; + /** Video duration (for videos) */ duration: string; exifInfo?: ExifResponseDto; /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ fileCreatedAt: string; /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ fileModifiedAt: string; + /** Whether asset has metadata */ hasMetadata: boolean; + /** Asset height */ + height: number | null; + /** Asset ID */ id: string; + /** Is archived */ isArchived: boolean; + /** Is edited */ + isEdited: boolean; + /** Is favorite */ isFavorite: boolean; + /** Is offline */ isOffline: boolean; + /** Is trashed */ isTrashed: boolean; + /** Library ID */ libraryId?: string | null; + /** Live photo video ID */ livePhotoVideoId?: string | null; /** The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months. */ localDateTime: string; + /** Original file name */ originalFileName: string; + /** Original MIME type */ originalMimeType?: string; + /** Original file path */ originalPath: string; owner?: UserResponseDto; + /** Owner user ID */ ownerId: string; people?: PersonWithFacesResponseDto[]; + /** Is resized */ resized?: boolean; stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; + /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ thumbhash: string | null; + /** Asset type */ "type": AssetTypeEnum; unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ updatedAt: string; + /** Asset visibility */ visibility: AssetVisibility; + /** Asset width */ + width: number | null; }; export type ContributorCountResponseDto = { + /** Number of assets contributed */ assetCount: number; + /** User ID */ userId: string; }; export type AlbumResponseDto = { + /** Album name */ albumName: string; + /** Thumbnail asset ID */ albumThumbnailAssetId: string | null; albumUsers: AlbumUserResponseDto[]; + /** Number of assets */ assetCount: number; assets: AssetResponseDto[]; contributorCounts?: ContributorCountResponseDto[]; + /** Creation date */ createdAt: string; + /** Album description */ description: string; + /** End date (latest asset) */ endDate?: string; + /** Has shared link */ hasSharedLink: boolean; + /** Album ID */ id: string; + /** Activity feed enabled */ isActivityEnabled: boolean; + /** Last modified asset timestamp */ lastModifiedAssetTimestamp?: string; + /** Asset sort order */ order?: AssetOrder; owner: UserResponseDto; + /** Owner user ID */ ownerId: string; + /** Is shared album */ shared: boolean; + /** Start date (earliest asset) */ startDate?: string; + /** Last update date */ updatedAt: string; }; export type AlbumUserCreateDto = { + /** Album user role */ role: AlbumUserRole; + /** User ID */ userId: string; }; export type CreateAlbumDto = { + /** Album name */ albumName: string; + /** Album users */ albumUsers?: AlbumUserCreateDto[]; + /** Initial asset IDs */ assetIds?: string[]; + /** Album description */ description?: string; }; export type AlbumsAddAssetsDto = { + /** Album IDs */ albumIds: string[]; + /** Asset IDs */ assetIds: string[]; }; export type AlbumsAddAssetsResponseDto = { + /** Error reason */ error?: BulkIdErrorReason; + /** Operation success */ success: boolean; }; export type AlbumStatisticsResponseDto = { + /** Number of non-shared albums */ notShared: number; + /** Number of owned albums */ owned: number; + /** Number of shared albums */ shared: number; }; export type UpdateAlbumDto = { + /** Album name */ albumName?: string; + /** Album thumbnail asset ID */ albumThumbnailAssetId?: string; + /** Album description */ description?: string; + /** Enable activity feed */ isActivityEnabled?: boolean; + /** Asset sort order */ order?: AssetOrder; }; export type BulkIdsDto = { + /** IDs to process */ ids: string[]; }; export type BulkIdResponseDto = { + /** Error reason if failed */ error?: Error; + /** ID */ id: string; + /** Whether operation succeeded */ success: boolean; }; export type UpdateAlbumUserDto = { + /** Album user role */ role: AlbumUserRole; }; export type AlbumUserAddDto = { + /** Album user role */ role?: AlbumUserRole; + /** User ID */ userId: string; }; export type AddUsersDto = { + /** Album users to add */ albumUsers: AlbumUserAddDto[]; }; export type ApiKeyResponseDto = { + /** Creation date */ createdAt: string; + /** API key ID */ id: string; + /** API key name */ name: string; + /** List of permissions */ permissions: Permission[]; + /** Last update date */ updatedAt: string; }; export type ApiKeyCreateDto = { + /** API key name */ name?: string; + /** List of permissions */ permissions: Permission[]; }; export type ApiKeyCreateResponseDto = { apiKey: ApiKeyResponseDto; + /** API key secret (only shown once) */ secret: string; }; export type ApiKeyUpdateDto = { + /** API key name */ name?: string; + /** List of permissions */ permissions?: Permission[]; }; export type AssetBulkDeleteDto = { + /** Force delete even if in use */ force?: boolean; + /** IDs to process */ ids: string[]; }; export type AssetMetadataUpsertItemDto = { - key: AssetMetadataKey; + /** Metadata key */ + key: string; + /** Metadata value (object) */ value: object; }; export type AssetMediaCreateDto = { + /** Asset file data */ assetData: Blob; + /** Device asset ID */ deviceAssetId: string; + /** Device ID */ deviceId: string; + /** Duration (for videos) */ duration?: string; + /** File creation date */ fileCreatedAt: string; + /** File modification date */ fileModifiedAt: string; + /** Filename */ filename?: string; + /** Mark as favorite */ isFavorite?: boolean; + /** Live photo video ID */ livePhotoVideoId?: string; - metadata: AssetMetadataUpsertItemDto[]; + /** Asset metadata items */ + metadata?: AssetMetadataUpsertItemDto[]; + /** Sidecar file data */ sidecarData?: Blob; + /** Asset visibility */ visibility?: AssetVisibility; }; export type AssetMediaResponseDto = { + /** Asset media ID */ id: string; + /** Upload status */ status: AssetMediaStatus; }; export type AssetBulkUpdateDto = { + /** Original date and time */ dateTimeOriginal?: string; + /** Relative time offset in seconds */ dateTimeRelative?: number; + /** Asset description */ description?: string; + /** Duplicate ID */ duplicateId?: string | null; + /** Asset IDs to update */ ids: string[]; + /** Mark as favorite */ isFavorite?: boolean; + /** Latitude coordinate */ latitude?: number; + /** Longitude coordinate */ longitude?: number; - rating?: number; + /** Rating in range [1-5], or null for unrated */ + rating?: number | null; + /** Time zone (IANA timezone) */ timeZone?: string; + /** Asset visibility */ visibility?: AssetVisibility; }; export type AssetBulkUploadCheckItem = { - /** base64 or hex encoded sha1 hash */ + /** Base64 or hex encoded SHA1 hash */ checksum: string; + /** Asset ID */ id: string; }; export type AssetBulkUploadCheckDto = { + /** Assets to check */ assets: AssetBulkUploadCheckItem[]; }; export type AssetBulkUploadCheckResult = { + /** Upload action */ action: Action; + /** Existing asset ID if duplicate */ assetId?: string; + /** Asset ID */ id: string; + /** Whether existing asset is trashed */ isTrashed?: boolean; + /** Rejection reason if rejected */ reason?: Reason; }; export type AssetBulkUploadCheckResponseDto = { + /** Upload check results */ results: AssetBulkUploadCheckResult[]; }; export type AssetCopyDto = { + /** Copy album associations */ albums?: boolean; + /** Copy favorite status */ favorite?: boolean; + /** Copy shared links */ sharedLinks?: boolean; + /** Copy sidecar file */ sidecar?: boolean; + /** Source asset ID */ sourceId: string; + /** Copy stack association */ stack?: boolean; + /** Target asset ID */ targetId: string; }; export type CheckExistingAssetsDto = { + /** Device asset IDs to check */ deviceAssetIds: string[]; + /** Device ID */ deviceId: string; }; export type CheckExistingAssetsResponseDto = { + /** Existing asset IDs */ existingIds: string[]; }; export type AssetJobsDto = { + /** Asset IDs */ assetIds: string[]; + /** Job name */ name: AssetJobName; }; +export type AssetMetadataBulkDeleteItemDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; +}; +export type AssetMetadataBulkDeleteDto = { + /** Metadata items to delete */ + items: AssetMetadataBulkDeleteItemDto[]; +}; +export type AssetMetadataBulkUpsertItemDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; + /** Metadata value (object) */ + value: object; +}; +export type AssetMetadataBulkUpsertDto = { + /** Metadata items to upsert */ + items: AssetMetadataBulkUpsertItemDto[]; +}; +export type AssetMetadataBulkResponseDto = { + /** Asset ID */ + assetId: string; + /** Metadata key */ + key: string; + /** Last update date */ + updatedAt: string; + /** Metadata value (object) */ + value: object; +}; export type UpdateAssetDto = { + /** Original date and time */ dateTimeOriginal?: string; + /** Asset description */ description?: string; + /** Mark as favorite */ isFavorite?: boolean; + /** Latitude coordinate */ latitude?: number; + /** Live photo video ID */ livePhotoVideoId?: string | null; + /** Longitude coordinate */ longitude?: number; - rating?: number; + /** Rating in range [1-5], or null for unrated */ + rating?: number | null; + /** Asset visibility */ visibility?: AssetVisibility; }; +export type CropParameters = { + /** Height of the crop */ + height: number; + /** Width of the crop */ + width: number; + /** Top-Left X coordinate of crop */ + x: number; + /** Top-Left Y coordinate of crop */ + y: number; +}; +export type RotateParameters = { + /** Rotation angle in degrees */ + angle: number; +}; +export type MirrorParameters = { + /** Axis to mirror along */ + axis: MirrorAxis; +}; +export type AssetEditActionItemResponseDto = { + /** Type of edit action to perform */ + action: AssetEditAction; + id: string; + /** List of edit actions to apply (crop, rotate, or mirror) */ + parameters: CropParameters | RotateParameters | MirrorParameters; +}; +export type AssetEditsResponseDto = { + /** Asset ID these edits belong to */ + assetId: string; + /** List of edit actions applied to the asset */ + edits: AssetEditActionItemResponseDto[]; +}; +export type AssetEditActionItemDto = { + /** Type of edit action to perform */ + action: AssetEditAction; + /** List of edit actions to apply (crop, rotate, or mirror) */ + parameters: CropParameters | RotateParameters | MirrorParameters; +}; +export type AssetEditsCreateDto = { + /** List of edit actions to apply (crop, rotate, or mirror) */ + edits: AssetEditActionItemDto[]; +}; export type AssetMetadataResponseDto = { - key: AssetMetadataKey; + /** Metadata key */ + key: string; + /** Last update date */ updatedAt: string; + /** Metadata value (object) */ value: object; }; export type AssetMetadataUpsertDto = { + /** Metadata items to upsert */ items: AssetMetadataUpsertItemDto[]; }; export type AssetOcrResponseDto = { @@ -588,136 +1029,223 @@ export type AssetOcrResponseDto = { y4: number; }; export type AssetMediaReplaceDto = { + /** Asset file data */ assetData: Blob; + /** Device asset ID */ deviceAssetId: string; + /** Device ID */ deviceId: string; + /** Duration (for videos) */ duration?: string; + /** File creation date */ fileCreatedAt: string; + /** File modification date */ fileModifiedAt: string; + /** Filename */ filename?: string; }; export type SignUpDto = { + /** User email */ email: string; + /** User name */ name: string; + /** User password */ password: string; }; export type ChangePasswordDto = { + /** Invalidate all other sessions */ invalidateSessions?: boolean; + /** New password (min 8 characters) */ newPassword: string; + /** Current password */ password: string; }; export type LoginCredentialDto = { + /** User email */ email: string; + /** User password */ password: string; }; export type LoginResponseDto = { + /** Access token */ accessToken: string; + /** Is admin user */ isAdmin: boolean; + /** Is onboarded */ isOnboarded: boolean; + /** User name */ name: string; + /** Profile image path */ profileImagePath: string; + /** Should change password */ shouldChangePassword: boolean; + /** User email */ userEmail: string; + /** User ID */ userId: string; }; export type LogoutResponseDto = { + /** Redirect URI */ redirectUri: string; + /** Logout successful */ successful: boolean; }; export type PinCodeResetDto = { + /** User password (required if PIN code is not provided) */ password?: string; + /** New PIN code (4-6 digits) */ pinCode?: string; }; export type PinCodeSetupDto = { + /** PIN code (4-6 digits) */ pinCode: string; }; export type PinCodeChangeDto = { + /** New PIN code (4-6 digits) */ newPinCode: string; + /** User password (required if PIN code is not provided) */ password?: string; + /** New PIN code (4-6 digits) */ pinCode?: string; }; export type SessionUnlockDto = { + /** User password (required if PIN code is not provided) */ password?: string; + /** New PIN code (4-6 digits) */ pinCode?: string; }; export type AuthStatusResponseDto = { + /** Session expiration date */ expiresAt?: string; + /** Is elevated session */ isElevated: boolean; + /** Has password set */ password: boolean; + /** Has PIN code set */ pinCode: boolean; + /** PIN expiration date */ pinExpiresAt?: string; }; export type ValidateAccessTokenResponseDto = { + /** Authentication status */ authStatus: boolean; }; -export type AssetIdsDto = { +export type DownloadArchiveDto = { + /** Asset IDs */ assetIds: string[]; + /** Download edited asset if available */ + edited?: boolean; }; export type DownloadInfoDto = { + /** Album ID to download */ albumId?: string; + /** Archive size limit in bytes */ archiveSize?: number; + /** Asset IDs to download */ assetIds?: string[]; + /** User ID to download assets from */ userId?: string; }; export type DownloadArchiveInfo = { + /** Asset IDs in this archive */ assetIds: string[]; + /** Archive size in bytes */ size: number; }; export type DownloadResponseDto = { + /** Archive information */ archives: DownloadArchiveInfo[]; + /** Total size in bytes */ totalSize: number; }; export type DuplicateResponseDto = { + /** Duplicate assets */ assets: AssetResponseDto[]; + /** Duplicate group ID */ duplicateId: string; }; export type PersonResponseDto = { + /** Person date of birth */ birthDate: string | null; + /** Person color (hex) */ color?: string; + /** Person ID */ id: string; + /** Is favorite */ isFavorite?: boolean; + /** Is hidden */ isHidden: boolean; + /** Person name */ name: string; + /** Thumbnail path */ thumbnailPath: string; + /** Last update date */ updatedAt?: string; }; export type AssetFaceResponseDto = { + /** Bounding box X1 coordinate */ boundingBoxX1: number; + /** Bounding box X2 coordinate */ boundingBoxX2: number; + /** Bounding box Y1 coordinate */ boundingBoxY1: number; + /** Bounding box Y2 coordinate */ boundingBoxY2: number; + /** Face ID */ id: string; + /** Image height in pixels */ imageHeight: number; + /** Image width in pixels */ imageWidth: number; + /** Person associated with face */ person: (PersonResponseDto) | null; + /** Face detection source type */ sourceType?: SourceType; }; export type AssetFaceCreateDto = { + /** Asset ID */ assetId: string; + /** Face bounding box height */ height: number; + /** Image height in pixels */ imageHeight: number; + /** Image width in pixels */ imageWidth: number; + /** Person ID */ personId: string; + /** Face bounding box width */ width: number; + /** Face bounding box X coordinate */ x: number; + /** Face bounding box Y coordinate */ y: number; }; export type AssetFaceDeleteDto = { + /** Force delete even if person has other faces */ force: boolean; }; export type FaceDto = { + /** Face ID */ id: string; }; export type QueueStatisticsDto = { + /** Number of active jobs */ active: number; + /** Number of completed jobs */ completed: number; + /** Number of delayed jobs */ delayed: number; + /** Number of failed jobs */ failed: number; + /** Number of paused jobs */ paused: number; + /** Number of waiting jobs */ waiting: number; }; export type QueueStatusLegacyDto = { + /** Whether the queue is currently active (has running jobs) */ isActive: boolean; + /** Whether the queue is paused */ isPaused: boolean; }; export type QueueResponseLegacyDto = { @@ -728,6 +1256,7 @@ export type QueuesResponseLegacyDto = { backgroundTask: QueueResponseLegacyDto; backupDatabase: QueueResponseLegacyDto; duplicateDetection: QueueResponseLegacyDto; + editor: QueueResponseLegacyDto; faceDetection: QueueResponseLegacyDto; facialRecognition: QueueResponseLegacyDto; library: QueueResponseLegacyDto; @@ -744,234 +1273,363 @@ export type QueuesResponseLegacyDto = { workflow: QueueResponseLegacyDto; }; export type JobCreateDto = { + /** Job name */ name: ManualJobName; }; export type QueueCommandDto = { + /** Queue command to execute */ command: QueueCommand; + /** Force the command execution (if applicable) */ force?: boolean; }; export type LibraryResponseDto = { + /** Number of assets */ assetCount: number; + /** Creation date */ createdAt: string; + /** Exclusion patterns */ exclusionPatterns: string[]; + /** Library ID */ id: string; + /** Import paths */ importPaths: string[]; + /** Library name */ name: string; + /** Owner user ID */ ownerId: string; + /** Last refresh date */ refreshedAt: string | null; + /** Last update date */ updatedAt: string; }; export type CreateLibraryDto = { + /** Exclusion patterns (max 128) */ exclusionPatterns?: string[]; + /** Import paths (max 128) */ importPaths?: string[]; + /** Library name */ name?: string; + /** Owner user ID */ ownerId: string; }; export type UpdateLibraryDto = { + /** Exclusion patterns (max 128) */ exclusionPatterns?: string[]; + /** Import paths (max 128) */ importPaths?: string[]; + /** Library name */ name?: string; }; export type LibraryStatsResponseDto = { + /** Number of photos */ photos: number; + /** Total number of assets */ total: number; + /** Storage usage in bytes */ usage: number; + /** Number of videos */ videos: number; }; export type ValidateLibraryDto = { + /** Exclusion patterns (max 128) */ exclusionPatterns?: string[]; + /** Import paths to validate (max 128) */ importPaths?: string[]; }; export type ValidateLibraryImportPathResponseDto = { + /** Import path */ importPath: string; + /** Is valid */ isValid: boolean; + /** Validation message */ message?: string; }; export type ValidateLibraryResponseDto = { + /** Validation results for import paths */ importPaths?: ValidateLibraryImportPathResponseDto[]; }; export type MapMarkerResponseDto = { + /** City name */ city: string | null; + /** Country name */ country: string | null; + /** Asset ID */ id: string; + /** Latitude */ lat: number; + /** Longitude */ lon: number; + /** State/Province name */ state: string | null; }; export type MapReverseGeocodeResponseDto = { + /** City name */ city: string | null; + /** Country name */ country: string | null; + /** State/Province name */ state: string | null; }; export type OnThisDayDto = { + /** Year for on this day memory */ year: number; }; export type MemoryResponseDto = { assets: AssetResponseDto[]; + /** Creation date */ createdAt: string; data: OnThisDayDto; + /** Deletion date */ deletedAt?: string; + /** Date when memory should be hidden */ hideAt?: string; + /** Memory ID */ id: string; + /** Is memory saved */ isSaved: boolean; + /** Memory date */ memoryAt: string; + /** Owner user ID */ ownerId: string; + /** Date when memory was seen */ seenAt?: string; + /** Date when memory should be shown */ showAt?: string; + /** Memory type */ "type": MemoryType; + /** Last update date */ updatedAt: string; }; export type MemoryCreateDto = { + /** Asset IDs to associate with memory */ assetIds?: string[]; data: OnThisDayDto; + /** Date when memory should be hidden */ + hideAt?: string; + /** Is memory saved */ isSaved?: boolean; + /** Memory date */ memoryAt: string; + /** Date when memory was seen */ seenAt?: string; + /** Date when memory should be shown */ + showAt?: string; + /** Memory type */ "type": MemoryType; }; export type MemoryStatisticsResponseDto = { + /** Total number of memories */ total: number; }; export type MemoryUpdateDto = { + /** Is memory saved */ isSaved?: boolean; + /** Memory date */ memoryAt?: string; + /** Date when memory was seen */ seenAt?: string; }; export type NotificationDeleteAllDto = { + /** Notification IDs to delete */ ids: string[]; }; export type NotificationUpdateAllDto = { + /** Notification IDs to update */ ids: string[]; + /** Date when notifications were read */ readAt?: string | null; }; export type NotificationUpdateDto = { + /** Date when notification was read */ readAt?: string | null; }; export type OAuthConfigDto = { + /** OAuth code challenge (PKCE) */ codeChallenge?: string; + /** OAuth redirect URI */ redirectUri: string; + /** OAuth state parameter */ state?: string; }; export type OAuthAuthorizeResponseDto = { + /** OAuth authorization URL */ url: string; }; export type OAuthCallbackDto = { + /** OAuth code verifier (PKCE) */ codeVerifier?: string; + /** OAuth state parameter */ state?: string; + /** OAuth callback URL */ url: string; }; export type PartnerResponseDto = { + /** Avatar color */ avatarColor: UserAvatarColor; + /** User email */ email: string; + /** User ID */ id: string; + /** Show in timeline */ inTimeline?: boolean; + /** User name */ name: string; + /** Profile change date */ profileChangedAt: string; + /** Profile image path */ profileImagePath: string; }; export type PartnerCreateDto = { + /** User ID to share with */ sharedWithId: string; }; export type PartnerUpdateDto = { + /** Show partner assets in timeline */ inTimeline: boolean; }; export type PeopleResponseDto = { + /** Whether there are more pages */ hasNextPage?: boolean; + /** Number of hidden people */ hidden: number; + /** List of people */ people: PersonResponseDto[]; + /** Total number of people */ total: number; }; export type PersonCreateDto = { - /** Person date of birth. - Note: the mobile app cannot currently set the birth date to null. */ + /** Person date of birth */ birthDate?: string | null; + /** Person color (hex) */ color?: string | null; + /** Mark as favorite */ isFavorite?: boolean; - /** Person visibility */ + /** Person visibility (hidden) */ isHidden?: boolean; - /** Person name. */ + /** Person name */ name?: string; }; export type PeopleUpdateItem = { - /** Person date of birth. - Note: the mobile app cannot currently set the birth date to null. */ + /** Person date of birth */ birthDate?: string | null; + /** Person color (hex) */ color?: string | null; - /** Asset is used to get the feature face thumbnail. */ + /** Asset ID used for feature face thumbnail */ featureFaceAssetId?: string; - /** Person id. */ + /** Person ID */ id: string; + /** Mark as favorite */ isFavorite?: boolean; - /** Person visibility */ + /** Person visibility (hidden) */ isHidden?: boolean; - /** Person name. */ + /** Person name */ name?: string; }; export type PeopleUpdateDto = { + /** People to update */ people: PeopleUpdateItem[]; }; export type PersonUpdateDto = { - /** Person date of birth. - Note: the mobile app cannot currently set the birth date to null. */ + /** Person date of birth */ birthDate?: string | null; + /** Person color (hex) */ color?: string | null; - /** Asset is used to get the feature face thumbnail. */ + /** Asset ID used for feature face thumbnail */ featureFaceAssetId?: string; + /** Mark as favorite */ isFavorite?: boolean; - /** Person visibility */ + /** Person visibility (hidden) */ isHidden?: boolean; - /** Person name. */ + /** Person name */ name?: string; }; export type MergePersonDto = { + /** Person IDs to merge */ ids: string[]; }; export type AssetFaceUpdateItem = { + /** Asset ID */ assetId: string; + /** Person ID */ personId: string; }; export type AssetFaceUpdateDto = { + /** Face update items */ data: AssetFaceUpdateItem[]; }; export type PersonStatisticsResponseDto = { + /** Number of assets */ assets: number; }; export type PluginActionResponseDto = { + /** Action description */ description: string; + /** Action ID */ id: string; + /** Method name */ methodName: string; + /** Plugin ID */ pluginId: string; + /** Action schema */ schema: object | null; - supportedContexts: PluginContext[]; + /** Supported contexts */ + supportedContexts: PluginContextType[]; + /** Action title */ title: string; }; export type PluginFilterResponseDto = { + /** Filter description */ description: string; + /** Filter ID */ id: string; + /** Method name */ methodName: string; + /** Plugin ID */ pluginId: string; + /** Filter schema */ schema: object | null; - supportedContexts: PluginContext[]; + /** Supported contexts */ + supportedContexts: PluginContextType[]; + /** Filter title */ title: string; }; export type PluginResponseDto = { + /** Plugin actions */ actions: PluginActionResponseDto[]; + /** Plugin author */ author: string; + /** Creation date */ createdAt: string; + /** Plugin description */ description: string; + /** Plugin filters */ filters: PluginFilterResponseDto[]; + /** Plugin ID */ id: string; + /** Plugin name */ name: string; + /** Plugin title */ title: string; + /** Last update date */ updatedAt: string; + /** Plugin version */ version: string; }; +export type PluginTriggerResponseDto = { + /** Context type */ + contextType: PluginContextType; + /** Trigger type */ + "type": PluginTriggerType; +}; export type QueueResponseDto = { + /** Whether the queue is paused */ isPaused: boolean; + /** Queue name */ name: QueueName; statistics: QueueStatisticsDto; }; export type QueueUpdateDto = { + /** Whether to pause the queue */ isPaused?: boolean; }; export type QueueDeleteDto = { @@ -979,84 +1637,143 @@ export type QueueDeleteDto = { failed?: boolean; }; export type QueueJobResponseDto = { + /** Job data payload */ data: object; + /** Job ID */ id?: string; + /** Job name */ name: JobName; + /** Job creation timestamp */ timestamp: number; }; export type SearchExploreItem = { data: AssetResponseDto; + /** Explore value */ value: string; }; export type SearchExploreResponseDto = { + /** Explore field name */ fieldName: string; items: SearchExploreItem[]; }; export type MetadataSearchDto = { + /** Filter by album IDs */ albumIds?: string[]; + /** Filter by file checksum */ checksum?: string; + /** Filter by city name */ city?: string | null; + /** Filter by country name */ country?: string | null; + /** Filter by creation date (after) */ createdAfter?: string; + /** Filter by creation date (before) */ createdBefore?: string; + /** Filter by description text */ description?: string; + /** Filter by device asset ID */ deviceAssetId?: string; + /** Device ID to filter by */ deviceId?: string; + /** Filter by encoded video file path */ encodedVideoPath?: string; + /** Filter by asset ID */ id?: string; + /** Filter by encoded status */ isEncoded?: boolean; + /** Filter by favorite status */ isFavorite?: boolean; + /** Filter by motion photo status */ isMotion?: boolean; + /** Filter assets not in any album */ isNotInAlbum?: boolean; + /** Filter by offline status */ isOffline?: boolean; + /** Filter by lens model */ lensModel?: string | null; + /** Library ID to filter by */ libraryId?: string | null; + /** Filter by camera make */ make?: string; + /** Filter by camera model */ model?: string | null; + /** Filter by OCR text content */ ocr?: string; + /** Sort order */ order?: AssetOrder; + /** Filter by original file name */ originalFileName?: string; + /** Filter by original file path */ originalPath?: string; + /** Page number */ page?: number; + /** Filter by person IDs */ personIds?: string[]; + /** Filter by preview file path */ previewPath?: string; - rating?: number; + /** Filter by rating [1-5], or null for unrated */ + rating?: number | null; + /** Number of results to return */ size?: number; + /** Filter by state/province name */ state?: string | null; + /** Filter by tag IDs */ tagIds?: string[] | null; + /** Filter by taken date (after) */ takenAfter?: string; + /** Filter by taken date (before) */ takenBefore?: string; + /** Filter by thumbnail file path */ thumbnailPath?: string; + /** Filter by trash date (after) */ trashedAfter?: string; + /** Filter by trash date (before) */ trashedBefore?: string; + /** Asset type filter */ "type"?: AssetTypeEnum; + /** Filter by update date (after) */ updatedAfter?: string; + /** Filter by update date (before) */ updatedBefore?: string; + /** Filter by visibility */ visibility?: AssetVisibility; + /** Include deleted assets */ withDeleted?: boolean; + /** Include EXIF data in response */ withExif?: boolean; + /** Include assets with people */ withPeople?: boolean; + /** Include stacked assets */ withStacked?: boolean; }; export type SearchFacetCountResponseDto = { + /** Number of assets with this facet value */ count: number; + /** Facet value */ value: string; }; export type SearchFacetResponseDto = { + /** Facet counts */ counts: SearchFacetCountResponseDto[]; + /** Facet field name */ fieldName: string; }; export type SearchAlbumResponseDto = { + /** Number of albums in this page */ count: number; facets: SearchFacetResponseDto[]; items: AlbumResponseDto[]; + /** Total number of matching albums */ total: number; }; export type SearchAssetResponseDto = { + /** Number of assets in this page */ count: number; facets: SearchFacetResponseDto[]; items: AssetResponseDto[]; + /** Next page token */ nextPage: string | null; + /** Total number of matching assets */ total: number; }; export type SearchResponseDto = { @@ -1064,189 +1781,351 @@ export type SearchResponseDto = { assets: SearchAssetResponseDto; }; export type PlacesResponseDto = { + /** Administrative level 1 name (state/province) */ admin1name?: string; + /** Administrative level 2 name (county/district) */ admin2name?: string; + /** Latitude coordinate */ latitude: number; + /** Longitude coordinate */ longitude: number; + /** Place name */ name: string; }; export type RandomSearchDto = { + /** Filter by album IDs */ albumIds?: string[]; + /** Filter by city name */ city?: string | null; + /** Filter by country name */ country?: string | null; + /** Filter by creation date (after) */ createdAfter?: string; + /** Filter by creation date (before) */ createdBefore?: string; + /** Device ID to filter by */ deviceId?: string; + /** Filter by encoded status */ isEncoded?: boolean; + /** Filter by favorite status */ isFavorite?: boolean; + /** Filter by motion photo status */ isMotion?: boolean; + /** Filter assets not in any album */ isNotInAlbum?: boolean; + /** Filter by offline status */ isOffline?: boolean; + /** Filter by lens model */ lensModel?: string | null; + /** Library ID to filter by */ libraryId?: string | null; + /** Filter by camera make */ make?: string; + /** Filter by camera model */ model?: string | null; + /** Filter by OCR text content */ ocr?: string; + /** Filter by person IDs */ personIds?: string[]; - rating?: number; + /** Filter by rating [1-5], or null for unrated */ + rating?: number | null; + /** Number of results to return */ size?: number; + /** Filter by state/province name */ state?: string | null; + /** Filter by tag IDs */ tagIds?: string[] | null; + /** Filter by taken date (after) */ takenAfter?: string; + /** Filter by taken date (before) */ takenBefore?: string; + /** Filter by trash date (after) */ trashedAfter?: string; + /** Filter by trash date (before) */ trashedBefore?: string; + /** Asset type filter */ "type"?: AssetTypeEnum; + /** Filter by update date (after) */ updatedAfter?: string; + /** Filter by update date (before) */ updatedBefore?: string; + /** Filter by visibility */ visibility?: AssetVisibility; + /** Include deleted assets */ withDeleted?: boolean; + /** Include EXIF data in response */ withExif?: boolean; + /** Include assets with people */ withPeople?: boolean; + /** Include stacked assets */ withStacked?: boolean; }; export type SmartSearchDto = { + /** Filter by album IDs */ albumIds?: string[]; + /** Filter by city name */ city?: string | null; + /** Filter by country name */ country?: string | null; + /** Filter by creation date (after) */ createdAfter?: string; + /** Filter by creation date (before) */ createdBefore?: string; + /** Device ID to filter by */ deviceId?: string; + /** Filter by encoded status */ isEncoded?: boolean; + /** Filter by favorite status */ isFavorite?: boolean; + /** Filter by motion photo status */ isMotion?: boolean; + /** Filter assets not in any album */ isNotInAlbum?: boolean; + /** Filter by offline status */ isOffline?: boolean; + /** Search language code */ language?: string; + /** Filter by lens model */ lensModel?: string | null; + /** Library ID to filter by */ libraryId?: string | null; + /** Filter by camera make */ make?: string; + /** Filter by camera model */ model?: string | null; + /** Filter by OCR text content */ ocr?: string; + /** Page number */ page?: number; + /** Filter by person IDs */ personIds?: string[]; + /** Natural language search query */ query?: string; + /** Asset ID to use as search reference */ queryAssetId?: string; - rating?: number; + /** Filter by rating [1-5], or null for unrated */ + rating?: number | null; + /** Number of results to return */ size?: number; + /** Filter by state/province name */ state?: string | null; + /** Filter by tag IDs */ tagIds?: string[] | null; + /** Filter by taken date (after) */ takenAfter?: string; + /** Filter by taken date (before) */ takenBefore?: string; + /** Filter by trash date (after) */ trashedAfter?: string; + /** Filter by trash date (before) */ trashedBefore?: string; + /** Asset type filter */ "type"?: AssetTypeEnum; + /** Filter by update date (after) */ updatedAfter?: string; + /** Filter by update date (before) */ updatedBefore?: string; + /** Filter by visibility */ visibility?: AssetVisibility; + /** Include deleted assets */ withDeleted?: boolean; + /** Include EXIF data in response */ withExif?: boolean; }; export type StatisticsSearchDto = { + /** Filter by album IDs */ albumIds?: string[]; + /** Filter by city name */ city?: string | null; + /** Filter by country name */ country?: string | null; + /** Filter by creation date (after) */ createdAfter?: string; + /** Filter by creation date (before) */ createdBefore?: string; + /** Filter by description text */ description?: string; + /** Device ID to filter by */ deviceId?: string; + /** Filter by encoded status */ isEncoded?: boolean; + /** Filter by favorite status */ isFavorite?: boolean; + /** Filter by motion photo status */ isMotion?: boolean; + /** Filter assets not in any album */ isNotInAlbum?: boolean; + /** Filter by offline status */ isOffline?: boolean; + /** Filter by lens model */ lensModel?: string | null; + /** Library ID to filter by */ libraryId?: string | null; + /** Filter by camera make */ make?: string; + /** Filter by camera model */ model?: string | null; + /** Filter by OCR text content */ ocr?: string; + /** Filter by person IDs */ personIds?: string[]; - rating?: number; + /** Filter by rating [1-5], or null for unrated */ + rating?: number | null; + /** Filter by state/province name */ state?: string | null; + /** Filter by tag IDs */ tagIds?: string[] | null; + /** Filter by taken date (after) */ takenAfter?: string; + /** Filter by taken date (before) */ takenBefore?: string; + /** Filter by trash date (after) */ trashedAfter?: string; + /** Filter by trash date (before) */ trashedBefore?: string; + /** Asset type filter */ "type"?: AssetTypeEnum; + /** Filter by update date (after) */ updatedAfter?: string; + /** Filter by update date (before) */ updatedBefore?: string; + /** Filter by visibility */ visibility?: AssetVisibility; }; export type SearchStatisticsResponseDto = { + /** Total number of matching assets */ total: number; }; export type ServerAboutResponseDto = { + /** Build identifier */ build?: string; + /** Build image name */ buildImage?: string; + /** Build image URL */ buildImageUrl?: string; + /** Build URL */ buildUrl?: string; + /** ExifTool version */ exiftool?: string; + /** FFmpeg version */ ffmpeg?: string; + /** ImageMagick version */ imagemagick?: string; + /** libvips version */ libvips?: string; + /** Whether the server is licensed */ licensed: boolean; + /** Node.js version */ nodejs?: string; + /** Repository name */ repository?: string; + /** Repository URL */ repositoryUrl?: string; + /** Source commit hash */ sourceCommit?: string; + /** Source reference (branch/tag) */ sourceRef?: string; + /** Source URL */ sourceUrl?: string; + /** Third-party bug/feature URL */ thirdPartyBugFeatureUrl?: string; + /** Third-party documentation URL */ thirdPartyDocumentationUrl?: string; + /** Third-party source URL */ thirdPartySourceUrl?: string; + /** Third-party support URL */ thirdPartySupportUrl?: string; + /** Server version */ version: string; + /** URL to version information */ versionUrl: string; }; export type ServerApkLinksDto = { + /** APK download link for ARM64 v8a architecture */ arm64v8a: string; + /** APK download link for ARM EABI v7a architecture */ armeabiv7a: string; + /** APK download link for universal architecture */ universal: string; + /** APK download link for x86_64 architecture */ x86_64: string; }; export type ServerConfigDto = { + /** External domain URL */ externalDomain: string; + /** Whether the server has been initialized */ isInitialized: boolean; + /** Whether the admin has completed onboarding */ isOnboarded: boolean; + /** Login page message */ loginPageMessage: string; + /** Whether maintenance mode is active */ maintenanceMode: boolean; + /** Map dark style URL */ mapDarkStyleUrl: string; + /** Map light style URL */ mapLightStyleUrl: string; + /** OAuth button text */ oauthButtonText: string; + /** Whether public user registration is enabled */ publicUsers: boolean; + /** Number of days before trashed assets are permanently deleted */ trashDays: number; + /** Delay in days before deleted users are permanently removed */ userDeleteDelay: number; }; export type ServerFeaturesDto = { + /** Whether config file is available */ configFile: boolean; + /** Whether duplicate detection is enabled */ duplicateDetection: boolean; + /** Whether email notifications are enabled */ email: boolean; + /** Whether facial recognition is enabled */ facialRecognition: boolean; + /** Whether face import is enabled */ importFaces: boolean; + /** Whether map feature is enabled */ map: boolean; + /** Whether OAuth is enabled */ oauth: boolean; + /** Whether OAuth auto-launch is enabled */ oauthAutoLaunch: boolean; + /** Whether OCR is enabled */ ocr: boolean; + /** Whether password login is enabled */ passwordLogin: boolean; + /** Whether reverse geocoding is enabled */ reverseGeocoding: boolean; + /** Whether search is enabled */ search: boolean; + /** Whether sidecar files are supported */ sidecar: boolean; + /** Whether smart search is enabled */ smartSearch: boolean; + /** Whether trash feature is enabled */ trash: boolean; }; export type LicenseResponseDto = { + /** Activation date */ activatedAt: string; + /** Activation key */ activationKey: string; + /** License key (format: IM(SV|CL)(-XXXX){8}) */ licenseKey: string; }; export type LicenseKeyDto = { + /** Activation key */ activationKey: string; + /** License key (format: IM(SV|CL)(-XXXX){8}) */ licenseKey: string; }; export type ServerMediaTypesResponseDto = { + /** Supported image MIME types */ image: string[]; + /** Supported sidecar MIME types */ sidecar: string[]; + /** Supported video MIME types */ video: string[]; }; export type ServerPingResponse = {}; @@ -1254,211 +2133,348 @@ export type ServerPingResponseRead = { res: string; }; export type UsageByUserDto = { + /** Number of photos */ photos: number; + /** User quota size in bytes (null if unlimited) */ quotaSizeInBytes: number | null; + /** Total storage usage in bytes */ usage: number; + /** Storage usage for photos in bytes */ usagePhotos: number; + /** Storage usage for videos in bytes */ usageVideos: number; + /** User ID */ userId: string; + /** User name */ userName: string; + /** Number of videos */ videos: number; }; export type ServerStatsResponseDto = { + /** Total number of photos */ photos: number; + /** Total storage usage in bytes */ usage: number; usageByUser: UsageByUserDto[]; + /** Storage usage for photos in bytes */ usagePhotos: number; + /** Storage usage for videos in bytes */ usageVideos: number; + /** Total number of videos */ videos: number; }; export type ServerStorageResponseDto = { + /** Available disk space (human-readable format) */ diskAvailable: string; + /** Available disk space in bytes */ diskAvailableRaw: number; + /** Total disk size (human-readable format) */ diskSize: string; + /** Total disk size in bytes */ diskSizeRaw: number; + /** Disk usage percentage (0-100) */ diskUsagePercentage: number; + /** Used disk space (human-readable format) */ diskUse: string; + /** Used disk space in bytes */ diskUseRaw: number; }; export type ServerThemeDto = { + /** Custom CSS for theming */ customCss: string; }; export type ServerVersionResponseDto = { + /** Major version number */ major: number; + /** Minor version number */ minor: number; + /** Patch version number */ patch: number; }; export type VersionCheckStateResponseDto = { + /** Last check timestamp */ checkedAt: string | null; + /** Release version */ releaseVersion: string | null; }; export type ServerVersionHistoryResponseDto = { + /** When this version was first seen */ createdAt: string; + /** Version history entry ID */ id: string; + /** Version string */ version: string; }; export type SessionCreateDto = { + /** Device OS */ deviceOS?: string; + /** Device type */ deviceType?: string; - /** session duration, in seconds */ + /** Session duration in seconds */ duration?: number; }; export type SessionCreateResponseDto = { + /** App version */ appVersion: string | null; + /** Creation date */ createdAt: string; + /** Is current session */ current: boolean; + /** Device OS */ deviceOS: string; + /** Device type */ deviceType: string; + /** Expiration date */ expiresAt?: string; + /** Session ID */ id: string; + /** Is pending sync reset */ isPendingSyncReset: boolean; + /** Session token */ token: string; + /** Last update date */ updatedAt: string; }; export type SessionUpdateDto = { + /** Reset pending sync state */ isPendingSyncReset?: boolean; }; export type SharedLinkResponseDto = { album?: AlbumResponseDto; + /** Allow downloads */ allowDownload: boolean; + /** Allow uploads */ allowUpload: boolean; assets: AssetResponseDto[]; + /** Creation date */ createdAt: string; + /** Link description */ description: string | null; + /** Expiration date */ expiresAt: string | null; + /** Shared link ID */ id: string; + /** Encryption key (base64url) */ key: string; + /** Has password */ password: string | null; + /** Show metadata */ showMetadata: boolean; + /** Custom URL slug */ slug: string | null; + /** Access token */ token?: string | null; + /** Shared link type */ "type": SharedLinkType; + /** Owner user ID */ userId: string; }; export type SharedLinkCreateDto = { + /** Album ID (for album sharing) */ albumId?: string; + /** Allow downloads */ allowDownload?: boolean; + /** Allow uploads */ allowUpload?: boolean; + /** Asset IDs (for individual assets) */ assetIds?: string[]; + /** Link description */ description?: string | null; + /** Expiration date */ expiresAt?: string | null; + /** Link password */ password?: string | null; + /** Show metadata */ showMetadata?: boolean; + /** Custom URL slug */ slug?: string | null; + /** Shared link type */ "type": SharedLinkType; }; +export type SharedLinkLoginDto = { + /** Shared link password */ + password: string; +}; export type SharedLinkEditDto = { + /** Allow downloads */ allowDownload?: boolean; + /** Allow uploads */ allowUpload?: boolean; - /** Few clients cannot send null to set the expiryTime to never. - Setting this flag and not sending expiryAt is considered as null instead. - Clients that can send null values can ignore this. */ + /** Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this. */ changeExpiryTime?: boolean; + /** Link description */ description?: string | null; + /** Expiration date */ expiresAt?: string | null; + /** Link password */ password?: string | null; + /** Show metadata */ showMetadata?: boolean; + /** Custom URL slug */ slug?: string | null; }; +export type AssetIdsDto = { + /** Asset IDs */ + assetIds: string[]; +}; export type AssetIdsResponseDto = { + /** Asset ID */ assetId: string; + /** Error reason if failed */ error?: Error2; + /** Whether operation succeeded */ success: boolean; }; export type StackResponseDto = { + /** Stack assets */ assets: AssetResponseDto[]; + /** Stack ID */ id: string; + /** Primary asset ID */ primaryAssetId: string; }; export type StackCreateDto = { - /** first asset becomes the primary */ + /** Asset IDs (first becomes primary, min 2) */ assetIds: string[]; }; export type StackUpdateDto = { + /** Primary asset ID */ primaryAssetId?: string; }; export type SyncAckDeleteDto = { + /** Sync entity types to delete acks for */ types?: SyncEntityType[]; }; export type SyncAckDto = { + /** Acknowledgment ID */ ack: string; + /** Sync entity type */ "type": SyncEntityType; }; export type SyncAckSetDto = { + /** Acknowledgment IDs (max 1000) */ acks: string[]; }; export type AssetDeltaSyncDto = { + /** Sync assets updated after this date */ updatedAfter: string; + /** User IDs to sync */ userIds: string[]; }; export type AssetDeltaSyncResponseDto = { + /** Deleted asset IDs */ deleted: string[]; + /** Whether full sync is needed */ needsFullSync: boolean; + /** Upserted assets */ upserted: AssetResponseDto[]; }; export type AssetFullSyncDto = { + /** Last asset ID (pagination) */ lastId?: string; + /** Maximum number of assets to return */ limit: number; + /** Sync assets updated until this date */ updatedUntil: string; + /** Filter by user ID */ userId?: string; }; export type SyncStreamDto = { + /** Reset sync state */ reset?: boolean; + /** Sync request types */ types: SyncRequestType[]; }; export type DatabaseBackupConfig = { + /** Cron expression */ cronExpression: string; + /** Enabled */ enabled: boolean; + /** Keep last amount */ keepLastAmount: number; }; export type SystemConfigBackupsDto = { database: DatabaseBackupConfig; }; export type SystemConfigFFmpegDto = { + /** Transcode hardware acceleration */ accel: TranscodeHWAccel; + /** Accelerated decode */ accelDecode: boolean; + /** Accepted audio codecs */ acceptedAudioCodecs: AudioCodec[]; + /** Accepted containers */ acceptedContainers: VideoContainer[]; + /** Accepted video codecs */ acceptedVideoCodecs: VideoCodec[]; + /** B-frames */ bframes: number; + /** CQ mode */ cqMode: CQMode; + /** CRF */ crf: number; + /** GOP size */ gopSize: number; + /** Max bitrate */ maxBitrate: string; + /** Preferred hardware device */ preferredHwDevice: string; + /** Preset */ preset: string; + /** References */ refs: number; + /** Target audio codec */ targetAudioCodec: AudioCodec; + /** Target resolution */ targetResolution: string; + /** Target video codec */ targetVideoCodec: VideoCodec; + /** Temporal AQ */ temporalAQ: boolean; + /** Threads */ threads: number; + /** Tone mapping */ tonemap: ToneMapping; + /** Transcode policy */ transcode: TranscodePolicy; + /** Two pass */ twoPass: boolean; }; export type SystemConfigGeneratedFullsizeImageDto = { + /** Enabled */ enabled: boolean; + /** Image format */ format: ImageFormat; + /** Progressive */ + progressive?: boolean; + /** Quality */ quality: number; }; export type SystemConfigGeneratedImageDto = { + /** Image format */ format: ImageFormat; + progressive?: boolean; + /** Quality */ quality: number; + /** Size */ size: number; }; export type SystemConfigImageDto = { + /** Colorspace */ colorspace: Colorspace; + /** Extract embedded */ extractEmbedded: boolean; fullsize: SystemConfigGeneratedFullsizeImageDto; preview: SystemConfigGeneratedImageDto; thumbnail: SystemConfigGeneratedImageDto; }; export type JobSettingsDto = { + /** Concurrency */ concurrency: number; }; export type SystemConfigJobDto = { backgroundTask: JobSettingsDto; + editor: JobSettingsDto; faceDetection: JobSettingsDto; library: JobSettingsDto; metadataExtraction: JobSettingsDto; @@ -1474,9 +2490,11 @@ export type SystemConfigJobDto = { }; export type SystemConfigLibraryScanDto = { cronExpression: string; + /** Enabled */ enabled: boolean; }; export type SystemConfigLibraryWatchDto = { + /** Enabled */ enabled: boolean; }; export type SystemConfigLibraryDto = { @@ -1484,40 +2502,57 @@ export type SystemConfigLibraryDto = { watch: SystemConfigLibraryWatchDto; }; export type SystemConfigLoggingDto = { + /** Enabled */ enabled: boolean; level: LogLevel; }; export type MachineLearningAvailabilityChecksDto = { + /** Enabled */ enabled: boolean; interval: number; timeout: number; }; export type ClipConfig = { + /** Whether the task is enabled */ enabled: boolean; + /** Name of the model to use */ modelName: string; }; export type DuplicateDetectionConfig = { + /** Whether the task is enabled */ enabled: boolean; + /** Maximum distance threshold for duplicate detection */ maxDistance: number; }; export type FacialRecognitionConfig = { + /** Whether the task is enabled */ enabled: boolean; + /** Maximum distance threshold for face recognition */ maxDistance: number; + /** Minimum number of faces required for recognition */ minFaces: number; + /** Minimum confidence score for face detection */ minScore: number; + /** Name of the model to use */ modelName: string; }; export type OcrConfig = { + /** Whether the task is enabled */ enabled: boolean; + /** Maximum resolution for OCR processing */ maxResolution: number; + /** Minimum confidence score for text detection */ minDetectionScore: number; + /** Minimum confidence score for text recognition */ minRecognitionScore: number; + /** Name of the model to use */ modelName: string; }; export type SystemConfigMachineLearningDto = { availabilityChecks: MachineLearningAvailabilityChecksDto; clip: ClipConfig; duplicateDetection: DuplicateDetectionConfig; + /** Enabled */ enabled: boolean; facialRecognition: FacialRecognitionConfig; ocr: OcrConfig; @@ -1525,63 +2560,96 @@ export type SystemConfigMachineLearningDto = { }; export type SystemConfigMapDto = { darkStyle: string; + /** Enabled */ enabled: boolean; lightStyle: string; }; export type SystemConfigFacesDto = { + /** Import */ "import": boolean; }; export type SystemConfigMetadataDto = { faces: SystemConfigFacesDto; }; export type SystemConfigNewVersionCheckDto = { + /** Enabled */ enabled: boolean; }; export type SystemConfigNightlyTasksDto = { + /** Cluster new faces */ clusterNewFaces: boolean; + /** Database cleanup */ databaseCleanup: boolean; + /** Generate memories */ generateMemories: boolean; + /** Missing thumbnails */ missingThumbnails: boolean; startTime: string; + /** Sync quota usage */ syncQuotaUsage: boolean; }; export type SystemConfigNotificationsDto = { smtp: SystemConfigSmtpDto; }; export type SystemConfigOAuthDto = { + /** Auto launch */ autoLaunch: boolean; + /** Auto register */ autoRegister: boolean; + /** Button text */ buttonText: string; + /** Client ID */ clientId: string; + /** Client secret */ clientSecret: string; + /** Default storage quota */ defaultStorageQuota: number | null; + /** Enabled */ enabled: boolean; + /** Issuer URL */ issuerUrl: string; + /** Mobile override enabled */ mobileOverrideEnabled: boolean; + /** Mobile redirect URI */ mobileRedirectUri: string; + /** Profile signing algorithm */ profileSigningAlgorithm: string; + /** Role claim */ roleClaim: string; + /** Scope */ scope: string; signingAlgorithm: string; + /** Storage label claim */ storageLabelClaim: string; + /** Storage quota claim */ storageQuotaClaim: string; + /** Timeout */ timeout: number; + /** Token endpoint auth method */ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; }; export type SystemConfigPasswordLoginDto = { + /** Enabled */ enabled: boolean; }; export type SystemConfigReverseGeocodingDto = { + /** Enabled */ enabled: boolean; }; export type SystemConfigServerDto = { + /** External domain */ externalDomain: string; + /** Login page message */ loginPageMessage: string; + /** Public users */ publicUsers: boolean; }; export type SystemConfigStorageTemplateDto = { + /** Enabled */ enabled: boolean; + /** Hash verification enabled */ hashVerificationEnabled: boolean; + /** Template */ template: string; }; export type SystemConfigTemplateEmailsDto = { @@ -1593,13 +2661,17 @@ export type SystemConfigTemplatesDto = { email: SystemConfigTemplateEmailsDto; }; export type SystemConfigThemeDto = { + /** Custom CSS for theming */ customCss: string; }; export type SystemConfigTrashDto = { + /** Days */ days: number; + /** Enabled */ enabled: boolean; }; export type SystemConfigUserDto = { + /** Delete delay */ deleteDelay: number; }; export type SystemConfigDto = { @@ -1626,38 +2698,57 @@ export type SystemConfigDto = { user: SystemConfigUserDto; }; export type SystemConfigTemplateStorageOptionDto = { + /** Available day format options for storage template */ dayOptions: string[]; + /** Available hour format options for storage template */ hourOptions: string[]; + /** Available minute format options for storage template */ minuteOptions: string[]; + /** Available month format options for storage template */ monthOptions: string[]; + /** Available preset template options */ presetOptions: string[]; + /** Available second format options for storage template */ secondOptions: string[]; + /** Available week format options for storage template */ weekOptions: string[]; + /** Available year format options for storage template */ yearOptions: string[]; }; export type AdminOnboardingUpdateDto = { + /** Is admin onboarded */ isOnboarded: boolean; }; export type ReverseGeocodingStateResponseDto = { + /** Last import file name */ lastImportFileName: string | null; + /** Last update timestamp */ lastUpdate: string | null; }; export type TagCreateDto = { + /** Tag color (hex) */ color?: string; + /** Tag name */ name: string; + /** Parent tag ID */ parentId?: string | null; }; export type TagUpsertDto = { + /** Tag names to upsert */ tags: string[]; }; export type TagBulkAssetsDto = { + /** Asset IDs */ assetIds: string[]; + /** Tag IDs */ tagIds: string[]; }; export type TagBulkAssetsResponseDto = { + /** Number of assets tagged */ count: number; }; export type TagUpdateDto = { + /** Tag color (hex) */ color?: string | null; }; export type TimeBucketAssetResponseDto = { @@ -1667,7 +2758,7 @@ export type TimeBucketAssetResponseDto = { country: (string | null)[]; /** Array of video durations in HH:MM:SS format (null for images) */ duration: (string | null)[]; - /** Array of file creation timestamps in UTC (ISO 8601 format, without timezone) */ + /** Array of file creation timestamps in UTC */ fileCreatedAt: string[]; /** Array of asset IDs in the time bucket */ id: string[]; @@ -1705,75 +2796,492 @@ export type TimeBucketsResponseDto = { timeBucket: string; }; export type TrashResponseDto = { + /** Number of items in trash */ count: number; }; export type UserUpdateMeDto = { + /** Avatar color */ avatarColor?: (UserAvatarColor) | null; + /** User email */ email?: string; + /** User name */ name?: string; + /** User password (deprecated, use change password endpoint) */ password?: string; }; export type OnboardingResponseDto = { + /** Is user onboarded */ isOnboarded: boolean; }; export type OnboardingDto = { + /** Is user onboarded */ isOnboarded: boolean; }; export type CreateProfileImageDto = { + /** Profile image file */ file: Blob; }; export type CreateProfileImageResponseDto = { + /** Profile image change date */ profileChangedAt: string; + /** Profile image file path */ profileImagePath: string; + /** User ID */ userId: string; }; export type WorkflowActionResponseDto = { + /** Action configuration */ actionConfig: object | null; + /** Action ID */ id: string; + /** Action order */ order: number; + /** Plugin action ID */ pluginActionId: string; + /** Workflow ID */ workflowId: string; }; export type WorkflowFilterResponseDto = { + /** Filter configuration */ filterConfig: object | null; + /** Filter ID */ id: string; + /** Filter order */ order: number; + /** Plugin filter ID */ pluginFilterId: string; + /** Workflow ID */ workflowId: string; }; export type WorkflowResponseDto = { + /** Workflow actions */ actions: WorkflowActionResponseDto[]; + /** Creation date */ createdAt: string; + /** Workflow description */ description: string; + /** Workflow enabled */ enabled: boolean; + /** Workflow filters */ filters: WorkflowFilterResponseDto[]; + /** Workflow ID */ id: string; + /** Workflow name */ name: string | null; + /** Owner user ID */ ownerId: string; - triggerType: TriggerType; + /** Workflow trigger type */ + triggerType: PluginTriggerType; }; export type WorkflowActionItemDto = { + /** Action configuration */ actionConfig?: object; + /** Plugin action ID */ pluginActionId: string; }; export type WorkflowFilterItemDto = { + /** Filter configuration */ filterConfig?: object; + /** Plugin filter ID */ pluginFilterId: string; }; export type WorkflowCreateDto = { + /** Workflow actions */ actions: WorkflowActionItemDto[]; + /** Workflow description */ description?: string; + /** Workflow enabled */ enabled?: boolean; + /** Workflow filters */ filters: WorkflowFilterItemDto[]; + /** Workflow name */ name: string; + /** Workflow trigger type */ triggerType: PluginTriggerType; }; export type WorkflowUpdateDto = { + /** Workflow actions */ actions?: WorkflowActionItemDto[]; + /** Workflow description */ description?: string; + /** Workflow enabled */ enabled?: boolean; + /** Workflow filters */ filters?: WorkflowFilterItemDto[]; + /** Workflow name */ name?: string; + /** Workflow trigger type */ + triggerType?: PluginTriggerType; +}; +export type SyncAckV1 = {}; +export type SyncAlbumDeleteV1 = { + /** Album ID */ + albumId: string; +}; +export type SyncAlbumToAssetDeleteV1 = { + /** Album ID */ + albumId: string; + /** Asset ID */ + assetId: string; +}; +export type SyncAlbumToAssetV1 = { + /** Album ID */ + albumId: string; + /** Asset ID */ + assetId: string; +}; +export type SyncAlbumUserDeleteV1 = { + /** Album ID */ + albumId: string; + /** User ID */ + userId: string; +}; +export type SyncAlbumUserV1 = { + /** Album ID */ + albumId: string; + /** Album user role */ + role: AlbumUserRole; + /** User ID */ + userId: string; +}; +export type SyncAlbumV1 = { + /** Created at */ + createdAt: string; + /** Album description */ + description: string; + /** Album ID */ + id: string; + /** Is activity enabled */ + isActivityEnabled: boolean; + /** Album name */ + name: string; + order: AssetOrder; + /** Owner ID */ + ownerId: string; + /** Thumbnail asset ID */ + thumbnailAssetId: string | null; + /** Updated at */ + updatedAt: string; +}; +export type SyncAssetDeleteV1 = { + /** Asset ID */ + assetId: string; +}; +export type SyncAssetEditDeleteV1 = { + editId: string; +}; +export type SyncAssetEditV1 = { + action: AssetEditAction; + assetId: string; + id: string; + parameters: object; + sequence: number; +}; +export type SyncAssetExifV1 = { + /** Asset ID */ + assetId: string; + /** City */ + city: string | null; + /** Country */ + country: string | null; + /** Date time original */ + dateTimeOriginal: string | null; + /** Description */ + description: string | null; + /** Exif image height */ + exifImageHeight: number | null; + /** Exif image width */ + exifImageWidth: number | null; + /** Exposure time */ + exposureTime: string | null; + /** F number */ + fNumber: number | null; + /** File size in byte */ + fileSizeInByte: number | null; + /** Focal length */ + focalLength: number | null; + /** FPS */ + fps: number | null; + /** ISO */ + iso: number | null; + /** Latitude */ + latitude: number | null; + /** Lens model */ + lensModel: string | null; + /** Longitude */ + longitude: number | null; + /** Make */ + make: string | null; + /** Model */ + model: string | null; + /** Modify date */ + modifyDate: string | null; + /** Orientation */ + orientation: string | null; + /** Profile description */ + profileDescription: string | null; + /** Projection type */ + projectionType: string | null; + /** Rating */ + rating: number | null; + /** State */ + state: string | null; + /** Time zone */ + timeZone: string | null; +}; +export type SyncAssetFaceDeleteV1 = { + /** Asset face ID */ + assetFaceId: string; +}; +export type SyncAssetFaceV1 = { + /** Asset ID */ + assetId: string; + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + /** Asset face ID */ + id: string; + imageHeight: number; + imageWidth: number; + /** Person ID */ + personId: string | null; + /** Source type */ + sourceType: string; +}; +export type SyncAssetFaceV2 = { + /** Asset ID */ + assetId: string; + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + /** Face deleted at */ + deletedAt: string | null; + /** Asset face ID */ + id: string; + imageHeight: number; + imageWidth: number; + /** Is the face visible in the asset */ + isVisible: boolean; + /** Person ID */ + personId: string | null; + /** Source type */ + sourceType: string; +}; +export type SyncAssetMetadataDeleteV1 = { + /** Asset ID */ + assetId: string; + /** Key */ + key: string; +}; +export type SyncAssetMetadataV1 = { + /** Asset ID */ + assetId: string; + /** Key */ + key: string; + /** Value */ + value: object; +}; +export type SyncAssetV1 = { + /** Checksum */ + checksum: string; + /** Deleted at */ + deletedAt: string | null; + /** Duration */ + duration: string | null; + /** File created at */ + fileCreatedAt: string | null; + /** File modified at */ + fileModifiedAt: string | null; + /** Asset height */ + height: number | null; + /** Asset ID */ + id: string; + /** Is edited */ + isEdited: boolean; + /** Is favorite */ + isFavorite: boolean; + /** Library ID */ + libraryId: string | null; + /** Live photo video ID */ + livePhotoVideoId: string | null; + /** Local date time */ + localDateTime: string | null; + /** Original file name */ + originalFileName: string; + /** Owner ID */ + ownerId: string; + /** Stack ID */ + stackId: string | null; + /** Thumbhash */ + thumbhash: string | null; + /** Asset type */ + "type": AssetTypeEnum; + /** Asset visibility */ + visibility: AssetVisibility; + /** Asset width */ + width: number | null; +}; +export type SyncAuthUserV1 = { + /** User avatar color */ + avatarColor: (UserAvatarColor) | null; + /** User deleted at */ + deletedAt: string | null; + /** User email */ + email: string; + /** User has profile image */ + hasProfileImage: boolean; + /** User ID */ + id: string; + /** User is admin */ + isAdmin: boolean; + /** User name */ + name: string; + /** User OAuth ID */ + oauthId: string; + /** User pin code */ + pinCode: string | null; + /** User profile changed at */ + profileChangedAt: string; + quotaSizeInBytes: number | null; + quotaUsageInBytes: number; + /** User storage label */ + storageLabel: string | null; +}; +export type SyncCompleteV1 = {}; +export type SyncMemoryAssetDeleteV1 = { + /** Asset ID */ + assetId: string; + /** Memory ID */ + memoryId: string; +}; +export type SyncMemoryAssetV1 = { + /** Asset ID */ + assetId: string; + /** Memory ID */ + memoryId: string; +}; +export type SyncMemoryDeleteV1 = { + /** Memory ID */ + memoryId: string; +}; +export type SyncMemoryV1 = { + /** Created at */ + createdAt: string; + /** Data */ + data: object; + /** Deleted at */ + deletedAt: string | null; + /** Hide at */ + hideAt: string | null; + /** Memory ID */ + id: string; + /** Is saved */ + isSaved: boolean; + /** Memory at */ + memoryAt: string; + /** Owner ID */ + ownerId: string; + /** Seen at */ + seenAt: string | null; + /** Show at */ + showAt: string | null; + /** Memory type */ + "type": MemoryType; + /** Updated at */ + updatedAt: string; +}; +export type SyncPartnerDeleteV1 = { + /** Shared by ID */ + sharedById: string; + /** Shared with ID */ + sharedWithId: string; +}; +export type SyncPartnerV1 = { + /** In timeline */ + inTimeline: boolean; + /** Shared by ID */ + sharedById: string; + /** Shared with ID */ + sharedWithId: string; +}; +export type SyncPersonDeleteV1 = { + /** Person ID */ + personId: string; +}; +export type SyncPersonV1 = { + /** Birth date */ + birthDate: string | null; + /** Color */ + color: string | null; + /** Created at */ + createdAt: string; + /** Face asset ID */ + faceAssetId: string | null; + /** Person ID */ + id: string; + /** Is favorite */ + isFavorite: boolean; + /** Is hidden */ + isHidden: boolean; + /** Person name */ + name: string; + /** Owner ID */ + ownerId: string; + /** Updated at */ + updatedAt: string; +}; +export type SyncResetV1 = {}; +export type SyncStackDeleteV1 = { + /** Stack ID */ + stackId: string; +}; +export type SyncStackV1 = { + /** Created at */ + createdAt: string; + /** Stack ID */ + id: string; + /** Owner ID */ + ownerId: string; + /** Primary asset ID */ + primaryAssetId: string; + /** Updated at */ + updatedAt: string; +}; +export type SyncUserDeleteV1 = { + /** User ID */ + userId: string; +}; +export type SyncUserMetadataDeleteV1 = { + /** User metadata key */ + key: UserMetadataKey; + /** User ID */ + userId: string; +}; +export type SyncUserMetadataV1 = { + /** User metadata key */ + key: UserMetadataKey; + /** User ID */ + userId: string; + /** User metadata value */ + value: object; +}; +export type SyncUserV1 = { + /** User avatar color */ + avatarColor: (UserAvatarColor) | null; + /** User deleted at */ + deletedAt: string | null; + /** User email */ + email: string; + /** User has profile image */ + hasProfileImage: boolean; + /** User ID */ + id: string; + /** User name */ + name: string; + /** User profile changed at */ + profileChangedAt: string; }; /** * List all activities @@ -1850,6 +3358,63 @@ export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { method: "POST" })); } +/** + * Delete database backup + */ +export function deleteDatabaseBackup({ databaseBackupDeleteDto }: { + databaseBackupDeleteDto: DatabaseBackupDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups", oazapfts.json({ + ...opts, + method: "DELETE", + body: databaseBackupDeleteDto + }))); +} +/** + * List database backups + */ +export function listDatabaseBackups(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: DatabaseBackupListResponseDto; + }>("/admin/database-backups", { + ...opts + })); +} +/** + * Start database backup restore flow + */ +export function startDatabaseRestoreFlow(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups/start-restore", { + ...opts, + method: "POST" + })); +} +/** + * Upload database backup + */ +export function uploadDatabaseBackup({ databaseBackupUploadDto }: { + databaseBackupUploadDto: DatabaseBackupUploadDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/database-backups/upload", oazapfts.multipart({ + ...opts, + method: "POST", + body: databaseBackupUploadDto + }))); +} +/** + * Download database backup + */ +export function downloadDatabaseBackup({ filename }: { + filename: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchBlob<{ + status: 200; + data: Blob; + }>(`/admin/database-backups/${encodeURIComponent(filename)}`, { + ...opts + })); +} /** * Set maintenance mode */ @@ -1862,6 +3427,17 @@ export function setMaintenanceMode({ setMaintenanceModeDto }: { body: setMaintenanceModeDto }))); } +/** + * Detect existing install + */ +export function detectPriorInstall(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceDetectInstallResponseDto; + }>("/admin/maintenance/detect-install", { + ...opts + })); +} /** * Log into maintenance mode */ @@ -1877,6 +3453,17 @@ export function maintenanceLogin({ maintenanceLoginDto }: { body: maintenanceLoginDto }))); } +/** + * Get maintenance mode status + */ +export function getMaintenanceStatus(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceStatusResponseDto; + }>("/admin/maintenance/status", { + ...opts + })); +} /** * Create a notification */ @@ -2364,6 +3951,9 @@ export function uploadAsset({ key, slug, xImmichChecksum, assetMediaCreateDto }: assetMediaCreateDto: AssetMediaCreateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetMediaResponseDto; + } | { status: 201; data: AssetMediaResponseDto; }>(`/assets${QS.query(QS.explode({ @@ -2457,6 +4047,33 @@ export function runAssetJobs({ assetJobsDto }: { body: assetJobsDto }))); } +/** + * Delete asset metadata + */ +export function deleteBulkAssetMetadata({ assetMetadataBulkDeleteDto }: { + assetMetadataBulkDeleteDto: AssetMetadataBulkDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/assets/metadata", oazapfts.json({ + ...opts, + method: "DELETE", + body: assetMetadataBulkDeleteDto + }))); +} +/** + * Upsert asset metadata + */ +export function updateBulkAssetMetadata({ assetMetadataBulkUpsertDto }: { + assetMetadataBulkUpsertDto: AssetMetadataBulkUpsertDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetMetadataBulkResponseDto[]; + }>("/assets/metadata", oazapfts.json({ + ...opts, + method: "PUT", + body: assetMetadataBulkUpsertDto + }))); +} /** * Get random assets */ @@ -2525,6 +4142,46 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } +/** + * Remove edits from an existing asset + */ +export function removeAssetEdits({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/edits`, { + ...opts, + method: "DELETE" + })); +} +/** + * Retrieve edits for an existing asset + */ +export function getAssetEdits({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetEditsResponseDto; + }>(`/assets/${encodeURIComponent(id)}/edits`, { + ...opts + })); +} +/** + * Apply edits to an existing asset + */ +export function editAsset({ id, assetEditsCreateDto }: { + id: string; + assetEditsCreateDto: AssetEditsCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetEditsResponseDto; + }>(`/assets/${encodeURIComponent(id)}/edits`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetEditsCreateDto + }))); +} /** * Get asset metadata */ @@ -2559,7 +4216,7 @@ export function updateAssetMetadata({ id, assetMetadataUpsertDto }: { */ export function deleteAssetMetadata({ id, key }: { id: string; - key: AssetMetadataKey; + key: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/metadata/${encodeURIComponent(key)}`, { ...opts, @@ -2571,7 +4228,7 @@ export function deleteAssetMetadata({ id, key }: { */ export function getAssetMetadataByKey({ id, key }: { id: string; - key: AssetMetadataKey; + key: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2596,7 +4253,8 @@ export function getAssetOcr({ id }: { /** * Download original asset */ -export function downloadAsset({ id, key, slug }: { +export function downloadAsset({ edited, id, key, slug }: { + edited?: boolean; id: string; key?: string; slug?: string; @@ -2605,6 +4263,7 @@ export function downloadAsset({ id, key, slug }: { status: 200; data: Blob; }>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({ + edited, key, slug }))}`, { @@ -2635,7 +4294,8 @@ export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: { /** * View asset thumbnail */ -export function viewAsset({ id, key, size, slug }: { +export function viewAsset({ edited, id, key, size, slug }: { + edited?: boolean; id: string; key?: string; size?: AssetMediaSize; @@ -2645,6 +4305,7 @@ export function viewAsset({ id, key, size, slug }: { status: 200; data: Blob; }>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({ + edited, key, size, slug @@ -2831,10 +4492,10 @@ export function validateAccessToken(opts?: Oazapfts.RequestOpts) { /** * Download asset archive */ -export function downloadArchive({ key, slug, assetIdsDto }: { +export function downloadArchive({ key, slug, downloadArchiveDto }: { key?: string; slug?: string; - assetIdsDto: AssetIdsDto; + downloadArchiveDto: DownloadArchiveDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchBlob<{ status: 200; @@ -2845,7 +4506,7 @@ export function downloadArchive({ key, slug, assetIdsDto }: { }))}`, oazapfts.json({ ...opts, method: "POST", - body: assetIdsDto + body: downloadArchiveDto }))); } /** @@ -3677,6 +5338,17 @@ export function getPlugins(opts?: Oazapfts.RequestOpts) { ...opts })); } +/** + * List all plugin triggers + */ +export function getPluginTriggers(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PluginTriggerResponseDto[]; + }>("/plugins/triggers", { + ...opts + })); +} /** * Retrieve a plugin */ @@ -3803,7 +5475,7 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat model?: string | null; ocr?: string; personIds?: string[]; - rating?: number; + rating?: number | null; size?: number; state?: string | null; tagIds?: string[] | null; @@ -4223,14 +5895,16 @@ export function lockSession({ id }: { /** * Retrieve all shared links */ -export function getAllSharedLinks({ albumId }: { +export function getAllSharedLinks({ albumId, id }: { albumId?: string; + id?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: SharedLinkResponseDto[]; }>(`/shared-links${QS.query(QS.explode({ - albumId + albumId, + id }))}`, { ...opts })); @@ -4250,6 +5924,26 @@ export function createSharedLink({ sharedLinkCreateDto }: { body: sharedLinkCreateDto }))); } +/** + * Shared link login + */ +export function sharedLinkLogin({ key, slug, sharedLinkLoginDto }: { + key?: string; + slug?: string; + sharedLinkLoginDto: SharedLinkLoginDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: SharedLinkResponseDto; + }>(`/shared-links/login${QS.query(QS.explode({ + key, + slug + }))}`, oazapfts.json({ + ...opts, + method: "POST", + body: sharedLinkLoginDto + }))); +} /** * Retrieve current shared link */ @@ -4748,8 +6442,9 @@ export function tagAssets({ id, bulkIdsDto }: { /** * Get time bucket */ -export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: { +export function getTimeBucket({ albumId, bbox, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: { albumId?: string; + bbox?: string; isFavorite?: boolean; isTrashed?: boolean; key?: string; @@ -4769,6 +6464,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers data: TimeBucketAssetResponseDto; }>(`/timeline/bucket${QS.query(QS.explode({ albumId, + bbox, isFavorite, isTrashed, key, @@ -4789,8 +6485,9 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers /** * Get time buckets */ -export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, bbox, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: { albumId?: string; + bbox?: string; isFavorite?: boolean; isTrashed?: boolean; key?: string; @@ -4809,6 +6506,7 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per data: TimeBucketsResponseDto[]; }>(`/timeline/buckets${QS.query(QS.explode({ albumId, + bbox, isFavorite, isTrashed, key, @@ -5161,7 +6859,17 @@ export enum UserAvatarColor { } export enum MaintenanceAction { Start = "start", - End = "end" + End = "end", + SelectDatabaseRestore = "select_database_restore", + RestoreDatabase = "restore_database" +} +export enum StorageFolder { + EncodedVideo = "encoded-video", + Library = "library", + Upload = "upload", + Profile = "profile", + Thumbs = "thumbs", + Backups = "backups" } export enum NotificationLevel { Success = "success", @@ -5240,6 +6948,10 @@ export enum Permission { AssetUpload = "asset.upload", AssetReplace = "asset.replace", AssetCopy = "asset.copy", + AssetDerive = "asset.derive", + AssetEditGet = "asset.edit.get", + AssetEditCreate = "asset.edit.create", + AssetEditDelete = "asset.edit.delete", AlbumCreate = "album.create", AlbumRead = "album.read", AlbumUpdate = "album.update", @@ -5255,12 +6967,17 @@ export enum Permission { AuthChangePassword = "auth.changePassword", AuthDeviceDelete = "authDevice.delete", ArchiveRead = "archive.read", + BackupList = "backup.list", + BackupDownload = "backup.download", + BackupUpload = "backup.upload", + BackupDelete = "backup.delete", DuplicateRead = "duplicate.read", DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", FaceRead = "face.read", FaceUpdate = "face.update", FaceDelete = "face.delete", + FolderRead = "folder.read", JobCreate = "job.create", JobRead = "job.read", LibraryCreate = "library.create", @@ -5271,6 +6988,8 @@ export enum Permission { TimelineRead = "timeline.read", TimelineDownload = "timeline.download", Maintenance = "maintenance", + MapRead = "map.read", + MapSearch = "map.search", MemoryCreate = "memory.create", MemoryRead = "memory.read", MemoryUpdate = "memory.update", @@ -5366,9 +7085,6 @@ export enum Permission { AdminSessionRead = "adminSession.read", AdminAuthUnlinkAll = "adminAuth.unlinkAll" } -export enum AssetMetadataKey { - MobileApp = "mobile-app" -} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", @@ -5388,7 +7104,17 @@ export enum AssetJobName { RegenerateThumbnail = "regenerate-thumbnail", TranscodeVideo = "transcode-video" } +export enum AssetEditAction { + Crop = "crop", + Rotate = "rotate", + Mirror = "mirror" +} +export enum MirrorAxis { + Horizontal = "horizontal", + Vertical = "vertical" +} export enum AssetMediaSize { + Original = "original", Fullsize = "fullsize", Preview = "preview", Thumbnail = "thumbnail" @@ -5418,7 +7144,8 @@ export enum QueueName { Notifications = "notifications", BackupDatabase = "backupDatabase", Ocr = "ocr", - Workflow = "workflow" + Workflow = "workflow", + Editor = "editor" } export enum QueueCommand { Start = "start", @@ -5439,11 +7166,15 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } -export enum PluginContext { +export enum PluginContextType { Asset = "asset", Album = "album", Person = "person" } +export enum PluginTriggerType { + AssetCreate = "AssetCreate", + PersonRecognized = "PersonRecognized" +} export enum QueueJobStatus { Active = "active", Failed = "failed", @@ -5459,6 +7190,7 @@ export enum JobName { AssetDetectFaces = "AssetDetectFaces", AssetDetectDuplicatesQueueAll = "AssetDetectDuplicatesQueueAll", AssetDetectDuplicates = "AssetDetectDuplicates", + AssetEditThumbnailGeneration = "AssetEditThumbnailGeneration", AssetEncodeVideoQueueAll = "AssetEncodeVideoQueueAll", AssetEncodeVideo = "AssetEncodeVideo", AssetEmptyTrash = "AssetEmptyTrash", @@ -5533,6 +7265,8 @@ export enum SyncEntityType { AssetV1 = "AssetV1", AssetDeleteV1 = "AssetDeleteV1", AssetExifV1 = "AssetExifV1", + AssetEditV1 = "AssetEditV1", + AssetEditDeleteV1 = "AssetEditDeleteV1", AssetMetadataV1 = "AssetMetadataV1", AssetMetadataDeleteV1 = "AssetMetadataDeleteV1", PartnerV1 = "PartnerV1", @@ -5568,6 +7302,7 @@ export enum SyncEntityType { PersonV1 = "PersonV1", PersonDeleteV1 = "PersonDeleteV1", AssetFaceV1 = "AssetFaceV1", + AssetFaceV2 = "AssetFaceV2", AssetFaceDeleteV1 = "AssetFaceDeleteV1", UserMetadataV1 = "UserMetadataV1", UserMetadataDeleteV1 = "UserMetadataDeleteV1", @@ -5583,6 +7318,7 @@ export enum SyncRequestType { AlbumAssetExifsV1 = "AlbumAssetExifsV1", AssetsV1 = "AssetsV1", AssetExifsV1 = "AssetExifsV1", + AssetEditsV1 = "AssetEditsV1", AssetMetadataV1 = "AssetMetadataV1", AuthUsersV1 = "AuthUsersV1", MemoriesV1 = "MemoriesV1", @@ -5595,6 +7331,7 @@ export enum SyncRequestType { UsersV1 = "UsersV1", PeopleV1 = "PeopleV1", AssetFacesV1 = "AssetFacesV1", + AssetFacesV2 = "AssetFacesV2", UserMetadataV1 = "UserMetadataV1" } export enum TranscodeHWAccel { @@ -5660,11 +7397,8 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" } -export enum TriggerType { - AssetCreate = "AssetCreate", - PersonRecognized = "PersonRecognized" -} -export enum PluginTriggerType { - AssetCreate = "AssetCreate", - PersonRecognized = "PersonRecognized" +export enum UserMetadataKey { + Preferences = "preferences", + License = "license", + Onboarding = "onboarding" } diff --git a/package.json b/package.json index a5421c0e36..b49e12c3e9 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "immich-monorepo", - "version": "0.0.1", + "version": "2.5.6", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", + "packageManager": "pnpm@10.30.0+sha512.2b5753de015d480eeb88f5b5b61e0051f05b4301808a82ec8b840c9d2adf7748eb352c83f5c1593ca703ff1017295bc3fdd3119abb9686efc96b9fcb18200937", "engines": { "pnpm": ">=10.0.0" } diff --git a/plugins/manifest.json b/plugins/manifest.json index 1172530c1e..4d2de275ca 100644 --- a/plugins/manifest.json +++ b/plugins/manifest.json @@ -1,30 +1,36 @@ { "name": "immich-core", - "version": "2.0.0", + "version": "2.0.1", "title": "Immich Core", "description": "Core workflow capabilities for Immich", "author": "Immich Team", - "wasm": { "path": "dist/plugin.wasm" }, - "filters": [ { "methodName": "filterFileName", "title": "Filter by filename", "description": "Filter assets by filename pattern using text matching or regular expressions", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { "pattern": { "type": "string", + "title": "Filename pattern", "description": "Text or regex pattern to match against filename" }, "matchType": { "type": "string", - "enum": ["contains", "regex", "exact"], + "title": "Match type", + "enum": [ + "contains", + "regex", + "exact" + ], "default": "contains", "description": "Type of pattern matching to perform" }, @@ -34,43 +40,57 @@ "description": "Whether matching should be case-sensitive" } }, - "required": ["pattern"] + "required": [ + "pattern" + ] } }, { "methodName": "filterFileType", "title": "Filter by file type", "description": "Filter assets by file type", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { "fileTypes": { "type": "array", + "title": "File types", "items": { "type": "string", - "enum": ["IMAGE", "VIDEO"] + "enum": [ + "image", + "video" + ] }, "description": "Allowed file types" } }, - "required": ["fileTypes"] + "required": [ + "fileTypes" + ] } }, { "methodName": "filterPerson", "title": "Filter by person", "description": "Filter by detected person", - "supportedContexts": ["person"], + "supportedContexts": [ + "person" + ], "schema": { "type": "object", "properties": { "personIds": { "type": "array", + "title": "Person IDs", "items": { "type": "string" }, - "description": "List of person to match" + "description": "List of person to match", + "subType": "people-picker" }, "matchAny": { "type": "boolean", @@ -78,24 +98,29 @@ "description": "Match any name (true) or require all names (false)" } }, - "required": ["personIds"] + "required": [ + "personIds" + ] } } ], - "actions": [ { "methodName": "actionArchive", "title": "Archive", "description": "Move the asset to archive", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": {} }, { "methodName": "actionFavorite", "title": "Favorite", "description": "Mark the asset as favorite or unfavorite", - "supportedContexts": ["asset"], + "supportedContexts": [ + "asset" + ], "schema": { "type": "object", "properties": { @@ -111,16 +136,23 @@ "methodName": "actionAddToAlbum", "title": "Add to Album", "description": "Add the item to a specified album", - "supportedContexts": ["asset", "person"], + "supportedContexts": [ + "asset", + "person" + ], "schema": { "type": "object", "properties": { "albumId": { "type": "string", - "description": "Target album ID" + "title": "Album ID", + "description": "Target album ID", + "subType": "album-picker" } }, - "required": ["albumId"] + "required": [ + "albumId" + ] } } ] diff --git a/plugins/mise.toml b/plugins/mise.toml index c1001e574b..66a107674d 100644 --- a/plugins/mise.toml +++ b/plugins/mise.toml @@ -1,7 +1,7 @@ [tools] "github:extism/cli" = "1.6.3" "github:webassembly/binaryen" = "version_124" -"github:extism/js-pdk" = "1.5.1" +"github:extism/js-pdk" = "1.6.0" [tasks.install] run = "pnpm install --frozen-lockfile" diff --git a/plugins/package-lock.json b/plugins/package-lock.json index ca3c99b516..9ebaa59a02 100644 --- a/plugins/package-lock.json +++ b/plugins/package-lock.json @@ -15,9 +15,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -32,9 +32,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -49,9 +49,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -66,9 +66,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -83,9 +83,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -117,9 +117,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -151,9 +151,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -168,9 +168,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -185,9 +185,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -202,9 +202,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -219,9 +219,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -236,9 +236,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -253,9 +253,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -270,9 +270,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -287,9 +287,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -321,9 +321,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -338,9 +338,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -355,9 +355,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -372,9 +372,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -389,9 +389,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -406,9 +406,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -423,9 +423,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -440,9 +440,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -467,9 +467,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -480,32 +480,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/typescript": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 390393ab85..2c28fe7cd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,7 +11,7 @@ overrides: packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54= -pnpmfileChecksum: sha256-AG/qwrPNpmy9q60PZwCpecoYVptglTHgH+N6RKQHOM0= +pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA= importers: @@ -21,7 +21,7 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 cli: dependencies: @@ -33,19 +33,19 @@ importers: version: 3.3.3 fastq: specifier: ^1.17.1 - version: 1.19.1 + version: 1.20.1 lodash-es: specifier: ^4.17.21 - version: 4.17.21 + version: 4.17.23 micromatch: specifier: ^4.0.8 version: 4.0.8 devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@types/byte-size': specifier: ^8.1.0 @@ -63,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^24.10.3 - version: 24.10.4 + specifier: ^24.10.13 + version: 24.11.0 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -78,47 +78,47 @@ importers: specifier: ^12.0.0 version: 12.1.0 eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: - specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + specifier: ^6.0.0 + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) yaml: specifier: ^2.3.1 version: 2.8.2 @@ -127,13 +127,16 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.9.0 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/preset-classic': specifier: ~3.9.0 - version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + version: 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/theme-common': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-mermaid': + specifier: ~3.9.0 + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -142,13 +145,13 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.7)(react@18.3.1) + version: 3.1.1(@types/react@19.2.14)(react@18.3.1) autoprefixer: specifier: ^10.4.17 - version: 10.4.23(postcss@8.5.6) + version: 10.4.24(postcss@8.5.6) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -160,7 +163,7 @@ importers: version: 2.4.1(react@18.3.1) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.103.0) + version: 4.0.2(webpack@5.104.1) react: specifier: ^18.0.0 version: 18.3.1 @@ -169,7 +172,7 @@ importers: version: 18.3.1(react@18.3.1) tailwindcss: specifier: ^3.2.4 - version: 3.4.19(yaml@2.8.2) + version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) url: specifier: ^0.11.0 version: 0.11.4 @@ -185,7 +188,7 @@ importers: version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -193,20 +196,23 @@ importers: e2e: devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@faker-js/faker': specifier: ^10.1.0 - version: 10.1.0 + version: 10.3.0 '@immich/cli': - specifier: file:../cli + specifier: workspace:* version: link:../cli + '@immich/e2e-auth-server': + specifier: workspace:* + version: link:../e2e-auth-server '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@playwright/test': specifier: ^1.44.1 - version: 1.57.0 + version: 1.58.2 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -214,11 +220,8 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^24.10.3 - version: 24.10.4 - '@types/oidc-provider': - specifier: ^9.0.0 - version: 9.5.0 + specifier: ^24.10.13 + version: 24.11.0 '@types/pg': specifier: ^8.15.1 version: 8.16.0 @@ -230,77 +233,95 @@ importers: version: 6.0.3 dotenv: specifier: ^17.2.3 - version: 17.2.3 + version: 17.3.1 eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) exiftool-vendored: - specifier: ^34.0.0 - version: 34.1.0 + specifier: ^35.0.0 + version: 35.10.1 globals: - specifier: ^16.0.0 - version: 16.5.0 - jose: - specifier: ^5.6.3 - version: 5.10.0 + specifier: ^17.0.0 + version: 17.3.0 luxon: specifier: ^3.4.4 version: 3.7.2 - oidc-provider: - specifier: ^9.0.0 - version: 9.6.0 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.18.0 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 socket.io-client: specifier: ^4.7.4 - version: 4.8.1 + version: 4.8.3 supertest: specifier: ^7.0.0 - version: 7.1.4 + version: 7.2.2 typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + e2e-auth-server: + devDependencies: + '@types/oidc-provider': + specifier: ^9.0.0 + version: 9.5.0 + jose: + specifier: ^5.6.3 + version: 5.10.0 + oidc-provider: + specifier: ^9.0.0 + version: 9.6.1 + tsx: + specifier: ^4.20.6 + version: 4.21.0 + + i18n: + devDependencies: + prettier: + specifier: ^3.7.4 + version: 3.8.1 + prettier-plugin-sort-json: + specifier: ^4.1.1 + version: 4.2.0(prettier@3.8.1) open-api/typescript-sdk: dependencies: '@oazapfts/runtime': specifier: ^1.0.2 - version: 1.1.0 + version: 1.2.0 devDependencies: '@types/node': - specifier: ^24.10.3 - version: 24.10.4 + specifier: ^24.10.13 + version: 24.11.0 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -312,7 +333,7 @@ importers: version: 1.1.1 esbuild: specifier: ^0.27.0 - version: 0.27.1 + version: 0.27.3 typescript: specifier: ^5.3.2 version: 5.9.3 @@ -322,75 +343,78 @@ importers: '@extism/extism': specifier: 2.0.0-rc13 version: 2.0.0-rc13 + '@immich/sql-tools': + specifier: ^0.3.2 + version: 0.3.2 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.66.0) + version: 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 6.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.6(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.56.0 - version: 0.56.0(@opentelemetry/api@1.9.0) + specifier: ^0.60.0 + version: 0.60.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.55.0 - version: 0.55.0(@opentelemetry/api@1.9.0) + specifier: ^0.58.0 + version: 0.58.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.61.0 - version: 0.61.1(@opentelemetry/api@1.9.0) + specifier: ^0.64.0 + version: 0.64.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.212.0 + version: 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 - version: 1.38.0 + version: 1.39.0 '@react-email/components': specifier: ^0.5.0 - version: 0.5.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-email/render': specifier: ^1.1.2 - version: 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@socket.io/redis-adapter': specifier: ^8.3.0 - version: 8.3.0(socket.io-adapter@2.5.5) + version: 8.3.0(socket.io-adapter@2.5.6) ajv: specifier: ^8.17.1 - version: 8.17.1 + version: 8.18.0 archiver: specifier: ^7.0.0 version: 7.0.1 @@ -402,10 +426,10 @@ importers: version: 6.0.0 body-parser: specifier: ^2.2.0 - version: 2.2.1 + version: 2.2.2 bullmq: specifier: ^5.51.0 - version: 5.66.0 + version: 5.69.3 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -425,11 +449,11 @@ importers: specifier: ^1.4.7 version: 1.4.7 cron: - specifier: 4.3.5 - version: 4.3.5 + specifier: 4.4.0 + version: 4.4.0 exiftool-vendored: - specifier: ^34.0.0 - version: 34.1.0 + specifier: ^35.0.0 + version: 35.10.1 express: specifier: ^5.1.0 version: 5.2.1 @@ -441,7 +465,7 @@ importers: version: 2.1.3 geo-tz: specifier: ^8.0.0 - version: 8.1.4 + version: 8.1.5 handlebars: specifier: ^4.7.8 version: 4.7.8 @@ -450,7 +474,7 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.8.2 + version: 5.9.3 jose: specifier: ^5.10.0 version: 5.10.0 @@ -465,10 +489,10 @@ importers: version: 0.28.2 kysely-postgres-js: specifier: ^3.0.0 - version: 3.0.0(kysely@0.28.2)(postgres@3.4.7) + version: 3.0.0(kysely@0.28.2)(postgres@3.4.8) lodash: specifier: ^4.17.21 - version: 4.17.21 + version: 4.17.23 luxon: specifier: ^3.4.2 version: 3.7.2 @@ -477,43 +501,43 @@ importers: version: 0.40.3 multer: specifier: ^2.0.2 - version: 2.0.2 + version: 2.1.0 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@types/inquirer@8.2.12)(@types/node@24.10.4)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@types/inquirer@8.2.12)(@types/node@24.11.0)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 - version: 5.4.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 7.0.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) nodemailer: specifier: ^7.0.0 - version: 7.0.11 + version: 7.0.13 openid-client: specifier: ^6.3.3 - version: 6.8.1 + version: 6.8.2 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.18.0 pg-connection-string: specifier: ^2.9.1 - version: 2.9.1 + version: 2.11.0 picomatch: specifier: ^4.0.2 version: 4.0.3 postgres: - specifier: 3.4.7 - version: 3.4.7 + specifier: 3.4.8 + version: 3.4.8 react: specifier: ^19.0.0 - version: 19.2.3 + version: 19.2.4 react-dom: specifier: ^19.0.0 - version: 19.2.3(react@19.2.3) + version: 19.2.4(react@19.2.4) react-email: specifier: ^4.0.0 version: 4.3.2 @@ -528,10 +552,10 @@ importers: version: 1.6.3 sanitize-html: specifier: ^2.14.0 - version: 2.17.0 + version: 2.17.1 semver: specifier: ^7.6.2 - version: 7.7.3 + version: 7.7.4 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -540,38 +564,41 @@ importers: version: 3.0.2 socket.io: specifier: ^4.8.1 - version: 4.8.1 + version: 4.8.3 tailwindcss-preset-email: specifier: ^1.4.0 - version: 1.4.1(tailwindcss@3.4.19(yaml@2.8.2)) + version: 1.4.1(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) thumbhash: specifier: ^0.1.1 version: 0.1.1 + transformation-matrix: + specifier: ^3.1.0 + version: 3.1.0 ua-parser-js: specifier: ^2.0.0 - version: 2.0.7 + version: 2.0.9 uuid: specifier: ^11.1.0 version: 11.1.0 validator: specifier: ^13.12.0 - version: 13.15.23 + version: 13.15.26 devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.14(@swc/core@1.15.5(@swc/helpers@0.5.17))(@types/node@24.10.4) + version: 11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.11.0) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) + version: 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14) '@swc/core': specifier: ^1.4.14 - version: 1.15.5(@swc/helpers@0.5.17) + version: 1.15.11(@swc/helpers@0.5.17) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -604,7 +631,7 @@ importers: version: 9.0.10 '@types/lodash': specifier: ^4.14.197 - version: 4.17.21 + version: 4.17.23 '@types/luxon': specifier: ^3.6.2 version: 3.7.1 @@ -615,11 +642,11 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^24.10.3 - version: 24.10.4 + specifier: ^24.10.13 + version: 24.11.0 '@types/nodemailer': specifier: ^7.0.0 - version: 7.0.4 + version: 7.0.10 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -628,7 +655,7 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.14 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 @@ -646,124 +673,121 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) eslint: - specifier: ^9.14.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4) + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 node-gyp: specifier: ^12.0.0 - version: 12.1.0 + version: 12.2.0 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 - version: 15.6.12 + version: 15.7.2 supertest: specifier: ^7.1.0 - version: 7.1.4 + version: 7.2.2 tailwindcss: specifier: ^3.4.0 - version: 3.4.19(yaml@2.8.2) + version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) testcontainers: specifier: ^11.0.0 - version: 11.10.0 + version: 11.12.0 typescript: specifier: ^5.9.2 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.5(@swc/helpers@0.5.17))(rollup@4.53.4) + version: 1.5.9(@swc/core@1.15.11(@swc/helpers@0.5.17))(rollup@4.55.1) vite-tsconfig-paths: - specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + specifier: ^6.0.0 + version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web: dependencies: '@formatjs/icu-messageformat-parser': - specifier: ^2.9.8 - version: 2.11.4 + specifier: ^3.0.0 + version: 3.5.1 '@immich/justified-layout-wasm': specifier: ^0.4.3 version: 0.4.3 '@immich/sdk': - specifier: file:../open-api/typescript-sdk + specifier: workspace:* version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.50.1 - version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + specifier: ^0.64.0 + version: 0.64.0(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) '@mapbox/mapbox-gl-rtl-text': - specifier: 0.2.3 - version: 0.2.3(mapbox-gl@1.13.3) + specifier: 0.3.0 + version: 0.3.0 '@mdi/js': specifier: ^7.4.47 version: 7.4.47 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.14.0 + version: 5.14.1 '@photo-sphere-viewer/equirectangular-tiles-adapter': - specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + specifier: ^5.14.1 + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.14.0(@photo-sphere-viewer/core@5.14.0) + version: 5.14.1(@photo-sphere-viewer/core@5.14.1) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 '@zoom-image/core': - specifier: ^0.41.0 - version: 0.41.4 + specifier: ^0.42.0 + version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.8(svelte@5.43.3) - async-mutex: - specifier: ^0.5.0 - version: 0.5.0 + version: 0.3.9(svelte@5.53.5) dom-to-image: specifier: ^2.6.0 version: 2.6.0 fabric: - specifier: ^6.5.4 - version: 6.9.1 + specifier: ^7.0.0 + version: 7.2.0(encoding@0.1.13) geo-coordinates-parser: specifier: ^1.7.4 version: 1.7.4 @@ -775,25 +799,25 @@ importers: version: 4.7.8 happy-dom: specifier: ^20.0.0 - version: 20.0.11 + version: 20.6.3 intl-messageformat: - specifier: ^10.7.11 - version: 10.7.18 + specifier: ^11.0.0 + version: 11.1.2 justified-layout: specifier: ^4.1.0 version: 4.1.0 lodash-es: specifier: ^4.17.21 - version: 4.17.21 + version: 4.17.23 luxon: specifier: ^3.4.4 version: 3.7.2 maplibre-gl: specifier: ^5.6.2 - version: 5.14.0 + version: 5.18.0 pmtiles: specifier: ^4.3.0 - version: 4.3.0 + version: 4.4.0 qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -802,62 +826,68 @@ importers: version: 15.22.0 socket.io-client: specifier: ~4.8.0 - version: 4.8.1 + version: 4.8.3 svelte-gestures: specifier: ^5.2.2 version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.43.3) + version: 4.0.1(svelte@5.53.5) + svelte-jsoneditor: + specifier: ^3.10.0 + version: 3.11.0(svelte@5.53.5) svelte-maplibre: specifier: ^1.2.5 - version: 1.2.5(svelte@5.43.3) + version: 1.2.6(svelte@5.53.5) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.43.3) + version: 0.12.0(svelte@5.53.5) tabbable: specifier: ^6.2.0 - version: 6.3.0 + version: 6.4.0 thumbhash: specifier: ^0.1.1 version: 0.1.1 + transformation-matrix: + specifier: ^3.1.0 + version: 3.1.0 uplot: specifier: ^1.6.32 version: 1.6.32 devDependencies: '@eslint/js': - specifier: ^9.36.0 - version: 9.39.2 + specifier: ^10.0.0 + version: 10.0.1(eslint@10.0.2(jiti@2.6.1)) '@faker-js/faker': specifier: ^10.0.0 - version: 10.1.0 + version: 10.3.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 - version: 0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 0.2.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) '@socket.io/component-emitter': specifier: ^3.1.0 version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) '@sveltejs/enhanced-img': - specifier: ^0.9.0 - version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + specifier: ^0.10.0 + version: 0.10.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': - specifier: 6.2.1 - version: 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + specifier: 6.2.4 + version: 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 4.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -881,70 +911,70 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 - version: 17.2.3 + version: 17.3.1 eslint: - specifier: ^9.36.0 - version: 9.39.2(jiti@2.6.1) + specifier: ^10.0.0 + version: 10.0.2(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + version: 10.1.8(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-compat: specifier: ^6.0.2 - version: 6.0.2(eslint@9.39.2(jiti@2.6.1)) + version: 6.2.0(eslint@10.0.2(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.43.3) + version: 3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.5) eslint-plugin-unicorn: - specifier: ^62.0.0 - version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) + specifier: ^63.0.0 + version: 63.0.0(eslint@10.0.2(jiti@2.6.1)) factory.ts: specifier: ^1.4.1 version: 1.4.2 globals: - specifier: ^16.0.0 - version: 16.5.0 + specifier: ^17.0.0 + version: 17.3.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.7.4)(typescript@5.9.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.9.3) prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.7.4) + version: 4.2.0(prettier@3.8.1) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.1(prettier@3.7.4)(svelte@5.43.3) + version: 3.5.0(prettier@3.8.1)(svelte@5.53.5) rollup-plugin-visualizer: specifier: ^6.0.0 - version: 6.0.5(rollup@4.53.4) + version: 6.0.5(rollup@4.55.1) svelte: - specifier: 5.43.3 - version: 5.43.3 + specifier: 5.53.5 + version: 5.53.5 svelte-check: specifier: ^4.1.5 - version: 4.3.4(picomatch@4.0.3)(svelte@5.43.3)(typescript@5.9.3) + version: 4.4.1(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.4.1(svelte@5.43.3) + version: 1.4.1(svelte@5.53.5) tailwindcss: specifier: ^4.1.7 - version: 4.1.18 + version: 4.2.0 typescript: specifier: ^5.8.3 version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1089,137 +1119,14 @@ packages: resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==} 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'} - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-sesv2@3.952.0': - resolution: {integrity: sha512-0avirspZ7/RkHqp9It12xx6UJ2rkO6B6EeNScIgDkgyELl4tGsmF8bhBSPDqeJMZ1HQGYglanzkDRrYFgTN6iA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/client-sso@3.948.0': - resolution: {integrity: sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/core@3.947.0': - resolution: {integrity: sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-env@3.947.0': - resolution: {integrity: sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-http@3.947.0': - resolution: {integrity: sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-ini@3.952.0': - resolution: {integrity: sha512-N5B15SwzMkZ8/LLopNksTlPEWWZn5tbafZAUfMY5Xde4rSHGWmv5H/ws2M3P8L0X77E2wKnOJsNmu+GsArBreQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-login@3.952.0': - resolution: {integrity: sha512-jL9zc+e+7sZeJrHzYKK9GOjl1Ktinh0ORU3cM2uRBi7fuH/0zV9pdMN8PQnGXz0i4tJaKcZ1lrE4V0V6LB9NQg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-node@3.952.0': - resolution: {integrity: sha512-pj7nidLrb3Dz9llcUPh6N0Yv1dBYTS9xJqi8u0kI8D5sn72HJMB+fIOhcDQVXXAw/dpVolOAH9FOAbog5JDAMg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-process@3.947.0': - resolution: {integrity: sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-sso@3.952.0': - resolution: {integrity: sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.952.0': - resolution: {integrity: sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-host-header@3.936.0': - resolution: {integrity: sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-logger@3.936.0': - resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.948.0': - resolution: {integrity: sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-sdk-s3@3.947.0': - resolution: {integrity: sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-user-agent@3.947.0': - resolution: {integrity: sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/nested-clients@3.952.0': - resolution: {integrity: sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/region-config-resolver@3.936.0': - resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.947.0': - resolution: {integrity: sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/token-providers@3.952.0': - resolution: {integrity: sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/types@3.936.0': - resolution: {integrity: sha512-uz0/VlMd2pP5MepdrHizd+T+OKfyK4r3OA9JI+L/lPKg0YFQosdJNCKisr6o70E3dh8iMpFYxF1UN/4uZsyARg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-arn-parser@3.893.0': - resolution: {integrity: sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-endpoints@3.936.0': - resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-locate-window@3.893.0': - resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-user-agent-browser@3.936.0': - resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} - - '@aws-sdk/util-user-agent-node@3.947.0': - resolution: {integrity: sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.930.0': - resolution: {integrity: sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==} - engines: {node: '>=18.0.0'} - - '@aws/lambda-invoke-store@0.2.2': - resolution: {integrity: sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==} - engines: {node: '>=18.0.0'} - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.5': @@ -1772,8 +1679,8 @@ packages: resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': @@ -1795,8 +1702,50 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@borewit/text-codec@0.1.1': - resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@borewit/text-codec@0.2.1': + resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.3': + resolution: {integrity: sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==} + + '@codemirror/view@6.39.8': + resolution: {integrity: sha512-1rASYd9Z/mE3tkbC9wInRlCNyCkSn+nLsiQKZhEDUUJiUfs/5FHDpCUDaQpoTIaNGeDc6/bhaEAyLmeEucEFPw==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -2266,6 +2215,17 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@docusaurus/theme-mermaid@3.9.2': + resolution: {integrity: sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==} + engines: {node: '>=20.0'} + peerDependencies: + '@mermaid-js/layout-elk': ^0.1.9 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@mermaid-js/layout-elk': + optional: true + '@docusaurus/theme-search-algolia@3.9.2': resolution: {integrity: sha512-GBDSFNwjnh5/LdkxCKQHkgO2pIMX1447BxYUBG2wBiajS21uj64a+gH/qlbQjDLxmGrbrllBrtJkUHxIsiwRnw==} engines: {node: '>=20.0'} @@ -2313,8 +2273,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.1': - resolution: {integrity: sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2331,8 +2291,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.1': - resolution: {integrity: sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2349,8 +2309,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.1': - resolution: {integrity: sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2367,8 +2327,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.1': - resolution: {integrity: sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2385,8 +2345,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.1': - resolution: {integrity: sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2403,8 +2363,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.1': - resolution: {integrity: sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2421,8 +2381,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.1': - resolution: {integrity: sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2439,8 +2399,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.1': - resolution: {integrity: sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2457,8 +2417,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.1': - resolution: {integrity: sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2475,8 +2435,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.1': - resolution: {integrity: sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2493,8 +2453,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.1': - resolution: {integrity: sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2511,8 +2471,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.1': - resolution: {integrity: sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2529,8 +2489,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.1': - resolution: {integrity: sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2547,8 +2507,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.1': - resolution: {integrity: sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2565,8 +2525,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.1': - resolution: {integrity: sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -2583,8 +2543,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.1': - resolution: {integrity: sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -2601,8 +2561,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.1': - resolution: {integrity: sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -2613,8 +2573,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.1': - resolution: {integrity: sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -2631,8 +2591,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.1': - resolution: {integrity: sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -2643,8 +2603,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.1': - resolution: {integrity: sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -2661,8 +2621,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.1': - resolution: {integrity: sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2673,8 +2633,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.1': - resolution: {integrity: sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==} + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -2691,8 +2651,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.1': - resolution: {integrity: sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2709,8 +2669,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.1': - resolution: {integrity: sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2727,8 +2687,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.1': - resolution: {integrity: sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2745,14 +2705,14 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.1': - resolution: {integrity: sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -2761,33 +2721,34 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.23.2': + resolution: {integrity: sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.2': + resolution: {integrity: sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.0': + resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.2': + resolution: {integrity: sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.0': + resolution: {integrity: sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@extism/extism@2.0.0-rc13': resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==} @@ -2795,8 +2756,8 @@ packages: '@extism/js-pdk@1.1.1': resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==} - '@faker-js/faker@10.1.0': - resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==} + '@faker-js/faker@10.3.0': + resolution: {integrity: sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@fig/complete-commander@3.2.0': @@ -2816,18 +2777,45 @@ packages: '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + '@formatjs/ecma402-abstract@3.1.1': + resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==} + '@formatjs/fast-memoize@2.2.7': resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + '@formatjs/fast-memoize@3.1.0': + resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==} + '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + '@formatjs/icu-messageformat-parser@3.5.1': + resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==} + '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + '@formatjs/icu-skeleton-parser@2.1.1': + resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==} + '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@formatjs/intl-localematcher@0.8.1': + resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} + + '@fortawesome/fontawesome-common-types@7.1.0': + resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} + engines: {node: '>=6'} + + '@fortawesome/free-regular-svg-icons@7.1.0': + resolution: {integrity: sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@7.1.0': + resolution: {integrity: sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==} + engines: {node: '>=6'} + '@golevelup/nestjs-discovery@5.0.0': resolution: {integrity: sha512-NaIWLCLI+XvneUK05LH2idHLmLNITYT88YnpOuUQmllKtiJNIS3woSt7QXrMZ5k3qUWuZpehEVz1JtlX4I1KyA==} peerDependencies: @@ -2870,6 +2858,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -2900,89 +2894,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3010,13 +3020,17 @@ packages: '@immich/justified-layout-wasm@0.4.3': resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==} - '@immich/svelte-markdown-preprocess@0.1.0': - resolution: {integrity: sha512-jgSOJEGLPKEXQCNRI4r4YUayeM2b0ZYLdzgKGl891jZBhOQIetlY7rU44kPpV1AA3/8wGDwNFKduIQZZ/qJYzg==} + '@immich/sql-tools@0.3.2': + resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==} + hasBin: true + + '@immich/svelte-markdown-preprocess@0.2.1': + resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.50.1': - resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==} + '@immich/ui@0.64.0': + resolution: {integrity: sha512-jbPN1x9KAAcW18h4RO7skbFYjkR4Lg+mEVjSDzsPC2NBNzSi4IA0PIHhFEwnD5dk4OS7+UjRG8m5/QTyotrm4A==} peerDependencies: svelte: ^5.0.0 @@ -3166,16 +3180,8 @@ packages: '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} - '@ioredis/commands@1.4.0': - resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} - - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} @@ -3219,6 +3225,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -3255,12 +3273,16 @@ packages: peerDependencies: tslib: '2' + '@jsonquerylang/jsonquery@5.1.1': + resolution: {integrity: sha512-Fj4SoA6Ku09EF+t7OEI8QLipA2A+fJCdEOwnDWG84o5jXMRjkcN5NCMH7kFZb5fP62xz914XV5LBOiDdiUXObg==} + hasBin: true + '@koa/cors@5.0.0': resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} engines: {node: '>= 14.0.0'} - '@koa/router@15.1.0': - resolution: {integrity: sha512-0zCmuapmgBHrfVSFjBfCdgnkBnXwRGcG5qHnxVs8ZoTNEJiwSSspgJ5+2NugiqLJS/S0d96KMeNntLqTNWaioQ==} + '@koa/router@15.3.0': + resolution: {integrity: sha512-s87hWJjFYky2Z97u8jzah73sSHp4IZivD/2PZCuspHRvcKU69OPLoBIbKigVlBmS50yFTh9GHFfr1hDag4+wXw==} engines: {node: '>= 20'} peerDependencies: koa: ^2.0.0 || ^3.0.0 @@ -3268,9 +3290,24 @@ packages: '@koddsson/eslint-plugin-tscompat@0.2.0': resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.6': + resolution: {integrity: sha512-u42yGuGBsHgodm86lwi0HAtUTNSs23yl9RoaI5em90B+OGm9/XuWkNiJ46sKkCgp8Tp4zgoBQbepcshfKLhFdw==} + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -3279,48 +3316,26 @@ packages: resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} hasBin: true - '@mapbox/geojson-types@1.0.2': - resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} - '@mapbox/jsonlint-lines-primitives@2.0.2': resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} engines: {node: '>= 0.6'} - '@mapbox/mapbox-gl-rtl-text@0.2.3': - resolution: {integrity: sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw==} - peerDependencies: - mapbox-gl: '>=0.32.1 <2.0.0' - - '@mapbox/mapbox-gl-supported@1.5.0': - resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} - peerDependencies: - mapbox-gl: '>=0.32.1 <2.0.0' + '@mapbox/mapbox-gl-rtl-text@0.3.0': + resolution: {integrity: sha512-OwQplFqAAEYRobrTKm2wiVP+wcpUVlgXXiUMNQ8tcm5gPN5SQRXFADmITdQOaec4LhDhuuFchS7TS8ua8dUl4w==} '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true - '@mapbox/point-geometry@0.1.0': - resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} - '@mapbox/point-geometry@1.1.0': resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} - '@mapbox/tiny-sdf@1.2.5': - resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} - '@mapbox/tiny-sdf@2.0.7': resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} - '@mapbox/unitbezier@0.0.0': - resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} - '@mapbox/unitbezier@0.0.1': resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} - '@mapbox/vector-tile@1.3.1': - resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} - '@mapbox/vector-tile@2.0.4': resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} @@ -3328,15 +3343,21 @@ packages: resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} engines: {node: '>=6.0.0'} + '@maplibre/geojson-vt@5.0.4': + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==} + '@maplibre/maplibre-gl-style-spec@24.4.1': resolution: {integrity: sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==} hasBin: true - '@maplibre/mlt@1.1.2': - resolution: {integrity: sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==} + '@maplibre/mlt@1.1.6': + resolution: {integrity: sha512-rgtY3x65lrrfXycLf6/T22ZnjTg5WgIOsptOIoCaMZy4O4UAKTyZlYY0h6v8le721pTptF94U65yMDQkug+URw==} - '@maplibre/vt-pbf@4.2.0': - resolution: {integrity: sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==} + '@maplibre/vt-pbf@4.2.1': + resolution: {integrity: sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} '@mdi/js@7.4.47': resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} @@ -3359,6 +3380,9 @@ packages: '@types/react': '>=16' react: '>=16' + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} @@ -3408,8 +3432,8 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - '@nestjs/cli@11.0.14': - resolution: {integrity: sha512-YwP03zb5VETTwelXU+AIzMVbEZKk/uxJL+z9pw0mdG9ogAtqZ6/mpmIM4nEq/NU8D0a7CBRLcMYUmWW/55pfqw==} + '@nestjs/cli@11.0.16': + resolution: {integrity: sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3421,8 +3445,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.9': - resolution: {integrity: sha512-zDntUTReRbAThIfSp3dQZ9kKqI+LjgLp5YZN5c1bgNRDuoeLySAoZg46Bg1a+uV8TMgIRziHocglKGNzr6l+bQ==} + '@nestjs/common@11.1.14': + resolution: {integrity: sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3434,8 +3458,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.1.9': - resolution: {integrity: sha512-a00B0BM4X+9z+t3UxJqIZlemIwCQdYoPKrMcM+ky4z3pkqqG1eTWexjs+YXpGObnLnjtMPVKWlcZHp3adDYvUw==} + '@nestjs/core@11.1.14': + resolution: {integrity: sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3465,21 +3489,21 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.9': - resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==} + '@nestjs/platform-express@11.1.14': + resolution: {integrity: sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.9': - resolution: {integrity: sha512-OaAW+voXo5BXbFKd9Ot3SL05tEucRMhZRdw5wdWZf/RpIl9hB6G6OHr8DDxNbUGvuQWzNnZHCDHx3EQJzjcIyA==} + '@nestjs/platform-socket.io@11.1.14': + resolution: {integrity: sha512-LLSIWkYz4FcvUhfepillYQboo9qbjq1YtQj8XC3zyex+EaqNXvxhZntx/1uJhAjc655pJts9HfZwWXei8jrRGw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 rxjs: ^7.1.0 - '@nestjs/schedule@6.1.0': - resolution: {integrity: sha512-W25Ydc933Gzb1/oo7+bWzzDiOissE+h/dhIAPugA39b9MuIzBbLybuXpc1AjoQLczO3v0ldmxaffVl87W0uqoQ==} + '@nestjs/schedule@6.1.1': + resolution: {integrity: sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 @@ -3489,10 +3513,10 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.2.3': - resolution: {integrity: sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==} + '@nestjs/swagger@11.2.6': + resolution: {integrity: sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==} peerDependencies: - '@fastify/static': ^8.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -3506,8 +3530,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.9': - resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==} + '@nestjs/testing@11.1.14': + resolution: {integrity: sha512-cQxX0ronsTbpfHz8/LYOVWXxoTxv6VoxrnuZoQaVX7QV2PSMqxWE7/9jSQR0GcqAFUEmFP34c6EJqfkjfX/k4Q==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3519,8 +3543,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.9': - resolution: {integrity: sha512-kkkdeTVcc3X7ZzvVqUVpOAJoh49kTRUjWNUXo5jmG+27OvZoHfs/vuSiqxidrrbIgydSqN15HUsf1wZwQUrxCQ==} + '@nestjs/websockets@11.1.14': + resolution: {integrity: sha512-fVP6RmmrmtLIitTXN9er7BUOIjjxcdIewN/zUtBlwgfng+qKBTxpNFOs3AXXbCu8bQr2xjzhjrBTfqri0Ske7w==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3560,91 +3584,97 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true - '@oazapfts/runtime@1.1.0': - resolution: {integrity: sha512-PwCn69pexqg/uhc0bpEHSlRFdfTtSnq3icXHd0wf4BQwZSMKsCerTnydzegVScEegYkokzIxMcl9li7on86A2w==} + '@oazapfts/runtime@1.2.0': + resolution: {integrity: sha512-fi7dp7dNayyh/vzqhf0ZdoPfC7tJvYfjaE8MBL1yR+iIsH7cFoqHt+DV70VU49OMCqLc7wQa+yVJcSmIRnV4wA==} - '@opentelemetry/api-logs@0.208.0': - resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.2.0': - resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} + '@opentelemetry/configuration@0.212.0': + resolution: {integrity: sha512-D8sAY6RbqMa1W8lCeiaSL2eMCW2MF87QI3y+I6DQE1j+5GrDMwiKPLdzpa/2/+Zl9v1//74LmooCTCJBvWR8Iw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.5.1': + resolution: {integrity: sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.2.0': - resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + '@opentelemetry/core@2.5.1': + resolution: {integrity: sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': - resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/0bk6fQG+eSFZ4L6NlckGTgUous/ib5+OVdg0x4OdwYeHzV3lTEo3it1HgnPY6UKpmX7ki+hJvxjsOql8rCeZA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.208.0': - resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} + '@opentelemetry/exporter-logs-otlp-http@0.212.0': + resolution: {integrity: sha512-JidJasLwG/7M9RTxV/64xotDKmFAUSBc9SNlxI32QYuUMK5rVKhHNWMPDzC7E0pCAL3cu+FyiKvsTwLi2KqPYw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.208.0': - resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} + '@opentelemetry/exporter-logs-otlp-proto@0.212.0': + resolution: {integrity: sha512-RpKB5UVfxc7c6Ta1UaCrxXDTQ0OD7BCGT66a97Q5zR1x3+9fw4dSaiqMXT/6FAWj2HyFbem6Rcu1UzPZikGTWQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0': - resolution: {integrity: sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0': + resolution: {integrity: sha512-/6Gqf9wpBq22XsomR1i0iPGnbQtCq2Vwnrq5oiDPjYSqveBdK1jtQbhGfmpK2mLLxk4cPDtD1ZEYdIou5K8EaA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.208.0': - resolution: {integrity: sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==} + '@opentelemetry/exporter-metrics-otlp-http@0.212.0': + resolution: {integrity: sha512-8hgBw3aTTRpSTkU4b9MLf/2YVLnfWp+hfnLq/1Fa2cky+vx6HqTodo+Zv1GTIrAKMOOwgysOjufy0gTxngqeBg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.208.0': - resolution: {integrity: sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw==} + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0': + resolution: {integrity: sha512-C7I4WN+ghn3g7SnxXm2RK3/sRD0k/BYcXaK6lGU3yPjiM7a1M25MLuM6zY3PeVPPzzTZPfuS7+wgn/tHk768Xw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.208.0': - resolution: {integrity: sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw==} + '@opentelemetry/exporter-prometheus@0.212.0': + resolution: {integrity: sha512-hJFLhCJba5MW5QHexZMHZdMhBfNqNItxOsN0AZojwD1W2kU9xM+BEICowFGJFo/vNV+I2BJvTtmuKafeDSAo7Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.208.0': - resolution: {integrity: sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw==} + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0': + resolution: {integrity: sha512-9xTuYWp8ClBhljDGAoa0NSsJcsxJsC9zCFKMSZJp1Osb9pjXCMRdA6fwXtlubyqe7w8FH16EWtQNKx/FWi+Ghw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.208.0': - resolution: {integrity: sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==} + '@opentelemetry/exporter-trace-otlp-http@0.212.0': + resolution: {integrity: sha512-v/0wMozNoiEPRolzC4YoPo4rAT0q8r7aqdnRw3Nu7IDN0CGFzNQazkfAlBJ6N5y0FYJkban7Aw5WnN73//6YlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.208.0': - resolution: {integrity: sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA==} + '@opentelemetry/exporter-trace-otlp-proto@0.212.0': + resolution: {integrity: sha512-d1ivqPT0V+i0IVOOdzGaLqonjtlk5jYrW7ItutWzXL/Mk+PiYb59dymy/i2reot9dDnBFWfrsvxyqdutGF5Vig==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.2.0': - resolution: {integrity: sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg==} + '@opentelemetry/exporter-zipkin@2.5.1': + resolution: {integrity: sha512-Me6JVO7WqXGXsgr4+7o+B7qwKJQbt0c8WamFnxpkR43avgG9k/niTntwCaXiXUTjonWy0+61ZuX6CGzj9nn8CQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3655,62 +3685,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.208.0': - resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} + '@opentelemetry/instrumentation-http@0.212.0': + resolution: {integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.56.0': - resolution: {integrity: sha512-XSWeqsd3rKSsT3WBz/JKJDcZD4QYElZEa0xVdX8f9dh4h4QgXhKRLorVsVkK3uXFbC2sZKAS2Ds+YolGwD83Dg==} + '@opentelemetry/instrumentation-ioredis@0.60.0': + resolution: {integrity: sha512-R+nnbPD9l2ruzu248qM3YDWzpdmWVaFFFv08lQqsc0EP4pT/B1GGUg06/tHOSo3L5njB2eejwyzpkvJkjaQEMA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.55.0': - resolution: {integrity: sha512-JFLNhbbEGnnQrMKOYoXx0nNk5N9cPeghu4xP/oup40a7VaSeYruyOiFbg9nkbS4ZQiI8aMuRqUT3Mo4lQjKEKg==} + '@opentelemetry/instrumentation-nestjs-core@0.58.0': + resolution: {integrity: sha512-0lE9oW8j6nmvBHJoOxIQgKzMQQYNfX1nhiWZdXD0sNAMFsWBtvECWS7NAPSroKrEP53I04TcHCyyhcK4I9voXg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.61.1': - resolution: {integrity: sha512-VKKts/XcOCa7IPBxVjL2B4UyG+YTNa4Dh1Xx2vqL0jOEQBJlNsv++I12BUw/8NRLEr2K/gOM5tpVU7QqhWA65A==} + '@opentelemetry/instrumentation-pg@0.64.0': + resolution: {integrity: sha512-NbfB/rlfsRI3zpTjnbvJv3qwuoGLsN8FxR/XoI+ZTn1Rs62x1IenO+TSSvk4NO+7FlXpd2MiOe8LT/oNbydHGA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.208.0': - resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.208.0': - resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} + '@opentelemetry/otlp-exporter-base@0.212.0': + resolution: {integrity: sha512-HoMv5pQlzbuxiMS0hN7oiUtg8RsJR5T7EhZccumIWxYfNo/f4wFc7LPDfFK6oHdG2JF/+qTocfqIHoom+7kLpw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.208.0': - resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} + '@opentelemetry/otlp-grpc-exporter-base@0.212.0': + resolution: {integrity: sha512-YidOSlzpsun9uw0iyIWrQp6HxpMtBlECE3tiHGAsnpEqJWbAUWcMnIffvIuvTtTQ1OyRtwwaE79dWSQ8+eiB7g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.208.0': - resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} + '@opentelemetry/otlp-transformer@0.212.0': + resolution: {integrity: sha512-bj7zYFOg6Db7NUwsRZQ/WoVXpAf41WY2gsd3kShSfdpZQDRKHWJiRZIg7A8HvWsf97wb05rMFzPbmSHyjEl9tw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.2.0': - resolution: {integrity: sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw==} + '@opentelemetry/propagator-b3@2.5.1': + resolution: {integrity: sha512-AU6sZgunZrZv/LTeHP+9IQsSSH5p3PtOfDPe8VTdwYH69nZCfvvvXehhzu+9fMW2mgJMh5RVpiH8M9xuYOu5Dg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.2.0': - resolution: {integrity: sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA==} + '@opentelemetry/propagator-jaeger@2.5.1': + resolution: {integrity: sha512-8+SB94/aSIOVGDUPRFSBRHVUm2A8ye1vC6/qcf/D+TF4qat7PC6rbJhRxiUGDXZtMtKEPM/glgv5cBGSJQymSg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3719,44 +3749,44 @@ packages: resolution: {integrity: sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.2.0': - resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + '@opentelemetry/resources@2.5.1': + resolution: {integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.208.0': - resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} + '@opentelemetry/sdk-logs@0.212.0': + resolution: {integrity: sha512-qglb5cqTf0mOC1sDdZ7nfrPjgmAqs2OxkzOPIf2+Rqx8yKBK0pS7wRtB1xH30rqahBIut9QJDbDePyvtyqvH/Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.2.0': - resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} + '@opentelemetry/sdk-metrics@2.5.1': + resolution: {integrity: sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.208.0': - resolution: {integrity: sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw==} + '@opentelemetry/sdk-node@0.212.0': + resolution: {integrity: sha512-tJzVDk4Lo44MdgJLlP+gdYdMnjxSNsjC/IiTxj5CFSnsjzpHXwifgl3BpUX67Ty3KcdubNVfedeBc/TlqHXwwg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.2.0': - resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + '@opentelemetry/sdk-trace-base@2.5.1': + resolution: {integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.2.0': - resolution: {integrity: sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==} + '@opentelemetry/sdk-trace-node@2.5.1': + resolution: {integrity: sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.41.2': @@ -3768,43 +3798,131 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} - '@photo-sphere-viewer/core@5.14.0': - resolution: {integrity: sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==} + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] - '@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.0': - resolution: {integrity: sha512-PEZreZg79tdkYbiswKppmLuwkJnMnhLVHTHJdWYqpjFt6PJ/ieh7Eg/8fIACc+DPtFrgzyCcH/6QAHPUE9nblQ==} + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@photo-sphere-viewer/core@5.14.1': + resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==} + + '@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1': + resolution: {integrity: sha512-QHd9y5cIFXAAZInbKbh+nUz5uzSRTiR8HYApm+ONlDu8JHAp410xNhB1vNm2Q1mVgg8IigQpD9Za5mPq10fESA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0': - resolution: {integrity: sha512-Ez88sZ4sj3fONpZSortnN3gLXlvV/hn5U/88LsWtxI73YwhkZ06ZtXFYLXU4MBaJvqCbMGaR6j39uVXTWFo5rw==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.1': + resolution: {integrity: sha512-rZ6igEy1TEfgHB8Ak/8N0rZNYQLbNEGLVmhwNxDMWESCJ9nrNx3tJHFn7k6eZYjj9zJA73xF5YdY6XWUCpZDzg==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/video-plugin': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/video-plugin': 5.14.1 - '@photo-sphere-viewer/markers-plugin@5.14.0': - resolution: {integrity: sha512-w7txVHtLxXMS61m0EbNjgvdNXQYRh6Aa0oatft5oruKgoXLg/UlCu1mG6Btg+zrNsG05W2zl4gRM3fcWoVdneA==} + '@photo-sphere-viewer/markers-plugin@5.14.1': + resolution: {integrity: sha512-tKMrVem19sZFVQwH6IlubEIucDD2EtwxzmWClHCEojM/+ajucuTDvO2N+I6HEqJClBcNsdHAUwA/zyY6MGOu2Q==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/resolution-plugin@5.14.0': - resolution: {integrity: sha512-PvDMX1h+8FzWdySxiorQ2bSmyBGTPsZjNNFRBqIfmb5C+01aWCIE7kuXodXGHwpXQNcOojsVX9IiX0Vz4CiW4A==} + '@photo-sphere-viewer/resolution-plugin@5.14.1': + resolution: {integrity: sha512-OiNie5psqEFSQYCSe8wIlE8slnoh2Lk7oBGEQxJXtj/j08J5E5xg46uTmKgN+lWxQd0+LM3pgY7U7tTUqeH6ZQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/settings-plugin': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/settings-plugin': 5.14.1 - '@photo-sphere-viewer/settings-plugin@5.14.0': - resolution: {integrity: sha512-sMLX4hFSE2PjiP2iUmH9qUAz6GV+UN2WX1zu/D58BBWzF3+8mV+FC9l50qxruO8qvWqqLwYysHUElHnmPPtpTg==} + '@photo-sphere-viewer/settings-plugin@5.14.1': + resolution: {integrity: sha512-urVNMe/E+uffoe1Z8oMIt0e/6Wpf5mTnSJVJ65405trQKEGAIqJ57FlpxVp4UKPulstwpa1fRw5u1C1lzdanJA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/video-plugin@5.14.0': - resolution: {integrity: sha512-jWMZBNlfwYq8Lgc8ncs3ptwHR6Yk7Wl8o1BCFYhmhoRkGZFHEjoOQj7gMPXCET+3iYXQ1TsjTh4ZCW8UUOi+pg==} + '@photo-sphere-viewer/video-plugin@5.14.1': + resolution: {integrity: sha512-7yItXiD+eS/+9lgtaE9+wXSIpdYVU0kBsBN4vNtChaoJZF3JB8WUXjYLbszKp1yhwsvZ6eNxDcMBUgYdK6CrQA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photostructure/tz-lookup@11.3.0': - resolution: {integrity: sha512-rYGy7ETBHTnXrwbzm47e3LJPKJmzpY7zXnbZhdosNU0lTGWVqzxptSjK4qZkJ1G+Kwy4F6XStNR9ZqMsXAoASQ==} + '@photostructure/tz-lookup@11.4.0': + resolution: {integrity: sha512-yrFaDbQQZVJIzpCTnoghWO8Rttu22Hg7/JkfP3CM8UKniXYzD80cuv4UAsFkzP5Z6XWceWNsQTqUJHKyGNXzLg==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -3814,8 +3932,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} engines: {node: '>=18'} hasBin: true @@ -3989,6 +4107,13 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@replit/codemirror-indentation-markers@6.5.3': + resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -3998,113 +4123,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.53.4': - resolution: {integrity: sha512-PWU3Y92H4DD0bOqorEPp1Y0tbzwAurFmIYpjcObv5axGVOtcTlB0b2UKMd2echo08MgN7jO8WQZSSysvfisFSQ==} + '@rollup/rollup-android-arm-eabi@4.55.1': + resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.53.4': - resolution: {integrity: sha512-Gw0/DuVm3rGsqhMGYkSOXXIx20cC3kTlivZeuaGt4gEgILivykNyBWxeUV5Cf2tDA2nPLah26vq3emlRrWVbng==} + '@rollup/rollup-android-arm64@4.55.1': + resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.53.4': - resolution: {integrity: sha512-+w06QvXsgzKwdVg5qRLZpTHh1bigHZIqoIUPtiqh05ZiJVUQ6ymOxaPkXTvRPRLH88575ZCRSRM3PwIoNma01Q==} + '@rollup/rollup-darwin-arm64@4.55.1': + resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.53.4': - resolution: {integrity: sha512-EB4Na9G2GsrRNRNFPuxfwvDRDUwQEzJPpiK1vo2zMVhEeufZ1k7J1bKnT0JYDfnPC7RNZ2H5YNQhW6/p2QKATw==} + '@rollup/rollup-darwin-x64@4.55.1': + resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.53.4': - resolution: {integrity: sha512-bldA8XEqPcs6OYdknoTMaGhjytnwQ0NClSPpWpmufOuGPN5dDmvIa32FygC2gneKK4A1oSx86V1l55hyUWUYFQ==} + '@rollup/rollup-freebsd-arm64@4.55.1': + resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.53.4': - resolution: {integrity: sha512-3T8GPjH6mixCd0YPn0bXtcuSXi1Lj+15Ujw2CEb7dd24j9thcKscCf88IV7n76WaAdorOzAgSSbuVRg4C8V8Qw==} + '@rollup/rollup-freebsd-x64@4.55.1': + resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.53.4': - resolution: {integrity: sha512-UPMMNeC4LXW7ZSHxeP3Edv09aLsFUMaD1TSVW6n1CWMECnUIJMFFB7+XC2lZTdPtvB36tYC0cJWc86mzSsaviw==} + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': + resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.53.4': - resolution: {integrity: sha512-H8uwlV0otHs5Q7WAMSoyvjV9DJPiy5nJ/xnHolY0QptLPjaSsuX7tw+SPIfiYH6cnVx3fe4EWFafo6gH6ekZKA==} + '@rollup/rollup-linux-arm-musleabihf@4.55.1': + resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.53.4': - resolution: {integrity: sha512-BLRwSRwICXz0TXkbIbqJ1ibK+/dSBpTJqDClF61GWIrxTXZWQE78ROeIhgl5MjVs4B4gSLPCFeD4xML9vbzvCQ==} + '@rollup/rollup-linux-arm64-gnu@4.55.1': + resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.53.4': - resolution: {integrity: sha512-6bySEjOTbmVcPJAywjpGLckK793A0TJWSbIa0sVwtVGfe/Nz6gOWHOwkshUIAp9j7wg2WKcA4Snu7Y1nUZyQew==} + '@rollup/rollup-linux-arm64-musl@4.55.1': + resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.53.4': - resolution: {integrity: sha512-U0ow3bXYJZ5MIbchVusxEycBw7bO6C2u5UvD31i5IMTrnt2p4Fh4ZbHSdc/31TScIJQYHwxbj05BpevB3201ug==} + '@rollup/rollup-linux-loong64-gnu@4.55.1': + resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.53.4': - resolution: {integrity: sha512-iujDk07ZNwGLVn0YIWM80SFN039bHZHCdCCuX9nyx3Jsa2d9V/0Y32F+YadzwbvDxhSeVo9zefkoPnXEImnM5w==} + '@rollup/rollup-linux-loong64-musl@4.55.1': + resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.53.4': - resolution: {integrity: sha512-MUtAktiOUSu+AXBpx1fkuG/Bi5rhlorGs3lw5QeJ2X3ziEGAq7vFNdWVde6XGaVqi0LGSvugwjoxSNJfHFTC0g==} + '@rollup/rollup-linux-ppc64-musl@4.55.1': + resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.53.4': - resolution: {integrity: sha512-btm35eAbDfPtcFEgaXCI5l3c2WXyzwiE8pArhd66SDtoLWmgK5/M7CUxmUglkwtniPzwvWioBKKl6IXLbPf2sQ==} + '@rollup/rollup-linux-riscv64-musl@4.55.1': + resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.53.4': - resolution: {integrity: sha512-uJlhKE9ccUTCUlK+HUz/80cVtx2RayadC5ldDrrDUFaJK0SNb8/cCmC9RhBhIWuZ71Nqj4Uoa9+xljKWRogdhA==} + '@rollup/rollup-linux-s390x-gnu@4.55.1': + resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.53.4': - resolution: {integrity: sha512-jjEMkzvASQBbzzlzf4os7nzSBd/cvPrpqXCUOqoeCh1dQ4BP3RZCJk8XBeik4MUln3m+8LeTJcY54C/u8wb3DQ==} + '@rollup/rollup-linux-x64-gnu@4.55.1': + resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.53.4': - resolution: {integrity: sha512-lu90KG06NNH19shC5rBPkrh6mrTpq5kviFylPBXQVpdEu0yzb0mDgyxLr6XdcGdBIQTH/UAhDJnL+APZTBu1aQ==} + '@rollup/rollup-linux-x64-musl@4.55.1': + resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-openharmony-arm64@4.53.4': - resolution: {integrity: sha512-dFDcmLwsUzhAm/dn0+dMOQZoONVYBtgik0VuY/d5IJUUb787L3Ko/ibvTvddqhb3RaB7vFEozYevHN4ox22R/w==} + '@rollup/rollup-openbsd-x64@4.55.1': + resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.55.1': + resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.53.4': - resolution: {integrity: sha512-WvUpUAWmUxZKtRnQWpRKnLW2DEO8HB/l8z6oFFMNuHndMzFTJEXzaYJ5ZAmzNw0L21QQJZsUQFt2oPf3ykAD/w==} + '@rollup/rollup-win32-arm64-msvc@4.55.1': + resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.53.4': - resolution: {integrity: sha512-JGbeF2/FDU0x2OLySw/jgvkwWUo05BSiJK0dtuI4LyuXbz3wKiC1xHhLB1Tqm5VU6ZZDmAorj45r/IgWNWku5g==} + '@rollup/rollup-win32-ia32-msvc@4.55.1': + resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.53.4': - resolution: {integrity: sha512-zuuC7AyxLWLubP+mlUwEyR8M1ixW1ERNPHJfXm8x7eQNP4Pzkd7hS3qBuKBR70VRiQ04Kw8FNfRMF5TNxuZq2g==} + '@rollup/rollup-win32-x64-gnu@4.55.1': + resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.53.4': - resolution: {integrity: sha512-Sbx45u/Lbb5RyptSbX7/3deP+/lzEmZ0BTSHxwxN/IMOZDZf8S0AGo0hJD5n/LQssxb5Z3B4og4P2X6Dd8acCA==} + '@rollup/rollup-win32-x64-msvc@4.55.1': + resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==} cpu: [x64] os: [win32] @@ -4143,178 +4296,6 @@ packages: '@slorber/remark-comment@1.0.0': resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} - '@smithy/abort-controller@4.2.6': - resolution: {integrity: sha512-P7JD4J+wxHMpGxqIg6SHno2tPkZbBUBLbPpR5/T1DEUvw/mEaINBMaPFZNM7lA+ToSCZ36j6nMHa+5kej+fhGg==} - engines: {node: '>=18.0.0'} - - '@smithy/config-resolver@4.4.4': - resolution: {integrity: sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.19.0': - resolution: {integrity: sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.6': - resolution: {integrity: sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.3.7': - resolution: {integrity: sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.6': - resolution: {integrity: sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.6': - resolution: {integrity: sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/is-array-buffer@4.2.0': - resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.2.6': - resolution: {integrity: sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.4.0': - resolution: {integrity: sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.16': - resolution: {integrity: sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.7': - resolution: {integrity: sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.6': - resolution: {integrity: sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.6': - resolution: {integrity: sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.4.6': - resolution: {integrity: sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.2.6': - resolution: {integrity: sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.6': - resolution: {integrity: sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.6': - resolution: {integrity: sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.6': - resolution: {integrity: sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.2.6': - resolution: {integrity: sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.1': - resolution: {integrity: sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.6': - resolution: {integrity: sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.10.1': - resolution: {integrity: sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.10.0': - resolution: {integrity: sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.6': - resolution: {integrity: sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.0': - resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.0': - resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.1': - resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-buffer-from@4.2.0': - resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.0': - resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.15': - resolution: {integrity: sha512-LiZQVAg/oO8kueX4c+oMls5njaD2cRLXRfcjlTYjhIqmwHnCwkQO5B3dMQH0c5PACILxGAQf6Mxsq7CjlDc76A==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.18': - resolution: {integrity: sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.2.6': - resolution: {integrity: sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.0': - resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.6': - resolution: {integrity: sha512-qrvXUkxBSAFomM3/OEMuDVwjh4wtqK8D2uDZPShzIqOylPst6gor2Cdp6+XrH4dyksAWq/bE2aSDYBTTnj0Rxg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.6': - resolution: {integrity: sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.7': - resolution: {integrity: sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.0': - resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@4.2.0': - resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.0': - resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} - engines: {node: '>=18.0.0'} - '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -4324,11 +4305,14 @@ packages: peerDependencies: socket.io-adapter: ^2.5.4 + '@sphinxxxx/color-conversion@2.2.2': + resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.8': - resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} peerDependencies: acorn: ^8.9.0 @@ -4337,25 +4321,28 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/enhanced-img@0.9.2': - resolution: {integrity: sha512-hAYZ8YFgYtqrQ0dXyq6rdmHBFyG+eIQnNjdIoVhqeZQEBIREXoBThkx+7FtDa6ZV35lTRaT9dgFKF4W+4LbuaQ==} + '@sveltejs/enhanced-img@0.10.2': + resolution: {integrity: sha512-HcIX7KFaLe+3ZD+GcMIlOGKODO8zb8p6I5tY8aoM9tz4GwueGyn9gILyTWZHqXYgg7PXto++ELB/q68wC9j4qw==} peerDependencies: '@sveltejs/vite-plugin-svelte': ^6.0.0 svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.49.2': - resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} + '@sveltejs/kit@2.53.3': + resolution: {integrity: sha512-tshOeBUid2v5LAblUpatIdFm5Cyykbw2EiKWOunAAX0A/oJaR7DOdC9wLR5Qqh9zUf3QUISA2m9A3suBdQSYQg==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + typescript: ^5.3.3 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + typescript: + optional: true '@sveltejs/vite-plugin-svelte-inspector@5.0.1': resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} @@ -4365,8 +4352,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@sveltejs/vite-plugin-svelte@6.2.1': - resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + '@sveltejs/vite-plugin-svelte@6.2.4': + resolution: {integrity: sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.0.0 @@ -4450,68 +4437,72 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.5': - resolution: {integrity: sha512-RvdpUcXrIz12yONzOdQrJbEnq23cOc2IHOU1eB8kPxPNNInlm4YTzZEA3zf3PusNpZZLxwArPVLCg0QsFQoTYw==} + '@swc/core-darwin-arm64@1.15.11': + resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.5': - resolution: {integrity: sha512-ufJnz3UAff/8G5OfqZZc5cTQfGtXyXVLTB8TGT0xjkvEbfFg8jZUMDBnZT/Cn0k214JhMjiLCNl0A8aY/OKsYQ==} + '@swc/core-darwin-x64@1.15.11': + resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.5': - resolution: {integrity: sha512-Yqu92wIT0FZKLDWes+69kBykX97hc8KmnyFwNZGXJlbKUGIE0hAIhbuBbcY64FGSwey4aDWsZ7Ojk89KUu9Kzw==} + '@swc/core-linux-arm-gnueabihf@1.15.11': + resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.5': - resolution: {integrity: sha512-3gR3b5V1abe/K1GpD0vVyZgqgV+ykuB5QNecDYzVroX4QuN+amCzQaNSsVM8Aj6DbShQCBTh3hGHd2f3vZ8gCw==} + '@swc/core-linux-arm64-gnu@1.15.11': + resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.5': - resolution: {integrity: sha512-Of+wmVh5h47tTpN9ghHVjfL0CJrgn99XmaJjmzWFW7agPdVY6gTDgkk6zQ6q4hcDQ7hXb0BGw6YFpuanBzNPow==} + '@swc/core-linux-arm64-musl@1.15.11': + resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] - '@swc/core-linux-x64-gnu@1.15.5': - resolution: {integrity: sha512-98kuPS0lZVgjmc/2uTm39r1/OfwKM0PM13ZllOAWi5avJVjRd/j1xA9rKeUzHDWt+ocH9mTCQsAT1jjKSq45bg==} + '@swc/core-linux-x64-gnu@1.15.11': + resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] - '@swc/core-linux-x64-musl@1.15.5': - resolution: {integrity: sha512-Rk+OtNQP3W/dZExL74LlaakXAQn6/vbrgatmjFqJPO4RZkq+nLo5g7eDUVjyojuERh7R2yhqNvZ/ZZQe8JQqqA==} + '@swc/core-linux-x64-musl@1.15.11': + resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.5': - resolution: {integrity: sha512-e3RTdJ769+PrN25iCAlxmsljEVu6iIWS7sE21zmlSiipftBQvSAOWuCDv2A8cH9lm5pSbZtwk8AUpIYCNsj2oQ==} + '@swc/core-win32-arm64-msvc@1.15.11': + resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.5': - resolution: {integrity: sha512-NmOdl6kyAw6zMz36zCdopTgaK2tcLA53NhUsTRopBc/796Fp87XdsslRHglybQ1HyXIGOQOKv2Y14IUbeci4BA==} + '@swc/core-win32-ia32-msvc@1.15.11': + resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.5': - resolution: {integrity: sha512-EPXJRf0A8eOi8woXf/qgVIWRl9yeSl0oN1ykGZNCGI7oElsfxUobJFmpJFJoVqKFfd1l0c+GPmWsN2xavTFkNw==} + '@swc/core-win32-x64-msvc@1.15.11': + resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.5': - resolution: {integrity: sha512-VRy+AEO0zqUkwV9uOgqXtdI5tNj3y3BZI+9u28fHNjNVTtWYVNIq3uYhoGgdBOv7gdzXlqfHKuxH5a9IFAvopQ==} + '@swc/core@1.15.11': + resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4532,65 +4523,69 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/node@4.2.0': + resolution: {integrity: sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==} - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-android-arm64@4.2.0': + resolution: {integrity: sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.18': - resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-arm64@4.2.0': + resolution: {integrity: sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.18': - resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-darwin-x64@4.2.0': + resolution: {integrity: sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.18': - resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-freebsd-x64@4.2.0': + resolution: {integrity: sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': + resolution: {integrity: sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': + resolution: {integrity: sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': + resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': + resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-linux-x64-musl@4.2.0': + resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + '@tailwindcss/oxide-wasm32-wasi@4.2.0': + resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4601,24 +4596,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': + resolution: {integrity: sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} - engines: {node: '>= 10'} + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': + resolution: {integrity: sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.18': - resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} - engines: {node: '>= 10'} + '@tailwindcss/oxide@4.2.0': + resolution: {integrity: sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==} + engines: {node: '>= 20'} - '@tailwindcss/vite@4.1.18': - resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + '@tailwindcss/vite@4.2.0': + resolution: {integrity: sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 @@ -4630,8 +4625,14 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - '@testing-library/svelte@5.2.9': - resolution: {integrity: sha512-p0Lg/vL1iEsEasXKSipvW9nBCtItQGhYvxL8OZ4w7/IDdC+LGoSJw4mMS5bndVFON/gWryitEhMr29AlO4FvBg==} + '@testing-library/svelte-core@1.0.0': + resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} + engines: {node: '>=16'} + peerDependencies: + svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 + + '@testing-library/svelte@5.3.1': + resolution: {integrity: sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==} engines: {node: '>= 10'} peerDependencies: svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 @@ -4649,29 +4650,25 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tokenizer/inflate@0.3.1': - resolution: {integrity: sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/once@2.0.0': - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@turf/boolean-point-in-polygon@7.3.1': - resolution: {integrity: sha512-BUPW63vE43LctwkgannjmEFTX1KFR/18SS7WzFahJWK1ZoP0s1jrfxGX+pi0BH/3Dd9mA71hkGKDDnj1Ndcz0g==} + '@turf/boolean-point-in-polygon@7.3.2': + resolution: {integrity: sha512-PAfPDQ0TW1+VLgZ7tReTSyZ/X41AW7/nMRQxVpY+h/aG7JomZJ779lojnODT4dWCn3IMTA3xD2dDDfVYBAQMYg==} - '@turf/helpers@7.3.1': - resolution: {integrity: sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==} + '@turf/helpers@7.3.2': + resolution: {integrity: sha512-5HFN42rgWjSobdTMxbuq+ZdXPcqp1IbMgFYULTLCplEQM3dXhsyRFe7DCss4Eiw12iW3q6Z5UeTNVfITsE5lgA==} - '@turf/invariant@7.3.1': - resolution: {integrity: sha512-IdZJfDjIDCLH+Gu2yLFoSM7H23sdetIo5t4ET1/25X8gi3GE2XSqbZwaGjuZgNh02nisBewLqNiJs2bo+hrqZA==} + '@turf/invariant@7.3.2': + resolution: {integrity: sha512-brGmL1EFhZH/YNXhq6S+8sPWBEnmvEyxMWJO8bUNOFZyWHYiRTwxQHZM+An1blkbQ77PiEzsdNAspZqE1j7YKA==} '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -4741,6 +4738,99 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -4750,8 +4840,8 @@ packages: '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} - '@types/dockerode@3.3.47': - resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} '@types/dom-to-image@2.6.7': resolution: {integrity: sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==} @@ -4762,6 +4852,9 @@ packages: '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -4789,9 +4882,6 @@ packages: '@types/fluent-ffmpeg@2.1.28': resolution: {integrity: sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==} - '@types/geojson-vt@3.2.5': - resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} - '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -4864,8 +4954,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} '@types/luxon@3.7.1': resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} @@ -4903,14 +4993,14 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.27': - resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/node@24.11.0': + resolution: {integrity: sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==} - '@types/node@24.10.4': - resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} - '@types/nodemailer@7.0.4': - resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} + '@types/nodemailer@7.0.10': + resolution: {integrity: sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==} '@types/oidc-provider@9.5.0': resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} @@ -4918,8 +5008,8 @@ packages: '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} - '@types/pg-pool@2.0.6': - resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} @@ -4954,8 +5044,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.7': - resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5011,6 +5101,9 @@ packages: '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ua-parser-js@0.7.39': resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} @@ -5035,63 +5128,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.50.0': - resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} + '@typescript-eslint/eslint-plugin@8.56.0': + resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.50.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/parser': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.50.0': - resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==} + '@typescript-eslint/parser@8.56.0': + resolution: {integrity: sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.50.0': - resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==} + '@typescript-eslint/project-service@8.56.0': + resolution: {integrity: sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.50.0': - resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==} + '@typescript-eslint/scope-manager@8.56.0': + resolution: {integrity: sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.50.0': - resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==} + '@typescript-eslint/tsconfig-utils@8.56.0': + resolution: {integrity: sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.50.0': - resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==} + '@typescript-eslint/type-utils@8.56.0': + resolution: {integrity: sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.50.0': - resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==} + '@typescript-eslint/types@8.56.0': + resolution: {integrity: sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.50.0': - resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==} + '@typescript-eslint/typescript-estree@8.56.0': + resolution: {integrity: sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.50.0': - resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==} + '@typescript-eslint/utils@8.56.0': + resolution: {integrity: sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.50.0': - resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==} + '@typescript-eslint/visitor-keys@8.56.0': + resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5190,18 +5283,14 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zoom-image/core@0.41.4': - resolution: {integrity: sha512-zUJNHWQzx8rmfNOlp2Rr0+n8I7QK9hLNThnusdtvz20/HN+J//RcDJmCuRDj6jUW/qJGh9FWR5sROMFBuPLPfQ==} + '@zoom-image/core@0.42.0': + resolution: {integrity: sha512-aF7siQqxqmOVlBd65deaCM7L/6V80Rp7HazZJpxtErh8zAn5itXXKBv1KA1NufSPfRZsXl1QtysxkjB3gVIzxw==} - '@zoom-image/svelte@0.3.8': - resolution: {integrity: sha512-rkXS+JS4qkBccmRK9+I5j+Pe4rp78GWK/7y0EduBJNtt38q+AwmKhhQs8oTMKTU6lOzLgxjXy1TI802mtvcAmw==} + '@zoom-image/svelte@0.3.9': + resolution: {integrity: sha512-27Nze2f0W7Jop12imiWYvZGqiAlmQbBCqMVJPtUvmaBdv2KY4BhrSe4k7pBJaQId5dMF9SwUPo7obrtm9dCzuQ==} peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 - abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - deprecated: Use your platform's native atob() and btoa() methods instead - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -5221,9 +5310,6 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn-globals@7.0.1: - resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: @@ -5244,8 +5330,8 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true @@ -5297,12 +5383,15 @@ packages: peerDependencies: ajv: ^8.8.2 - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + algoliasearch-helper@3.26.1: resolution: {integrity: sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw==} peerDependencies: @@ -5390,6 +5479,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -5430,9 +5523,6 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} - async-mutex@0.5.0: - resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} - async@0.2.10: resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==} @@ -5445,8 +5535,8 @@ packages: autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} - autoprefixer@10.4.23: - resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -5498,6 +5588,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + bare-events@2.8.2: resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} peerDependencies: @@ -5506,8 +5600,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.5.2: - resolution: {integrity: sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==} + bare-fs@4.5.4: + resolution: {integrity: sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -5522,8 +5616,8 @@ packages: bare-path@3.0.0: resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - bare-stream@2.7.0: - resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + bare-stream@2.8.0: + resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==} peerDependencies: bare-buffer: '*' bare-events: '*' @@ -5543,12 +5637,12 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.9.7: - resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true - batch-cluster@16.0.0: - resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==} + batch-cluster@17.3.1: + resolution: {integrity: sha512-/aWEgZKXgvEseV3WEIRyjDoFka9FTrpt5+FYCxn+giUgveGBKxWjz3cl26V3aD+1kvOBP3nmANZZfcXDmKzcAA==} engines: {node: '>=20'} batch@0.6.1: @@ -5571,8 +5665,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bits-ui@2.14.4: - resolution: {integrity: sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==} + bits-ui@2.16.0: + resolution: {integrity: sha512-utsUZE7W7MxOQF1jmSYfzUrt2nZxgkq0yPqQcBQ0WQDMq8ETd1yEiHlPpqhMrpKU7IivjSf4XVysDDy+UVkMUw==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -5585,8 +5679,8 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} bonjour-service@1.3.0: @@ -5595,9 +5689,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.13.1: - resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} - boxen@6.2.1: resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5612,6 +5703,10 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brace-expansion@5.0.3: + resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -5645,8 +5740,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.66.0: - resolution: {integrity: sha512-LSe8yEiVTllOOq97Q0C/EhczKS5Yd0AUJleGJCIh0cyJE5nWUqEpGC/uZQuuAYniBSoMT8LqwrxE7N5MZVrLoQ==} + bullmq@5.69.3: + resolution: {integrity: sha512-P9uLsR7fDvejH/1m6uur6j7U9mqY6nNt+XvhlhStOUe7jdwbZoP/c2oWNtE+8ljOlubw4pRUKymtRqkyvloc4A==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -5731,8 +5826,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001774: + resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} canvas@2.11.2: resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==} @@ -5786,6 +5881,14 @@ packages: resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} engines: {node: '>= 6'} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5820,8 +5923,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -5900,6 +6003,13 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + codemirror-wrapped-line-indent@1.0.9: + resolution: {integrity: sha512-oc976hHLt35u6Ojbhub+IWOxEpapZSqYieLEdGhsgFZ4rtYQtdb5KjxzgjCCyVe3t0yk+a6hmaIOEsjU/tZRxQ==} + peerDependencies: + '@codemirror/language': ^6.9.0 + '@codemirror/state': ^6.2.1 + '@codemirror/view': ^6.17.1 + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -5950,6 +6060,10 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -5998,6 +6112,9 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} @@ -6089,10 +6206,16 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -6115,12 +6238,15 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} - cron@4.3.5: - resolution: {integrity: sha512-hKPP7fq1+OfyCqoePkKfVq7tNAdFwiQORr4lZUHwrf0tebC65fYEeWgOrXOL6prn1/fegGOdTfrM6e34PJfksg==} + cron@4.4.0: + resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} engines: {node: '>=18.x'} cross-spawn@7.0.6: @@ -6216,9 +6342,6 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - csscolorparser@1.0.3: - resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} - cssdb@8.5.2: resolution: {integrity: sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==} @@ -6255,34 +6378,179 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssom@0.3.8: - resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} - - cssom@0.5.0: - resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} - - cssstyle@2.3.0: - resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} - engines: {node: '>=8'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + d3-geo@3.1.1: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} - data-urls@3.0.2: - resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} - engines: {node: '>=12'} + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -6384,6 +6652,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6414,6 +6685,11 @@ packages: detect-europe-js@0.1.2: resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -6426,8 +6702,8 @@ packages: engines: {node: '>= 4.0.0'} hasBin: true - devalue@5.6.1: - resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + devalue@5.6.3: + resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -6441,6 +6717,10 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -6462,8 +6742,8 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} - docker-compose@1.3.0: - resolution: {integrity: sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==} + docker-compose@1.3.1: + resolution: {integrity: sha512-rF0wH69G3CCcmkN9J1RVMQBaKe8o77LT/3XmqcLIltWWVxcWAzp2TnO7wS3n/umZHN3/EVrlT3exSBMal+Ou1w==} engines: {node: '>= 6.0.0'} docker-modem@5.0.6: @@ -6503,11 +6783,6 @@ packages: domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - domexception@4.0.0: - resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} - engines: {node: '>=12'} - deprecated: Use your platform's native DOMException instead - domhandler@4.3.1: resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} engines: {node: '>= 4'} @@ -6516,6 +6791,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} @@ -6529,8 +6807,8 @@ packages: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} engines: {node: '>=10'} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -6540,9 +6818,6 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - earcut@2.2.4: - resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} - earcut@3.0.2: resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} @@ -6555,8 +6830,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -6587,19 +6862,19 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - engine.io-client@6.6.3: - resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - engine.io@6.6.4: - resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engine.io@6.6.5: + resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} entities@2.2.0: @@ -6613,6 +6888,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -6634,6 +6913,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -6672,8 +6954,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.1: - resolution: {integrity: sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -6700,25 +6982,20 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-plugin-compat@6.0.2: - resolution: {integrity: sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==} + eslint-plugin-compat@6.2.0: + resolution: {integrity: sha512-Ihz4zAeHKzyksDDUTObrYQxaqnV/pFlAiZoWkMuWM9XGf4O191ReQFYv516zcs9QVJ2vX+MMpqr1yEfTkXVETQ==} engines: {node: '>=18.x'} peerDependencies: - eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 - eslint-plugin-prettier@5.5.4: - resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -6731,18 +7008,18 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-svelte@3.13.1: - resolution: {integrity: sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==} + eslint-plugin-svelte@3.15.0: + resolution: {integrity: sha512-QKB7zqfuB8aChOfBTComgDptMf2yxiJx7FE04nneCmtQzgTHvY8UJkuh8J2Rz7KB9FFV9aTHX6r7rdYGvG8T9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.1 || ^9.0.0 + eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: optional: true - eslint-plugin-unicorn@62.0.0: - resolution: {integrity: sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==} + eslint-plugin-unicorn@63.0.0: + resolution: {integrity: sha512-Iqecl9118uQEXYh7adylgEmGfkn5es3/mlQTLLkd4pXkIk9CTGrAbeUux+YljSa2ohXCBmQQ0+Ej1kZaFgcfkA==} engines: {node: ^20.10.0 || >=21.0.0} peerDependencies: eslint: '>=9.38.0' @@ -6755,6 +7032,10 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.1: + resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6763,9 +7044,13 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.0.2: + resolution: {integrity: sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -6784,17 +7069,21 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@11.1.1: + resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.1: - resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} + esrap@2.2.3: + resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -6843,8 +7132,8 @@ packages: resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} engines: {node: '>=6.0.0'} - eta@4.5.0: - resolution: {integrity: sha512-qifAYjuW5AM1eEEIsFnOwB+TGqu6ynU3OKj9WbUTOtUBHFPZqL03XUW34kbp3zm19Ald+U8dEyRXaVsUck+Y1g==} + eta@4.5.1: + resolution: {integrity: sha512-EaNCGm+8XEIU7YNcc+THptWAO5NfKBHHARxt+wxZljj9bTr/+arRoOm9/MpGt4n6xn9fLnPFRSoLD0WFYGFUxQ==} engines: {node: '>=20'} etag@1.8.1: @@ -6880,17 +7169,17 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exiftool-vendored.exe@13.44.0: - resolution: {integrity: sha512-PzQrrz9k4YzxtcX1r/hEy+xzj6MKXXiEBCU+FhYlipr4fuKKeXgspB7kliPSfSSFAChYoSH294zd23ZUXgB4TQ==} + exiftool-vendored.exe@13.51.0: + resolution: {integrity: sha512-Q49J2c4e+XSGYDJf9PYMVI/IUfUkHLRsPUeDJ2ZekEBVLuw2g7ye9x0vQGWZKwEeZTlnXol7SeBJB0wtAmzM9w==} os: [win32] - exiftool-vendored.pl@13.44.0: - resolution: {integrity: sha512-KPqyZK5guU/HKJ4x7OdxC0bqwClz34AtQYeirvvGFBjvfpG6Ewt+Kx9TEd/JbvJyLgMS5k5GHvkH5R5iAL+Arg==} + exiftool-vendored.pl@13.51.0: + resolution: {integrity: sha512-RhDM10w4kv5YNCvECj0aLXZXi0UWyzVo2OS4P/hpmyCHL+NGCkZ6N9z/Yc3ek0cEfCj4AiLhe8C96pnz/Fw9Yg==} os: ['!win32'] hasBin: true - exiftool-vendored@34.1.0: - resolution: {integrity: sha512-piPUu8oaBT0JQcR0gH/ZLjnTrHu51lMs+GjAQPrtVyvt8bfB1vzTWYbw+9jN4qyO8HwCweJuj7xivmWS0/fa/A==} + exiftool-vendored@35.10.1: + resolution: {integrity: sha512-orD61HdNcdlegfD80wI+3JE/n+iobYPztpFqv2drLHb1rb2QEKR1QY62r+O0wZHHNIf3Bje+xjweS1hxWignQA==} engines: {node: '>=20.0.0'} expect-type@1.3.0: @@ -6904,10 +7193,6 @@ packages: resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} - express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -6925,9 +7210,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - fabric@6.9.1: - resolution: {integrity: sha512-TqG08Xbt4rtlPsXgCjSUcZz/RsyEP57Qo21nCVRkw7zz9nR0co4SLkL9Q/zQh3tC1Yxap6M5jKFHUKV6SgPovg==} - engines: {node: '>=16.20.0'} + fabric@7.2.0: + resolution: {integrity: sha512-XSYmSqSMrlbCg+/j7/uU/PFeZuA5hHRDp7sGbDlMvz/T6BHt2MQSOYtz/AIdr+kmReA1s5jTzHJ8AjHwYUcmfQ==} + engines: {node: '>=20.0.0'} factory.ts@1.4.2: resolution: {integrity: sha512-8x2hqK1+EGkja4Ah8H3nkP7rDUJsBK1N3iFDqzqsaOV114o2IphSdVkFIw9nDHHr37gFFy2NXeN6n10ieqHzZg==} @@ -6961,12 +7246,8 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@5.2.5: - resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} - hasBin: true - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fault@2.0.1: resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} @@ -7008,8 +7289,8 @@ packages: file-source@0.6.1: resolution: {integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==} - file-type@21.1.0: - resolution: {integrity: sha512-boU4EHmP3JXkwDo4uhyBhTt5pPstxB6eEXKJBu2yu2l7aAMMm7QQYQEzssJmKReZYrFdFOJS8koVo6bXIBGDqA==} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} fill-range@7.1.1: @@ -7114,6 +7395,9 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -7164,20 +7448,14 @@ packages: geo-coordinates-parser@1.7.4: resolution: {integrity: sha512-gVGxBW+s1csexXVMf5bIwz3TH9n4sCEglOOOqmrPk8YazUI5f79jCowKjTw05m/0h1//3+Z2m/nv8IIozgZyUw==} - geo-tz@8.1.4: - resolution: {integrity: sha512-xayeOC05wgy6JATU/k7GFHTMfSimzL1Fi3KSzt2GqvEnP1ZFXyQ9V4VAiTrTYhZSmRr0dbchZkximSegHZNUfA==} + geo-tz@8.1.5: + resolution: {integrity: sha512-C0g6Zyo/4/wtaONcprVq6gHq4LnbheC7HXXi0nZMG8lbxqvOj8IZcTolCd0MeOmBekXnyXKKeDlh6g2o4Yy3qw==} engines: {node: '>=16'} geobuf@3.0.2: resolution: {integrity: sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==} hasBin: true - geojson-vt@3.2.1: - resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} - - geojson-vt@4.0.2: - resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} - geojson@0.5.0: resolution: {integrity: sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==} engines: {node: '>= 0.10'} @@ -7209,6 +7487,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + github-slugger@1.5.0: resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} @@ -7234,29 +7515,31 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} + glob@13.0.2: + resolution: {integrity: sha512-035InabNu/c1lW0tzPhAgapKctblppqsKKG9ZaNzbr+gXwWMjXoiyGSyB9sArzrjG7jY+zntRq5ZSUYemrnWVQ==} + engines: {node: 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -7265,6 +7548,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + globals@17.3.0: + resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + engines: {node: '>=18'} + globalyzer@0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} @@ -7297,13 +7584,13 @@ packages: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} - grid-index@1.1.0: - resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} - gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} @@ -7312,8 +7599,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.0.11: - resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} + happy-dom@20.6.3: + resolution: {integrity: sha512-QAMY7d228dHs8gb9NG4SJ3OxQo4r+NGN8pOXGZ3SGfQf/XYuuYubrtZ25QVY2WoUQdskhRXSXb4R4mcRk+hV1w==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -7414,9 +7701,9 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -7485,10 +7772,6 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -7538,8 +7821,8 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.1: - resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} icss-utils@5.1.0: @@ -7571,12 +7854,18 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} + immutable-json-patch@6.0.2: + resolution: {integrity: sha512-KwCA5DXJiyldda8SPha1zB+6+vbEi5/jRRcYii/6yFXlyu9ZjiSH/wPq8Ri2Hk8iGjjTMcHW3Z21S4MOpl7sOw==} + + immutable@5.1.4: + resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-in-the-middle@2.0.0: - resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} @@ -7622,6 +7911,9 @@ packages: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -7629,11 +7921,18 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + intl-messageformat@11.1.2: + resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.8.2: - resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} + ioredis@5.9.2: + resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} + engines: {node: '>=12.22.0'} + + ioredis@5.9.3: + resolution: {integrity: sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==} engines: {node: '>=12.22.0'} ip-address@10.1.0: @@ -7829,9 +8128,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} isobject@3.0.1: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} @@ -7888,6 +8187,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jmespath@0.16.0: + resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} + engines: {node: '>= 0.6.0'} + joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -7911,15 +8214,19 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@20.0.3: - resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} - engines: {node: '>=14'} + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} peerDependencies: canvas: 2.11.2 peerDependenciesMeta: canvas: optional: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -7940,6 +8247,9 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-source-map@0.6.1: + resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -7957,6 +8267,15 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpath-plus@10.3.0: + resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==} + engines: {node: '>=18.0.0'} + hasBin: true + + jsonrepair@3.13.1: + resolution: {integrity: sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==} + hasBin: true + jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -7973,8 +8292,9 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - kdbush@3.0.0: - resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} + katex@0.16.27: + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} + hasBin: true kdbush@4.0.2: resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} @@ -7982,11 +8302,13 @@ packages: keygrip@1.1.0: resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} engines: {node: '>= 0.6'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -8019,10 +8341,18 @@ packages: postgres: optional: true + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + kysely@0.28.2: resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} engines: {node: '>=18.0.0'} + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + latest-version@7.0.0: resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} engines: {node: '>=14.16'} @@ -8030,6 +8360,12 @@ packages: launch-editor@2.12.0: resolution: {integrity: sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -8048,74 +8384,78 @@ packages: libphonenumber-js@1.12.31: resolution: {integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==} - lightningcss-android-arm64@1.30.2: - resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + lightningcss-android-arm64@1.31.1: + resolution: {integrity: sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.30.2: - resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + lightningcss-darwin-arm64@1.31.1: + resolution: {integrity: sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.2: - resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + lightningcss-darwin-x64@1.31.1: + resolution: {integrity: sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.2: - resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + lightningcss-freebsd-x64@1.31.1: + resolution: {integrity: sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.2: - resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + lightningcss-linux-arm-gnueabihf@1.31.1: + resolution: {integrity: sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.2: - resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + lightningcss-linux-arm64-gnu@1.31.1: + resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] - lightningcss-linux-arm64-musl@1.30.2: - resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + lightningcss-linux-arm64-musl@1.31.1: + resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] - lightningcss-linux-x64-gnu@1.30.2: - resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + lightningcss-linux-x64-gnu@1.31.1: + resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] - lightningcss-linux-x64-musl@1.30.2: - resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + lightningcss-linux-x64-musl@1.31.1: + resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] - lightningcss-win32-arm64-msvc@1.30.2: - resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + lightningcss-win32-arm64-msvc@1.31.1: + resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.2: - resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + lightningcss-win32-x64-msvc@1.31.1: + resolution: {integrity: sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.2: - resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + lightningcss@1.31.1: + resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} lilconfig@2.1.0: @@ -8163,6 +8503,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -8196,17 +8539,14 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -8243,8 +8583,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -8288,12 +8628,8 @@ packages: resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} engines: {node: ^20.17.0 || >=22.9.0} - mapbox-gl@1.13.3: - resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} - engines: {node: '>=6.4.0'} - - maplibre-gl@5.14.0: - resolution: {integrity: sha512-O2ok6N/bQ9NA9nJ22r/PRQQYkUe9JwfDMjBPkQ+8OwsVH4TpA5skIAM2wc0k+rni5lVbAVONVyBvgi1rF2vEPA==} + maplibre-gl@5.18.0: + resolution: {integrity: sha512-UtWxPBpHuFvEkM+5FVfcFG9ZKEWZQI6+PZkvLErr8Zs5ux+O7/KQ3JjSUvAfOlMeMgd/77qlHpOw0yHL7JU5cw==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} mark.js@8.11.1: @@ -8319,6 +8655,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@17.0.3: + resolution: {integrity: sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -8398,6 +8739,9 @@ packages: memfs@4.51.1: resolution: {integrity: sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} @@ -8416,6 +8760,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + mermaid@11.12.2: + resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -8614,9 +8961,9 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} + minimatch@10.2.2: + resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -8625,8 +8972,8 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + minimatch@9.0.6: + resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==} engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: @@ -8636,8 +8983,8 @@ packages: resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} engines: {node: '>=16 || 14 >=14.17'} - minipass-fetch@5.0.0: - resolution: {integrity: sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==} + minipass-fetch@5.0.1: + resolution: {integrity: sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==} engines: {node: ^20.17.0 || >=22.9.0} minipass-flush@1.0.5: @@ -8648,8 +8995,8 @@ packages: resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} engines: {node: '>=8'} - minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + minipass-sized@2.0.0: + resolution: {integrity: sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==} engines: {node: '>=8'} minipass@3.3.6: @@ -8688,6 +9035,14 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mnemonist@0.40.3: resolution: {integrity: sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==} @@ -8726,6 +9081,10 @@ packages: resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} engines: {node: '>= 10.16.0'} + multer@2.1.0: + resolution: {integrity: sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==} + engines: {node: '>= 10.16.0'} + multicast-dns@7.2.5: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true @@ -8743,8 +9102,8 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nan@2.24.0: - resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} + nan@2.25.0: + resolution: {integrity: sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -8756,6 +9115,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -8821,6 +9183,9 @@ packages: node-addon-api@4.3.0: resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-addon-api@8.5.0: resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} engines: {node: ^18 || ^20 || >= 21} @@ -8853,16 +9218,16 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@12.1.0: - resolution: {integrity: sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==} + node-gyp@12.2.0: + resolution: {integrity: sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nodemailer@7.0.11: - resolution: {integrity: sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==} + nodemailer@7.0.13: + resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -8921,8 +9286,8 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth4webapi@3.8.3: - resolution: {integrity: sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==} + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -8950,8 +9315,11 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - oidc-provider@9.6.0: - resolution: {integrity: sha512-CCRUYPOumEy/DT+L86H40WgXjXfDHlsJYZdyd4ZKGFxJh/kAd7DxMX3dwpbX0g+WjB+NWU+kla1b/yZmHNcR0Q==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + oidc-provider@9.6.1: + resolution: {integrity: sha512-8AtFXE4gEV6MLd8Re78VhqGNjBm/SUw0fUxrP2XwQc+5DZKw6GyuTuy2M4jkidpH3jRrhtkkqQpXlxD1Awi6tg==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -8984,8 +9352,8 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - openid-client@6.8.1: - resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==} + openid-client@6.8.2: + resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -9062,6 +9430,9 @@ packages: resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} engines: {node: '>=14.16'} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -9101,6 +9472,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -9168,30 +9542,30 @@ packages: peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - pg-cloudflare@1.2.7: - resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.9.1: - resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.10.1: - resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} peerDependencies: pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg@8.16.3: - resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9229,16 +9603,19 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} engines: {node: '>=18'} hasBin: true @@ -9249,8 +9626,8 @@ packages: pmtiles@3.2.1: resolution: {integrity: sha512-3R4fBwwoli5mw7a6t1IGwOtfmcSAODq6Okz0zkXhS1zi9sz1ssjjIfslwPvcWw5TNhdjNBUg9fgfPLeqZlH6ng==} - pmtiles@4.3.0: - resolution: {integrity: sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA==} + pmtiles@4.4.0: + resolution: {integrity: sha512-tCLI1C5134MR54i8izUWhse0QUtO/EC33n9yWp1N5dYLLvyc197U0fkF5gAJhq1TdWO9Tvl+9hgvFvM0fR27Zg==} pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} @@ -9263,6 +9640,12 @@ packages: point-in-polygon-hao@1.2.4: resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postcss-attribute-case-insensitive@7.0.1: resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==} engines: {node: '>=18'} @@ -9718,8 +10101,8 @@ packages: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} engines: {node: '>=0.10.0'} postgres-date@1.0.7: @@ -9730,13 +10113,10 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres@3.4.7: - resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + postgres@3.4.8: + resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} - potpack@1.0.2: - resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} - potpack@2.1.0: resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} @@ -9744,8 +10124,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} prettier-plugin-organize-imports@4.3.0: @@ -9758,20 +10138,20 @@ packages: vue-tsc: optional: true - prettier-plugin-sort-json@4.1.1: - resolution: {integrity: sha512-uJ49wCzwJ/foKKV4tIPxqi4jFFvwUzw4oACMRG2dcmDhBKrxBv0L2wSKkAqHCmxKCvj0xcCZS4jO2kSJO/tRJw==} + prettier-plugin-sort-json@4.2.0: + resolution: {integrity: sha512-jK1w3/7otTvHtv1eoLji2U9mEoOGeyl7QQQ/afLnjht1YtRLSUUk8o0rIIC/HUVXhoGPCFe4SVZbRGYjjUVgvA==} engines: {node: '>=18.0.0'} peerDependencies: prettier: ^3.0.0 - prettier-plugin-svelte@3.4.1: - resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} + prettier-plugin-svelte@3.5.0: + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.7.4: - resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -9820,9 +10200,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - properties-reader@2.3.0: - resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} - engines: {node: '>=14'} + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} property-information@5.6.0: resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} @@ -9837,6 +10217,10 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + protobufjs@8.0.0: + resolution: {integrity: sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==} + engines: {node: '>=12.0.0'} + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -9844,9 +10228,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - psl@1.15.0: - resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} - pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -9866,13 +10247,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + qs@6.14.1: + resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9884,9 +10262,6 @@ packages: resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==} engines: {node: '>=18'} - quickselect@2.0.0: - resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} - quickselect@3.0.0: resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} @@ -9931,10 +10306,10 @@ packages: peerDependencies: react: ^18.3.1 - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.3 + react: ^19.2.4 react-email@4.3.2: resolution: {integrity: sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA==} @@ -9986,8 +10361,8 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -10150,6 +10525,9 @@ packages: resolve-pathname@3.0.0: resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} @@ -10211,15 +10589,21 @@ packages: rollup: optional: true - rollup@4.53.4: - resolution: {integrity: sha512-YpXaaArg0MvrnJpvduEDYIp7uGOqKXbH9NsHGQ6SxKCOsNAjZF018MmxefFUulVP2KLtiGw1UvZbr+/ekjvlDg==} + rollup@4.55.1: + resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + rtlcss@4.3.0: resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} engines: {node: '>=12.0.0'} @@ -10270,8 +10654,13 @@ packages: sanitize-filename@1.6.3: resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} - sanitize-html@2.17.0: - resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + sanitize-html@2.17.1: + resolution: {integrity: sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw==} + + sass@1.97.1: + resolution: {integrity: sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==} + engines: {node: '>=14.0.0'} + hasBin: true sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -10322,8 +10711,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -10356,8 +10745,8 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.0.1: + resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -10432,6 +10821,10 @@ packages: resolution: {integrity: sha512-i/w5Ie4tENfGYbdCo2iJ+oies0vOFd8QXWHopKOUzudfLCvnmeheF2PpHp89Z2azpc+c2su3lMiWO/SpP+429A==} engines: {node: '>=0.12.18'} + simple-icons@16.9.0: + resolution: {integrity: sha512-aKst2C7cLkFyaiQ/Crlwxt9xYOpGPk05XuJZ0ZTJNNCzHCKYrGWz2ebJSi5dG8CmTCxUF/BGs6A8uyJn/EQxqw==} + engines: {node: '>=0.12.18'} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -10470,19 +10863,19 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - socket.io-adapter@2.5.5: - resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} - socket.io-client@4.8.1: - resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} engines: {node: '>=10.0.0'} - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} engines: {node: '>=10.0.0'} - socket.io@4.8.1: - resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} sockjs@0.3.24: @@ -10542,8 +10935,8 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-formatter@15.6.12: - resolution: {integrity: sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ==} + sql-formatter@15.7.2: + resolution: {integrity: sha512-b0BGoM81KFRVSpZFwPpIPU5gng4YD8DI/taLD96NXCFRf5af3FzSE4aSwjKmxcyTmf/MfPu91j75883nRrWDBw==} hasBin: true srcset@4.0.0: @@ -10557,8 +10950,8 @@ packages: resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} engines: {node: '>=10.16.0'} - ssri@13.0.0: - resolution: {integrity: sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==} + ssri@13.0.1: + resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==} engines: {node: ^20.17.0 || >=22.9.0} stackback@0.0.2: @@ -10656,13 +11049,13 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.1.2: - resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} - strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -10675,23 +11068,23 @@ packages: peerDependencies: postcss: ^8.4.31 + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superagent@10.2.3: - resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} - supercluster@7.1.5: - resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} - supercluster@8.0.1: resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} - supertest@7.1.4: - resolution: {integrity: sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==} + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} supports-color@7.2.0: @@ -10706,8 +11099,13 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-check@4.3.4: - resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} + svelte-awesome@3.3.5: + resolution: {integrity: sha512-RIi+OI6CEn+fTdYy7UOgImEUWvdQSwP9SiMC44UKyFO+8+gjj+NgTG67hI8j2rTHQVvCP820Uj+4UoZG8CCUfA==} + peerDependencies: + svelte: '>= 3.43.1 < 6' + + svelte-check@4.4.1: + resolution: {integrity: sha512-y1bBT0CRCMMfdjyqX1e5zCygLgEEr4KJV1qP6GSUReHl90bmcQaAWjZygHPfQ8K63f1eR8IuivuZMwmCg3zT2Q==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -10723,6 +11121,9 @@ packages: svelte: optional: true + svelte-floating-ui@1.5.8: + resolution: {integrity: sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==} + svelte-gestures@5.2.2: resolution: {integrity: sha512-Y+chXPaSx8OsPoFppUwPk8PJzgrZ7xoDJKXeiEc7JBqyKKzXer9hlf8F9O34eFuAWB4/WQEvccACvyBplESL7A==} @@ -10736,8 +11137,13 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 - svelte-maplibre@1.2.5: - resolution: {integrity: sha512-Uklcbi6inW9GA0MuSusbXmFr/MQPmXrjuP8hS1+yFX3ySvCQ477tsM3I7Jo/fUDK3XAxFSIHW6hZfucnM3kXwQ==} + svelte-jsoneditor@3.11.0: + resolution: {integrity: sha512-OypU/0ALZQPXc4wZWSokNGdkKPI5SZBbtsjhUmFuF3hq6Gjk6ll95mWPV4ckW/Wr4M53k7zuSLCqOHZCS7PZyw==} + peerDependencies: + svelte: ^5.0.0 + + svelte-maplibre@1.2.6: + resolution: {integrity: sha512-NntxiZptS07HwblUxIkDllAeBSj6DTyEtECkOqxEi3e/uam7Qunkd/Cp535NN1K7eIx5MLs4cyAa8jgPDgGLFw==} peerDependencies: '@deck.gl/core': ^9 '@deck.gl/layers': ^9 @@ -10762,14 +11168,17 @@ packages: peerDependencies: svelte: ^3.48.0 || ^4 || ^5 + svelte-select@5.8.3: + resolution: {integrity: sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA==} + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} peerDependencies: svelte: ^5.30.2 - svelte@5.43.3: - resolution: {integrity: sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==} + svelte@5.53.5: + resolution: {integrity: sha512-YkqERnF05g8KLdDZwZrF8/i1eSbj6Eoat8Jjr2IfruZz9StLuBqo8sfCSzjosNKd+ZrQ8DkKZDjpO5y3ht1Pow==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -10780,8 +11189,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - swagger-ui-dist@5.30.2: - resolution: {integrity: sha512-HWCg1DTNE/Nmapt+0m2EPXFwNKNeKK4PwMjkwveN/zn1cV2Kxi9SURd+m0SpdcSgWEK/O64sf8bzXdtUhigtHA==} + swagger-ui-dist@5.31.0: + resolution: {integrity: sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==} swr@2.3.8: resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} @@ -10795,8 +11204,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.11: - resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} systeminformation@5.23.8: @@ -10805,8 +11214,8 @@ packages: os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true - tabbable@6.3.0: - resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -10843,8 +11252,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.1.18: - resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tailwindcss@4.2.0: + resolution: {integrity: sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} @@ -10866,10 +11275,15 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} @@ -10896,8 +11310,8 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - testcontainers@11.10.0: - resolution: {integrity: sha512-8hwK2EnrOZfrHPpDC7CPe03q7H8Vv8j3aXdcmFFyNV8dzpBzgZYmqyDtduJ8YQ5kbzj+A+jUXMQ6zI8B5U3z+g==} + testcontainers@11.12.0: + resolution: {integrity: sha512-VWtH+UQejVYYvb53ohEZRbx2naxyDvwO9lQ6A0VgmVE2Oh8r9EF09I+BfmrXpd9N9ntpzhao9di2yNwibSz5KA==} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -10957,6 +11371,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -10965,9 +11383,6 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} - tinyqueue@2.0.3: - resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} - tinyqueue@3.0.0: resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} @@ -10979,6 +11394,13 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -10994,24 +11416,27 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - token-types@6.1.1: - resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@4.1.4: - resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} - engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@3.0.0: - resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} - engines: {node: '>=12'} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + transformation-matrix@3.1.0: + resolution: {integrity: sha512-oYubRWTi2tYFHAL2J8DLvPIqIYcYZ0fSOi2vmSy042Ho4jBW2ce6VP7QfD44t65WQz6bw5w1Pk22J7lcUpaTKA==} tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} @@ -11031,12 +11456,16 @@ packages: truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -11065,6 +11494,11 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -11101,11 +11535,11 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.50.0: - resolution: {integrity: sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==} + typescript-eslint@8.56.0: + resolution: {integrity: sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.0.0' typescript@5.9.3: @@ -11116,10 +11550,13 @@ packages: ua-is-frozen@0.1.2: resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - ua-parser-js@2.0.7: - resolution: {integrity: sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w==} + ua-parser-js@2.0.9: + resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} hasBin: true + ufo@1.6.2: + resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -11140,14 +11577,14 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.16.0: - resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -11221,10 +11658,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -11242,8 +11675,8 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.2.2: - resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -11268,9 +11701,6 @@ packages: file-loader: optional: true - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -11316,13 +11746,16 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - validator@13.15.23: - resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} value-equal@1.0.1: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + vanilla-picker@2.12.3: + resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -11345,8 +11778,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-imagetools@9.0.2: - resolution: {integrity: sha512-FV5DXw4swU81t+g8JOLT+T7gKuBOXuVsZ0WGhi7y0R182+GfBYkcf6V9/T0Nweu/vn1X0DA2p5ePMnaGZlRl1A==} + vite-imagetools@9.0.3: + resolution: {integrity: sha512-FwjApRNZyN+RucPW9Z9kf0dyzyi3r3zlDfrTnzHXNaYpmT3pZ5w//d6QkApy1iypbDm+3fq+Gwfv+PYA4j4uYw==} engines: {node: '>=20.0.0'} vite-node@3.2.4: @@ -11354,16 +11787,13 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@5.1.4: - resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + vite-tsconfig-paths@6.1.1: + resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: vite: '*' - peerDependenciesMeta: - vite: - optional: true - vite@7.3.0: - resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -11444,15 +11874,35 @@ packages: jsdom: optional: true - vt-pbf@3.1.3: - resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} - w3c-xmlserializer@4.0.0: - resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} - engines: {node: '>=14'} + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} wbuf@1.7.3: @@ -11520,8 +11970,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.103.0: - resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} + webpack@5.104.1: + resolution: {integrity: sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -11544,17 +11994,22 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} - whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} - whatwg-url@11.0.0: - resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} - engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -11571,8 +12026,8 @@ packages: engines: {node: '>= 8'} hasBin: true - which@6.0.0: - resolution: {integrity: sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==} + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} engines: {node: ^20.17.0 || >=22.9.0} hasBin: true @@ -11628,8 +12083,8 @@ packages: utf-8-validate: optional: true - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11640,8 +12095,8 @@ packages: utf-8-validate: optional: true - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -11664,9 +12119,9 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true - xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -11922,11 +12377,11 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.19(@types/node@24.10.4)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.19(@types/node@24.11.0)(chokidar@4.0.3)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@24.10.4) + '@inquirer/prompts': 7.3.2(@types/node@24.11.0) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -11954,405 +12409,21 @@ snapshots: transitivePeerDependencies: - chokidar - '@aws-crypto/sha256-browser@5.2.0': + '@antfu/install-pkg@1.1.0': dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-locate-window': 3.893.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 - '@aws-crypto/sha256-js@5.2.0': + '@asamuzakjp/css-color@3.2.0': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 - tslib: 2.8.1 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-sesv2@3.952.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-node': 3.952.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/signature-v4-multi-region': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sso@3.948.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.947.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@aws-sdk/xml-builder': 3.930.0 - '@smithy/core': 3.19.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.947.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.947.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/node-http-handler': 4.4.6 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-stream': 4.5.7 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.952.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-login': 3.952.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.952.0 - '@aws-sdk/credential-provider-web-identity': 3.952.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.952.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.952.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.947.0 - '@aws-sdk/credential-provider-http': 3.947.0 - '@aws-sdk/credential-provider-ini': 3.952.0 - '@aws-sdk/credential-provider-process': 3.947.0 - '@aws-sdk/credential-provider-sso': 3.952.0 - '@aws-sdk/credential-provider-web-identity': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.947.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.952.0': - dependencies: - '@aws-sdk/client-sso': 3.948.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/token-providers': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.952.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/middleware-host-header@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.948.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@aws/lambda-invoke-store': 0.2.2 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.947.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-arn-parser': 3.893.0 - '@smithy/core': 3.19.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-stream': 4.5.7 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.947.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@smithy/core': 3.19.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.952.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.947.0 - '@aws-sdk/middleware-host-header': 3.936.0 - '@aws-sdk/middleware-logger': 3.936.0 - '@aws-sdk/middleware-recursion-detection': 3.948.0 - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/region-config-resolver': 3.936.0 - '@aws-sdk/types': 3.936.0 - '@aws-sdk/util-endpoints': 3.936.0 - '@aws-sdk/util-user-agent-browser': 3.936.0 - '@aws-sdk/util-user-agent-node': 3.947.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/core': 3.19.0 - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/hash-node': 4.2.6 - '@smithy/invalid-dependency': 4.2.6 - '@smithy/middleware-content-length': 4.2.6 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-retry': 4.4.16 - '@smithy/middleware-serde': 4.2.7 - '@smithy/middleware-stack': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/node-http-handler': 4.4.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.15 - '@smithy/util-defaults-mode-node': 4.2.18 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/config-resolver': 4.4.4 - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.947.0': - dependencies: - '@aws-sdk/middleware-sdk-s3': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/signature-v4': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.952.0': - dependencies: - '@aws-sdk/core': 3.947.0 - '@aws-sdk/nested-clients': 3.952.0 - '@aws-sdk/types': 3.936.0 - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.936.0': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/util-arn-parser@3.893.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-endpoints': 3.2.6 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.893.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.936.0': - dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 - bowser: 2.13.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.947.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.947.0 - '@aws-sdk/types': 3.936.0 - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.930.0': - dependencies: - '@smithy/types': 4.10.0 - fast-xml-parser: 5.2.5 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.2.2': {} - - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -12362,7 +12433,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -13080,17 +13151,17 @@ snapshots: dependencies: core-js-pure: 3.47.0 - '@babel/runtime@7.28.4': {} + '@babel/runtime@7.28.6': {} '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -13109,7 +13180,77 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@borewit/text-codec@0.1.1': {} + '@borewit/text-codec@0.2.1': {} + + '@braintree/sanitize-url@7.1.1': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + '@lezer/common': 1.5.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + '@lezer/common': 1.5.0 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.1': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.6 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + crelt: 1.0.6 + + '@codemirror/state@6.5.3': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.39.8': + dependencies: + '@codemirror/state': 6.5.3 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 '@colors/colors@1.5.0': optional: true @@ -13414,26 +13555,26 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.3.1(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docsearch/core@4.3.1(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) '@docsearch/css@4.3.2': {} - '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@ai-sdk/react': 2.0.115(react@18.3.1)(zod@4.2.1) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) - '@docsearch/core': 4.3.1(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docsearch/core': 4.3.1(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docsearch/css': 4.3.2 ai: 5.0.113(zod@4.2.1) algoliasearch: 5.46.0 marked: 16.4.2 zod: 4.2.1 optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -13449,7 +13590,7 @@ snapshots: '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/preset-react': 7.28.5(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@babel/runtime-corejs3': 7.28.4 '@babel/traverse': 7.28.5 '@docusaurus/logger': 3.9.2 @@ -13474,24 +13615,24 @@ snapshots: '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.103.0) + babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.104.1) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.103.0) - css-loader: 6.11.0(webpack@5.103.0) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.103.0) + copy-webpack-plugin: 11.0.0(webpack@5.104.1) + css-loader: 6.11.0(webpack@5.104.1) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.104.1) cssnano: 6.1.2(postcss@8.5.6) - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.103.0) - null-loader: 4.0.1(webpack@5.103.0) + mini-css-extract-plugin: 2.9.4(webpack@5.104.1) + null-loader: 4.0.1(webpack@5.104.1) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1) postcss-preset-env: 10.5.0(postcss@8.5.6) - terser-webpack-plugin: 5.3.16(webpack@5.103.0) + terser-webpack-plugin: 5.3.16(webpack@5.104.1) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) - webpack: 5.103.0 - webpackbar: 6.0.1(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) + webpack: 5.104.1 + webpackbar: 6.0.1(webpack@5.104.1) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -13507,7 +13648,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: '@docusaurus/babel': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/bundler': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) @@ -13516,7 +13657,7 @@ snapshots: '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13531,9 +13672,9 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.2 html-tags: 3.3.1 - html-webpack-plugin: 5.6.5(webpack@5.103.0) + html-webpack-plugin: 5.6.5(webpack@5.104.1) leven: 3.1.0 - lodash: 4.17.21 + lodash: 4.17.23 open: 8.4.2 p-map: 4.0.0 prompts: 2.4.2 @@ -13541,18 +13682,18 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.104.1) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) - semver: 7.7.3 + semver: 7.7.4 serve-handler: 6.1.6 tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.2(webpack@5.103.0) + webpack-dev-server: 5.2.2(webpack@5.104.1) webpack-merge: 6.0.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13592,7 +13733,7 @@ snapshots: '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) fs-extra: 11.3.2 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -13608,9 +13749,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) vfile: 6.0.3 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@swc/core' - esbuild @@ -13622,7 +13763,7 @@ snapshots: dependencies: '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -13636,13 +13777,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13650,7 +13791,7 @@ snapshots: cheerio: 1.0.0-rc.12 feed: 4.2.2 fs-extra: 11.3.2 - lodash: 4.17.21 + lodash: 4.17.23 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) schema-dts: 1.1.5 @@ -13658,7 +13799,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13677,13 +13818,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13692,13 +13833,13 @@ snapshots: combine-promises: 1.2.0 fs-extra: 11.3.2 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13717,9 +13858,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13728,7 +13869,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13747,9 +13888,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13774,9 +13915,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 @@ -13802,9 +13943,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13828,9 +13969,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 @@ -13855,9 +13996,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13881,9 +14022,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13912,9 +14053,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13923,7 +14064,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13942,22 +14083,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -13984,28 +14125,28 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 18.3.1 - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@18.3.1) clsx: 2.1.1 infima: 0.2.0-alpha.45 - lodash: 4.17.21 + lodash: 4.17.23 nprogress: 0.2.0 postcss: 8.5.6 prism-react-renderer: 2.4.1(react@18.3.1) @@ -14034,15 +14175,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -14058,13 +14199,43 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/theme-mermaid@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + mermaid: 11.12.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@docusaurus/plugin-content-docs' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.46.0)(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + dependencies: + '@docsearch/react': 4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.14)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14073,7 +14244,7 @@ snapshots: clsx: 2.1.1 eta: 2.2.0 fs-extra: 11.3.2 - lodash: 4.17.21 + lodash: 4.17.23 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 @@ -14111,14 +14282,14 @@ snapshots: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.7 + '@types/react': 19.2.14 commander: 5.1.0 joi: 17.13.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -14148,7 +14319,7 @@ snapshots: fs-extra: 11.3.2 joi: 17.13.3 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 tslib: 2.8.1 transitivePeerDependencies: - '@swc/core' @@ -14166,22 +14337,22 @@ snapshots: '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.103.0) + file-loader: 6.2.0(webpack@5.104.1) fs-extra: 11.3.2 github-slugger: 1.5.0 globby: 11.1.0 gray-matter: 4.0.3 jiti: 1.21.7 js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 micromatch: 4.0.8 p-queue: 6.6.2 prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1) utility-types: 3.11.0 - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - '@swc/core' - esbuild @@ -14202,7 +14373,7 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.1': + '@esbuild/aix-ppc64@0.27.3': optional: true '@esbuild/android-arm64@0.19.12': @@ -14211,7 +14382,7 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.1': + '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm@0.19.12': @@ -14220,7 +14391,7 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.1': + '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-x64@0.19.12': @@ -14229,7 +14400,7 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.1': + '@esbuild/android-x64@0.27.3': optional: true '@esbuild/darwin-arm64@0.19.12': @@ -14238,7 +14409,7 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.1': + '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-x64@0.19.12': @@ -14247,7 +14418,7 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.1': + '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.19.12': @@ -14256,7 +14427,7 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.1': + '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-x64@0.19.12': @@ -14265,7 +14436,7 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.1': + '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/linux-arm64@0.19.12': @@ -14274,7 +14445,7 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.1': + '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm@0.19.12': @@ -14283,7 +14454,7 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.1': + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-ia32@0.19.12': @@ -14292,7 +14463,7 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.1': + '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-loong64@0.19.12': @@ -14301,7 +14472,7 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.1': + '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-mips64el@0.19.12': @@ -14310,7 +14481,7 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.1': + '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-ppc64@0.19.12': @@ -14319,7 +14490,7 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.1': + '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-riscv64@0.19.12': @@ -14328,7 +14499,7 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.1': + '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-s390x@0.19.12': @@ -14337,7 +14508,7 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.1': + '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-x64@0.19.12': @@ -14346,13 +14517,13 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.1': + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.1': + '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-x64@0.19.12': @@ -14361,13 +14532,13 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.1': + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.1': + '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-x64@0.19.12': @@ -14376,13 +14547,13 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.1': + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.1': + '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/sunos-x64@0.19.12': @@ -14391,7 +14562,7 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.1': + '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/win32-arm64@0.19.12': @@ -14400,7 +14571,7 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.1': + '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-ia32@0.19.12': @@ -14409,7 +14580,7 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.1': + '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-x64@0.19.12': @@ -14418,53 +14589,41 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.1': + '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.2(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.23.2': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.2 debug: 4.4.3 - minimatch: 3.1.2 + minimatch: 10.2.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.2': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 - '@eslint/core@0.17.0': + '@eslint/core@1.1.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': + '@eslint/js@10.0.1(eslint@10.0.2(jiti@2.6.1))': + optionalDependencies: + eslint: 10.0.2(jiti@2.6.1) + + '@eslint/object-schema@3.0.2': {} + + '@eslint/plugin-kit@0.6.0': dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.0 levn: 0.4.1 '@extism/extism@2.0.0-rc13': {} @@ -14473,12 +14632,12 @@ snapshots: dependencies: urlpattern-polyfill: 8.0.2 - '@faker-js/faker@10.1.0': {} + '@faker-js/faker@10.3.0': {} '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.7.4 + prettier: 3.8.1 '@floating-ui/core@1.7.3': dependencies: @@ -14498,30 +14657,67 @@ snapshots: decimal.js: 10.6.0 tslib: 2.8.1 + '@formatjs/ecma402-abstract@3.1.1': + dependencies: + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/intl-localematcher': 0.8.1 + decimal.js: 10.6.0 + tslib: 2.8.1 + '@formatjs/fast-memoize@2.2.7': dependencies: tslib: 2.8.1 + '@formatjs/fast-memoize@3.1.0': + dependencies: + tslib: 2.8.1 + '@formatjs/icu-messageformat-parser@2.11.4': dependencies: '@formatjs/ecma402-abstract': 2.3.6 '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 + '@formatjs/icu-messageformat-parser@3.5.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-skeleton-parser': 2.1.1 + tslib: 2.8.1 + '@formatjs/icu-skeleton-parser@1.8.16': dependencies: '@formatjs/ecma402-abstract': 2.3.6 tslib: 2.8.1 + '@formatjs/icu-skeleton-parser@2.1.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + tslib: 2.8.1 + '@formatjs/intl-localematcher@0.6.2': dependencies: tslib: 2.8.1 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@formatjs/intl-localematcher@0.8.1': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - lodash: 4.17.21 + '@formatjs/fast-memoize': 3.1.0 + tslib: 2.8.1 + + '@fortawesome/fontawesome-common-types@7.1.0': {} + + '@fortawesome/free-regular-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@fortawesome/free-solid-svg-icons@7.1.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.1.0 + + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': + dependencies: + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + lodash: 4.17.23 '@grpc/grpc-js@1.14.3': dependencies: @@ -14559,6 +14755,14 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -14657,177 +14861,182 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} - '@immich/svelte-markdown-preprocess@0.1.0(svelte@5.43.3)': + '@immich/sql-tools@0.3.2': dependencies: - svelte: 5.43.3 + commander: 14.0.3 + kysely: 0.28.11 + kysely-postgres-js: 3.0.0(kysely@0.28.11)(postgres@3.4.8) + pg-connection-string: 2.11.0 + postgres: 3.4.8 - '@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)': + '@immich/svelte-markdown-preprocess@0.2.1(svelte@5.53.5)': dependencies: - '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.3) + front-matter: 4.0.2 + marked: 17.0.3 + node-emoji: 2.2.0 + svelte: 5.53.5 + + '@immich/ui@0.64.0(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)': + dependencies: + '@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.53.5) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + bits-ui: 2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) luxon: 3.7.2 - simple-icons: 15.22.0 - svelte: 5.43.3 + simple-icons: 16.9.0 + svelte: 5.53.5 svelte-highlight: 7.9.0 tailwind-merge: 3.4.0 - tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) - tailwindcss: 4.1.18 + tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.2.0) + tailwindcss: 4.2.0 transitivePeerDependencies: - '@sveltejs/kit' '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@24.10.4)': + '@inquirer/checkbox@4.3.2(@types/node@24.11.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.11.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/confirm@5.1.21(@types/node@24.10.4)': + '@inquirer/confirm@5.1.21(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/core@10.3.2(@types/node@24.10.4)': + '@inquirer/core@10.3.2(@types/node@24.11.0)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.11.0) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/editor@4.2.23(@types/node@24.10.4)': + '@inquirer/editor@4.2.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/external-editor': 1.0.3(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/external-editor': 1.0.3(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/expand@4.0.23(@types/node@24.10.4)': + '@inquirer/expand@4.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/external-editor@1.0.3(@types/node@24.10.4)': + '@inquirer/external-editor@1.0.3(@types/node@24.11.0)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@24.10.4)': + '@inquirer/input@4.3.1(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/number@3.0.23(@types/node@24.10.4)': + '@inquirer/number@3.0.23(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/password@4.0.23(@types/node@24.10.4)': + '@inquirer/password@4.0.23(@types/node@24.11.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/prompts@7.10.1(@types/node@24.10.4)': + '@inquirer/prompts@7.10.1(@types/node@24.11.0)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@24.10.4) - '@inquirer/confirm': 5.1.21(@types/node@24.10.4) - '@inquirer/editor': 4.2.23(@types/node@24.10.4) - '@inquirer/expand': 4.0.23(@types/node@24.10.4) - '@inquirer/input': 4.3.1(@types/node@24.10.4) - '@inquirer/number': 3.0.23(@types/node@24.10.4) - '@inquirer/password': 4.0.23(@types/node@24.10.4) - '@inquirer/rawlist': 4.1.11(@types/node@24.10.4) - '@inquirer/search': 3.2.2(@types/node@24.10.4) - '@inquirer/select': 4.4.2(@types/node@24.10.4) + '@inquirer/checkbox': 4.3.2(@types/node@24.11.0) + '@inquirer/confirm': 5.1.21(@types/node@24.11.0) + '@inquirer/editor': 4.2.23(@types/node@24.11.0) + '@inquirer/expand': 4.0.23(@types/node@24.11.0) + '@inquirer/input': 4.3.1(@types/node@24.11.0) + '@inquirer/number': 3.0.23(@types/node@24.11.0) + '@inquirer/password': 4.0.23(@types/node@24.11.0) + '@inquirer/rawlist': 4.1.11(@types/node@24.11.0) + '@inquirer/search': 3.2.2(@types/node@24.11.0) + '@inquirer/select': 4.4.2(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/prompts@7.3.2(@types/node@24.10.4)': + '@inquirer/prompts@7.3.2(@types/node@24.11.0)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@24.10.4) - '@inquirer/confirm': 5.1.21(@types/node@24.10.4) - '@inquirer/editor': 4.2.23(@types/node@24.10.4) - '@inquirer/expand': 4.0.23(@types/node@24.10.4) - '@inquirer/input': 4.3.1(@types/node@24.10.4) - '@inquirer/number': 3.0.23(@types/node@24.10.4) - '@inquirer/password': 4.0.23(@types/node@24.10.4) - '@inquirer/rawlist': 4.1.11(@types/node@24.10.4) - '@inquirer/search': 3.2.2(@types/node@24.10.4) - '@inquirer/select': 4.4.2(@types/node@24.10.4) + '@inquirer/checkbox': 4.3.2(@types/node@24.11.0) + '@inquirer/confirm': 5.1.21(@types/node@24.11.0) + '@inquirer/editor': 4.2.23(@types/node@24.11.0) + '@inquirer/expand': 4.0.23(@types/node@24.11.0) + '@inquirer/input': 4.3.1(@types/node@24.11.0) + '@inquirer/number': 3.0.23(@types/node@24.11.0) + '@inquirer/password': 4.0.23(@types/node@24.11.0) + '@inquirer/rawlist': 4.1.11(@types/node@24.11.0) + '@inquirer/search': 3.2.2(@types/node@24.11.0) + '@inquirer/select': 4.4.2(@types/node@24.11.0) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/rawlist@4.1.11(@types/node@24.10.4)': + '@inquirer/rawlist@4.1.11(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) + '@inquirer/type': 3.0.10(@types/node@24.11.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/search@3.2.2(@types/node@24.10.4)': + '@inquirer/search@3.2.2(@types/node@24.11.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.11.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/select@4.4.2(@types/node@24.10.4)': + '@inquirer/select@4.4.2(@types/node@24.11.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.11.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.11.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 - '@inquirer/type@3.0.10(@types/node@24.10.4)': + '@inquirer/type@3.0.10(@types/node@24.11.0)': optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 - '@ioredis/commands@1.4.0': {} - - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 + '@ioredis/commands@1.5.0': {} '@isaacs/cliui@8.0.2': dependencies: @@ -14853,7 +15062,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -14883,6 +15092,14 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -14919,11 +15136,13 @@ snapshots: '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) tslib: 2.8.1 + '@jsonquerylang/jsonquery@5.1.1': {} + '@koa/cors@5.0.0': dependencies: vary: 1.1.2 - '@koa/router@15.1.0(koa@3.1.1)': + '@koa/router@15.3.0(koa@3.1.1)': dependencies: debug: 4.4.3 http-errors: 2.0.1 @@ -14933,19 +15152,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.1 transitivePeerDependencies: - eslint - supports-color - typescript + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@leichtgewicht/ip-codec@2.0.5': {} + '@lezer/common@1.5.0': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.0 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.6 + + '@lezer/lr@1.4.6': + dependencies: + '@lezer/common': 1.5.0 + '@lukeed/csprng@1.1.0': {} '@mapbox/geojson-rewind@0.5.2': @@ -14953,33 +15194,9 @@ snapshots: get-stream: 6.0.1 minimist: 1.2.8 - '@mapbox/geojson-types@1.0.2': {} - '@mapbox/jsonlint-lines-primitives@2.0.2': {} - '@mapbox/mapbox-gl-rtl-text@0.2.3(mapbox-gl@1.13.3)': - dependencies: - mapbox-gl: 1.13.3 - - '@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3)': - dependencies: - mapbox-gl: 1.13.3 - - '@mapbox/node-pre-gyp@1.0.11': - dependencies: - detect-libc: 2.1.2 - https-proxy-agent: 5.0.1 - make-dir: 3.1.0 - node-fetch: 2.7.0 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.7.3 - tar: 6.2.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true + '@mapbox/mapbox-gl-rtl-text@0.3.0': {} '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: @@ -14990,28 +15207,18 @@ snapshots: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.7.3 + semver: 7.7.4 tar: 6.2.1 transitivePeerDependencies: - encoding - supports-color - '@mapbox/point-geometry@0.1.0': {} - '@mapbox/point-geometry@1.1.0': {} - '@mapbox/tiny-sdf@1.2.5': {} - '@mapbox/tiny-sdf@2.0.7': {} - '@mapbox/unitbezier@0.0.0': {} - '@mapbox/unitbezier@0.0.1': {} - '@mapbox/vector-tile@1.3.1': - dependencies: - '@mapbox/point-geometry': 0.1.0 - '@mapbox/vector-tile@2.0.4': dependencies: '@mapbox/point-geometry': 1.1.0 @@ -15020,6 +15227,8 @@ snapshots: '@mapbox/whoots-js@3.1.0': {} + '@maplibre/geojson-vt@5.0.4': {} + '@maplibre/maplibre-gl-style-spec@24.4.1': dependencies: '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -15030,20 +15239,22 @@ snapshots: rw: 1.3.3 tinyqueue: 3.0.0 - '@maplibre/mlt@1.1.2': + '@maplibre/mlt@1.1.6': dependencies: '@mapbox/point-geometry': 1.1.0 - '@maplibre/vt-pbf@4.2.0': + '@maplibre/vt-pbf@4.2.1': dependencies: '@mapbox/point-geometry': 1.1.0 '@mapbox/vector-tile': 2.0.4 - '@types/geojson-vt': 3.2.5 + '@maplibre/geojson-vt': 5.0.4 + '@types/geojson': 7946.0.16 '@types/supercluster': 7.1.3 - geojson-vt: 4.0.2 pbf: 4.0.1 supercluster: 8.0.1 + '@marijn/find-cluster-break@1.0.2': {} + '@mdi/js@7.4.47': {} '@mdi/react@1.6.1': @@ -15060,7 +15271,7 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 - acorn: 8.15.0 + acorn: 8.16.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -15069,7 +15280,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.15.0) + recma-jsx: 1.0.1(acorn@8.16.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 remark-mdx: 3.1.1 @@ -15084,12 +15295,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.7 + '@types/react': 19.2.14 react: 18.3.1 + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + '@microsoft/tsdoc@0.16.0': {} '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': @@ -15112,51 +15327,51 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.66.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.66.0 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.69.3 tslib: 2.8.1 - '@nestjs/cli@11.0.14(@swc/core@1.15.5(@swc/helpers@0.5.17))(@types/node@24.10.4)': + '@nestjs/cli@11.0.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@24.11.0)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.10.4)(chokidar@4.0.3) - '@inquirer/prompts': 7.10.1(@types/node@24.10.4) + '@angular-devkit/schematics-cli': 19.2.19(@types/node@24.11.0)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.11.0) '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))) glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.5(@swc/helpers@0.5.17) + '@swc/core': 1.15.11(@swc/helpers@0.5.17) transitivePeerDependencies: - '@types/node' - esbuild - uglify-js - webpack-cli - '@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.1.0 + file-type: 21.3.0 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -15169,9 +15384,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -15181,46 +15396,46 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/websockets': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/platform-express@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cors: 2.8.5 - express: 5.1.0 + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 multer: 2.0.2 path-to-regexp: 8.3.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 - socket.io: 4.8.1 + socket.io: 4.8.3 tslib: 2.8.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@nestjs/schedule@6.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/schedule@6.1.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - cron: 4.3.5 + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 4.4.0 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': dependencies: @@ -15233,40 +15448,40 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.6(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) js-yaml: 4.1.1 - lodash: 4.17.21 + lodash: 4.17.23 path-to-regexp: 8.3.0 reflect-metadata: 0.2.2 - swagger-ui-dist: 5.30.2 + swagger-ui-dist: 5.31.0 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': + '@nestjs/testing@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-express@11.1.14)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/platform-express': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) - '@nestjs/websockets@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@nestjs/platform-socket.io@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.14)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -15280,351 +15495,422 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + fastq: 1.20.1 '@npmcli/agent@4.0.0': dependencies: agent-base: 7.1.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.4 + lru-cache: 11.2.6 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color '@npmcli/fs@5.0.0': dependencies: - semver: 7.7.3 + semver: 7.7.4 '@nuxt/opencollective@0.4.1': dependencies: consola: 3.4.2 - '@oazapfts/runtime@1.1.0': {} + '@oazapfts/runtime@1.2.0': {} - '@opentelemetry/api-logs@0.208.0': + '@opentelemetry/api-logs@0.212.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api@1.9.0': {} - '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/configuration@0.212.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/host-metrics@0.36.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.56.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.60.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.55.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.58.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.61.1(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.64.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 - '@types/pg-pool': 2.0.6 + '@types/pg-pool': 2.0.7 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - import-in-the-middle: 2.0.0 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.212.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.4 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 - '@opentelemetry/propagator-b3@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.212.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-b3': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/api-logs': 0.212.0 + '@opentelemetry/configuration': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.212.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.5.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.5.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions@1.38.0': {} + '@opentelemetry/semantic-conventions@1.39.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.5.1(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.3.1': dependencies: '@noble/hashes': 1.8.0 - '@photo-sphere-viewer/core@5.14.0': + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@photo-sphere-viewer/core@5.14.1': dependencies: three: 0.179.1 - '@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': + '@photo-sphere-viewer/equirectangular-video-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))': dependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/video-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/video-plugin': 5.14.1(@photo-sphere-viewer/core@5.14.1) three: 0.182.0 - '@photo-sphere-viewer/markers-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/markers-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/resolution-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)(@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0))': + '@photo-sphere-viewer/resolution-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))': dependencies: - '@photo-sphere-viewer/core': 5.14.0 - '@photo-sphere-viewer/settings-plugin': 5.14.0(@photo-sphere-viewer/core@5.14.0) + '@photo-sphere-viewer/core': 5.14.1 + '@photo-sphere-viewer/settings-plugin': 5.14.1(@photo-sphere-viewer/core@5.14.1) - '@photo-sphere-viewer/settings-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/settings-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 - '@photo-sphere-viewer/video-plugin@5.14.0(@photo-sphere-viewer/core@5.14.0)': + '@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1)': dependencies: - '@photo-sphere-viewer/core': 5.14.0 + '@photo-sphere-viewer/core': 5.14.1 three: 0.182.0 - '@photostructure/tz-lookup@11.3.0': {} + '@photostructure/tz-lookup@11.4.0': {} '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.9': {} - '@playwright/test@1.57.0': + '@playwright/test@1.58.2': dependencies: - playwright: 1.57.0 + playwright: 1.58.2 '@pnpm/config.env-replace@1.1.0': {} @@ -15663,190 +15949,205 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@react-email/body@0.1.0(react@19.2.3)': + '@react-email/body@0.1.0(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/button@0.2.0(react@19.2.3)': + '@react-email/button@0.2.0(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/code-block@0.1.0(react@19.2.3)': + '@react-email/code-block@0.1.0(react@19.2.4)': dependencies: prismjs: 1.30.0 - react: 19.2.3 + react: 19.2.4 - '@react-email/code-inline@0.0.5(react@19.2.3)': + '@react-email/code-inline@0.0.5(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/column@0.0.13(react@19.2.3)': + '@react-email/column@0.0.13(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/components@0.5.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/components@0.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-email/body': 0.1.0(react@19.2.3) - '@react-email/button': 0.2.0(react@19.2.3) - '@react-email/code-block': 0.1.0(react@19.2.3) - '@react-email/code-inline': 0.0.5(react@19.2.3) - '@react-email/column': 0.0.13(react@19.2.3) - '@react-email/container': 0.0.15(react@19.2.3) - '@react-email/font': 0.0.9(react@19.2.3) - '@react-email/head': 0.0.12(react@19.2.3) - '@react-email/heading': 0.0.15(react@19.2.3) - '@react-email/hr': 0.0.11(react@19.2.3) - '@react-email/html': 0.0.11(react@19.2.3) - '@react-email/img': 0.0.11(react@19.2.3) - '@react-email/link': 0.0.12(react@19.2.3) - '@react-email/markdown': 0.0.16(react@19.2.3) - '@react-email/preview': 0.0.13(react@19.2.3) - '@react-email/render': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@react-email/row': 0.0.12(react@19.2.3) - '@react-email/section': 0.0.16(react@19.2.3) - '@react-email/tailwind': 1.2.2(react@19.2.3) - '@react-email/text': 0.1.5(react@19.2.3) - react: 19.2.3 + '@react-email/body': 0.1.0(react@19.2.4) + '@react-email/button': 0.2.0(react@19.2.4) + '@react-email/code-block': 0.1.0(react@19.2.4) + '@react-email/code-inline': 0.0.5(react@19.2.4) + '@react-email/column': 0.0.13(react@19.2.4) + '@react-email/container': 0.0.15(react@19.2.4) + '@react-email/font': 0.0.9(react@19.2.4) + '@react-email/head': 0.0.12(react@19.2.4) + '@react-email/heading': 0.0.15(react@19.2.4) + '@react-email/hr': 0.0.11(react@19.2.4) + '@react-email/html': 0.0.11(react@19.2.4) + '@react-email/img': 0.0.11(react@19.2.4) + '@react-email/link': 0.0.12(react@19.2.4) + '@react-email/markdown': 0.0.16(react@19.2.4) + '@react-email/preview': 0.0.13(react@19.2.4) + '@react-email/render': 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-email/row': 0.0.12(react@19.2.4) + '@react-email/section': 0.0.16(react@19.2.4) + '@react-email/tailwind': 1.2.2(react@19.2.4) + '@react-email/text': 0.1.5(react@19.2.4) + react: 19.2.4 transitivePeerDependencies: - react-dom - '@react-email/container@0.0.15(react@19.2.3)': + '@react-email/container@0.0.15(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/font@0.0.9(react@19.2.3)': + '@react-email/font@0.0.9(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/head@0.0.12(react@19.2.3)': + '@react-email/head@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/heading@0.0.15(react@19.2.3)': + '@react-email/heading@0.0.15(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/hr@0.0.11(react@19.2.3)': + '@react-email/hr@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/html@0.0.11(react@19.2.3)': + '@react-email/html@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/img@0.0.11(react@19.2.3)': + '@react-email/img@0.0.11(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/link@0.0.12(react@19.2.3)': + '@react-email/link@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/markdown@0.0.16(react@19.2.3)': + '@react-email/markdown@0.0.16(react@19.2.4)': dependencies: marked: 15.0.12 - react: 19.2.3 + react: 19.2.4 - '@react-email/preview@0.0.13(react@19.2.3)': + '@react-email/preview@0.0.13(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-email/render@1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: html-to-text: 9.0.5 - prettier: 3.7.4 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + prettier: 3.8.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react-promise-suspense: 0.3.4 - '@react-email/row@0.0.12(react@19.2.3)': + '@react-email/row@0.0.12(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/section@0.0.16(react@19.2.3)': + '@react-email/section@0.0.16(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/tailwind@1.2.2(react@19.2.3)': + '@react-email/tailwind@1.2.2(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@react-email/text@0.1.5(react@19.2.3)': + '@react-email/text@0.1.5(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@rollup/pluginutils@5.3.0(rollup@4.53.4)': + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8)': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + + '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.3 optionalDependencies: - rollup: 4.53.4 + rollup: 4.55.1 - '@rollup/rollup-android-arm-eabi@4.53.4': + '@rollup/rollup-android-arm-eabi@4.55.1': optional: true - '@rollup/rollup-android-arm64@4.53.4': + '@rollup/rollup-android-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-arm64@4.53.4': + '@rollup/rollup-darwin-arm64@4.55.1': optional: true - '@rollup/rollup-darwin-x64@4.53.4': + '@rollup/rollup-darwin-x64@4.55.1': optional: true - '@rollup/rollup-freebsd-arm64@4.53.4': + '@rollup/rollup-freebsd-arm64@4.55.1': optional: true - '@rollup/rollup-freebsd-x64@4.53.4': + '@rollup/rollup-freebsd-x64@4.55.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.53.4': + '@rollup/rollup-linux-arm-gnueabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.53.4': + '@rollup/rollup-linux-arm-musleabihf@4.55.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.53.4': + '@rollup/rollup-linux-arm64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.53.4': + '@rollup/rollup-linux-arm64-musl@4.55.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.53.4': + '@rollup/rollup-linux-loong64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.53.4': + '@rollup/rollup-linux-loong64-musl@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.53.4': + '@rollup/rollup-linux-ppc64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.53.4': + '@rollup/rollup-linux-ppc64-musl@4.55.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.53.4': + '@rollup/rollup-linux-riscv64-gnu@4.55.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.53.4': + '@rollup/rollup-linux-riscv64-musl@4.55.1': optional: true - '@rollup/rollup-linux-x64-musl@4.53.4': + '@rollup/rollup-linux-s390x-gnu@4.55.1': optional: true - '@rollup/rollup-openharmony-arm64@4.53.4': + '@rollup/rollup-linux-x64-gnu@4.55.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.53.4': + '@rollup/rollup-linux-x64-musl@4.55.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.53.4': + '@rollup/rollup-openbsd-x64@4.55.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.53.4': + '@rollup/rollup-openharmony-arm64@4.55.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.53.4': + '@rollup/rollup-win32-arm64-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.55.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true '@scarf/scarf@1.4.0': {} @@ -15872,7 +16173,7 @@ snapshots: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 invariant: 2.2.4 prop-types: 15.8.1 react: 18.3.1 @@ -15886,354 +16187,82 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 - '@smithy/abort-controller@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/config-resolver@4.4.4': - dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 - '@smithy/util-config-provider': 4.2.0 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 - tslib: 2.8.1 - - '@smithy/core@3.19.0': - dependencies: - '@smithy/middleware-serde': 4.2.7 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-stream': 4.5.7 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.6': - dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.7': - dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 - '@smithy/util-base64': 4.3.0 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.6': - dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.4.0': - dependencies: - '@smithy/core': 3.19.0 - '@smithy/middleware-serde': 4.2.7 - '@smithy/node-config-provider': 4.3.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - '@smithy/url-parser': 4.2.6 - '@smithy/util-middleware': 4.2.6 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.16': - dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/service-error-classification': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-retry': 4.2.6 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.7': - dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.3.6': - dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.4.6': - dependencies: - '@smithy/abort-controller': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - '@smithy/util-uri-escape': 4.2.0 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - - '@smithy/shared-ini-file-loader@4.4.1': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.6': - dependencies: - '@smithy/is-array-buffer': 4.2.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-uri-escape': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/smithy-client@4.10.1': - dependencies: - '@smithy/core': 3.19.0 - '@smithy/middleware-endpoint': 4.4.0 - '@smithy/middleware-stack': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 - '@smithy/util-stream': 4.5.7 - tslib: 2.8.1 - - '@smithy/types@4.10.0': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.2.6': - dependencies: - '@smithy/querystring-parser': 4.2.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-base64@4.3.0': - dependencies: - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@4.2.1': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-buffer-from@4.2.0': - dependencies: - '@smithy/is-array-buffer': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-config-provider@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@4.3.15': - dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@4.2.18': - dependencies: - '@smithy/config-resolver': 4.4.4 - '@smithy/credential-provider-imds': 4.2.6 - '@smithy/node-config-provider': 4.3.6 - '@smithy/property-provider': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.2.6': - dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.6': - dependencies: - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-retry@4.2.6': - dependencies: - '@smithy/service-error-classification': 4.2.6 - '@smithy/types': 4.10.0 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.7': - dependencies: - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/node-http-handler': 4.4.6 - '@smithy/types': 4.10.0 - '@smithy/util-base64': 4.3.0 - '@smithy/util-buffer-from': 4.2.0 - '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-utf8': 4.2.0 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@4.2.0': - dependencies: - '@smithy/util-buffer-from': 4.2.0 - tslib: 2.8.1 - - '@smithy/uuid@1.1.0': - dependencies: - tslib: 2.8.1 - '@socket.io/component-emitter@3.1.2': {} - '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.5)': + '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)': dependencies: debug: 4.3.7 notepack.io: 3.0.1 - socket.io-adapter: 2.5.5 + socket.io-adapter: 2.5.6 uid2: 1.0.0 transitivePeerDependencies: - supports-color + '@sphinxxxx/color-conversion@2.2.2': {} + '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/kit': 2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/enhanced-img@0.10.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.43.3 - svelte-parse-markup: 0.1.5(svelte@5.43.3) - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vite-imagetools: 9.0.2(rollup@4.53.4) + svelte: 5.53.5 + svelte-parse-markup: 0.1.5(svelte@5.53.5) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-imagetools: 9.0.3(rollup@4.55.1) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 - acorn: 8.15.0 + acorn: 8.16.0 cookie: 0.6.0 - devalue: 5.6.1 + devalue: 5.6.3 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 - sade: 1.8.1 - set-cookie-parser: 2.7.2 + set-cookie-parser: 3.0.1 sirv: 3.0.2 - svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + svelte: 5.53.5 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 + typescript: 5.9.3 - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + svelte: 5.53.5 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) - debug: 4.4.3 + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.43.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + obug: 2.1.1 + svelte: 5.53.5 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -16330,51 +16359,51 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.5': + '@swc/core-darwin-arm64@1.15.11': optional: true - '@swc/core-darwin-x64@1.15.5': + '@swc/core-darwin-x64@1.15.11': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.5': + '@swc/core-linux-arm-gnueabihf@1.15.11': optional: true - '@swc/core-linux-arm64-gnu@1.15.5': + '@swc/core-linux-arm64-gnu@1.15.11': optional: true - '@swc/core-linux-arm64-musl@1.15.5': + '@swc/core-linux-arm64-musl@1.15.11': optional: true - '@swc/core-linux-x64-gnu@1.15.5': + '@swc/core-linux-x64-gnu@1.15.11': optional: true - '@swc/core-linux-x64-musl@1.15.5': + '@swc/core-linux-x64-musl@1.15.11': optional: true - '@swc/core-win32-arm64-msvc@1.15.5': + '@swc/core-win32-arm64-msvc@1.15.11': optional: true - '@swc/core-win32-ia32-msvc@1.15.5': + '@swc/core-win32-ia32-msvc@1.15.11': optional: true - '@swc/core-win32-x64-msvc@1.15.5': + '@swc/core-win32-x64-msvc@1.15.11': optional: true - '@swc/core@1.15.5(@swc/helpers@0.5.17)': + '@swc/core@1.15.11(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.5 - '@swc/core-darwin-x64': 1.15.5 - '@swc/core-linux-arm-gnueabihf': 1.15.5 - '@swc/core-linux-arm64-gnu': 1.15.5 - '@swc/core-linux-arm64-musl': 1.15.5 - '@swc/core-linux-x64-gnu': 1.15.5 - '@swc/core-linux-x64-musl': 1.15.5 - '@swc/core-win32-arm64-msvc': 1.15.5 - '@swc/core-win32-ia32-msvc': 1.15.5 - '@swc/core-win32-x64-msvc': 1.15.5 + '@swc/core-darwin-arm64': 1.15.11 + '@swc/core-darwin-x64': 1.15.11 + '@swc/core-linux-arm-gnueabihf': 1.15.11 + '@swc/core-linux-arm64-gnu': 1.15.11 + '@swc/core-linux-arm64-musl': 1.15.11 + '@swc/core-linux-x64-gnu': 1.15.11 + '@swc/core-linux-x64-musl': 1.15.11 + '@swc/core-win32-arm64-msvc': 1.15.11 + '@swc/core-win32-ia32-msvc': 1.15.11 + '@swc/core-win32-x64-msvc': 1.15.11 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -16391,78 +16420,78 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.1.18': + '@tailwindcss/node@4.2.0': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.18 + tailwindcss: 4.2.0 - '@tailwindcss/oxide-android-arm64@4.1.18': + '@tailwindcss/oxide-android-arm64@4.2.0': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.18': + '@tailwindcss/oxide-darwin-arm64@4.2.0': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.18': + '@tailwindcss/oxide-darwin-x64@4.2.0': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.18': + '@tailwindcss/oxide-freebsd-x64@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + '@tailwindcss/oxide-linux-arm64-musl@4.2.0': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + '@tailwindcss/oxide-linux-x64-gnu@4.2.0': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.18': + '@tailwindcss/oxide-linux-x64-musl@4.2.0': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.18': + '@tailwindcss/oxide-wasm32-wasi@4.2.0': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + '@tailwindcss/oxide-win32-x64-msvc@4.2.0': optional: true - '@tailwindcss/oxide@4.1.18': + '@tailwindcss/oxide@4.2.0': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-x64': 4.1.18 - '@tailwindcss/oxide-freebsd-x64': 4.1.18 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-x64-musl': 4.1.18 - '@tailwindcss/oxide-wasm32-wasi': 4.1.18 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/oxide-android-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-arm64': 4.2.0 + '@tailwindcss/oxide-darwin-x64': 4.2.0 + '@tailwindcss/oxide-freebsd-x64': 4.2.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.0 + '@tailwindcss/oxide-linux-x64-musl': 4.2.0 + '@tailwindcss/oxide-wasm32-wasi': 4.2.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.0 - '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tailwindcss/node': 4.1.18 - '@tailwindcss/oxide': 4.1.18 - tailwindcss: 4.1.18 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + '@tailwindcss/node': 4.2.0 + '@tailwindcss/oxide': 4.2.0 + tailwindcss: 4.2.0 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -16479,55 +16508,56 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@testing-library/svelte-core@1.0.0(svelte@5.53.5)': + dependencies: + svelte: 5.53.5 + + '@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 - svelte: 5.43.3 + '@testing-library/svelte-core': 1.0.0(svelte@5.53.5) + svelte: 5.53.5 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 - '@tokenizer/inflate@0.3.1': + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 - fflate: 0.8.2 - token-types: 6.1.1 + token-types: 6.1.2 transitivePeerDependencies: - supports-color '@tokenizer/token@0.3.0': {} - '@tootallnate/once@2.0.0': - optional: true - '@trysound/sax@0.2.0': {} - '@turf/boolean-point-in-polygon@7.3.1': + '@turf/boolean-point-in-polygon@7.3.2': dependencies: - '@turf/helpers': 7.3.1 - '@turf/invariant': 7.3.1 + '@turf/helpers': 7.3.2 + '@turf/invariant': 7.3.2 '@types/geojson': 7946.0.16 point-in-polygon-hao: 1.2.4 tslib: 2.8.1 - '@turf/helpers@7.3.1': + '@turf/helpers@7.3.2': dependencies: '@types/geojson': 7946.0.16 tslib: 2.8.1 - '@turf/invariant@7.3.1': + '@turf/invariant@7.3.2': dependencies: - '@turf/helpers': 7.3.1 + '@turf/helpers': 7.3.2 '@types/geojson': 7946.0.16 tslib: 2.8.1 '@types/accepts@1.3.7': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/archiver@7.0.0': dependencies: @@ -16539,16 +16569,16 @@ snapshots: '@types/bcrypt@6.0.0': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/bonjour@3.5.13': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/braces@3.0.5': {} @@ -16570,21 +16600,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.6 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.1.0 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/content-disposition@0.5.9': {} @@ -16601,11 +16631,128 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.6 '@types/keygrip': 1.0.6 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 '@types/debug@4.1.12': dependencies: @@ -16615,13 +16762,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ssh2': 1.15.5 - '@types/dockerode@3.3.47': + '@types/dockerode@4.0.1': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -16636,6 +16783,8 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/esrecurse@4.3.1': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -16644,14 +16793,14 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -16677,11 +16826,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.28': dependencies: - '@types/node': 24.10.4 - - '@types/geojson-vt@3.2.5': - dependencies: - '@types/geojson': 7946.0.16 + '@types/node': 24.11.0 '@types/geojson@7946.0.16': {} @@ -16709,7 +16854,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/inquirer@8.2.12': dependencies: @@ -16733,7 +16878,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/justified-layout@4.1.4': {} @@ -16752,7 +16897,7 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.9 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/leaflet@1.9.21': dependencies: @@ -16760,9 +16905,9 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.21 + '@types/lodash': 4.17.23 - '@types/lodash@4.17.21': {} + '@types/lodash@4.17.23': {} '@types/luxon@3.7.1': {} @@ -16782,7 +16927,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ms@2.1.0': {} @@ -16792,7 +16937,7 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/node@17.0.45': {} @@ -16800,56 +16945,54 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.27': - dependencies: - undici-types: 6.21.0 - - '@types/node@24.10.4': + '@types/node@24.11.0': dependencies: undici-types: 7.16.0 - '@types/nodemailer@7.0.4': + '@types/node@25.3.0': dependencies: - '@aws-sdk/client-sesv2': 3.952.0 - '@types/node': 24.10.4 - transitivePeerDependencies: - - aws-crt + undici-types: 7.18.2 + optional: true + + '@types/nodemailer@7.0.10': + dependencies: + '@types/node': 24.11.0 '@types/oidc-provider@9.5.0': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.1 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/parse5@5.0.3': {} - '@types/pg-pool@2.0.6': + '@types/pg-pool@2.0.7': dependencies: '@types/pg': 8.16.0 '@types/pg@8.15.6': dependencies: - '@types/node': 24.10.4 - pg-protocol: 1.10.3 + '@types/node': 24.11.0 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.16.0': dependencies: - '@types/node': 24.10.4 - pg-protocol: 1.10.3 + '@types/node': 24.11.0 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/picomatch@4.0.2': {} '@types/pngjs@6.0.5': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/qs@6.14.0': {} @@ -16858,27 +17001,27 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.14 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.14 - '@types/react@19.2.7': + '@types/react@19.2.14': dependencies: csstype: 3.2.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/retry@0.12.2': {} @@ -16888,18 +17031,18 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/semver@7.7.1': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/send@1.2.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/serve-index@1.9.4': dependencies: @@ -16908,25 +17051,25 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/send': 0.17.6 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/sockjs@0.3.36': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ssh2@0.5.52': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -16937,7 +17080,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 24.10.4 + '@types/node': 24.11.0 form-data: 4.0.5 '@types/supercluster@7.1.3': @@ -16951,7 +17094,9 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 + + '@types/trusted-types@2.0.7': {} '@types/ua-parser-js@0.7.39': {} @@ -16965,7 +17110,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 '@types/yargs-parser@21.0.3': {} @@ -16973,102 +17118,102 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/type-utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 + eslint: 10.0.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.56.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.50.0': + '@typescript-eslint/scope-manager@8.56.0': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 - '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.56.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.1.0(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.50.0': {} + '@typescript-eslint/types@8.56.0': {} - '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.56.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3) - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/visitor-keys': 8.50.0 + '@typescript-eslint/project-service': 8.56.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.56.0(typescript@5.9.3) + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/visitor-keys': 8.56.0 debug: 4.4.3 - minimatch: 9.0.5 - semver: 7.7.3 + minimatch: 9.0.6 + semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.50.0 - '@typescript-eslint/types': 8.50.0 - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.56.0 + '@typescript-eslint/types': 8.56.0 + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.50.0': + '@typescript-eslint/visitor-keys@8.56.0': dependencies: - '@typescript-eslint/types': 8.50.0 - eslint-visitor-keys: 4.2.1 + '@typescript-eslint/types': 8.56.0 + eslint-visitor-keys: 5.0.1 '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17083,7 +17228,26 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.8 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17095,13 +17259,21 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17209,17 +17381,14 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zoom-image/core@0.41.4': + '@zoom-image/core@0.42.0': dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.8(svelte@5.43.3)': + '@zoom-image/svelte@0.3.9(svelte@5.53.5)': dependencies: - '@zoom-image/core': 0.41.4 - svelte: 5.43.3 - - abab@2.0.6: - optional: true + '@zoom-image/core': 0.42.0 + svelte: 5.53.5 abbrev@1.1.1: {} @@ -17239,29 +17408,23 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-globals@7.0.1: + acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: - acorn: 8.15.0 - acorn-walk: 8.3.4 - optional: true + acorn: 8.16.0 - acorn-import-attributes@1.9.5(acorn@8.15.0): + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn-import-phases@1.0.4(acorn@8.15.0): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.15.0 - - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn-walk@8.3.4: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 - acorn@8.15.0: {} + acorn@8.16.0: {} address@1.2.2: {} @@ -17286,24 +17449,24 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.2.1 - ajv-formats@2.1.1(ajv@8.17.1): + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv-keywords@3.5.2(ajv@6.12.6): + ajv-keywords@3.5.2(ajv@6.14.0): dependencies: - ajv: 6.12.6 + ajv: 6.14.0 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 - ajv@6.12.6: + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -17317,6 +17480,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch-helper@3.26.1(algoliasearch@5.46.0): dependencies: '@algolia/events': 4.0.1 @@ -17382,7 +17552,7 @@ snapshots: graceful-fs: 4.2.11 is-stream: 2.0.1 lazystream: 1.0.1 - lodash: 4.17.21 + lodash: 4.17.23 normalize-path: 3.0.0 readable-stream: 4.7.0 @@ -17416,6 +17586,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.1: {} + aria-query@5.3.2: {} array-flatten@1.1.1: {} @@ -17448,10 +17620,6 @@ snapshots: async-lock@1.4.1: {} - async-mutex@0.5.0: - dependencies: - tslib: 2.8.1 - async@0.2.10: {} async@3.2.6: {} @@ -17462,10 +17630,10 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.4.23(postcss@8.5.6): + autoprefixer@10.4.24(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001774 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -17475,12 +17643,12 @@ snapshots: b4a@1.7.3: {} - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.104.1): dependencies: '@babel/core': 7.28.5 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.103.0 + webpack: 5.104.1 babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -17516,13 +17684,15 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + bare-events@2.8.2: {} - bare-fs@4.5.2: + bare-fs@4.5.4: dependencies: bare-events: 2.8.2 bare-path: 3.0.0 - bare-stream: 2.7.0(bare-events@2.8.2) + bare-stream: 2.8.0(bare-events@2.8.2) bare-url: 2.3.2 fast-fifo: 1.3.2 transitivePeerDependencies: @@ -17538,9 +17708,10 @@ snapshots: bare-os: 3.6.2 optional: true - bare-stream@2.7.0(bare-events@2.8.2): + bare-stream@2.8.0(bare-events@2.8.2): dependencies: streamx: 2.23.0 + teex: 1.0.1 optionalDependencies: bare-events: 2.8.2 transitivePeerDependencies: @@ -17557,9 +17728,9 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.9.7: {} + baseline-browser-mapping@2.9.19: {} - batch-cluster@16.0.0: {} + batch-cluster@17.3.1: {} batch@0.6.1: {} @@ -17572,7 +17743,7 @@ snapshots: bcrypt@6.0.0: dependencies: node-addon-api: 8.5.0 - node-gyp: 12.1.0 + node-gyp: 12.2.0 node-gyp-build: 4.8.4 transitivePeerDependencies: - supports-color @@ -17581,16 +17752,16 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + bits-ui@2.16.0(@internationalized/date@3.10.0)(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) - svelte: 5.43.3 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) - tabbable: 6.3.0 + runed: 0.35.1(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + svelte: 5.53.5 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) + tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -17610,22 +17781,22 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.14.1 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - body-parser@2.2.1: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 http-errors: 2.0.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.0 + qs: 6.14.1 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -17638,8 +17809,6 @@ snapshots: boolbase@1.0.0: {} - bowser@2.13.1: {} - boxen@6.2.1: dependencies: ansi-align: 3.0.1 @@ -17671,17 +17840,21 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.3: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.7 - caniuse-lite: 1.0.30001760 - electron-to-chromium: 1.5.267 + baseline-browser-mapping: 2.9.19 + caniuse-lite: 1.0.30001774 + electron-to-chromium: 1.5.286 node-releases: 2.0.27 - update-browserslist-db: 1.2.2(browserslist@4.28.1) + update-browserslist-db: 1.2.3(browserslist@4.28.1) buffer-crc32@1.0.0: {} @@ -17704,13 +17877,13 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.66.0: + bullmq@5.69.3: dependencies: cron-parser: 4.9.0 - ioredis: 5.8.2 + ioredis: 5.9.2 msgpackr: 1.11.5 node-abort-controller: 3.1.1 - semver: 7.7.3 + semver: 7.7.4 tslib: 2.8.1 uuid: 11.1.0 transitivePeerDependencies: @@ -17738,14 +17911,14 @@ snapshots: dependencies: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 - glob: 13.0.0 - lru-cache: 11.2.4 + glob: 13.0.2 + lru-cache: 11.2.6 minipass: 7.1.2 minipass-collect: 2.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 p-map: 7.0.4 - ssri: 13.0.0 + ssri: 13.0.1 unique-filename: 5.0.0 cacheable-lookup@7.0.0: {} @@ -17795,26 +17968,16 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 + caniuse-lite: 1.0.30001774 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001760: {} - - canvas@2.11.2: - dependencies: - '@mapbox/node-pre-gyp': 1.0.11 - nan: 2.24.0 - simple-get: 3.1.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true + caniuse-lite@1.0.30001774: {} canvas@2.11.2(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) - nan: 2.24.0 + nan: 2.25.0 simple-get: 3.1.1 transitivePeerDependencies: - encoding @@ -17873,6 +18036,20 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.23 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -17905,7 +18082,7 @@ snapshots: dependencies: consola: 3.4.2 - cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.0: {} class-transformer@0.5.1: {} @@ -17913,7 +18090,7 @@ snapshots: dependencies: '@types/validator': 13.15.10 libphonenumber-js: 1.12.31 - validator: 13.15.23 + validator: 13.15.26 clean-css@5.3.3: dependencies: @@ -17983,6 +18160,12 @@ snapshots: cluster-key-slot@1.1.2: {} + codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8): + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -18015,6 +18198,8 @@ snapshots: commander@13.1.0: {} + commander@14.0.3: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -18068,6 +18253,8 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + confbox@0.1.8: {} + confbox@0.2.2: {} config-chain@1.1.13: @@ -18125,7 +18312,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - copy-webpack-plugin@11.0.0(webpack@5.103.0): + copy-webpack-plugin@11.0.0(webpack@5.104.1): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -18133,7 +18320,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 core-js-compat@3.47.0: dependencies: @@ -18145,11 +18332,19 @@ snapshots: core-util-is@1.0.3: {} - cors@2.8.5: + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -18162,7 +18357,7 @@ snapshots: cpu-features@0.0.10: dependencies: buildcheck: 0.0.7 - nan: 2.24.0 + nan: 2.25.0 optional: true crc-32@1.2.2: {} @@ -18172,11 +18367,13 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 + crelt@1.0.6: {} + cron-parser@4.9.0: dependencies: luxon: 3.7.2 - cron@4.3.5: + cron@4.4.0: dependencies: '@types/luxon': 3.7.1 luxon: 3.7.2 @@ -18207,7 +18404,7 @@ snapshots: postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.103.0): + css-loader@6.11.0(webpack@5.104.1): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -18216,11 +18413,11 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.103.0): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.6) @@ -18228,7 +18425,7 @@ snapshots: postcss: 8.5.6 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.103.0 + webpack: 5.104.1 optionalDependencies: clean-css: 5.3.3 @@ -18268,15 +18465,13 @@ snapshots: css.escape@1.5.1: {} - csscolorparser@1.0.3: {} - cssdb@8.5.2: {} cssesc@3.0.0: {} cssnano-preset-advanced@6.1.2(postcss@8.5.6): dependencies: - autoprefixer: 10.4.23(postcss@8.5.6) + autoprefixer: 10.4.24(postcss@8.5.6) browserslist: 4.28.1 cssnano-preset-default: 6.1.2(postcss@8.5.6) postcss: 8.5.6 @@ -18333,39 +18528,211 @@ snapshots: dependencies: css-tree: 2.2.1 - cssom@0.3.8: - optional: true - - cssom@0.5.0: - optional: true - - cssstyle@2.3.0: + cssstyle@4.6.0: dependencies: - cssom: 0.3.8 + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 optional: true csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + d3-array@3.2.4: dependencies: internmap: 2.0.3 + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + d3-geo@3.1.1: dependencies: d3-array: 3.2.4 + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + d@1.0.2: dependencies: es5-ext: 0.10.64 type: 2.7.3 - data-urls@3.0.2: + dagre-d3-es@7.0.13: dependencies: - abab: 2.0.6 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 + d3: 7.9.0 + lodash-es: 4.17.23 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 optional: true + dayjs@1.11.19: {} + debounce@1.2.1: {} debounce@2.2.0: {} @@ -18438,6 +18805,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -18454,6 +18825,9 @@ snapshots: detect-europe-js@0.1.2: {} + detect-libc@1.0.3: + optional: true + detect-libc@2.1.2: {} detect-node@2.1.0: {} @@ -18465,7 +18839,7 @@ snapshots: transitivePeerDependencies: - supports-color - devalue@5.6.1: {} + devalue@5.6.3: {} devlop@1.1.0: dependencies: @@ -18480,6 +18854,8 @@ snapshots: didyoumean@1.2.2: {} + diff-sequences@29.6.3: {} + dijkstrajs@1.0.3: {} dir-glob@3.0.1: @@ -18496,7 +18872,7 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 - docker-compose@1.3.0: + docker-compose@1.3.1: dependencies: yaml: 2.8.2 @@ -18521,9 +18897,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -18565,11 +18941,6 @@ snapshots: domelementtype@2.3.0: {} - domexception@4.0.0: - dependencies: - webidl-conversions: 7.0.0 - optional: true - domhandler@4.3.1: dependencies: domelementtype: 2.3.0 @@ -18578,6 +18949,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 @@ -18599,7 +18974,7 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@17.2.3: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: dependencies: @@ -18609,8 +18984,6 @@ snapshots: duplexer@0.1.2: {} - earcut@2.2.4: {} - earcut@3.0.2: {} eastasianwidth@0.2.0: {} @@ -18621,7 +18994,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.267: {} + electron-to-chromium@1.5.286: {} emoji-regex@10.6.0: {} @@ -18646,12 +19019,12 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.3: + engine.io-client@6.6.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.17.1 + ws: 8.18.3 xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -18660,23 +19033,23 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.4: + engine.io@6.6.5: dependencies: '@types/cors': 2.8.19 - '@types/node': 24.10.4 + '@types/node': 24.11.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 - cors: 2.8.5 - debug: 4.3.7 + cors: 2.8.6 + debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.17.1 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - enhanced-resolve@5.18.4: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -18687,6 +19060,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + env-paths@2.2.1: {} err-code@2.0.3: {} @@ -18701,6 +19076,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -18747,7 +19124,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.15.0 + acorn: 8.16.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -18806,34 +19183,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.1: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.1 - '@esbuild/android-arm': 0.27.1 - '@esbuild/android-arm64': 0.27.1 - '@esbuild/android-x64': 0.27.1 - '@esbuild/darwin-arm64': 0.27.1 - '@esbuild/darwin-x64': 0.27.1 - '@esbuild/freebsd-arm64': 0.27.1 - '@esbuild/freebsd-x64': 0.27.1 - '@esbuild/linux-arm': 0.27.1 - '@esbuild/linux-arm64': 0.27.1 - '@esbuild/linux-ia32': 0.27.1 - '@esbuild/linux-loong64': 0.27.1 - '@esbuild/linux-mips64el': 0.27.1 - '@esbuild/linux-ppc64': 0.27.1 - '@esbuild/linux-riscv64': 0.27.1 - '@esbuild/linux-s390x': 0.27.1 - '@esbuild/linux-x64': 0.27.1 - '@esbuild/netbsd-arm64': 0.27.1 - '@esbuild/netbsd-x64': 0.27.1 - '@esbuild/openbsd-arm64': 0.27.1 - '@esbuild/openbsd-x64': 0.27.1 - '@esbuild/openharmony-arm64': 0.27.1 - '@esbuild/sunos-x64': 0.27.1 - '@esbuild/win32-arm64': 0.27.1 - '@esbuild/win32-ia32': 0.27.1 - '@esbuild/win32-x64': 0.27.1 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -18847,70 +19224,59 @@ snapshots: escape-string-regexp@5.0.0: {} - escodegen@2.1.0: + eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)): dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - optional: true + eslint: 10.0.2(jiti@2.6.1) - eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-compat@6.2.0(eslint@10.0.2(jiti@2.6.1)): dependencies: - eslint: 9.39.2(jiti@2.6.1) - - eslint-plugin-compat@6.0.2(eslint@9.39.2(jiti@2.6.1)): - dependencies: - '@mdn/browser-compat-data': 5.7.6 + '@mdn/browser-compat-data': 6.1.5 ast-metadata-inferer: 0.8.1 browserslist: 4.28.1 - caniuse-lite: 1.0.30001760 - eslint: 9.39.2(jiti@2.6.1) + caniuse-lite: 1.0.30001774 + eslint: 10.0.2(jiti@2.6.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.7.3 + semver: 7.7.4 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.7.4): + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.0.2(jiti@2.6.1)))(eslint@10.0.2(jiti@2.6.1))(prettier@3.8.1): dependencies: - eslint: 9.39.2(jiti@2.6.1) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 + eslint: 10.0.2(jiti@2.6.1) + prettier: 3.8.1 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.0.2(jiti@2.6.1)) - eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.43.3): + eslint-plugin-svelte@3.15.0(eslint@10.0.2(jiti@2.6.1))(svelte@5.53.5): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.0.2(jiti@2.6.1) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 postcss: 8.5.6 postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) - semver: 7.7.3 - svelte-eslint-parser: 1.4.1(svelte@5.43.3) + semver: 7.7.4 + svelte-eslint-parser: 1.4.1(svelte@5.53.5) optionalDependencies: - svelte: 5.43.3 + svelte: 5.53.5 transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-unicorn@63.0.0(eslint@10.0.2(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) - '@eslint/plugin-kit': 0.4.1 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) change-case: 5.4.4 ci-info: 4.3.1 clean-regexp: 1.0.0 core-js-compat: 3.47.0 - eslint: 9.39.2(jiti@2.6.1) - esquery: 1.6.0 + eslint: 10.0.2(jiti@2.6.1) find-up-simple: 1.0.1 globals: 16.5.0 indent-string: 5.0.0 @@ -18919,7 +19285,7 @@ snapshots: pluralize: 8.0.0 regexp-tree: 0.1.27 regjsparser: 0.13.0 - semver: 7.7.3 + semver: 7.7.4 strip-indent: 4.1.1 eslint-scope@5.1.1: @@ -18932,33 +19298,39 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 + eslint-scope@9.1.1: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-visitor-keys@3.4.3: {} eslint-visitor-keys@4.2.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint-visitor-keys@5.0.1: {} + + eslint@10.0.2(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.2(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.2 + '@eslint/config-helpers': 0.5.2 + '@eslint/core': 1.1.0 + '@eslint/plugin-kit': 0.6.0 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 + ajv: 6.14.0 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 + eslint-scope: 9.1.1 + eslint-visitor-keys: 5.0.1 + espree: 11.1.1 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -18968,8 +19340,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 10.2.2 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -18988,17 +19359,23 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 + espree@11.1.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + esprima@4.0.1: {} - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 - esrap@2.2.1: + esrap@2.2.3: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -19053,13 +19430,13 @@ snapshots: eta@2.2.0: {} - eta@4.5.0: {} + eta@4.5.1: {} etag@1.8.1: {} eval@0.1.8: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 require-like: 0.1.2 event-emitter@0.3.5: @@ -19093,21 +19470,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exiftool-vendored.exe@13.44.0: + exiftool-vendored.exe@13.51.0: optional: true - exiftool-vendored.pl@13.44.0: {} + exiftool-vendored.pl@13.51.0: {} - exiftool-vendored@34.1.0: + exiftool-vendored@35.10.1: dependencies: - '@photostructure/tz-lookup': 11.3.0 + '@photostructure/tz-lookup': 11.4.0 '@types/luxon': 3.7.1 - batch-cluster: 16.0.0 - exiftool-vendored.pl: 13.44.0 + batch-cluster: 17.3.1 + exiftool-vendored.pl: 13.51.0 he: 1.2.0 luxon: 3.7.2 optionalDependencies: - exiftool-vendored.exe: 13.44.0 + exiftool-vendored.exe: 13.51.0 expect-type@1.3.0: {} @@ -19136,7 +19513,7 @@ snapshots: parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 safe-buffer: 5.2.1 send: 0.19.2 @@ -19149,42 +19526,10 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.1.0: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.1 + body-parser: 2.2.2 content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 @@ -19203,7 +19548,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.0 + qs: 6.14.1 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -19226,10 +19571,10 @@ snapshots: extend@3.0.2: {} - fabric@6.9.1: + fabric@7.2.0(encoding@0.1.13): optionalDependencies: - canvas: 2.11.2 - jsdom: 20.0.3(canvas@2.11.2) + canvas: 2.11.2(encoding@0.1.13) + jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - bufferutil - encoding @@ -19265,11 +19610,7 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@5.2.5: - dependencies: - strnum: 2.1.2 - - fastq@1.19.1: + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -19299,21 +19640,21 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.103.0): + file-loader@6.2.0(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 file-source@0.6.1: dependencies: stream-source: 0.3.5 - file-type@21.1.0: + file-type@21.3.0: dependencies: - '@tokenizer/inflate': 0.3.1 + '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 - token-types: 6.1.1 + token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color @@ -19388,9 +19729,9 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 chalk: 4.1.2 chokidar: 4.0.3 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -19400,10 +19741,10 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} @@ -19433,6 +19774,10 @@ snapshots: fresh@2.0.0: {} + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.2 + fs-constants@1.0.0: {} fs-extra@10.1.0: @@ -19483,10 +19828,10 @@ snapshots: geo-coordinates-parser@1.7.4: {} - geo-tz@8.1.4: + geo-tz@8.1.5: dependencies: - '@turf/boolean-point-in-polygon': 7.3.1 - '@turf/helpers': 7.3.1 + '@turf/boolean-point-in-polygon': 7.3.2 + '@turf/helpers': 7.3.2 geobuf: 3.0.2 pbf: 3.3.0 @@ -19496,10 +19841,6 @@ snapshots: pbf: 3.3.0 shapefile: 0.6.6 - geojson-vt@3.2.1: {} - - geojson-vt@4.0.2: {} - geojson@0.5.0: {} get-caller-file@2.0.5: {} @@ -19530,6 +19871,10 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@1.5.0: {} gl-matrix@3.4.4: {} @@ -19552,7 +19897,7 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 3.4.3 - minimatch: 9.0.5 + minimatch: 9.0.6 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 1.11.1 @@ -19561,14 +19906,20 @@ snapshots: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.1.1 + minimatch: 10.2.2 minipass: 7.1.2 package-json-from-dist: 1.0.1 path-scurry: 2.0.1 glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.2.2 + minipass: 7.1.2 + path-scurry: 2.0.1 + + glob@13.0.2: + dependencies: + minimatch: 10.2.2 minipass: 7.1.2 path-scurry: 2.0.1 @@ -19585,12 +19936,12 @@ snapshots: dependencies: ini: 2.0.0 - globals@14.0.0: {} - globals@15.15.0: {} globals@16.5.0: {} + globals@17.3.0: {} + globalyzer@0.1.0: {} globby@11.1.0: @@ -19639,12 +19990,12 @@ snapshots: section-matter: 1.0.0 strip-bom-string: 1.0.0 - grid-index@1.1.0: {} - gzip-size@6.0.0: dependencies: duplexer: 0.1.2 + hachure-fill@0.5.2: {} + handle-thing@2.0.1: {} handlebars@4.7.8: @@ -19656,11 +20007,17 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.0.11: + happy-dom@20.6.3: dependencies: - '@types/node': 20.19.27 + '@types/node': 24.11.0 '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate has-flag@4.0.0: {} @@ -19832,7 +20189,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -19855,9 +20212,9 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 - html-encoding-sniffer@3.0.0: + html-encoding-sniffer@4.0.0: dependencies: - whatwg-encoding: 2.0.0 + whatwg-encoding: 3.1.1 optional: true html-escaper@2.0.2: {} @@ -19894,15 +20251,15 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.5(webpack@5.103.0): + html-webpack-plugin@5.6.5(webpack@5.104.1): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 pretty-error: 4.0.0 tapable: 2.3.0 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 htmlparser2@6.1.0: dependencies: @@ -19952,15 +20309,6 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -20022,9 +20370,8 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - optional: true - iconv-lite@0.7.1: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -20044,16 +20391,20 @@ snapshots: immediate@3.3.0: {} + immutable-json-patch@6.0.2: {} + + immutable@5.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-in-the-middle@2.0.0: + import-in-the-middle@2.0.6: dependencies: - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) - cjs-module-lexer: 1.4.3 + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 import-lazy@4.0.0: {} @@ -20081,15 +20432,15 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@8.2.7(@types/node@24.10.4): + inquirer@8.2.7(@types/node@24.11.0): dependencies: - '@inquirer/external-editor': 1.0.3(@types/node@24.10.4) + '@inquirer/external-editor': 1.0.3(@types/node@24.11.0) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 cli-width: 3.0.0 figures: 3.2.0 - lodash: 4.17.21 + lodash: 4.17.23 mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 @@ -20101,6 +20452,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + internmap@1.0.1: {} + internmap@2.0.3: {} intl-messageformat@10.7.18: @@ -20110,13 +20463,34 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 + intl-messageformat@11.1.2: + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 + tslib: 2.8.1 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 - ioredis@5.8.2: + ioredis@5.9.2: dependencies: - '@ioredis/commands': 1.4.0 + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ioredis@5.9.3: + dependencies: + '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 debug: 4.4.3 denque: 2.1.0 @@ -20257,7 +20631,7 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.1: {} + isexe@4.0.0: {} isobject@3.0.1: {} @@ -20297,7 +20671,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.10.4 + '@types/node': 24.11.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20305,13 +20679,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20322,6 +20696,8 @@ snapshots: jiti@2.6.1: {} + jmespath@0.16.0: {} + joi@17.13.3: dependencies: '@hapi/hoek': 9.3.0 @@ -20347,34 +20723,28 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)): + jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)): dependencies: - abab: 2.0.6 - acorn: 8.15.0 - acorn-globals: 7.0.1 - cssom: 0.5.0 - cssstyle: 2.3.0 - data-urls: 3.0.2 + cssstyle: 4.6.0 + data-urls: 5.0.0 decimal.js: 10.6.0 - domexception: 4.0.0 - escodegen: 2.1.0 - form-data: 4.0.5 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.23 parse5: 7.3.0 + rrweb-cssom: 0.8.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 4.0.0 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 - ws: 8.18.3 - xml-name-validator: 4.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 optionalDependencies: canvas: 2.11.2(encoding@0.1.13) transitivePeerDependencies: @@ -20383,41 +20753,7 @@ snapshots: - utf-8-validate optional: true - jsdom@20.0.3(canvas@2.11.2): - dependencies: - abab: 2.0.6 - acorn: 8.15.0 - acorn-globals: 7.0.1 - cssom: 0.5.0 - cssstyle: 2.3.0 - data-urls: 3.0.2 - decimal.js: 10.6.0 - domexception: 4.0.0 - escodegen: 2.1.0 - form-data: 4.0.5 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.23 - parse5: 7.3.0 - saxes: 6.0.0 - symbol-tree: 3.2.4 - tough-cookie: 4.1.4 - w3c-xmlserializer: 4.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 11.0.0 - ws: 8.18.3 - xml-name-validator: 4.0.0 - optionalDependencies: - canvas: 2.11.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - optional: true + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -20431,6 +20767,8 @@ snapshots: json-schema@0.4.0: {} + json-source-map@0.6.1: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-pretty-compact@4.0.0: {} @@ -20445,6 +20783,14 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpath-plus@10.3.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + + jsonrepair@3.13.1: {} + jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -20456,7 +20802,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.3 + semver: 7.7.4 just-compare@2.3.0: {} @@ -20473,7 +20819,9 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 - kdbush@3.0.0: {} + katex@0.16.27: + dependencies: + commander: 8.3.0 kdbush@4.0.2: {} @@ -20485,6 +20833,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kind-of@6.0.3: {} kleur@3.0.3: {} @@ -20516,14 +20866,30 @@ snapshots: type-is: 2.0.1 vary: 1.1.2 - kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.7): + kysely-postgres-js@3.0.0(kysely@0.28.11)(postgres@3.4.8): + dependencies: + kysely: 0.28.11 + optionalDependencies: + postgres: 3.4.8 + + kysely-postgres-js@3.0.0(kysely@0.28.2)(postgres@3.4.8): dependencies: kysely: 0.28.2 optionalDependencies: - postgres: 3.4.7 + postgres: 3.4.8 + + kysely@0.28.11: {} kysely@0.28.2: {} + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + latest-version@7.0.0: dependencies: package-json: 8.1.1 @@ -20533,6 +20899,10 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -20548,54 +20918,54 @@ snapshots: libphonenumber-js@1.12.31: {} - lightningcss-android-arm64@1.30.2: + lightningcss-android-arm64@1.31.1: optional: true - lightningcss-darwin-arm64@1.30.2: + lightningcss-darwin-arm64@1.31.1: optional: true - lightningcss-darwin-x64@1.30.2: + lightningcss-darwin-x64@1.31.1: optional: true - lightningcss-freebsd-x64@1.30.2: + lightningcss-freebsd-x64@1.31.1: optional: true - lightningcss-linux-arm-gnueabihf@1.30.2: + lightningcss-linux-arm-gnueabihf@1.31.1: optional: true - lightningcss-linux-arm64-gnu@1.30.2: + lightningcss-linux-arm64-gnu@1.31.1: optional: true - lightningcss-linux-arm64-musl@1.30.2: + lightningcss-linux-arm64-musl@1.31.1: optional: true - lightningcss-linux-x64-gnu@1.30.2: + lightningcss-linux-x64-gnu@1.31.1: optional: true - lightningcss-linux-x64-musl@1.30.2: + lightningcss-linux-x64-musl@1.31.1: optional: true - lightningcss-win32-arm64-msvc@1.30.2: + lightningcss-win32-arm64-msvc@1.31.1: optional: true - lightningcss-win32-x64-msvc@1.30.2: + lightningcss-win32-x64-msvc@1.31.1: optional: true - lightningcss@1.30.2: + lightningcss@1.31.1: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.30.2 - lightningcss-darwin-arm64: 1.30.2 - lightningcss-darwin-x64: 1.30.2 - lightningcss-freebsd-x64: 1.30.2 - lightningcss-linux-arm-gnueabihf: 1.30.2 - lightningcss-linux-arm64-gnu: 1.30.2 - lightningcss-linux-arm64-musl: 1.30.2 - lightningcss-linux-x64-gnu: 1.30.2 - lightningcss-linux-x64-musl: 1.30.2 - lightningcss-win32-arm64-msvc: 1.30.2 - lightningcss-win32-x64-msvc: 1.30.2 + lightningcss-android-arm64: 1.31.1 + lightningcss-darwin-arm64: 1.31.1 + lightningcss-darwin-x64: 1.31.1 + lightningcss-freebsd-x64: 1.31.1 + lightningcss-linux-arm-gnueabihf: 1.31.1 + lightningcss-linux-arm64-gnu: 1.31.1 + lightningcss-linux-arm64-musl: 1.31.1 + lightningcss-linux-x64-gnu: 1.31.1 + lightningcss-linux-x64-musl: 1.31.1 + lightningcss-win32-arm64-msvc: 1.31.1 + lightningcss-win32-x64-msvc: 1.31.1 lilconfig@2.1.0: {} @@ -20631,6 +21001,8 @@ snapshots: lodash-es@4.17.21: {} + lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -20653,13 +21025,11 @@ snapshots: lodash.memoize@4.1.2: {} - lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} lodash.uniq@4.5.0: {} - lodash@4.17.21: {} + lodash@4.17.23: {} log-symbols@4.1.0: dependencies: @@ -20694,7 +21064,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: @@ -20732,7 +21102,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 make-fetch-happen@15.0.3: dependencies: @@ -20740,42 +21110,17 @@ snapshots: cacache: 20.0.3 http-cache-semantics: 4.2.0 minipass: 7.1.2 - minipass-fetch: 5.0.0 + minipass-fetch: 5.0.1 minipass-flush: 1.0.5 minipass-pipeline: 1.2.4 negotiator: 1.0.0 proc-log: 6.1.0 promise-retry: 2.0.1 - ssri: 13.0.0 + ssri: 13.0.1 transitivePeerDependencies: - supports-color - mapbox-gl@1.13.3: - dependencies: - '@mapbox/geojson-rewind': 0.5.2 - '@mapbox/geojson-types': 1.0.2 - '@mapbox/jsonlint-lines-primitives': 2.0.2 - '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) - '@mapbox/point-geometry': 0.1.0 - '@mapbox/tiny-sdf': 1.2.5 - '@mapbox/unitbezier': 0.0.0 - '@mapbox/vector-tile': 1.3.1 - '@mapbox/whoots-js': 3.1.0 - csscolorparser: 1.0.3 - earcut: 2.2.4 - geojson-vt: 3.2.1 - gl-matrix: 3.4.4 - grid-index: 1.1.0 - murmurhash-js: 1.0.0 - pbf: 3.3.0 - potpack: 1.0.2 - quickselect: 2.0.0 - rw: 1.3.3 - supercluster: 7.1.5 - tinyqueue: 2.0.3 - vt-pbf: 3.1.3 - - maplibre-gl@5.14.0: + maplibre-gl@5.18.0: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -20784,14 +21129,13 @@ snapshots: '@mapbox/unitbezier': 0.0.1 '@mapbox/vector-tile': 2.0.4 '@mapbox/whoots-js': 3.1.0 + '@maplibre/geojson-vt': 5.0.4 '@maplibre/maplibre-gl-style-spec': 24.4.1 - '@maplibre/mlt': 1.1.2 - '@maplibre/vt-pbf': 4.2.0 + '@maplibre/mlt': 1.1.6 + '@maplibre/vt-pbf': 4.2.1 '@types/geojson': 7946.0.16 - '@types/geojson-vt': 3.2.5 '@types/supercluster': 7.1.3 earcut: 3.0.2 - geojson-vt: 4.0.2 gl-matrix: 3.4.4 kdbush: 4.0.2 murmurhash-js: 1.0.0 @@ -20815,6 +21159,8 @@ snapshots: marked@16.4.2: {} + marked@17.0.3: {} + math-intrinsics@1.1.0: {} mdast-util-directive@3.1.0: @@ -21026,6 +21372,8 @@ snapshots: tree-dump: 1.1.0(tslib@2.8.1) tslib: 2.8.1 + memoize-one@6.0.0: {} + memoizee@0.4.17: dependencies: d: 1.0.2 @@ -21045,6 +21393,29 @@ snapshots: merge2@1.4.1: {} + mermaid@11.12.2: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 0.6.3 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.27 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + methods@1.1.2: {} micromark-core-commonmark@2.0.3: @@ -21183,8 +21554,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -21382,17 +21753,17 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.103.0): + mini-css-extract-plugin@2.9.4(webpack@5.104.1): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.103.0 + webpack: 5.104.1 minimalistic-assert@1.0.1: {} - minimatch@10.1.1: + minimatch@10.2.2: dependencies: - '@isaacs/brace-expansion': 5.0.0 + brace-expansion: 5.0.3 minimatch@3.1.2: dependencies: @@ -21402,9 +21773,9 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimatch@9.0.5: + minimatch@9.0.6: dependencies: - brace-expansion: 2.0.2 + brace-expansion: 5.0.3 minimist@1.2.8: {} @@ -21412,10 +21783,10 @@ snapshots: dependencies: minipass: 7.1.2 - minipass-fetch@5.0.0: + minipass-fetch@5.0.1: dependencies: minipass: 7.1.2 - minipass-sized: 1.0.3 + minipass-sized: 2.0.0 minizlib: 3.1.0 optionalDependencies: encoding: 0.1.13 @@ -21428,9 +21799,9 @@ snapshots: dependencies: minipass: 3.3.6 - minipass-sized@1.0.3: + minipass-sized@2.0.0: dependencies: - minipass: 3.3.6 + minipass: 7.1.2 minipass@3.3.6: dependencies: @@ -21459,6 +21830,15 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.2 + mnemonist@0.40.3: dependencies: obliterator: 2.0.5 @@ -21503,6 +21883,13 @@ snapshots: type-is: 1.6.18 xtend: 4.0.2 + multer@2.1.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + multicast-dns@7.2.5: dependencies: dns-packet: 5.6.1 @@ -21520,13 +21907,15 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nan@2.24.0: + nan@2.25.0: optional: true nanoid@3.3.11: {} nanoid@5.1.6: {} + natural-compare-lite@1.4.0: {} + natural-compare@1.4.0: {} nearley@2.20.1: @@ -21544,39 +21933,39 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@types/inquirer@8.2.12)(@types/node@24.10.4)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(@types/inquirer@8.2.12)(@types/node@24.11.0)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.12 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) - inquirer: 8.2.7(@types/node@24.10.4) + inquirer: 8.2.7(@types/node@24.11.0) transitivePeerDependencies: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.2 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9): + nestjs-otel@7.0.1(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.14(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.14)(@nestjs/websockets@11.1.14)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.0 '@opentelemetry/host-metrics': 0.36.2(@opentelemetry/api@1.9.0) response-time: 2.3.4 @@ -21593,11 +21982,14 @@ snapshots: node-addon-api@4.3.0: {} + node-addon-api@7.1.1: + optional: true + node-addon-api@8.5.0: {} node-emoji@1.11.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 node-emoji@2.2.0: dependencies: @@ -21606,11 +21998,6 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - optional: true - node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -21626,7 +22013,7 @@ snapshots: node-gyp-build@4.8.4: {} - node-gyp@12.1.0: + node-gyp@12.2.0: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.3 @@ -21634,16 +22021,16 @@ snapshots: make-fetch-happen: 15.0.3 nopt: 9.0.0 proc-log: 6.1.0 - semver: 7.7.3 - tar: 7.5.2 + semver: 7.7.4 + tar: 7.5.7 tinyglobby: 0.2.15 - which: 6.0.0 + which: 6.0.1 transitivePeerDependencies: - supports-color node-releases@2.0.27: {} - nodemailer@7.0.11: {} + nodemailer@7.0.13: {} nopt@1.0.10: dependencies: @@ -21682,11 +22069,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.103.0): + null-loader@4.0.1(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 nwsapi@2.2.23: optional: true @@ -21699,7 +22086,7 @@ snapshots: pkg-types: 2.3.0 tinyexec: 0.3.2 - oauth4webapi@3.8.3: {} + oauth4webapi@3.8.5: {} object-assign@4.1.1: {} @@ -21722,12 +22109,14 @@ snapshots: obuf@1.1.2: {} - oidc-provider@9.6.0: + obug@2.1.1: {} + + oidc-provider@9.6.1: dependencies: '@koa/cors': 5.0.0 - '@koa/router': 15.1.0(koa@3.1.1) + '@koa/router': 15.3.0(koa@3.1.1) debug: 4.4.3 - eta: 4.5.0 + eta: 4.5.1 jose: 6.1.3 jsesc: 3.1.0 koa: 3.1.1 @@ -21770,10 +22159,10 @@ snapshots: opener@1.5.2: {} - openid-client@6.8.1: + openid-client@6.8.2: dependencies: jose: 6.1.3 - oauth4webapi: 3.8.3 + oauth4webapi: 3.8.5 optionator@0.9.4: dependencies: @@ -21866,7 +22255,9 @@ snapshots: got: 12.6.1 registry-auth-token: 5.1.0 registry-url: 6.0.1 - semver: 7.7.3 + semver: 7.7.4 + + package-manager-detector@1.6.0: {} param-case@3.0.4: dependencies: @@ -21889,7 +22280,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -21921,6 +22312,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-data-parser@0.1.0: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -21940,7 +22333,7 @@ snapshots: path-scurry@2.0.1: dependencies: - lru-cache: 11.2.4 + lru-cache: 11.2.6 minipass: 7.1.2 path-source@0.1.3: @@ -21975,36 +22368,36 @@ snapshots: peberminta@0.9.0: {} - pg-cloudflare@1.2.7: + pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.9.1: {} + pg-connection-string@2.11.0: {} pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-pool@3.11.0(pg@8.18.0): dependencies: - pg: 8.16.3 + pg: 8.18.0 - pg-protocol@1.10.3: {} + pg-protocol@1.11.0: {} pg-types@2.2.0: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 - postgres-bytea: 1.0.0 + postgres-bytea: 1.0.1 postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.16.3: + pg@8.18.0: dependencies: - pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.16.3) - pg-protocol: 1.10.3 + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) + pg-protocol: 1.11.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: - pg-cloudflare: 1.2.7 + pg-cloudflare: 1.3.0 pgpass@1.0.5: dependencies: @@ -22026,17 +22419,23 @@ snapshots: dependencies: find-up: 6.3.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + pkg-types@2.3.0: dependencies: confbox: 0.2.2 exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.2: {} - playwright@1.57.0: + playwright@1.58.2: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.2 optionalDependencies: fsevents: 2.3.2 @@ -22047,7 +22446,7 @@ snapshots: '@types/leaflet': 1.9.21 fflate: 0.8.2 - pmtiles@4.3.0: + pmtiles@4.4.0: dependencies: fflate: 0.8.2 @@ -22059,6 +22458,13 @@ snapshots: dependencies: robust-predicates: 3.0.2 + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): dependencies: postcss: 8.5.6 @@ -22220,21 +22626,22 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.6 + tsx: 4.21.0 yaml: 2.8.2 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.104.1): dependencies: cosmiconfig: 8.3.6(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.6 - semver: 7.7.3 - webpack: 5.103.0 + semver: 7.7.4 + webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -22429,7 +22836,7 @@ snapshots: '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.6) '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) - autoprefixer: 10.4.23(postcss@8.5.6) + autoprefixer: 10.4.24(postcss@8.5.6) browserslist: 4.28.1 css-blank-pseudo: 7.0.1(postcss@8.5.6) css-has-pseudo: 7.0.3(postcss@8.5.6) @@ -22540,7 +22947,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -22548,37 +22955,35 @@ snapshots: dependencies: xtend: 4.0.2 - postgres@3.4.7: {} - - potpack@1.0.2: {} + postgres@3.4.8: {} potpack@2.1.0: {} prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.0: + prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.9.3): dependencies: - prettier: 3.7.4 + prettier: 3.8.1 typescript: 5.9.3 - prettier-plugin-sort-json@4.1.1(prettier@3.7.4): + prettier-plugin-sort-json@4.2.0(prettier@3.8.1): dependencies: - prettier: 3.7.4 + prettier: 3.8.1 - prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.43.3): + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.53.5): dependencies: - prettier: 3.7.4 - svelte: 5.43.3 + prettier: 3.8.1 + svelte: 5.53.5 - prettier@3.7.4: {} + prettier@3.8.1: {} pretty-error@4.0.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 renderkid: 3.0.0 pretty-format@27.5.1: @@ -22625,9 +23030,12 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - properties-reader@2.3.0: + properties-reader@3.0.1: dependencies: - mkdirp: 1.0.4 + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color property-information@5.6.0: dependencies: @@ -22649,7 +23057,22 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.4 + '@types/node': 24.11.0 + long: 5.3.2 + + protobufjs@8.0.0: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.11.0 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -22659,11 +23082,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - psl@1.15.0: - dependencies: - punycode: 2.3.1 - optional: true - pump@3.0.3: dependencies: end-of-stream: 1.4.5 @@ -22683,21 +23101,16 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 - querystringify@2.2.0: - optional: true - queue-microtask@1.2.3: {} quick-lru@5.1.1: {} quick-lru@7.3.0: {} - quickselect@2.0.0: {} - quickselect@3.0.0: {} railroad-diagrams@1.0.0: {} @@ -22726,14 +23139,14 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.103.0): + raw-loader@4.0.2(webpack@5.104.1): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 rc@1.2.8: dependencies: @@ -22748,9 +23161,9 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@19.2.3(react@19.2.3): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 scheduler: 0.27.0 react-email@4.3.2: @@ -22769,7 +23182,7 @@ snapshots: nypm: 0.6.0 ora: 8.2.0 prompts: 2.4.2 - socket.io: 4.8.1 + socket.io: 4.8.3 tsconfig-paths: 4.2.0 transitivePeerDependencies: - bufferutil @@ -22786,11 +23199,11 @@ snapshots: dependencies: react: 18.3.1 - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0): + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.104.1): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.103.0 + webpack: 5.104.1 react-promise-suspense@0.3.4: dependencies: @@ -22798,13 +23211,13 @@ snapshots: react-router-config@5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 react: 18.3.1 react-router: 5.3.4(react@18.3.1) react-router-dom@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -22815,7 +23228,7 @@ snapshots: react-router@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -22830,7 +23243,7 @@ snapshots: dependencies: loose-envify: 1.4.0 - react@19.2.3: {} + react@19.2.4: {} read-cache@1.0.0: dependencies: @@ -22876,10 +23289,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.15.0): + recma-jsx@1.0.1(acorn@8.16.0): dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -23036,7 +23449,7 @@ snapshots: css-select: 4.3.0 dom-converter: 0.2.0 htmlparser2: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 strip-ansi: 6.0.1 repeat-string@1.6.1: {} @@ -23064,6 +23477,8 @@ snapshots: resolve-pathname@3.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 @@ -23107,43 +23522,53 @@ snapshots: robust-predicates@3.0.2: {} - rollup-plugin-visualizer@6.0.5(rollup@4.53.4): + rollup-plugin-visualizer@6.0.5(rollup@4.55.1): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: - rollup: 4.53.4 + rollup: 4.55.1 - rollup@4.53.4: + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.4 - '@rollup/rollup-android-arm64': 4.53.4 - '@rollup/rollup-darwin-arm64': 4.53.4 - '@rollup/rollup-darwin-x64': 4.53.4 - '@rollup/rollup-freebsd-arm64': 4.53.4 - '@rollup/rollup-freebsd-x64': 4.53.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.4 - '@rollup/rollup-linux-arm-musleabihf': 4.53.4 - '@rollup/rollup-linux-arm64-gnu': 4.53.4 - '@rollup/rollup-linux-arm64-musl': 4.53.4 - '@rollup/rollup-linux-loong64-gnu': 4.53.4 - '@rollup/rollup-linux-ppc64-gnu': 4.53.4 - '@rollup/rollup-linux-riscv64-gnu': 4.53.4 - '@rollup/rollup-linux-riscv64-musl': 4.53.4 - '@rollup/rollup-linux-s390x-gnu': 4.53.4 - '@rollup/rollup-linux-x64-gnu': 4.53.4 - '@rollup/rollup-linux-x64-musl': 4.53.4 - '@rollup/rollup-openharmony-arm64': 4.53.4 - '@rollup/rollup-win32-arm64-msvc': 4.53.4 - '@rollup/rollup-win32-ia32-msvc': 4.53.4 - '@rollup/rollup-win32-x64-gnu': 4.53.4 - '@rollup/rollup-win32-x64-msvc': 4.53.4 + '@rollup/rollup-android-arm-eabi': 4.55.1 + '@rollup/rollup-android-arm64': 4.55.1 + '@rollup/rollup-darwin-arm64': 4.55.1 + '@rollup/rollup-darwin-x64': 4.55.1 + '@rollup/rollup-freebsd-arm64': 4.55.1 + '@rollup/rollup-freebsd-x64': 4.55.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.55.1 + '@rollup/rollup-linux-arm-musleabihf': 4.55.1 + '@rollup/rollup-linux-arm64-gnu': 4.55.1 + '@rollup/rollup-linux-arm64-musl': 4.55.1 + '@rollup/rollup-linux-loong64-gnu': 4.55.1 + '@rollup/rollup-linux-loong64-musl': 4.55.1 + '@rollup/rollup-linux-ppc64-gnu': 4.55.1 + '@rollup/rollup-linux-ppc64-musl': 4.55.1 + '@rollup/rollup-linux-riscv64-gnu': 4.55.1 + '@rollup/rollup-linux-riscv64-musl': 4.55.1 + '@rollup/rollup-linux-s390x-gnu': 4.55.1 + '@rollup/rollup-linux-x64-gnu': 4.55.1 + '@rollup/rollup-linux-x64-musl': 4.55.1 + '@rollup/rollup-openbsd-x64': 4.55.1 + '@rollup/rollup-openharmony-arm64': 4.55.1 + '@rollup/rollup-win32-arm64-msvc': 4.55.1 + '@rollup/rollup-win32-ia32-msvc': 4.55.1 + '@rollup/rollup-win32-x64-gnu': 4.55.1 + '@rollup/rollup-win32-x64-msvc': 4.55.1 fsevents: 2.3.3 + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + router@2.2.0: dependencies: debug: 4.4.3 @@ -23154,6 +23579,9 @@ snapshots: transitivePeerDependencies: - supports-color + rrweb-cssom@0.8.0: + optional: true + rtlcss@4.3.0: dependencies: escalade: 3.2.0 @@ -23169,14 +23597,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + runed@0.35.1(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.43.3 + svelte: 5.53.5 optionalDependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@sveltejs/kit': 2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -23202,7 +23630,7 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sanitize-html@2.17.0: + sanitize-html@2.17.1: dependencies: deepmerge: 4.3.1 escape-string-regexp: 4.0.0 @@ -23211,6 +23639,14 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.5.6 + sass@1.97.1: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.4 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + sax@1.4.3: {} saxes@6.0.0: @@ -23229,15 +23665,15 @@ snapshots: schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 - ajv: 6.12.6 - ajv-keywords: 3.5.2(ajv@6.12.6) + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) search-insights@2.17.3: {} @@ -23259,11 +23695,11 @@ snapshots: semver-diff@4.0.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 semver@6.3.1: {} - semver@7.7.3: {} + semver@7.7.4: {} send@0.19.2: dependencies: @@ -23345,7 +23781,7 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@2.7.2: {} + set-cookie-parser@3.0.1: {} set-function-length@1.2.2: dependencies: @@ -23380,8 +23816,8 @@ snapshots: '@img/colour': 1.0.0 detect-libc: 2.1.2 node-addon-api: 8.5.0 - node-gyp: 12.1.0 - semver: 7.7.3 + node-gyp: 12.2.0 + semver: 7.7.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -23464,6 +23900,8 @@ snapshots: simple-icons@15.22.0: {} + simple-icons@16.9.0: {} + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 @@ -23502,42 +23940,42 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-adapter@2.5.5: + socket.io-adapter@2.5.6: dependencies: - debug: 4.3.7 - ws: 8.17.1 + debug: 4.4.3 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-client@4.8.1: + socket.io-client@4.8.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 - engine.io-client: 6.6.3 - socket.io-parser: 4.2.4 + debug: 4.4.3 + engine.io-client: 6.6.4 + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.4: + socket.io-parser@4.2.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 transitivePeerDependencies: - supports-color - socket.io@4.8.1: + socket.io@4.8.3: dependencies: accepts: 1.3.8 base64id: 2.0.0 - cors: 2.8.5 - debug: 4.3.7 - engine.io: 6.6.4 - socket.io-adapter: 2.5.5 - socket.io-parser: 4.2.4 + cors: 2.8.6 + debug: 4.4.3 + engine.io: 6.6.5 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.5 transitivePeerDependencies: - bufferutil - supports-color @@ -23608,7 +24046,7 @@ snapshots: sprintf-js@1.0.3: {} - sql-formatter@15.6.12: + sql-formatter@15.7.2: dependencies: argparse: 2.0.1 nearley: 2.20.1 @@ -23626,9 +24064,9 @@ snapshots: bcrypt-pbkdf: 1.0.2 optionalDependencies: cpu-features: 0.0.10 - nan: 2.24.0 + nan: 2.25.0 - ssri@13.0.0: + ssri@13.0.1: dependencies: minipass: 7.1.2 @@ -23722,12 +24160,12 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.1.2: {} - strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -23742,6 +24180,8 @@ snapshots: postcss: 8.5.6 postcss-selector-parser: 6.1.2 + stylis@4.3.6: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -23752,7 +24192,7 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 - superagent@10.2.3: + superagent@10.3.0: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 @@ -23762,22 +24202,19 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 transitivePeerDependencies: - supports-color - supercluster@7.1.5: - dependencies: - kdbush: 3.0.0 - supercluster@8.0.1: dependencies: kdbush: 4.0.2 - supertest@7.1.4: + supertest@7.2.2: dependencies: + cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.2.3 + superagent: 10.3.0 transitivePeerDependencies: - supports-color @@ -23791,19 +24228,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.43.3)(typescript@5.9.3): + svelte-awesome@3.3.5(svelte@5.53.5): + dependencies: + svelte: 5.53.5 + + svelte-check@4.4.1(picomatch@4.0.3)(svelte@5.53.5)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.43.3 + svelte: 5.53.5 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.1(svelte@5.43.3): + svelte-eslint-parser@1.4.1(svelte@5.53.5): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -23812,7 +24253,12 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.43.3 + svelte: 5.53.5 + + svelte-floating-ui@1.5.8: + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/dom': 1.7.4 svelte-gestures@5.2.2: {} @@ -23820,7 +24266,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.43.3): + svelte-i18n@4.0.1(svelte@5.53.5): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -23828,47 +24274,85 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.43.3 + svelte: 5.53.5 tiny-glob: 0.2.9 - svelte-maplibre@1.2.5(svelte@5.43.3): + svelte-jsoneditor@3.11.0(svelte@5.53.5): + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.8 + '@fortawesome/free-regular-svg-icons': 7.1.0 + '@fortawesome/free-solid-svg-icons': 7.1.0 + '@jsonquerylang/jsonquery': 5.1.1 + '@lezer/highlight': 1.2.3 + '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8) + ajv: 8.18.0 + codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.1)(@codemirror/state@6.5.3)(@codemirror/view@6.39.8) + diff-sequences: 29.6.3 + immutable-json-patch: 6.0.2 + jmespath: 0.16.0 + json-source-map: 0.6.1 + jsonpath-plus: 10.3.0 + jsonrepair: 3.13.1 + lodash-es: 4.17.23 + memoize-one: 6.0.0 + natural-compare-lite: 1.4.0 + sass: 1.97.1 + svelte: 5.53.5 + svelte-awesome: 3.3.5(svelte@5.53.5) + svelte-select: 5.8.3 + vanilla-picker: 2.12.3 + + svelte-maplibre@1.2.6(svelte@5.53.5): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 - maplibre-gl: 5.14.0 + maplibre-gl: 5.18.0 pmtiles: 3.2.1 - svelte: 5.43.3 + svelte: 5.53.5 - svelte-parse-markup@0.1.5(svelte@5.43.3): + svelte-parse-markup@0.1.5(svelte@5.53.5): dependencies: - svelte: 5.43.3 + svelte: 5.53.5 - svelte-persisted-store@0.12.0(svelte@5.43.3): + svelte-persisted-store@0.12.0(svelte@5.53.5): dependencies: - svelte: 5.43.3 + svelte: 5.53.5 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3): + svelte-select@5.8.3: + dependencies: + svelte-floating-ui: 1.5.8 + + svelte-toolbelt@0.10.6(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3) + runed: 0.35.1(@sveltejs/kit@2.53.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.53.5) style-to-object: 1.0.14 - svelte: 5.43.3 + svelte: 5.53.5 transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.43.3: + svelte@5.53.5: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) '@types/estree': 1.0.8 - acorn: 8.15.0 - aria-query: 5.3.2 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 + devalue: 5.6.3 esm-env: 1.2.2 - esrap: 2.2.1 + esrap: 2.2.3 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -23886,7 +24370,7 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 - swagger-ui-dist@5.30.2: + swagger-ui-dist@5.31.0: dependencies: '@scarf/scarf': 1.4.0 @@ -23901,37 +24385,37 @@ snapshots: symbol-tree@3.2.4: optional: true - synckit@0.11.11: + synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 systeminformation@5.23.8: {} - tabbable@6.3.0: {} + tabbable@6.4.0: {} tailwind-merge@3.4.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18): + tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.2.0): dependencies: - tailwindcss: 4.1.18 + tailwindcss: 4.2.0 optionalDependencies: tailwind-merge: 3.4.0 - tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(yaml@2.8.2)): + tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.19(yaml@2.8.2) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) - tailwindcss-mso@2.0.3(tailwindcss@3.4.19(yaml@2.8.2)): + tailwindcss-mso@2.0.3(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.19(yaml@2.8.2) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) - tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(yaml@2.8.2)): + tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.19(yaml@2.8.2) - tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(yaml@2.8.2)) - tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(yaml@2.8.2)) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2) + tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) + tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)) - tailwindcss@3.4.19(yaml@2.8.2): + tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -23950,7 +24434,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -23959,7 +24443,7 @@ snapshots: - tsx - yaml - tailwindcss@4.1.18: {} + tailwindcss@4.2.0: {} tapable@2.3.0: {} @@ -23975,7 +24459,7 @@ snapshots: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.5.2 + bare-fs: 4.5.4 bare-path: 3.0.0 transitivePeerDependencies: - bare-abort-controller @@ -24008,7 +24492,7 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - tar@7.5.2: + tar@7.5.7: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -24016,30 +24500,38 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(@swc/core@1.15.5(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17))): + teex@1.0.1: dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17)) - optionalDependencies: - '@swc/core': 1.15.5(@swc/helpers@0.5.17) + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true - terser-webpack-plugin@5.3.16(webpack@5.103.0): + terser-webpack-plugin@5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.103.0 + webpack: 5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)) + optionalDependencies: + '@swc/core': 1.15.11(@swc/helpers@0.5.17) + + terser-webpack-plugin@5.3.16(webpack@5.104.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.104.1 terser@5.44.1: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 + acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -24047,25 +24539,25 @@ snapshots: dependencies: '@istanbuljs/schema': 0.1.3 glob: 10.5.0 - minimatch: 9.0.5 + minimatch: 9.0.6 - testcontainers@11.10.0: + testcontainers@11.12.0: dependencies: '@balena/dockerignore': 1.0.2 - '@types/dockerode': 3.3.47 + '@types/dockerode': 4.0.1 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 debug: 4.4.3 - docker-compose: 1.3.0 + docker-compose: 1.3.1 dockerode: 4.0.9 get-port: 7.1.0 proper-lockfile: 4.1.2 - properties-reader: 2.3.0 + properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.1 tmp: 0.2.5 - undici: 7.16.0 + undici: 7.22.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -24122,6 +24614,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -24129,14 +24623,20 @@ snapshots: tinypool@1.1.1: {} - tinyqueue@2.0.3: {} - tinyqueue@3.0.0: {} tinyrainbow@2.0.0: {} tinyspy@4.0.4: {} + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + tmp@0.2.5: {} to-regex-range@5.0.1: @@ -24150,29 +24650,28 @@ snapshots: toidentifier@1.0.1: {} - token-types@6.1.1: + token-types@6.1.2: dependencies: - '@borewit/text-codec': 0.1.1 + '@borewit/text-codec': 0.2.1 '@tokenizer/token': 0.3.0 ieee754: 1.2.1 totalist@3.0.1: {} - tough-cookie@4.1.4: + tough-cookie@5.1.2: dependencies: - psl: 1.15.0 - punycode: 2.3.1 - universalify: 0.2.0 - url-parse: 1.5.10 + tldts: 6.1.86 optional: true tr46@0.0.3: {} - tr46@3.0.0: + tr46@5.1.1: dependencies: punycode: 2.3.1 optional: true + transformation-matrix@3.1.0: {} + tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -24187,10 +24686,12 @@ snapshots: dependencies: utf8-byte-length: 1.0.5 - ts-api-utils@2.1.0(typescript@5.9.3): + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 + ts-dedent@2.2.0: {} + ts-interface-checker@0.1.13: {} tsconfck@3.1.6(typescript@5.9.3): @@ -24200,7 +24701,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 tapable: 2.3.0 tsconfig-paths: 4.2.0 @@ -24214,6 +24715,13 @@ snapshots: tsscmp@1.0.6: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + tweetnacl@0.14.5: {} type-check@0.4.0: @@ -24245,13 +24753,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.56.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.56.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -24260,12 +24768,14 @@ snapshots: ua-is-frozen@0.1.2: {} - ua-parser-js@2.0.7: + ua-parser-js@2.0.9: dependencies: detect-europe-js: 0.1.2 is-standalone-pwa: 0.1.1 ua-is-frozen: 0.1.2 + ufo@1.6.2: {} + uglify-js@3.19.3: optional: true @@ -24279,11 +24789,12 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.21.0: {} - undici-types@7.16.0: {} - undici@7.16.0: {} + undici-types@7.18.2: + optional: true + + undici@7.22.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -24378,17 +24889,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - universalify@0.2.0: - optional: true - universalify@2.0.1: {} unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.5(@swc/helpers@0.5.17))(rollup@4.53.4): + unplugin-swc@1.5.9(@swc/core@1.15.11(@swc/helpers@0.5.17))(rollup@4.55.1): dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.53.4) - '@swc/core': 1.15.5(@swc/helpers@0.5.17) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) + '@swc/core': 1.15.11(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -24397,11 +24905,11 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.15.0 + acorn: 8.16.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 - update-browserslist-db@1.2.2(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 @@ -24420,7 +24928,7 @@ snapshots: is-yarn-global: 0.4.1 latest-version: 7.0.0 pupa: 3.3.0 - semver: 7.7.3 + semver: 7.7.4 semver-diff: 4.0.0 xdg-basedir: 5.1.0 @@ -24430,25 +24938,19 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.104.1))(webpack@5.104.1): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.103.0 + webpack: 5.104.1 optionalDependencies: - file-loader: 6.2.0(webpack@5.103.0) - - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - optional: true + file-loader: 6.2.0(webpack@5.104.1) url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.14.0 + qs: 6.14.1 urlpattern-polyfill@8.0.2: {} @@ -24480,10 +24982,14 @@ snapshots: uuid@8.3.2: {} - validator@13.15.23: {} + validator@13.15.26: {} value-equal@1.0.1: {} + vanilla-picker@2.12.3: + dependencies: + '@sphinxxxx/color-conversion': 2.2.2 + vary@1.1.2: {} vfile-location@3.2.0: {} @@ -24515,22 +25021,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-imagetools@9.0.2(rollup@4.53.4): + vite-imagetools@9.0.3(rollup@4.55.1): dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.53.4) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) imagetools-core: 9.1.0 sharp: 0.34.5 transitivePeerDependencies: - rollup - supports-color - vite-node@3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -24545,46 +25051,86 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): + vite-node@3.2.4(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.1 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.53.4 + rollup: 4.55.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.11.0 fsevents: 2.3.3 jiti: 2.6.1 - lightningcss: 1.30.2 + lightningcss: 1.31.1 + sass: 1.97.1 terser: 5.44.1 + tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): - optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)): + vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.55.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.3.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + sass: 1.97.1 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vitefu@1.1.1(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + optionalDependencies: + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24602,14 +25148,14 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.4 - happy-dom: 20.0.11 - jsdom: 20.0.3(canvas@2.11.2(encoding@0.1.13)) + '@types/node': 24.11.0 + happy-dom: 20.6.3 + jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti - less @@ -24624,11 +25170,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24646,14 +25192,14 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 24.10.4 - happy-dom: 20.0.11 - jsdom: 20.0.3(canvas@2.11.2) + '@types/node': 25.3.0 + happy-dom: 20.6.3 + jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti - less @@ -24668,18 +25214,31 @@ snapshots: - tsx - yaml - vt-pbf@3.1.3: - dependencies: - '@mapbox/point-geometry': 0.1.0 - '@mapbox/vector-tile': 1.3.1 - pbf: 3.3.0 + vscode-jsonrpc@8.2.0: {} - w3c-xmlserializer@4.0.0: + vscode-languageserver-protocol@3.17.5: dependencies: - xml-name-validator: 4.0.0 + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 optional: true - watchpack@2.4.4: + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 @@ -24704,7 +25263,7 @@ snapshots: webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.15.0 + acorn: 8.16.0 acorn-walk: 8.3.4 commander: 7.2.0 debounce: 1.2.1 @@ -24719,7 +25278,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.103.0): + webpack-dev-middleware@7.4.5(webpack@5.104.1): dependencies: colorette: 2.0.20 memfs: 4.51.1 @@ -24728,9 +25287,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 - webpack-dev-server@5.2.2(webpack@5.103.0): + webpack-dev-server@5.2.2(webpack@5.104.1): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -24758,10 +25317,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.103.0) - ws: 8.18.3 + webpack-dev-middleware: 7.4.5(webpack@5.104.1) + ws: 8.19.0 optionalDependencies: - webpack: 5.103.0 + webpack: 5.104.1 transitivePeerDependencies: - bufferutil - debug @@ -24786,7 +25345,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.103.0: + webpack@5.104.1: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -24794,12 +25353,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + enhanced-resolve: 5.19.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -24810,15 +25369,15 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.103.0) - watchpack: 2.4.4 + terser-webpack-plugin: 5.3.16(webpack@5.104.1) + watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpack@5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17)): + webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -24826,12 +25385,12 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.1 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + enhanced-resolve: 5.19.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -24842,15 +25401,15 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(@swc/core@1.15.5(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.5(@swc/helpers@0.5.17))) - watchpack: 2.4.4 + terser-webpack-plugin: 5.3.16(@swc/core@1.15.11(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.11(@swc/helpers@0.5.17))) + watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.103.0): + webpackbar@6.0.1(webpack@5.104.1): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -24859,7 +25418,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.10.0 - webpack: 5.103.0 + webpack: 5.104.1 wrap-ansi: 7.0.0 websocket-driver@0.7.4: @@ -24870,16 +25429,19 @@ snapshots: websocket-extensions@0.1.4: {} - whatwg-encoding@2.0.0: + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 optional: true whatwg-mimetype@3.0.0: {} - whatwg-url@11.0.0: + whatwg-mimetype@4.0.0: + optional: true + + whatwg-url@14.2.0: dependencies: - tr46: 3.0.0 + tr46: 5.1.1 webidl-conversions: 7.0.0 optional: true @@ -24898,9 +25460,9 @@ snapshots: dependencies: isexe: 2.0.0 - which@6.0.0: + which@6.0.1: dependencies: - isexe: 3.1.1 + isexe: 4.0.0 why-is-node-running@2.3.0: dependencies: @@ -24950,10 +25512,10 @@ snapshots: ws@7.5.10: {} - ws@8.17.1: {} - ws@8.18.3: {} + ws@8.19.0: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 @@ -24964,7 +25526,7 @@ snapshots: dependencies: sax: 1.4.3 - xml-name-validator@4.0.0: + xml-name-validator@5.0.0: optional: true xmlchars@2.2.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 33aaa744b0..be30451965 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ packages: - cli - docs - e2e + - e2e-auth-server + - i18n - open-api/typescript-sdk - server - plugins @@ -9,6 +11,7 @@ packages: - .github ignoredBuiltDependencies: - '@nestjs/core' + - '@parcel/watcher' - '@scarf/scarf' - '@swc/core' - canvas diff --git a/readme_i18n/README_de_DE.md b/readme_i18n/README_de_DE.md index a8685e0902..488b05abcc 100644 --- a/readme_i18n/README_de_DE.md +++ b/readme_i18n/README_de_DE.md @@ -38,11 +38,6 @@ ā¸ ā¸˛ā¸Šā¸˛āš„ā¸—ā¸ĸ

-## Warnung - -- âš ī¸ Das Projekt befindet sich in **sehr aktiver** Entwicklung. -- âš ī¸ Gehe von mÃļglichen Fehlern und von Änderungen mit Breaking-Changes aus. -- âš ī¸ **Nutze die App auf keinen Fall als einziges Speichermedium fÃŧr deine Fotos und Videos.** - âš ī¸ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel fÃŧr deine wertvollen Fotos und Videos! > [!NOTE] @@ -62,7 +57,7 @@ ## Demo -Die Web-Demo kannst Du unter https://demo.immich.app finden. FÃŧr die Handy-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben. +Die Web-Demo kannst Du unter https://demo.immich.app finden. FÃŧr die Smartphone-App kannst Du `https://demo.immich.app` als `Server Endpoint URL` angeben. ### Login Daten @@ -93,7 +88,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. FÃŧr die Handy-App | LivePhoto/MotionPhoto Sicherung und Wiedergabe | Ja | Ja | | UnterstÃŧtzung fÃŧr 360-Grad-Bilder | Nein | Ja | | Benutzerdefinierte Speicherstruktur | Ja | Ja | -| Öffentliches Teilen | Nein | Ja | +| Öffentliches Teilen | Ja | Ja | | Archiv und Favoriten | Ja | Ja | | Globale Karte | Ja | Ja | | Partnerfreigabe (Teilen) | Ja | Ja | @@ -103,7 +98,7 @@ Die Web-Demo kannst Du unter https://demo.immich.app finden. FÃŧr die Handy-App | SchreibgeschÃŧtzte Gallerie | Ja | Ja | | Gestapelte Bilder | Ja | Ja | | Tags | Nein | Ja | -| Ordner-Ansicht | Nein | Ja | +| Ordner-Ansicht | Ja | Ja | ## Übersetzungen diff --git a/readme_i18n/README_th_TH.md b/readme_i18n/README_th_TH.md index cdc28b14e6..22a7f6a501 100644 --- a/readme_i18n/README_th_TH.md +++ b/readme_i18n/README_th_TH.md @@ -41,12 +41,11 @@ Tiáēŋng Viáģ‡t

-## ā¸‚āš‰ā¸­ā¸„ā¸§ā¸Ŗā¸Ŗā¸°ā¸§ā¸ąā¸‡ -- âš ī¸ āš‚ā¸žā¸Ŗāš€ā¸ˆā¸ā¸•āšŒā¸™ā¸ĩāš‰ā¸ā¸ŗā¸Ĩā¸ąā¸‡ā¸­ā¸ĸā¸šāšˆā¸Ŗā¸°ā¸Ģā¸§āšˆā¸˛ā¸‡ā¸ā¸˛ā¸Ŗā¸žā¸ąā¸’ā¸™ā¸˛**ā¸Ąā¸ĩā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩā¸‡ā¸šāšˆā¸­ā¸ĸā¸Ąā¸˛ā¸** -- âš ī¸ ā¸­ā¸˛ā¸ˆā¸ˆā¸°āš€ā¸ā¸´ā¸”ā¸‚āš‰ā¸­ā¸œā¸´ā¸”ā¸žā¸Ĩā¸˛ā¸”āšā¸Ĩā¸°ā¸ā¸˛ā¸Ŗāš€ā¸›ā¸Ĩā¸ĩāšˆā¸ĸā¸™āšā¸›ā¸Ĩ⏇⏗ā¸ĩāšˆā¸Ēāšˆā¸‡ā¸œā¸Ĩāš€ā¸Ēā¸ĩā¸ĸ -- âš ī¸ **ā¸Ģāš‰ā¸˛ā¸Ąāšƒā¸Šāš‰ā¸Ŗā¸°ā¸šā¸šā¸™ā¸ĩāš‰āš€ā¸›āš‡ā¸™ā¸§ā¸´ā¸˜ā¸ĩā¸ā¸˛ā¸Ŗāš€ā¸”ā¸ĩā¸ĸā¸§āšƒā¸™ā¸ā¸˛ā¸Ŗā¸ˆā¸ąā¸”āš€ā¸āš‡ā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“** -- âš ī¸ ā¸›ā¸ā¸´ā¸šā¸ąā¸•ā¸´ā¸•ā¸˛ā¸Ąāšā¸œā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸šā¸š [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸„ā¸ąā¸ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸­ā¸ĸā¸šāšˆāš€ā¸Ēā¸Ąā¸­ +> [!WARNING] +> âš ī¸ ā¸›ā¸ā¸´ā¸šā¸ąā¸•ā¸´ā¸•ā¸˛ā¸Ąāšā¸œā¸™ā¸ā¸˛ā¸Ŗā¸Ēā¸ŗā¸Ŗā¸­ā¸‡ā¸‚āš‰ā¸­ā¸Ąā¸šā¸Ĩāšā¸šā¸š [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) ā¸Ē⏺ā¸Ģā¸Ŗā¸ąā¸šā¸ ā¸˛ā¸žā¸–āšˆā¸˛ā¸ĸāšā¸Ĩ⏰⏧⏴⏔ā¸ĩāš‚ā¸­ā¸—ā¸ĩāšˆā¸Ēā¸ŗā¸„ā¸ąā¸ā¸‚ā¸­ā¸‡ā¸„ā¸¸ā¸“ā¸­ā¸ĸā¸šāšˆāš€ā¸Ēā¸Ąā¸­ +> + > [!NOTE] > ⏄⏏⏓ā¸Ēā¸˛ā¸Ąā¸˛ā¸Ŗā¸–ā¸Ģā¸˛ā¸„ā¸šāšˆā¸Ąā¸ˇā¸­ā¸Ģā¸Ĩā¸ąā¸ ā¸Ŗā¸§ā¸Ąā¸–ā¸ļā¸‡ā¸„ā¸šāšˆā¸Ąā¸ˇā¸­ā¸ā¸˛ā¸Ŗā¸•ā¸´ā¸”ā¸•ā¸ąāš‰ā¸‡ āš„ā¸”āš‰ā¸—ā¸ĩāšˆ https://immich.app/ diff --git a/server/.nvmrc b/server/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/server/Dockerfile b/server/Dockerfile index 918658e19f..a8a8b04713 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS builder +FROM ghcr.io/immich-app/base-server-dev:202601131104@sha256:8d907eb3fe10dba4a1e034fd0060ea68c01854d92fcc9debc6b868b98f888ba7 AS builder ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ COREPACK_HOME=/tmp \ @@ -52,7 +52,7 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.1.1@sha256:a55c391f7582f34c58bce1a85090cd526596402ba77fc32b06c49b8404ef9c14 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /usr/src/app COPY ./plugins/mise.toml ./plugins/ @@ -71,7 +71,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \ --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ cd plugins && mise run build -FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29 +FROM ghcr.io/immich-app/base-server-prod:202601131104@sha256:c649c5838b6348836d27db6d49cadbbc6157feae7a1a237180c3dec03577ba8f WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 5a71d61e2a..f778c20afb 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -1,21 +1,21 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS dev +FROM ghcr.io/immich-app/base-server-dev:202601131104@sha256:8d907eb3fe10dba4a1e034fd0060ea68c01854d92fcc9debc6b868b98f888ba7 AS dev ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ CI=1 \ - COREPACK_HOME=/tmp + COREPACK_HOME=/tmp \ + PNPM_HOME=/buildcache/pnpm-store RUN npm install --global corepack@latest && \ corepack enable pnpm && \ + echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \ echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ - echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc + echo "cache-dir=/buildcache/pnpm-cache" >> /usr/local/etc/npmrc && \ + echo "# Retry configuration - default is 2" >> /usr/local/etc/npmrc && \ + echo "fetch-retries=5" >> /usr/local/etc/npmrc && \ + mkdir -p /buildcache/pnpm-store /buildcache/pnpm-cache /buildcache/node-gyp && \ + chmod -R o+rw /buildcache -COPY ./package* ./pnpm* .pnpmfile.cjs /tmp/create-dep-cache/ -COPY ./web/package* ./web/pnpm* /tmp/create-dep-cache/web/ -COPY ./server/package* ./server/pnpm* /tmp/create-dep-cache/server/ -COPY ./open-api/typescript-sdk/package* ./open-api/typescript-sdk/pnpm* /tmp/create-dep-cache/open-api/typescript-sdk/ -WORKDIR /tmp/create-dep-cache -RUN pnpm fetch && rm -rf /tmp/create-dep-cache && chmod -R o+rw /buildcache WORKDIR /usr/src/app ENV PATH="${PATH}:/usr/src/app/server/bin:/usr/src/app/web/bin" \ @@ -27,16 +27,14 @@ ENTRYPOINT ["tini", "--", "/bin/bash", "-c"] FROM dev AS dev-container-server RUN apt-get update --allow-releaseinfo-change && \ - apt-get install sudo inetutils-ping openjdk-21-jre-headless \ + apt-get install inetutils-ping openjdk-21-jre-headless \ vim nano curl \ -y --no-install-recommends --fix-missing -RUN usermod -aG sudo node && \ - echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ - mkdir -p /workspaces/immich +RUN mkdir -p /workspaces && \ + ln -s /usr/src/app /workspaces/immich -RUN chown node:node -R /workspaces -COPY --chown=node:node --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/ +COPY --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/ WORKDIR /workspaces/immich diff --git a/server/bin/start.sh b/server/bin/start.sh index 0afff4c3a8..0a26be8e0b 100755 --- a/server/bin/start.sh +++ b/server/bin/start.sh @@ -1,5 +1,19 @@ #!/usr/bin/env bash -echo "Initializing Immich $IMMICH_SOURCE_REF" + +# Quiet mode suppresses informational output (enabled for immich-admin) +QUIET=false +if [ "$1" = "immich-admin" ]; then + QUIET=true +fi + +# Helper function that only logs when not in quiet mode +log_message() { + if [ "$QUIET" = "false" ]; then + echo "$@" + fi +} + +log_message "Initializing Immich $IMMICH_SOURCE_REF" # TODO: Update to mimalloc v3 when verified memory isn't released issue is fixed # lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.3" @@ -30,16 +44,20 @@ 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" + log_message "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" + log_message "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" "$@" + if [ "$QUIET" = "true" ]; then + exec node --no-warnings "${SERVER_HOME}/dist/main.js" "$@" + else + exec node "${SERVER_HOME}/dist/main.js" "$@" + fi else echo "Error: ${SERVER_HOME}/dist/main.js not found" if [ "$IMMICH_ENV" = "development" ]; then diff --git a/server/package.json b/server/package.json index 278ecede44..5578f242ae 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "2.4.0", + "version": "2.5.6", "description": "", "author": "", "private": true, @@ -9,32 +9,33 @@ "build": "nest build", "format": "prettier --check .", "format:fix": "prettier --write .", - "start": "npm run start:dev", + "start": "pnpm run start:dev", "nest": "nest", "start:dev": "nest start --watch --", "start:debug": "nest start --debug 0.0.0.0:9230 --watch --", "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0", - "lint:fix": "npm run lint -- --fix", + "lint:fix": "pnpm run lint --fix", "check": "tsc --noEmit", - "check:code": "npm run format && npm run lint && npm run check", - "check:all": "npm run check:code && npm run test:cov", + "check:code": "pnpm run format && pnpm run lint && pnpm run check", + "check:all": "pnpm run check:code && pnpm run test:cov", "test": "vitest --config test/vitest.config.mjs", "test:cov": "vitest --config test/vitest.config.mjs --coverage", "test:medium": "vitest --config test/vitest.config.medium.mjs", "typeorm": "typeorm", - "migrations:debug": "node ./dist/bin/migrations.js debug", - "migrations:generate": "node ./dist/bin/migrations.js generate", - "migrations:create": "node ./dist/bin/migrations.js create", - "migrations:run": "node ./dist/bin/migrations.js run", - "migrations:revert": "node ./dist/bin/migrations.js revert", - "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", + "migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug", + "migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate", + "migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate", + "migrations:run": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations run", + "migrations:revert": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations revert", + "schema:drop": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} query 'DROP schema public cascade; CREATE schema public;'", + "schema:reset": "pnpm run schema:drop && pnpm run migrations:run", "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" }, "dependencies": { "@extism/extism": "2.0.0-rc13", + "@immich/sql-tools": "^0.3.2", "@nestjs/bullmq": "^11.0.1", "@nestjs/common": "^11.0.4", "@nestjs/core": "^11.0.4", @@ -45,14 +46,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.208.0", - "@opentelemetry/instrumentation-http": "^0.208.0", - "@opentelemetry/instrumentation-ioredis": "^0.56.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.55.0", - "@opentelemetry/instrumentation-pg": "^0.61.0", + "@opentelemetry/exporter-prometheus": "^0.212.0", + "@opentelemetry/instrumentation-http": "^0.212.0", + "@opentelemetry/instrumentation-ioredis": "^0.60.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.58.0", + "@opentelemetry/instrumentation-pg": "^0.64.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.208.0", + "@opentelemetry/sdk-node": "^0.212.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -69,8 +70,8 @@ "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", - "cron": "4.3.5", - "exiftool-vendored": "^34.0.0", + "cron": "4.4.0", + "exiftool-vendored": "^35.0.0", "express": "^5.1.0", "fast-glob": "^3.3.2", "fluent-ffmpeg": "^2.1.2", @@ -96,7 +97,7 @@ "pg": "^8.11.3", "pg-connection-string": "^2.9.1", "picomatch": "^4.0.2", - "postgres": "3.4.7", + "postgres": "3.4.8", "react": "^19.0.0", "react-dom": "^19.0.0", "react-email": "^4.0.0", @@ -110,12 +111,13 @@ "socket.io": "^4.8.1", "tailwindcss-preset-email": "^1.4.0", "thumbhash": "^0.1.1", + "transformation-matrix": "^3.1.0", "ua-parser-js": "^2.0.0", "uuid": "^11.1.0", "validator": "^13.12.0" }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^10.0.0", "@nestjs/cli": "^11.0.2", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", @@ -128,13 +130,13 @@ "@types/cookie-parser": "^1.4.8", "@types/express": "^5.0.0", "@types/fluent-ffmpeg": "^2.1.21", - "@types/jsonwebtoken": "^9.0.10", "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.10", "@types/lodash": "^4.14.197", "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", "@types/multer": "^2.0.0", - "@types/node": "^24.10.3", + "@types/node": "^24.10.13", "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -145,11 +147,11 @@ "@types/ua-parser-js": "^0.7.36", "@types/validator": "^13.15.2", "@vitest/coverage-v8": "^3.0.0", - "eslint": "^9.14.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^62.0.0", - "globals": "^16.0.0", + "eslint-plugin-unicorn": "^63.0.0", + "globals": "^17.0.0", "mock-fs": "^5.2.0", "node-gyp": "^12.0.0", "pngjs": "^7.0.0", @@ -162,11 +164,11 @@ "typescript": "^5.9.2", "typescript-eslint": "^8.28.0", "unplugin-swc": "^1.4.5", - "vite-tsconfig-paths": "^5.0.0", + "vite-tsconfig-paths": "^6.0.0", "vitest": "^3.0.0" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" }, "overrides": { "sharp": "^0.34.5" diff --git a/server/src/app.module.ts b/server/src/app.module.ts index caa4ea4b6e..49b779ca18 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -10,6 +10,7 @@ import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; import { ImmichWorker } from 'src/enum'; import { MaintenanceAuthGuard } from 'src/maintenance/maintenance-auth.guard'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWorkerController } from 'src/maintenance/maintenance-worker.controller'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; @@ -21,14 +22,18 @@ import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { AppRepository } from 'src/repositories/app.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository'; import { WebsocketRepository } from 'src/repositories/websocket.repository'; import { services } from 'src/services'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { QueueService } from 'src/services/queue.service'; import { getKyselyConfig } from 'src/utils/database'; @@ -103,9 +108,14 @@ export class ApiModule extends BaseModule {} providers: [ ConfigRepository, LoggingRepository, + StorageRepository, + ProcessRepository, + DatabaseRepository, SystemMetadataRepository, AppRepository, + MaintenanceHealthRepository, MaintenanceWebsocketRepository, + DatabaseBackupService, MaintenanceWorkerService, ...commonMiddleware, { provide: APP_GUARD, useClass: MaintenanceAuthGuard }, @@ -116,9 +126,14 @@ export class MaintenanceModule { constructor( @Inject(IWorker) private worker: ImmichWorker, logger: LoggingRepository, + private maintenanceWorkerService: MaintenanceWorkerService, ) { logger.setAppName(this.worker); } + + async onModuleInit() { + await this.maintenanceWorkerService.init(); + } } @Module({ diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts deleted file mode 100644 index 588f358023..0000000000 --- a/server/src/bin/migrations.ts +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/env node -process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich'; - -import { Kysely, sql } from 'kysely'; -import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'; -import { basename, dirname, extname, join } from 'node:path'; -import postgres from 'postgres'; -import { ConfigRepository } from 'src/repositories/config.repository'; -import { DatabaseRepository } from 'src/repositories/database.repository'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import 'src/schema'; -import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; -import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; - -const main = async () => { - const command = process.argv[2]; - const path = process.argv[3] || 'src/Migration'; - - switch (command) { - case 'debug': { - await debug(); - return; - } - - case 'run': { - await runMigrations(); - return; - } - - case 'revert': { - await revert(); - return; - } - - case 'query': { - const query = process.argv[3]; - await runQuery(query); - return; - } - - case 'create': { - create(path, [], []); - return; - } - - case 'generate': { - await generate(path); - return; - } - - default: { - console.log(`Usage: - node dist/bin/migrations.js create - node dist/bin/migrations.js generate - node dist/bin/migrations.js run - node dist/bin/migrations.js revert -`); - } - } -}; - -const getDatabaseClient = () => { - const configRepository = new ConfigRepository(); - const { database } = configRepository.getEnv(); - return new Kysely(getKyselyConfig(database.config)); -}; - -const runQuery = async (query: string) => { - const db = getDatabaseClient(); - await sql.raw(query).execute(db); - await db.destroy(); -}; - -const runMigrations = async () => { - const configRepository = new ConfigRepository(); - const logger = LoggingRepository.create(); - const db = getDatabaseClient(); - const databaseRepository = new DatabaseRepository(db, logger, configRepository); - await databaseRepository.runMigrations(); - await db.destroy(); -}; - -const revert = async () => { - const configRepository = new ConfigRepository(); - const logger = LoggingRepository.create(); - const db = getDatabaseClient(); - const databaseRepository = new DatabaseRepository(db, logger, configRepository); - - try { - const migrationName = await databaseRepository.revertLastMigration(); - if (!migrationName) { - console.log('No migrations to revert'); - return; - } - - markMigrationAsReverted(migrationName); - } finally { - await db.destroy(); - } -}; - -const debug = async () => { - const { up } = await compare(); - const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n'); - // const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n'); - writeFileSync('./migrations.sql', upSql + '\n\n'); - console.log('Wrote migrations.sql'); -}; - -const generate = async (path: string) => { - const { up, down } = await compare(); - if (up.items.length === 0) { - console.log('No changes detected'); - return; - } - create(path, up.asSql(), down.asSql()); -}; - -const create = (path: string, up: string[], down: string[]) => { - const timestamp = Date.now(); - const name = basename(path, extname(path)); - const filename = `${timestamp}-${name}.ts`; - const folder = dirname(path); - const fullPath = join(folder, filename); - mkdirSync(folder, { recursive: true }); - writeFileSync(fullPath, asMigration({ up, down })); - console.log(`Wrote ${fullPath}`); -}; - -const compare = async () => { - const configRepository = new ConfigRepository(); - const { database } = configRepository.getEnv(); - const db = postgres(asPostgresConnectionConfig(database.config)); - - const source = schemaFromCode({ overrides: true, namingStrategy: 'default' }); - const target = await schemaFromDatabase(db, {}); - - 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, ignoreMissing: true }, - functions: { ignoreExtra: false }, - extensions: { ignoreMissing: true }, - parameters: { ignoreMissing: true }, - }); - - return { up, down }; -}; - -type MigrationProps = { - up: string[]; - down: string[]; -}; - -const asMigration = ({ up, down }: MigrationProps) => { - const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n'); - const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n'); - - return `import { Kysely, sql } from 'kysely'; - -export async function up(db: Kysely): Promise { -${upSql} -} - -export async function down(db: Kysely): Promise { -${downSql} -} -`; -}; - -const markMigrationAsReverted = (migrationName: string) => { - // eslint-disable-next-line unicorn/prefer-module - const distRoot = join(__dirname, '..'); - const projectRoot = join(distRoot, '..'); - const sourceFolder = join(projectRoot, 'src', 'schema', 'migrations'); - const distFolder = join(distRoot, 'schema', 'migrations'); - - const sourcePath = join(sourceFolder, `${migrationName}.ts`); - const revertedFolder = join(sourceFolder, 'reverted'); - const revertedPath = join(revertedFolder, `${migrationName}.ts`); - - if (existsSync(revertedPath)) { - console.log(`Migration ${migrationName} is already marked as reverted`); - } else if (existsSync(sourcePath)) { - mkdirSync(revertedFolder, { recursive: true }); - renameSync(sourcePath, revertedPath); - console.log(`Moved ${sourcePath} to ${revertedPath}`); - } else { - console.warn(`Source migration file not found for ${migrationName}`); - } - - const distBase = join(distFolder, migrationName); - for (const extension of ['.js', '.js.map', '.d.ts']) { - const filePath = `${distBase}${extension}`; - if (existsSync(filePath)) { - rmSync(filePath, { force: true }); - console.log(`Removed ${filePath}`); - } - } -}; - -main() - .then(() => { - process.exit(0); - }) - .catch((error) => { - console.error(error); - console.log('Something went wrong'); - process.exit(1); - }); diff --git a/server/src/commands/index.ts b/server/src/commands/index.ts index 2aef2e8c8b..2a2dd1857d 100644 --- a/server/src/commands/index.ts +++ b/server/src/commands/index.ts @@ -9,6 +9,7 @@ import { 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 { SchemaCheck } from 'src/commands/schema-check'; import { VersionCommand } from 'src/commands/version.command'; export const commandsAndQuestions = [ @@ -28,4 +29,5 @@ export const commandsAndQuestions = [ ChangeMediaLocationCommand, PromptMediaLocationQuestions, PromptConfirmMoveQuestions, + SchemaCheck, ]; diff --git a/server/src/commands/schema-check.ts b/server/src/commands/schema-check.ts new file mode 100644 index 0000000000..e0ccae8469 --- /dev/null +++ b/server/src/commands/schema-check.ts @@ -0,0 +1,60 @@ +import { asHuman } from '@immich/sql-tools'; +import { Command, CommandRunner } from 'nest-commander'; +import { ErrorMessages } from 'src/constants'; +import { CliService } from 'src/services/cli.service'; + +@Command({ + name: 'schema-check', + description: 'Verify database migrations and check for schema drift', +}) +export class SchemaCheck extends CommandRunner { + constructor(private service: CliService) { + super(); + } + + async run(): Promise { + try { + const { migrations, drift } = await this.service.schemaReport(); + + if (migrations.every((item) => item.status === 'applied')) { + console.log('Migrations are up to date'); + } else { + console.log('Migration issues detected:'); + for (const migration of migrations) { + switch (migration.status) { + case 'deleted': { + console.log(` - ${migration.name} was applied, but the file no longer exists on disk`); + break; + } + + case 'missing': { + console.log(` - ${migration.name} exists, but has not been applied to the database`); + break; + } + } + } + } + + if (drift.items.length === 0) { + console.log('\nNo schema drift detected'); + } else { + console.log(`\n${ErrorMessages.SchemaDrift}`); + for (const item of drift.items) { + console.log(` - ${item.type}: ` + asHuman(item)); + } + + console.log(` + +The below SQL is automatically generated and may be helpful for resolving drift. ** Use at your own risk! ** + +\`\`\`sql +${drift.asSql().join('\n')} +\`\`\` +`); + } + } catch (error) { + console.error(error); + console.error('Unable to debug migrations'); + } + } +} diff --git a/server/src/config.ts b/server/src/config.ts index c18acd79f8..2a43b51187 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -15,7 +15,7 @@ import { } from 'src/enum'; import { ConcurrentQueueName, FullsizeImageOptions, ImageOptions } from 'src/types'; -export interface SystemConfig { +export type SystemConfig = { backup: { database: { enabled: boolean; @@ -187,7 +187,7 @@ export interface SystemConfig { user: { deleteDelay: number; }; -} +}; export type MachineLearningConfig = SystemConfig['machineLearning']; @@ -236,6 +236,7 @@ export const defaults = Object.freeze({ [QueueName.Notification]: { concurrency: 5 }, [QueueName.Ocr]: { concurrency: 1 }, [QueueName.Workflow]: { concurrency: 5 }, + [QueueName.Editor]: { concurrency: 2 }, }, logging: { enabled: true, @@ -318,11 +319,13 @@ export const defaults = Object.freeze({ format: ImageFormat.Webp, size: 250, quality: 80, + progressive: false, }, preview: { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, }, colorspace: Colorspace.P3, extractEmbedded: false, @@ -330,6 +333,7 @@ export const defaults = Object.freeze({ enabled: false, format: ImageFormat.Jpeg, quality: 80, + progressive: false, }, }, newVersionCheck: { diff --git a/server/src/constants.ts b/server/src/constants.ts index 6df63ea1f4..2b55a15ec3 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -4,8 +4,15 @@ import { dirname, join } from 'node:path'; import { SemVer } from 'semver'; import { ApiTag, DatabaseExtension, ExifOrientation, VectorIndex } from 'src/enum'; +export const ErrorMessages = { + InconsistentMediaLocation: + 'Detected an inconsistent media location. For more information, see https://docs.immich.app/errors#inconsistent-media-location', + SchemaDrift: `Detected schema drift. For more information, see https://docs.immich.app/errors#schema-drift`, + TypeOrmUpgrade: 'Invalid upgrade path. For more information, see https://docs.immich.app/errors/#typeorm-upgrade', +}; + export const POSTGRES_VERSION_RANGE = '>=14.0.0'; -export const VECTORCHORD_VERSION_RANGE = '>=0.3 <0.6'; +export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2'; export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; export const VECTOR_VERSION_RANGE = '>=0.5 <1'; @@ -142,6 +149,7 @@ export const endpointTags: Record = { [ApiTag.Assets]: 'An asset is an image or video that has been uploaded to Immich.', [ApiTag.Authentication]: 'Endpoints related to user authentication, including OAuth.', [ApiTag.AuthenticationAdmin]: 'Administrative endpoints related to authentication.', + [ApiTag.DatabaseBackups]: 'Manage backups of the Immich database.', [ApiTag.Deprecated]: 'Deprecated endpoints that are planned for removal in the next major release.', [ApiTag.Download]: 'Endpoints for downloading assets or collections of assets.', [ApiTag.Duplicates]: 'Endpoints for managing and identifying duplicate assets.', diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 951126d7a6..bcf5d8b397 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -16,7 +16,7 @@ import { UploadedFiles, UseInterceptors, } from '@nestjs/common'; -import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiConsumes, ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; import { NextFunction, Request, Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { @@ -34,6 +34,7 @@ import { CheckExistingAssetsDto, UploadFieldName, } from 'src/dtos/asset-media.dto'; +import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ApiTag, ImmichHeader, Permission, RouteKey } from 'src/enum'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; @@ -63,6 +64,16 @@ export class AssetMediaController { required: false, }) @ApiBody({ description: 'Asset Upload Information', type: AssetMediaCreateDto }) + @ApiResponse({ + status: 200, + description: 'Asset is a duplicate', + type: AssetMediaResponseDto, + }) + @ApiResponse({ + status: 201, + description: 'Asset uploaded successfully', + type: AssetMediaResponseDto, + }) @Endpoint({ summary: 'Upload asset', description: 'Uploads a new asset to the server.', @@ -95,15 +106,21 @@ export class AssetMediaController { async downloadAsset( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, + @Query() dto: AssetDownloadOriginalDto, @Res() res: Response, @Next() next: NextFunction, ) { - await sendFile(res, next, () => this.service.downloadOriginal(auth, id), this.logger); + await sendFile(res, next, () => this.service.downloadOriginal(auth, id, dto), this.logger); } @Put(':id/original') @UseInterceptors(FileUploadInterceptor) @ApiConsumes('multipart/form-data') + @ApiResponse({ + status: 200, + description: 'Asset replaced successfully', + type: AssetMediaResponseDto, + }) @Endpoint({ summary: 'Replace asset', description: 'Replace the asset with new file, without changing its id.', @@ -131,7 +148,8 @@ export class AssetMediaController { @Authenticated({ permission: Permission.AssetView, sharedLink: true }) @Endpoint({ summary: 'View asset thumbnail', - description: 'Retrieve the thumbnail image for the specified asset.', + description: + 'Retrieve the thumbnail image for the specified asset. Viewing the fullsize thumbnail might redirect to downloadAsset, which requires a different permission.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) async viewAsset( @@ -206,7 +224,7 @@ export class AssetMediaController { } @Post('exist') - @Authenticated() + @Authenticated({ permission: Permission.AssetUpload }) @Endpoint({ summary: 'Check existing assets', description: 'Checks if multiple assets exist on the server and returns all existing - used by background backup', diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 649c80e850..69bf1f6443 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -24,6 +24,34 @@ describe(AssetController.name, () => { await request(ctx.getHttpServer()).put(`/assets`); expect(ctx.authenticate).toHaveBeenCalled(); }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets`) + .send({ ids: ['123'] }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + }); + + it('should require duplicateId to be a string', async () => { + const id = factory.uuid(); + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets`) + .send({ ids: [id], duplicateId: true }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['duplicateId must be a string'])); + }); + + it('should accept a null duplicateId', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()) + .put(`/assets`) + .send({ ids: [id], duplicateId: null }); + + expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ duplicateId: null })); + }); }); describe('DELETE /assets', () => { @@ -79,6 +107,74 @@ describe(AssetController.name, () => { }); }); + describe('PUT /assets/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/assets/metadata`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid assetId', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put('/assets/metadata') + .send({ items: [{ assetId: '123', key: 'test', value: {} }] }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + }); + + it('should require a key', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put('/assets/metadata') + .send({ items: [{ assetId: factory.uuid(), value: {} }] }); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + ), + ); + }); + + it('should work', async () => { + const { status } = await request(ctx.getHttpServer()) + .put('/assets/metadata') + .send({ items: [{ assetId: factory.uuid(), key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } }] }); + expect(status).toBe(200); + }); + }); + + describe('DELETE /assets/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/assets/metadata`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid assetId', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete('/assets/metadata') + .send({ items: [{ assetId: '123', key: 'test' }] }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + }); + + it('should require a key', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete('/assets/metadata') + .send({ items: [{ assetId: factory.uuid() }] }); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest( + expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + ), + ); + }); + + it('should work', async () => { + const { status } = await request(ctx.getHttpServer()) + .delete('/assets/metadata') + .send({ items: [{ assetId: factory.uuid(), key: AssetMetadataKey.MobileApp }] }); + expect(status).toBe(204); + }); + }); + describe('PUT /assets/:id', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/assets/123`); @@ -111,12 +207,28 @@ describe(AssetController.name, () => { }); it('should reject invalid rating', async () => { - for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { + for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest()); } }); + + it('should convert rating 0 to null', async () => { + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send({ rating: 0 }); + expect(service.update).toHaveBeenCalledWith(undefined, assetId, { rating: null }); + expect(status).toBe(200); + }); + + it('should leave correct ratings as-is', async () => { + const assetId = factory.uuid(); + for (const test of [{ rating: -1 }, { rating: 1 }, { rating: 5 }]) { + const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}`).send(test); + expect(service.update).toHaveBeenCalledWith(undefined, assetId, test); + expect(status).toBe(200); + } + }); }); describe('GET /assets/statistics', () => { @@ -169,12 +281,10 @@ describe(AssetController.name, () => { it('should require each item to have a valid key', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/assets/${factory.uuid()}/metadata`) - .send({ items: [{ key: 'someKey' }] }); + .send({ items: [{ value: { some: 'value' } }] }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest( - expect.arrayContaining([expect.stringContaining('items.0.key must be one of the following values')]), - ), + factory.responses.badRequest(['items.0.key must be a string', 'items.0.key should not be empty']), ); }); @@ -224,16 +334,89 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); }); + }); + + describe('PUT /assets/:id/edits', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/edits`).send({ edits: [] }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should accept valid edits and pass to service correctly', async () => { + const edits = [ + { + action: 'crop', + parameters: { + x: 0, + y: 0, + width: 100, + height: 100, + }, + }, + ]; + + const assetId = factory.uuid(); + const { status } = await request(ctx.getHttpServer()).put(`/assets/${assetId}/edits`).send({ + edits, + }); + + expect(service.editAsset).toHaveBeenCalledWith(undefined, assetId, { edits }); + expect(status).toBe(200); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/123/edits`) + .send({ + edits: [ + { + action: 'crop', + parameters: { + x: 0, + y: 0, + width: 100, + height: 100, + }, + }, + ], + }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + }); + + it('should check the action and parameters discriminator', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/edits`) + .send({ + edits: [ + { + action: 'rotate', + parameters: { + x: 0, + y: 0, + width: 100, + height: 100, + }, + }, + ], + }); - it('should require a valid key', async () => { - const { status, body } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/invalid`); expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining([expect.stringContaining('key must be one of the following value')]), + expect.arrayContaining([expect.stringContaining('parameters.angle must be one of the following values')]), ), ); }); + + it('should require at least one edit', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/assets/${factory.uuid()}/edits`) + .send({ edits: [] }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['edits must contain at least 1 elements'])); + }); }); describe('DELETE /assets/:id/metadata/:key', () => { @@ -247,13 +430,5 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); }); - - it('should require a valid key', async () => { - const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/invalid`); - expect(status).toBe(400); - expect(body).toEqual( - factory.responses.badRequest([expect.stringContaining('key must be one of the following values')]), - ); - }); }); }); diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index bcc13fbc06..2024760975 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -7,6 +7,9 @@ import { AssetBulkUpdateDto, AssetCopyDto, AssetJobsDto, + AssetMetadataBulkDeleteDto, + AssetMetadataBulkResponseDto, + AssetMetadataBulkUpsertDto, AssetMetadataResponseDto, AssetMetadataRouteParams, AssetMetadataUpsertDto, @@ -17,6 +20,7 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { ApiTag, Permission, RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -62,7 +66,7 @@ export class AssetController { } @Post('jobs') - @Authenticated() + @Authenticated({ permission: Permission.JobCreate }) @HttpCode(HttpStatus.NO_CONTENT) @Endpoint({ summary: 'Run an asset job', @@ -120,6 +124,32 @@ export class AssetController { return this.service.copy(auth, dto); } + @Put('metadata') + @Authenticated({ permission: Permission.AssetUpdate }) + @Endpoint({ + summary: 'Upsert asset metadata', + description: 'Upsert metadata key-value pairs for multiple assets.', + history: new HistoryBuilder().added('v1').beta('v2.5.0'), + }) + updateBulkAssetMetadata( + @Auth() auth: AuthDto, + @Body() dto: AssetMetadataBulkUpsertDto, + ): Promise { + return this.service.upsertBulkMetadata(auth, dto); + } + + @Delete('metadata') + @Authenticated({ permission: Permission.AssetUpdate }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Delete asset metadata', + description: 'Delete metadata key-value pairs for multiple assets.', + history: new HistoryBuilder().added('v1').beta('v2.5.0'), + }) + deleteBulkAssetMetadata(@Auth() auth: AuthDto, @Body() dto: AssetMetadataBulkDeleteDto): Promise { + return this.service.deleteBulkMetadata(auth, dto); + } + @Put(':id') @Authenticated({ permission: Permission.AssetUpdate }) @Endpoint({ @@ -197,4 +227,42 @@ export class AssetController { deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise { return this.service.deleteMetadataByKey(auth, id, key); } + + @Get(':id/edits') + @Authenticated({ permission: Permission.AssetEditGet }) + @Endpoint({ + summary: 'Retrieve edits for an existing asset', + description: 'Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.', + history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0'), + }) + getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getAssetEdits(auth, id); + } + + @Put(':id/edits') + @Authenticated({ permission: Permission.AssetEditCreate }) + @Endpoint({ + summary: 'Apply edits to an existing asset', + description: 'Apply a series of edit actions (crop, rotate, mirror) to the specified asset.', + history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0'), + }) + editAsset( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetEditsCreateDto, + ): Promise { + return this.service.editAsset(auth, id, dto); + } + + @Delete(':id/edits') + @Authenticated({ permission: Permission.AssetEditDelete }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Remove edits from an existing asset', + description: 'Removes all edit actions (crop, rotate, mirror) associated with the specified asset.', + history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0'), + }) + removeAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.removeAssetEdits(auth, id); + } } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index ea09e33080..63cdce4f32 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -112,6 +112,7 @@ export class AuthController { summary: 'Retrieve auth status', description: 'Get information about the current session, including whether the user has a password, and if the session can access locked assets.', + history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) getAuthStatus(@Auth() auth: AuthDto): Promise { return this.service.getAuthStatus(auth); diff --git a/server/src/controllers/database-backup.controller.ts b/server/src/controllers/database-backup.controller.ts new file mode 100644 index 0000000000..737c8f3958 --- /dev/null +++ b/server/src/controllers/database-backup.controller.ts @@ -0,0 +1,101 @@ +import { Body, Controller, Delete, Get, Next, Param, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { NextFunction, Response } from 'express'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { + DatabaseBackupDeleteDto, + DatabaseBackupListResponseDto, + DatabaseBackupUploadDto, +} from 'src/dtos/database-backup.dto'; +import { ApiTag, ImmichCookie, Permission } from 'src/enum'; +import { Authenticated, FileResponse, GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { LoginDetails } from 'src/services/auth.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import { sendFile } from 'src/utils/file'; +import { respondWithCookie } from 'src/utils/response'; +import { FilenameParamDto } from 'src/validation'; + +@ApiTags(ApiTag.DatabaseBackups) +@Controller('admin/database-backups') +export class DatabaseBackupController { + constructor( + private logger: LoggingRepository, + private service: DatabaseBackupService, + private maintenanceService: MaintenanceService, + ) {} + + @Get() + @Endpoint({ + summary: 'List database backups', + description: 'Get the list of the successful and failed backups', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + listDatabaseBackups(): Promise { + return this.service.listBackups(); + } + + @Get(':filename') + @FileResponse() + @Endpoint({ + summary: 'Download database backup', + description: 'Downloads the database backup file', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.BackupDownload, admin: true }) + async downloadDatabaseBackup( + @Param() { filename }: FilenameParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ): Promise { + await sendFile(res, next, () => this.service.downloadBackup(filename), this.logger); + } + + @Delete() + @Endpoint({ + summary: 'Delete database backup', + description: 'Delete a backup by its filename', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.BackupDelete, admin: true }) + async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { + return this.service.deleteBackup(dto.backups); + } + + @Post('start-restore') + @Endpoint({ + summary: 'Start database backup restore flow', + description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + async startDatabaseRestoreFlow( + @GetLoginDetails() loginDetails: LoginDetails, + @Res({ passthrough: true }) res: Response, + ): Promise { + const { jwt } = await this.maintenanceService.startRestoreFlow(); + return respondWithCookie(res, undefined, { + isSecure: loginDetails.isSecure, + values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], + }); + } + + @Post('upload') + @Authenticated({ permission: Permission.BackupUpload, admin: true }) + @ApiConsumes('multipart/form-data') + @ApiBody({ description: 'Backup Upload', type: DatabaseBackupUploadDto }) + @Endpoint({ + summary: 'Upload database backup', + description: 'Uploads .sql/.sql.gz file to restore backup from', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @UseInterceptors(FileInterceptor('file')) + uploadDatabaseBackup( + @UploadedFile() + file: Express.Multer.File, + ): Promise { + return this.service.uploadBackup(file); + } +} diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts index 942d44f4c3..e45eeb23f3 100644 --- a/server/src/controllers/download.controller.ts +++ b/server/src/controllers/download.controller.ts @@ -1,9 +1,8 @@ import { Body, Controller, HttpCode, HttpStatus, Post, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { DownloadArchiveDto, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { DownloadService } from 'src/services/download.service'; @@ -36,7 +35,7 @@ export class DownloadController { 'Download a ZIP archive containing the specified assets. The assets must have been previously requested via the "getDownloadInfo" endpoint.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - downloadArchive(@Auth() auth: AuthDto, @Body() dto: AssetIdsDto): Promise { + downloadArchive(@Auth() auth: AuthDto, @Body() dto: DownloadArchiveDto): Promise { return this.service.downloadArchive(auth, dto).then(asStreamableFile); } } diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 6ba3d38a73..dc3754ce24 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -6,6 +6,7 @@ import { AssetMediaController } from 'src/controllers/asset-media.controller'; import { AssetController } from 'src/controllers/asset.controller'; import { AuthAdminController } from 'src/controllers/auth-admin.controller'; import { AuthController } from 'src/controllers/auth.controller'; +import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; import { FaceController } from 'src/controllers/face.controller'; @@ -46,6 +47,7 @@ export const controllers = [ AssetMediaController, AuthController, AuthAdminController, + DatabaseBackupController, DownloadController, DuplicateController, FaceController, diff --git a/server/src/controllers/maintenance.controller.spec.ts b/server/src/controllers/maintenance.controller.spec.ts new file mode 100644 index 0000000000..094028687e --- /dev/null +++ b/server/src/controllers/maintenance.controller.spec.ts @@ -0,0 +1,39 @@ +import { MaintenanceController } from 'src/controllers/maintenance.controller'; +import { MaintenanceAction } from 'src/enum'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(MaintenanceController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(MaintenanceService); + + beforeAll(async () => { + ctx = await controllerSetup(MaintenanceController, [{ provide: MaintenanceService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('POST /admin/maintenance', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/admin/maintenance').send(); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a backup file when action is restore', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/admin/maintenance').send({ + action: MaintenanceAction.RestoreDatabase, + }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['restoreBackupFilename must be a string', 'restoreBackupFilename should not be empty']), + ); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 7b2aa17582..169fec7890 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -1,9 +1,15 @@ -import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Get, Post, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceLoginDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; @@ -15,6 +21,27 @@ import { respondWithCookie } from 'src/utils/response'; export class MaintenanceController { constructor(private service: MaintenanceService) {} + @Get('status') + @Endpoint({ + summary: 'Get maintenance mode status', + description: 'Fetch information about the currently running maintenance action.', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + getMaintenanceStatus(): MaintenanceStatusResponseDto { + return this.service.getMaintenanceStatus(); + } + + @Get('detect-install') + @Endpoint({ + summary: 'Detect existing install', + description: 'Collect integrity checks and other heuristics about local data.', + history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), + }) + @Authenticated({ permission: Permission.Maintenance, admin: true }) + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); + } + @Post('login') @Endpoint({ summary: 'Log into maintenance mode', @@ -38,8 +65,8 @@ export class MaintenanceController { @GetLoginDetails() loginDetails: LoginDetails, @Res({ passthrough: true }) res: Response, ): Promise { - if (dto.action === MaintenanceAction.Start) { - const { jwt } = await this.service.startMaintenance(auth.user.name); + if (dto.action !== MaintenanceAction.End) { + const { jwt } = await this.service.startMaintenance(dto, auth.user.name); return respondWithCookie(res, undefined, { isSecure: loginDetails.isSecure, values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts index dbd1082561..ae3b56af28 100644 --- a/server/src/controllers/map.controller.ts +++ b/server/src/controllers/map.controller.ts @@ -8,7 +8,7 @@ import { MapReverseGeocodeDto, MapReverseGeocodeResponseDto, } from 'src/dtos/map.dto'; -import { ApiTag } from 'src/enum'; +import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MapService } from 'src/services/map.service'; @@ -18,7 +18,7 @@ export class MapController { constructor(private service: MapService) {} @Get('markers') - @Authenticated() + @Authenticated({ permission: Permission.MapRead }) @Endpoint({ summary: 'Retrieve map markers', description: 'Retrieve a list of latitude and longitude coordinates for every asset with location data.', @@ -28,8 +28,8 @@ export class MapController { return this.service.getMapMarkers(auth, options); } - @Authenticated() @Get('reverse-geocode') + @Authenticated({ permission: Permission.MapSearch }) @HttpCode(HttpStatus.OK) @Endpoint({ summary: 'Reverse geocode coordinates', diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 8629b6c799..820819ee6e 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -51,6 +51,20 @@ describe(MemoryController.name, () => { errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), ); }); + + it('should accept showAt and hideAt', async () => { + const { status } = await request(ctx.getHttpServer()) + .post('/memories') + .send({ + type: 'on_this_day', + data: { year: 2020 }, + memoryAt: new Date(2021).toISOString(), + showAt: new Date(2022).toISOString(), + hideAt: new Date(2023).toISOString(), + }); + + expect(status).toBe(201); + }); }); describe('GET /memories/statistics', () => { diff --git a/server/src/controllers/notification-admin.controller.spec.ts b/server/src/controllers/notification-admin.controller.spec.ts new file mode 100644 index 0000000000..b93726eb32 --- /dev/null +++ b/server/src/controllers/notification-admin.controller.spec.ts @@ -0,0 +1,36 @@ +import { NotificationAdminController } from 'src/controllers/notification-admin.controller'; +import { NotificationAdminService } from 'src/services/notification-admin.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(NotificationAdminController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(NotificationAdminService); + + beforeAll(async () => { + ctx = await controllerSetup(NotificationAdminController, [ + { provide: NotificationAdminService, useValue: service }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('POST /admin/notifications', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/admin/notifications'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should accept a null readAt', async () => { + await request(ctx.getHttpServer()) + .post(`/admin/notifications`) + .send({ title: 'Test', userId: factory.uuid(), readAt: null }); + expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ readAt: null })); + }); + }); +}); diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts index 0dce7d73b5..a64aee2912 100644 --- a/server/src/controllers/notification.controller.spec.ts +++ b/server/src/controllers/notification.controller.spec.ts @@ -37,9 +37,33 @@ describe(NotificationController.name, () => { describe('PUT /notifications', () => { it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/notifications'); + await request(ctx.getHttpServer()).put('/notifications'); expect(ctx.authenticate).toHaveBeenCalled(); }); + + describe('ids', () => { + it('should require a list', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['ids must be an array']))); + }); + + it('should require uuids', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/notifications`) + .send({ ids: [true] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + + it('should accept valid uuids', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()) + .put(`/notifications`) + .send({ ids: [id] }); + expect(service.updateAll).toHaveBeenCalledWith(undefined, expect.objectContaining({ ids: [id] })); + }); + }); }); describe('GET /notifications/:id', () => { @@ -60,5 +84,11 @@ describe(NotificationController.name, () => { await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() }); expect(ctx.authenticate).toHaveBeenCalled(); }); + + it('should accept a null readAt', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()).put(`/notifications/${id}`).send({ readAt: null }); + expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ readAt: null })); + }); }); }); diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index 5b63fcc6cd..a28ac9b659 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -58,6 +58,11 @@ describe(PersonController.name, () => { await request(ctx.getHttpServer()).post('/people').send({ birthDate: '' }); expect(service.create).toHaveBeenCalledWith(undefined, { birthDate: null }); }); + + it('should map an empty color to null', async () => { + await request(ctx.getHttpServer()).post('/people').send({ color: '' }); + expect(service.create).toHaveBeenCalledWith(undefined, { color: null }); + }); }); describe('DELETE /people', () => { diff --git a/server/src/controllers/plugin.controller.ts b/server/src/controllers/plugin.controller.ts index a0a4d14b0b..52c833e93d 100644 --- a/server/src/controllers/plugin.controller.ts +++ b/server/src/controllers/plugin.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { PluginResponseDto } from 'src/dtos/plugin.dto'; +import { PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { PluginService } from 'src/services/plugin.service'; @@ -12,6 +12,17 @@ import { UUIDParamDto } from 'src/validation'; export class PluginController { constructor(private service: PluginService) {} + @Get('triggers') + @Authenticated({ permission: Permission.PluginRead }) + @Endpoint({ + summary: 'List all plugin triggers', + description: 'Retrieve a list of all available plugin triggers.', + history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), + }) + getPluginTriggers(): PluginTriggerResponseDto[] { + return this.service.getTriggers(); + } + @Get() @Authenticated({ permission: Permission.PluginRead }) @Endpoint({ diff --git a/server/src/controllers/shared-link.controller.spec.ts b/server/src/controllers/shared-link.controller.spec.ts new file mode 100644 index 0000000000..96c84040ca --- /dev/null +++ b/server/src/controllers/shared-link.controller.spec.ts @@ -0,0 +1,34 @@ +import { SharedLinkController } from 'src/controllers/shared-link.controller'; +import { SharedLinkType } from 'src/enum'; +import { SharedLinkService } from 'src/services/shared-link.service'; +import request from 'supertest'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SharedLinkController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(SharedLinkService); + + beforeAll(async () => { + ctx = await controllerSetup(SharedLinkController, [{ provide: SharedLinkService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('POST /shared-links', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/shared-links'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should allow an null expiresAt', async () => { + await request(ctx.getHttpServer()) + .post('/shared-links') + .send({ expiresAt: null, type: SharedLinkType.Individual }); + expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null })); + }); + }); +}); diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index 8875127a25..1f91409e80 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -22,21 +22,39 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { SharedLinkCreateDto, SharedLinkEditDto, + SharedLinkLoginDto, SharedLinkPasswordDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; import { ApiTag, ImmichCookie, Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { respondWithCookie } from 'src/utils/response'; import { UUIDParamDto } from 'src/validation'; +const getAuthTokens = (cookies: Record | undefined) => { + return cookies?.[ImmichCookie.SharedLinkToken]?.split(',') || []; +}; + +const merge = (cookies: Record | undefined, token: string) => { + const authTokens = getAuthTokens(cookies); + if (!authTokens.includes(token)) { + authTokens.push(token); + } + + return authTokens.join(','); +}; + @ApiTags(ApiTag.SharedLinks) @Controller('shared-links') export class SharedLinkController { - constructor(private service: SharedLinkService) {} + constructor( + private service: SharedLinkService, + private logger: LoggingRepository, + ) {} @Get() @Authenticated({ permission: Permission.SharedLinkRead }) @@ -49,6 +67,28 @@ export class SharedLinkController { return this.service.getAll(auth, dto); } + @Post('login') + @Authenticated({ sharedLink: true }) + @Endpoint({ + summary: 'Shared link login', + description: 'Login to a password protected shared link', + history: new HistoryBuilder().added('v2.6.0').beta('v2.6.0'), + }) + async sharedLinkLogin( + @Auth() auth: AuthDto, + @Body() dto: SharedLinkLoginDto, + @Req() req: Request, + @Res({ passthrough: true }) res: Response, + @GetLoginDetails() loginDetails: LoginDetails, + ): Promise { + const { sharedLink, token } = await this.service.login(auth, dto); + + return respondWithCookie(res, sharedLink, { + isSecure: loginDetails.isSecure, + values: [{ key: ImmichCookie.SharedLinkToken, value: merge(req.cookies, token) }], + }); + } + @Get('me') @Authenticated({ sharedLink: true }) @Endpoint({ @@ -59,19 +99,19 @@ export class SharedLinkController { async getMySharedLink( @Auth() auth: AuthDto, @Query() dto: SharedLinkPasswordDto, - @Req() request: Request, + @Req() req: Request, @Res({ passthrough: true }) res: Response, @GetLoginDetails() loginDetails: LoginDetails, ): Promise { - const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken]; - if (sharedLinkToken) { - dto.token = sharedLinkToken; + if (dto.password) { + this.logger.deprecate( + 'Passing shared link password via query parameters is deprecated and will be removed in the next major release. Please use POST /shared-links/login instead.', + ); + + return this.sharedLinkLogin(auth, { password: dto.password }, req, res, loginDetails); } - const body = await this.service.getMine(auth, dto); - return respondWithCookie(res, body, { - isSecure: loginDetails.isSecure, - values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [], - }); + + return this.service.getMine(auth, getAuthTokens(req.cookies)); } @Get(':id') diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 48b8c1bcf0..bbd1241dc5 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -70,5 +70,33 @@ describe(SystemConfigController.name, () => { expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); }); }); + + describe('image', () => { + it('should accept config without optional progressive property', async () => { + const config = _.cloneDeep(defaults); + delete config.image.thumbnail.progressive; + delete config.image.preview.progressive; + delete config.image.fullsize.progressive; + const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(200); + }); + + it('should accept config with progressive set to true', async () => { + const config = _.cloneDeep(defaults); + config.image.thumbnail.progressive = true; + config.image.preview.progressive = true; + config.image.fullsize.progressive = true; + const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(200); + }); + + it('should reject invalid progressive value', async () => { + const config = _.cloneDeep(defaults); + (config.image.thumbnail.progressive as any) = 'invalid'; + const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['image.thumbnail.progressive must be a boolean value'])); + }); + }); }); }); diff --git a/server/src/controllers/tag.controller.spec.ts b/server/src/controllers/tag.controller.spec.ts new file mode 100644 index 0000000000..60fc3d65ae --- /dev/null +++ b/server/src/controllers/tag.controller.spec.ts @@ -0,0 +1,73 @@ +import { TagController } from 'src/controllers/tag.controller'; +import { TagService } from 'src/services/tag.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(TagController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(TagService); + + beforeAll(async () => { + ctx = await controllerSetup(TagController, [{ provide: TagService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /tags', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/tags'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /tags', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/tags'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should a null parentId', async () => { + await request(ctx.getHttpServer()).post(`/tags`).send({ name: 'tag', parentId: null }); + expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ parentId: null })); + }); + }); + + describe('PUT /tags', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/tags'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /tags/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/tags/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + }); + }); + + describe('PUT /tags/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should allow setting a null color via an empty string', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()).put(`/tags/${id}`).send({ color: '' }); + expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ color: null })); + }); + }); +}); diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index bd9c966d42..edda974476 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -31,12 +31,55 @@ describe(UserAdminController.name, () => { }); }); + describe('PUT /admin/users/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + describe('POST /admin/users', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).post('/admin/users'); expect(ctx.authenticate).toHaveBeenCalled(); }); + it('should allow a null pinCode', async () => { + await request(ctx.getHttpServer()).post(`/admin/users`).send({ + name: 'Test user', + email: 'test@immich.cloud', + password: 'password', + pinCode: null, + }); + expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ pinCode: null })); + }); + + it('should allow a null avatarColor', async () => { + await request(ctx.getHttpServer()).post(`/admin/users`).send({ + name: 'Test user', + email: 'test@immich.cloud', + password: 'password', + avatarColor: null, + }); + expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ avatarColor: null })); + }); + + it(`should `, async () => { + const dto: UserAdminCreateDto = { + email: 'user@immich.app', + password: 'test', + name: 'Test User', + quotaSizeInBytes: 1.2, + }; + + const { status, body } = await request(ctx.getHttpServer()) + .post(`/admin/users`) + .set('Authorization', `Bearer token`) + .send(dto); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + }); + it(`should not allow decimal quota`, async () => { const dto: UserAdminCreateDto = { email: 'user@immich.app', @@ -75,5 +118,17 @@ describe(UserAdminController.name, () => { expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); }); + + it('should allow a null pinCode', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ pinCode: null }); + expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ pinCode: null })); + }); + + it('should allow a null avatarColor', async () => { + const id = factory.uuid(); + await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ avatarColor: null }); + expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ avatarColor: null })); + }); }); }); diff --git a/server/src/controllers/user.controller.spec.ts b/server/src/controllers/user.controller.spec.ts index 19f9e919de..3c3e103814 100644 --- a/server/src/controllers/user.controller.spec.ts +++ b/server/src/controllers/user.controller.spec.ts @@ -54,6 +54,14 @@ describe(UserController.name, () => { expect(body).toEqual(errorDto.badRequest()); }); } + + it('should allow an empty avatarColor', async () => { + await request(ctx.getHttpServer()) + .put(`/users/me`) + .set('Authorization', `Bearer token`) + .send({ avatarColor: null }); + expect(service.updateMe).toHaveBeenCalledWith(undefined, expect.objectContaining({ avatarColor: null })); + }); }); describe('GET /users/:id', () => { diff --git a/server/src/controllers/view.controller.ts b/server/src/controllers/view.controller.ts index 8a977e15bc..b07d83fe58 100644 --- a/server/src/controllers/view.controller.ts +++ b/server/src/controllers/view.controller.ts @@ -3,7 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ApiTag } from 'src/enum'; +import { ApiTag, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { ViewService } from 'src/services/view.service'; @@ -13,7 +13,7 @@ export class ViewController { constructor(private service: ViewService) {} @Get('folder/unique-paths') - @Authenticated() + @Authenticated({ permission: Permission.FolderRead }) @Endpoint({ summary: 'Retrieve unique paths', description: 'Retrieve a list of unique folder paths from asset original paths.', @@ -24,7 +24,7 @@ export class ViewController { } @Get('folder') - @Authenticated() + @Authenticated({ permission: Permission.FolderRead }) @Endpoint({ summary: 'Retrieve assets by original path', description: 'Retrieve assets that are children of a specific folder.', diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index f1a5958eec..575456dcb3 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,7 +1,16 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; import { StorageAsset } from 'src/database'; -import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum'; +import { + AssetFileType, + AssetPathType, + ImageFormat, + PathType, + PersonPathType, + RawExtractedFormat, + StorageFolder, + TilesFormat, +} from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -33,6 +42,8 @@ export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo export type ThumbnailPathEntity = { id: string; ownerId: string }; +export type ImagePathOptions = { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat | TilesFormat; isEdited: boolean }; + let instance: StorageCore | null; let mediaLocation: string | undefined; @@ -109,8 +120,12 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); } - static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp' | 'dz') { - return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`); + static getImagePath(asset: ThumbnailPathEntity, { fileType, format, isEdited }: ImagePathOptions) { + return StorageCore.getNestedPath( + StorageFolder.Thumbnails, + asset.ownerId, + `${asset.id}_${fileType}${isEdited ? '_edited' : ''}.${format}`, + ); } static getEncodedVideoPath(asset: ThumbnailPathEntity) { @@ -135,14 +150,14 @@ export class StorageCore { return normalizedPath.startsWith(normalizedAppMediaLocation); } - async moveAssetImage(asset: StorageAsset, pathType: GeneratedImageType, format: ImageFormat) { + async moveAssetImage(asset: StorageAsset, fileType: AssetFileType, format: ImageFormat) { const { id: entityId, files } = asset; - const oldFile = getAssetFile(files, pathType); + const oldFile = getAssetFile(files, fileType, { isEdited: false }); return this.moveFile({ entityId, - pathType, + pathType: fileType, oldPath: oldFile?.path || null, - newPath: StorageCore.getImagePath(asset, pathType, format), + newPath: StorageCore.getImagePath(asset, { fileType, format, isEdited: false }), }); } @@ -296,19 +311,19 @@ export class StorageCore { case AssetPathType.Original: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.FullSize: { + case AssetFileType.FullSize: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath }); } - case AssetPathType.Preview: { + case AssetFileType.Preview: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath }); } - case AssetPathType.Thumbnail: { + case AssetFileType.Thumbnail: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath }); } case AssetPathType.EncodedVideo: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); } - case AssetPathType.Sidecar: { + case AssetFileType.Sidecar: { return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath }); } case PersonPathType.Face: { diff --git a/server/src/database.ts b/server/src/database.ts index 9f4494b720..ec614df9e0 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -39,6 +39,7 @@ export type AssetFile = { id: string; type: AssetFileType; path: string; + isEdited: boolean; }; export type Library = { @@ -272,6 +273,7 @@ export type AssetFace = { person?: Person | null; updatedAt: Date; updateId: string; + isVisible: boolean; }; export type Plugin = Selectable; @@ -340,8 +342,18 @@ export const columns = { 'asset.originalPath', 'asset.ownerId', 'asset.type', + 'asset.width', + 'asset.height', + ], + assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type', 'asset_file.isEdited'], + assetFilesForThumbnail: [ + 'asset_file.id', + 'asset_file.path', + 'asset_file.type', + 'asset_file.isEdited', + 'asset_file.isProgressive', + 'asset_file.isTransparent', ], - 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.updatedAt', 'session.pinExpiresAt', 'session.appVersion'], @@ -390,6 +402,9 @@ export const columns = { 'asset.livePhotoVideoId', 'asset.stackId', 'asset.libraryId', + 'asset.width', + 'asset.height', + 'asset.isEdited', ], syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'], syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], @@ -422,6 +437,13 @@ export const columns = { 'asset_exif.rating', 'asset_exif.fps', ], + syncAssetEdit: [ + 'asset_edit.id', + 'asset_edit.assetId', + 'asset_edit.sequence', + 'asset_edit.action', + 'asset_edit.parameters', + ], exif: [ 'asset_exif.assetId', 'asset_exif.autoStackId', @@ -451,6 +473,7 @@ export const columns = { 'asset_exif.projectionType', 'asset_exif.rating', 'asset_exif.state', + 'asset_exif.tags', 'asset_exif.timeZone', ], plugin: [ @@ -474,4 +497,5 @@ export const lockableProperties = [ 'longitude', 'rating', 'timeZone', + 'tags', ] as const; diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 054bbf8fec..695adb4a36 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -1,10 +1,10 @@ +import { BeforeUpdateTrigger, Column, ColumnOptions } from '@immich/sql-tools'; import { SetMetadata, applyDecorators } from '@nestjs/common'; import { ApiOperation, ApiOperationOptions, ApiProperty, ApiPropertyOptions, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum'; import { EmitEvent } from 'src/repositories/event.repository'; import { immich_uuid_v7, updated_at } from 'src/schema/functions'; -import { BeforeUpdateTrigger, Column, ColumnOptions } from 'src/sql-tools'; import { setUnion } from 'src/utils/set'; const GeneratedUuidV7Column = (options: Omit = {}) => @@ -171,7 +171,7 @@ export const Endpoint = ({ history, ...options }: EndpointOptions) => { return applyDecorators(...decorators); }; -type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder }; +export type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder }; export const Property = ({ history, ...options }: PropertyOptions) => { const extensions = history?.getExtensions() ?? {}; diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 4b11a16e14..6464d88508 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; import { Activity } from 'src/database'; import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; @@ -17,48 +17,55 @@ export enum ReactionLevel { export type MaybeDuplicate = { duplicate: boolean; value: T }; export class ActivityResponseDto { + @ApiProperty({ description: 'Activity ID' }) id!: string; + @ApiProperty({ description: 'Creation date', format: 'date-time' }) createdAt!: Date; - @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type' }) type!: ReactionType; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) user!: UserResponseDto; + @ApiProperty({ description: 'Asset ID (if activity is for an asset)' }) assetId!: string | null; + @ApiPropertyOptional({ description: 'Comment text (for comment activities)' }) comment?: string | null; } export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of comments' }) comments!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of likes' }) likes!: number; } export class ActivityDto { - @ValidateUUID() + @ValidateUUID({ description: 'Album ID' }) albumId!: string; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) assetId?: string; } export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) type?: ReactionType; - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true }) + @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) level?: ReactionLevel; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by user ID' }) userId?: string; } const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; export class ActivityCreateDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType' }) + @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' }) type!: ReactionType; + @ApiPropertyOptional({ description: 'Comment text (required if type is comment)' }) @ValidateIf(isComment) @IsNotEmpty() @IsString() diff --git a/server/src/dtos/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts index dd8642598f..d3536a3482 100644 --- a/server/src/dtos/album-response.dto.spec.ts +++ b/server/src/dtos/album-response.dto.spec.ts @@ -1,15 +1,18 @@ import { mapAlbum } from 'src/dtos/album.dto'; -import { albumStub } from 'test/fixtures/album.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; describe('mapAlbum', () => { it('should set start and end dates', () => { - const dto = mapAlbum(albumStub.twoAssets, false); - expect(dto.startDate).toEqual(new Date('2020-12-31T23:59:00.000Z')); - expect(dto.endDate).toEqual(new Date('2025-01-01T01:02:03.456Z')); + const startDate = new Date('2023-02-22T05:06:29.716Z'); + const endDate = new Date('2025-01-01T01:02:03.456Z'); + const album = AlbumFactory.from().asset({ localDateTime: endDate }).asset({ localDateTime: startDate }).build(); + const dto = mapAlbum(album, false); + expect(dto.startDate).toEqual(startDate); + expect(dto.endDate).toEqual(endDate); }); it('should not set start and end dates for empty assets', () => { - const dto = mapAlbum(albumStub.empty, false); + const dto = mapAlbum(AlbumFactory.create(), false); expect(dto.startDate).toBeUndefined(); expect(dto.endDate).toBeUndefined(); }); diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 2f3f22099a..62013fbd92 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import _ from 'lodash'; @@ -11,156 +11,181 @@ import { AlbumUserRole, AssetOrder } from 'src/enum'; import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class AlbumInfoDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Exclude assets from response' }) withoutAssets?: boolean; } export class AlbumUserAddDto { - @ValidateUUID() + @ValidateUUID({ description: 'User ID' }) userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.Editor }) + @ValidateEnum({ + enum: AlbumUserRole, + name: 'AlbumUserRole', + description: 'Album user role', + default: AlbumUserRole.Editor, + }) role?: AlbumUserRole; } export class AddUsersDto { + @ApiProperty({ description: 'Album users to add' }) @ArrayNotEmpty() albumUsers!: AlbumUserAddDto[]; } export class AlbumUserCreateDto { - @ValidateUUID() + @ValidateUUID({ description: 'User ID' }) userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) role!: AlbumUserRole; } export class CreateAlbumDto { + @ApiProperty({ description: 'Album name' }) @IsString() - @ApiProperty() albumName!: string; + @ApiPropertyOptional({ description: 'Album description' }) @IsString() @Optional() description?: string; + @ApiPropertyOptional({ description: 'Album users' }) @Optional() @IsArray() @ValidateNested({ each: true }) @Type(() => AlbumUserCreateDto) albumUsers?: AlbumUserCreateDto[]; - @ValidateUUID({ optional: true, each: true }) + @ValidateUUID({ optional: true, each: true, description: 'Initial asset IDs' }) assetIds?: string[]; } export class AlbumsAddAssetsDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Album IDs' }) albumIds!: string[]; - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Asset IDs' }) assetIds!: string[]; } export class AlbumsAddAssetsResponseDto { + @ApiProperty({ description: 'Operation success' }) success!: boolean; - @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true }) + @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', description: 'Error reason', optional: true }) error?: BulkIdErrorReason; } export class UpdateAlbumDto { + @ApiPropertyOptional({ description: 'Album name' }) @Optional() @IsString() albumName?: string; + @ApiPropertyOptional({ description: 'Album description' }) @Optional() @IsString() description?: string; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Album thumbnail asset ID' }) albumThumbnailAssetId?: string; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Enable activity feed' }) isActivityEnabled?: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) order?: AssetOrder; } export class GetAlbumsDto { - @ValidateBoolean({ optional: true }) - /** - * true: only shared albums - * false: only non-shared own albums - * undefined: shared and owned albums - */ + @ValidateBoolean({ + optional: true, + description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums', + }) shared?: boolean; - /** - * Only returns albums that contain the asset - * Ignores the shared parameter - * undefined: get all albums - */ - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' }) assetId?: string; } export class AlbumStatisticsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of owned albums' }) owned!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of shared albums' }) shared!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of non-shared albums' }) notShared!: number; } export class UpdateAlbumUserDto { - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) role!: AlbumUserRole; } export class AlbumUserResponseDto { + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) user!: UserResponseDto; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) role!: AlbumUserRole; } export class ContributorCountResponseDto { - @ApiProperty() + @ApiProperty({ description: 'User ID' }) userId!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) assetCount!: number; } export class AlbumResponseDto { + @ApiProperty({ description: 'Album ID' }) id!: string; + @ApiProperty({ description: 'Owner user ID' }) ownerId!: string; + @ApiProperty({ description: 'Album name' }) albumName!: string; + @ApiProperty({ description: 'Album description' }) description!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; + @ApiProperty({ description: 'Thumbnail asset ID' }) albumThumbnailAssetId!: string | null; + @ApiProperty({ description: 'Is shared album' }) shared!: boolean; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) albumUsers!: AlbumUserResponseDto[]; + @ApiProperty({ description: 'Has shared link' }) hasSharedLink!: boolean; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) assets!: AssetResponseDto[]; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) owner!: UserResponseDto; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets' }) assetCount!: number; + @ApiPropertyOptional({ description: 'Last modified asset timestamp' }) lastModifiedAssetTimestamp?: Date; + @ApiPropertyOptional({ description: 'Start date (earliest asset)' }) startDate?: Date; + @ApiPropertyOptional({ description: 'End date (latest asset)' }) endDate?: Date; + @ApiProperty({ description: 'Activity feed enabled' }) isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) order?: AssetOrder; - // Optional per-user contribution counts for shared albums + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Type(() => ContributorCountResponseDto) - @ApiProperty({ type: [ContributorCountResponseDto], required: false }) contributorCounts?: ContributorCountResponseDto[]; } diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index c9475fa2b1..273082c41b 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,38 +1,55 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; import { Permission } from 'src/enum'; import { Optional, ValidateEnum } from 'src/validation'; + export class APIKeyCreateDto { + @ApiPropertyOptional({ description: 'API key name' }) @IsString() @IsNotEmpty() @Optional() name?: string; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) @ArrayMinSize(1) permissions!: Permission[]; } export class APIKeyUpdateDto { + @ApiPropertyOptional({ description: 'API key name' }) @Optional() @IsString() @IsNotEmpty() name?: string; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true }) + @ValidateEnum({ + enum: Permission, + name: 'Permission', + description: 'List of permissions', + each: true, + optional: true, + }) @ArrayMinSize(1) permissions?: Permission[]; } -export class APIKeyCreateResponseDto { - secret!: string; - apiKey!: APIKeyResponseDto; -} - export class APIKeyResponseDto { + @ApiProperty({ description: 'API key ID' }) id!: string; + @ApiProperty({ description: 'API key name' }) name!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true }) + @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) permissions!: Permission[]; } + +export class APIKeyCreateResponseDto { + @ApiProperty({ description: 'API key secret (only shown once)' }) + secret!: string; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) + apiKey!: APIKeyResponseDto; +} diff --git a/server/src/dtos/asset-ids.response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts index fdc9942e37..427117518d 100644 --- a/server/src/dtos/asset-ids.response.dto.ts +++ b/server/src/dtos/asset-ids.response.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ValidateUUID } from 'src/validation'; /** @deprecated Use `BulkIdResponseDto` instead */ @@ -9,8 +10,11 @@ export enum AssetIdErrorReason { /** @deprecated Use `BulkIdResponseDto` instead */ export class AssetIdsResponseDto { + @ApiProperty({ description: 'Asset ID' }) assetId!: string; + @ApiProperty({ description: 'Whether operation succeeded' }) success!: boolean; + @ApiPropertyOptional({ description: 'Error reason if failed', enum: AssetIdErrorReason }) error?: AssetIdErrorReason; } @@ -22,12 +26,15 @@ export enum BulkIdErrorReason { } export class BulkIdsDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'IDs to process' }) ids!: string[]; } export class BulkIdResponseDto { + @ApiProperty({ description: 'ID' }) id!: string; + @ApiProperty({ description: 'Whether operation succeeded' }) success!: boolean; + @ApiPropertyOptional({ description: 'Error reason if failed', enum: BulkIdErrorReason }) error?: BulkIdErrorReason; } diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 887762dbdd..345c1bf418 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ValidateEnum } from 'src/validation'; export enum AssetMediaStatus { @@ -6,8 +7,9 @@ export enum AssetMediaStatus { DUPLICATE = 'duplicate', } export class AssetMediaResponseDto { - @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' }) + @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus', description: 'Upload status' }) status!: AssetMediaStatus; + @ApiProperty({ description: 'Asset media ID' }) id!: string; } @@ -22,17 +24,24 @@ export enum AssetRejectReason { } export class AssetBulkUploadCheckResult { + @ApiProperty({ description: 'Asset ID' }) id!: string; + @ApiProperty({ description: 'Upload action', enum: AssetUploadAction }) action!: AssetUploadAction; + @ApiPropertyOptional({ description: 'Rejection reason if rejected', enum: AssetRejectReason }) reason?: AssetRejectReason; + @ApiPropertyOptional({ description: 'Existing asset ID if duplicate' }) assetId?: string; + @ApiPropertyOptional({ description: 'Whether existing asset is trashed' }) isTrashed?: boolean; } export class AssetBulkUploadCheckResponseDto { + @ApiProperty({ description: 'Upload check results' }) results!: AssetBulkUploadCheckResult[]; } export class CheckExistingAssetsResponseDto { + @ApiProperty({ description: 'Existing asset IDs' }) existingIds!: string[]; } diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 755069d827..4655850379 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { plainToInstance, Transform, Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; @@ -7,6 +7,7 @@ import { AssetVisibility } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { + Original = 'original', /** * An full-sized image extracted/converted from non-web-friendly formats like RAW/HIF. * or otherwise the original image itself. @@ -17,8 +18,11 @@ export enum AssetMediaSize { } export class AssetMediaOptionsDto { - @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true }) + @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', description: 'Asset media size', optional: true }) size?: AssetMediaSize; + + @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) + edited?: boolean; } export enum UploadFieldName { @@ -28,44 +32,49 @@ export enum UploadFieldName { } class AssetMediaBase { + @ApiProperty({ description: 'Device asset ID' }) @IsNotEmpty() @IsString() deviceAssetId!: string; + @ApiProperty({ description: 'Device ID' }) @IsNotEmpty() @IsString() deviceId!: string; - @ValidateDate() + @ValidateDate({ description: 'File creation date' }) fileCreatedAt!: Date; - @ValidateDate() + @ValidateDate({ description: 'File modification date' }) fileModifiedAt!: Date; + @ApiPropertyOptional({ description: 'Duration (for videos)' }) @Optional() @IsString() duration?: string; + @ApiPropertyOptional({ description: 'Filename' }) @Optional() @IsString() filename?: string; // The properties below are added to correctly generate the API docs // and client SDKs. Validation should be handled in the controller. - @ApiProperty({ type: 'string', format: 'binary' }) + @ApiProperty({ type: 'string', format: 'binary', description: 'Asset file data' }) [UploadFieldName.ASSET_DATA]!: any; } export class AssetMediaCreateDto extends AssetMediaBase { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) isFavorite?: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility', optional: true }) visibility?: AssetVisibility; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Live photo video ID' }) livePhotoVideoId?: string; + @ApiPropertyOptional({ description: 'Asset metadata items' }) @Transform(({ value }) => { try { const json = JSON.parse(value); @@ -78,26 +87,28 @@ export class AssetMediaCreateDto extends AssetMediaBase { @Optional() @ValidateNested({ each: true }) @IsArray() - metadata!: AssetMetadataUpsertItemDto[]; + metadata?: AssetMetadataUpsertItemDto[]; - @ApiProperty({ type: 'string', format: 'binary', required: false }) + @ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' }) [UploadFieldName.SIDECAR_DATA]?: any; } export class AssetMediaReplaceDto extends AssetMediaBase {} export class AssetBulkUploadCheckItem { + @ApiProperty({ description: 'Asset ID' }) @IsString() @IsNotEmpty() id!: string; - /** base64 or hex encoded sha1 hash */ + @ApiProperty({ description: 'Base64 or hex encoded SHA1 hash' }) @IsString() @IsNotEmpty() checksum!: string; } export class AssetBulkUploadCheckDto { + @ApiProperty({ description: 'Assets to check' }) @IsArray() @ValidateNested({ each: true }) @Type(() => AssetBulkUploadCheckItem) @@ -105,11 +116,13 @@ export class AssetBulkUploadCheckDto { } export class CheckExistingAssetsDto { + @ApiProperty({ description: 'Device asset IDs to check' }) @ArrayNotEmpty() @IsString({ each: true }) @IsNotEmpty({ each: true }) deviceAssetIds!: string[]; + @ApiProperty({ description: 'Device ID' }) @IsNotEmpty() deviceId!: string; } diff --git a/server/src/dtos/asset-response.dto.spec.ts b/server/src/dtos/asset-response.dto.spec.ts new file mode 100644 index 0000000000..ff3b3f6acd --- /dev/null +++ b/server/src/dtos/asset-response.dto.spec.ts @@ -0,0 +1,191 @@ +import { mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; + +describe('mapAsset', () => { + describe('peopleWithFaces', () => { + it('should transform all faces when a person has multiple faces in the same image', () => { + const person = PersonFactory.create(); + const face1 = { + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }; + + const face2 = { + boundingBoxX1: 300, + boundingBoxY1: 400, + boundingBoxX2: 400, + boundingBoxY2: 500, + imageWidth: 1000, + imageHeight: 800, + }; + + const asset = AssetFactory.from() + .face(face1, (builder) => builder.person(person)) + .face(face2, (builder) => builder.person(person)) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .edit({ + action: AssetEditAction.Crop, + parameters: { + width: 1512, + height: 1152, + x: 216, + y: 1512, + }, + }) + .build(); + + const result = mapAsset(asset); + + expect(result.people).toBeDefined(); + expect(result.people).toHaveLength(1); + expect(result.people![0].faces).toHaveLength(2); + + // Verify that both faces have been transformed (bounding boxes adjusted for crop) + const firstFace = result.people![0].faces[0]; + const secondFace = result.people![0].faces[1]; + + // After crop (x: 216, y: 1512), the coordinates should be adjusted + // Faces outside the crop area will be clamped + expect(firstFace.boundingBoxX1).toBe(-116); // 100 - 216 = -116 + expect(firstFace.boundingBoxY1).toBe(-1412); // 100 - 1512 = -1412 + expect(firstFace.boundingBoxX2).toBe(-16); // 200 - 216 = -16 + expect(firstFace.boundingBoxY2).toBe(-1312); // 200 - 1512 = -1312 + + expect(secondFace.boundingBoxX1).toBe(84); // 300 - 216 + expect(secondFace.boundingBoxY1).toBe(-1112); // 400 - 1512 = -1112 + expect(secondFace.boundingBoxX2).toBe(184); // 400 - 216 + expect(secondFace.boundingBoxY2).toBe(-1012); // 500 - 1512 = -1012 + }); + + it('should transform unassigned faces with edits and dimensions', () => { + const unassignedFace = AssetFaceFactory.create({ + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }); + + const asset = AssetFactory.from() + .face(unassignedFace) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .edit({ action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 500, height: 400 } }) + .build(); + + const result = mapAsset(asset); + + expect(result.unassignedFaces).toBeDefined(); + expect(result.unassignedFaces).toHaveLength(1); + + // Verify that unassigned face has been transformed + const face = result.unassignedFaces![0]; + expect(face.boundingBoxX1).toBe(50); // 100 - 50 + expect(face.boundingBoxY1).toBe(50); // 100 - 50 + expect(face.boundingBoxX2).toBe(150); // 200 - 50 + expect(face.boundingBoxY2).toBe(150); // 200 - 50 + }); + + it('should handle multiple people each with multiple faces', () => { + const person1Face1 = { + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }; + + const person1Face2 = { + boundingBoxX1: 300, + boundingBoxY1: 300, + boundingBoxX2: 400, + boundingBoxY2: 400, + imageWidth: 1000, + imageHeight: 800, + }; + + const person2Face1 = { + boundingBoxX1: 500, + boundingBoxY1: 100, + boundingBoxX2: 600, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }; + + const person = PersonFactory.create({ id: 'person-1' }); + + const asset = AssetFactory.from() + .face(person1Face1, (builder) => builder.person(person)) + .face(person1Face2, (builder) => builder.person(person)) + .face(person2Face1, (builder) => builder.person({ id: 'person-2' })) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .build(); + + const result = mapAsset(asset); + + expect(result.people).toBeDefined(); + expect(result.people).toHaveLength(2); + + const person1 = result.people!.find((p) => p.id === 'person-1'); + const person2 = result.people!.find((p) => p.id === 'person-2'); + + expect(person1).toBeDefined(); + expect(person1!.faces).toHaveLength(2); + // No edits, so coordinates should be unchanged + expect(person1!.faces[0].boundingBoxX1).toBe(100); + expect(person1!.faces[0].boundingBoxY1).toBe(100); + expect(person1!.faces[1].boundingBoxX1).toBe(300); + expect(person1!.faces[1].boundingBoxY1).toBe(300); + + expect(person2).toBeDefined(); + expect(person2!.faces).toHaveLength(1); + expect(person2!.faces[0].boundingBoxX1).toBe(500); + expect(person2!.faces[0].boundingBoxY1).toBe(100); + }); + + it('should combine faces of the same person into a single entry', () => { + const face1 = { + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }; + + const face2 = { + boundingBoxX1: 300, + boundingBoxY1: 300, + boundingBoxX2: 400, + boundingBoxY2: 400, + imageWidth: 1000, + imageHeight: 800, + }; + + const person = PersonFactory.create(); + + const asset = AssetFactory.from() + .face(face1, (builder) => builder.person(person)) + .face(face2, (builder) => builder.person(person)) + .exif({ exifImageWidth: 1000, exifImageHeight: 800 }) + .build(); + + const result = mapAsset(asset); + + expect(result.people).toBeDefined(); + expect(result.people).toHaveLength(1); + + expect(result.people![0].id).toBe(person.id); + expect(result.people![0].faces).toHaveLength(2); + }); + }); +}); diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index e228cd8f9f..a76df4abaa 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,8 +1,9 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Selectable } from 'kysely'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; import { HistoryBuilder, Property } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; import { AssetFaceWithoutPersonResponseDto, @@ -13,15 +14,23 @@ import { import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { ImageDimensions } from 'src/types'; +import { getDimensions } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { mimeTypes } from 'src/utils/mime-types'; -import { ValidateEnum } from 'src/validation'; +import { ValidateEnum, ValidateUUID } from 'src/validation'; export class SanitizedAssetResponseDto { + @ApiProperty({ description: 'Asset ID' }) id!: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) type!: AssetType; + @ApiProperty({ + description: + 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', + }) thumbhash!: string | null; + @ApiPropertyOptional({ description: 'Original MIME type' }) originalMimeType?: string; @ApiProperty({ type: 'string', @@ -31,9 +40,16 @@ export class SanitizedAssetResponseDto { example: '2024-01-15T14:30:00.000Z', }) localDateTime!: Date; + @ApiProperty({ description: 'Video duration (for videos)' }) duration!: string; + @ApiPropertyOptional({ description: 'Live photo video ID' }) livePhotoVideoId?: string | null; + @ApiProperty({ description: 'Whether asset has metadata' }) hasMetadata!: boolean; + @ApiProperty({ description: 'Asset width' }) + width!: number | null; + @ApiProperty({ description: 'Asset height' }) + height!: number | null; } export class AssetResponseDto extends SanitizedAssetResponseDto { @@ -44,13 +60,24 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { example: '2024-01-15T20:30:00.000Z', }) createdAt!: Date; + @ApiProperty({ description: 'Device asset ID' }) deviceAssetId!: string; + @ApiProperty({ description: 'Device ID' }) deviceId!: string; + @ApiProperty({ description: 'Owner user ID' }) ownerId!: string; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) owner?: UserResponseDto; - @Property({ history: new HistoryBuilder().added('v1').deprecated('v1') }) + @ValidateUUID({ + nullable: true, + description: 'Library ID', + history: new HistoryBuilder().added('v1').deprecated('v1'), + }) libraryId?: string | null; + @ApiProperty({ description: 'Original file path' }) originalPath!: string; + @ApiProperty({ description: 'Original file name' }) originalFileName!: string; @ApiProperty({ type: 'string', @@ -76,23 +103,40 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { example: '2024-01-16T12:45:30.000Z', }) updatedAt!: Date; + @ApiProperty({ description: 'Is favorite' }) isFavorite!: boolean; + @ApiProperty({ description: 'Is archived' }) isArchived!: boolean; + @ApiProperty({ description: 'Is trashed' }) isTrashed!: boolean; + @ApiProperty({ description: 'Is offline' }) isOffline!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) visibility!: AssetVisibility; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) exifInfo?: ExifResponseDto; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) tags?: TagResponseDto[]; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) people?: PersonWithFacesResponseDto[]; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - /**base64 encoded sha1 hash */ + @ApiProperty({ description: 'Base64 encoded SHA1 hash' }) checksum!: string; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) stack?: AssetStackResponseDto | null; + @ApiPropertyOptional({ description: 'Duplicate group ID' }) duplicateId?: string | null; - @Property({ history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) + @Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) resized?: boolean; + @Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) + isEdited!: boolean; } export type MapAsset = { @@ -107,6 +151,7 @@ export type MapAsset = { deviceId: string; duplicateId: string | null; duration: string | null; + edits?: AssetEditActionItem[]; encodedVideoPath: string | null; exifInfo?: Selectable | null; faces?: AssetFace[]; @@ -129,14 +174,19 @@ export type MapAsset = { tags?: Tag[]; thumbhash: Buffer | null; type: AssetType; + width: number | null; + height: number | null; + isEdited: boolean; }; export class AssetStackResponseDto { + @ApiProperty({ description: 'Stack ID' }) id!: string; + @ApiProperty({ description: 'Primary asset ID' }) primaryAssetId!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets in stack' }) assetCount!: number; } @@ -146,23 +196,30 @@ export type AssetMapOptions = { auth?: AuthDto; }; -// TODO: this is inefficient -const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => { - const result: PersonWithFacesResponseDto[] = []; - if (faces) { - for (const face of faces) { - if (face.person) { - const existingPersonEntry = result.find((item) => item.id === face.person!.id); - if (existingPersonEntry) { - existingPersonEntry.faces.push(face); - } else { - result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] }); - } - } - } +const peopleWithFaces = ( + faces?: AssetFace[], + edits?: AssetEditActionItem[], + assetDimensions?: ImageDimensions, +): PersonWithFacesResponseDto[] => { + if (!faces) { + return []; } - return result; + const peopleFaces: Map = new Map(); + + for (const face of faces) { + if (!face.person) { + continue; + } + + if (!peopleFaces.has(face.person.id)) { + peopleFaces.set(face.person.id, { ...mapPerson(face.person), faces: [] }); + } + const mappedFace = mapFacesWithoutPerson(face, edits, assetDimensions); + peopleFaces.get(face.person.id)!.faces.push(mappedFace); + } + + return [...peopleFaces.values()]; }; const mapStack = (entity: { stack?: Stack | null }) => { @@ -190,10 +247,14 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, + width: entity.width, + height: entity.height, }; return sanitizedAssetResponse as AssetResponseDto; } + const assetDimensions = entity.exifInfo ? getDimensions(entity.exifInfo) : undefined; + return { id: entity.id, createdAt: entity.createdAt, @@ -219,13 +280,18 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, tags: entity.tags?.map((tag) => mapTag(tag)), - people: peopleWithFaces(entity.faces), - unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), + people: peopleWithFaces(entity.faces, entity.edits, assetDimensions), + unassignedFaces: entity.faces + ?.filter((face) => !face.person) + .map((a) => mapFacesWithoutPerson(a, entity.edits, assetDimensions)), checksum: hexOrBufferToBase64(entity.checksum)!, stack: withStack ? mapStack(entity) : undefined, isOffline: entity.isOffline, hasMetadata: true, duplicateId: entity.duplicateId, resized: true, + width: entity.width, + height: entity.height, + isEdited: entity.isEdited, }; } diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 03d1e31fb9..b7bd7a18e8 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { IsArray, IsDateString, @@ -16,12 +16,14 @@ import { ValidateIf, ValidateNested, } from 'class-validator'; +import { HistoryBuilder, Property } from 'src/decorators'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetMetadataKey, AssetType, AssetVisibility } from 'src/enum'; +import { AssetType, AssetVisibility } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; export class DeviceIdDto { + @ApiProperty({ description: 'Device ID' }) @IsNotEmpty() @IsString() deviceId!: string; @@ -32,49 +34,63 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) => const ValidateGPS = () => ValidateIf(hasGPS); export class UpdateAssetBase { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) isFavorite?: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Asset visibility' }) visibility?: AssetVisibility; + @ApiProperty({ description: 'Original date and time' }) @Optional() @IsDateString() dateTimeOriginal?: string; + @ApiProperty({ description: 'Latitude coordinate' }) @ValidateGPS() @IsLatitude() @IsNotEmpty() latitude?: number; + @ApiProperty({ description: 'Longitude coordinate' }) @ValidateGPS() @IsLongitude() @IsNotEmpty() longitude?: number; - @Optional() + @Property({ + description: 'Rating in range [1-5], or null for unrated', + history: new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), + }) + @Optional({ nullable: true }) @IsInt() @Max(5) @Min(-1) - rating?: number; + @Transform(({ value }) => (value === 0 ? null : value)) + rating?: number | null; + @ApiProperty({ description: 'Asset description' }) @Optional() @IsString() description?: string; } export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Asset IDs to update' }) ids!: string[]; - @Optional() + @ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' }) duplicateId?: string | null; + @ApiProperty({ description: 'Relative time offset in seconds' }) @IsNotSiblingOf(['dateTimeOriginal']) @Optional() @IsInt() dateTimeRelative?: number; + @ApiProperty({ description: 'Time zone (IANA timezone)' }) @IsNotSiblingOf(['dateTimeOriginal']) @IsTimeZone() @Optional() @@ -82,11 +98,12 @@ export class AssetBulkUpdateDto extends UpdateAssetBase { } export class UpdateAssetDto extends UpdateAssetBase { - @ValidateUUID({ optional: true, nullable: true }) + @ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' }) livePhotoVideoId?: string | null; } export class RandomAssetsDto { + @ApiProperty({ description: 'Number of random assets to return' }) @Optional() @IsInt() @IsPositive() @@ -95,12 +112,12 @@ export class RandomAssetsDto { } export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Force delete even if in use' }) force?: boolean; } export class AssetIdsDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Asset IDs' }) assetIds!: string[]; } @@ -112,41 +129,42 @@ export enum AssetJobName { } export class AssetJobsDto extends AssetIdsDto { - @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' }) + @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName', description: 'Job name' }) name!: AssetJobName; } export class AssetStatsDto { - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true }) visibility?: AssetVisibility; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) isFavorite?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by trash status' }) isTrashed?: boolean; } export class AssetStatsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ description: 'Number of images', type: 'integer' }) images!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ description: 'Number of videos', type: 'integer' }) videos!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ description: 'Total number of assets', type: 'integer' }) total!: number; } export class AssetMetadataRouteParams { - @ValidateUUID() + @ValidateUUID({ description: 'Asset ID' }) id!: string; - @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) - key!: AssetMetadataKey; + @ValidateString({ description: 'Metadata key' }) + key!: string; } export class AssetMetadataUpsertDto { + @ApiProperty({ description: 'Metadata items to upsert' }) @IsArray() @ValidateNested({ each: true }) @Type(() => AssetMetadataUpsertItemDto) @@ -154,49 +172,94 @@ export class AssetMetadataUpsertDto { } export class AssetMetadataUpsertItemDto { - @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) - key!: AssetMetadataKey; + @ValidateString({ description: 'Metadata key' }) + key!: string; + @ApiProperty({ description: 'Metadata value (object)' }) @IsObject() value!: object; } -export class AssetMetadataMobileAppDto { - @IsString() - @Optional() - iCloudId?: string; +export class AssetMetadataBulkUpsertDto { + @ApiProperty({ description: 'Metadata items to upsert' }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetMetadataBulkUpsertItemDto) + items!: AssetMetadataBulkUpsertItemDto[]; +} + +export class AssetMetadataBulkUpsertItemDto { + @ValidateUUID({ description: 'Asset ID' }) + assetId!: string; + + @ValidateString({ description: 'Metadata key' }) + key!: string; + + @ApiProperty({ description: 'Metadata value (object)' }) + @IsObject() + value!: object; +} + +export class AssetMetadataBulkDeleteDto { + @ApiProperty({ description: 'Metadata items to delete' }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => AssetMetadataBulkDeleteItemDto) + items!: AssetMetadataBulkDeleteItemDto[]; +} + +export class AssetMetadataBulkDeleteItemDto { + @ValidateUUID({ description: 'Asset ID' }) + assetId!: string; + + @ValidateString({ description: 'Metadata key' }) + key!: string; } export class AssetMetadataResponseDto { - @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) - key!: AssetMetadataKey; + @ValidateString({ description: 'Metadata key' }) + key!: string; + + @ApiProperty({ description: 'Metadata value (object)' }) value!: object; + + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; } +export class AssetMetadataBulkResponseDto extends AssetMetadataResponseDto { + @ApiProperty({ description: 'Asset ID' }) + assetId!: string; +} + export class AssetCopyDto { - @ValidateUUID() + @ValidateUUID({ description: 'Source asset ID' }) sourceId!: string; - @ValidateUUID() + @ValidateUUID({ description: 'Target asset ID' }) targetId!: string; - @ValidateBoolean({ optional: true, default: true }) + @ValidateBoolean({ optional: true, description: 'Copy shared links', default: true }) sharedLinks?: boolean; - @ValidateBoolean({ optional: true, default: true }) + @ValidateBoolean({ optional: true, description: 'Copy album associations', default: true }) albums?: boolean; - @ValidateBoolean({ optional: true, default: true }) + @ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true }) sidecar?: boolean; - @ValidateBoolean({ optional: true, default: true }) + @ValidateBoolean({ optional: true, description: 'Copy stack association', default: true }) stack?: boolean; - @ValidateBoolean({ optional: true, default: true }) + @ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true }) favorite?: boolean; } +export class AssetDownloadOriginalDto { + @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) + edited?: boolean; +} + export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { images: stats[AssetType.Image], diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index d700fc2ab8..3df82f4ef4 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; @@ -12,34 +12,46 @@ export type CookieResponse = { }; export class AuthDto { + @ApiProperty({ description: 'Authenticated user' }) user!: AuthUser; + @ApiPropertyOptional({ description: 'API key (if authenticated via API key)' }) apiKey?: AuthApiKey; + @ApiPropertyOptional({ description: 'Shared link (if authenticated via shared link)' }) sharedLink?: AuthSharedLink; + @ApiPropertyOptional({ description: 'Session (if authenticated via session)' }) session?: AuthSession; } export class LoginCredentialDto { + @ApiProperty({ example: 'testuser@email.com', description: 'User email' }) @IsEmail({ require_tld: false }) @Transform(toEmail) @IsNotEmpty() - @ApiProperty({ example: 'testuser@email.com' }) email!: string; + @ApiProperty({ example: 'password', description: 'User password' }) @IsString() @IsNotEmpty() - @ApiProperty({ example: 'password' }) password!: string; } export class LoginResponseDto { + @ApiProperty({ description: 'Access token' }) accessToken!: string; + @ApiProperty({ description: 'User ID' }) userId!: string; + @ApiProperty({ description: 'User email' }) userEmail!: string; + @ApiProperty({ description: 'User name' }) name!: string; + @ApiProperty({ description: 'Profile image path' }) profileImagePath!: string; + @ApiProperty({ description: 'Is admin user' }) isAdmin!: boolean; + @ApiProperty({ description: 'Should change password' }) shouldChangePassword!: boolean; + @ApiProperty({ description: 'Is onboarded' }) isOnboarded!: boolean; } @@ -61,42 +73,47 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR } export class LogoutResponseDto { + @ApiProperty({ description: 'Logout successful' }) successful!: boolean; + @ApiProperty({ description: 'Redirect URI' }) redirectUri!: string; } export class SignUpDto extends LoginCredentialDto { + @ApiProperty({ example: 'Admin', description: 'User name' }) @IsString() @IsNotEmpty() - @ApiProperty({ example: 'Admin' }) name!: string; } export class ChangePasswordDto { + @ApiProperty({ example: 'password', description: 'Current password' }) @IsString() @IsNotEmpty() - @ApiProperty({ example: 'password' }) password!: string; + @ApiProperty({ example: 'password', description: 'New password (min 8 characters)' }) @IsString() @IsNotEmpty() @MinLength(8) - @ApiProperty({ example: 'password' }) newPassword!: string; - @ValidateBoolean({ optional: true, default: false }) + @ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' }) invalidateSessions?: boolean; } export class PinCodeSetupDto { + @ApiProperty({ description: 'PIN code (4-6 digits)' }) @PinCode() pinCode!: string; } export class PinCodeResetDto { + @ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' }) @PinCode({ optional: true }) pinCode?: string; + @ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' }) @Optional() @IsString() @IsNotEmpty() @@ -106,51 +123,64 @@ export class PinCodeResetDto { export class SessionUnlockDto extends PinCodeResetDto {} export class PinCodeChangeDto extends PinCodeResetDto { + @ApiProperty({ description: 'New PIN code (4-6 digits)' }) @PinCode() newPinCode!: string; } export class ValidateAccessTokenResponseDto { + @ApiProperty({ description: 'Authentication status' }) authStatus!: boolean; } export class OAuthCallbackDto { + @ApiProperty({ description: 'OAuth callback URL' }) @IsNotEmpty() @IsString() - @ApiProperty() url!: string; + @ApiPropertyOptional({ description: 'OAuth state parameter' }) @Optional() @IsString() state?: string; + @ApiPropertyOptional({ description: 'OAuth code verifier (PKCE)' }) @Optional() @IsString() codeVerifier?: string; } export class OAuthConfigDto { + @ApiProperty({ description: 'OAuth redirect URI' }) @IsNotEmpty() @IsString() redirectUri!: string; + @ApiPropertyOptional({ description: 'OAuth state parameter' }) @Optional() @IsString() state?: string; + @ApiPropertyOptional({ description: 'OAuth code challenge (PKCE)' }) @Optional() @IsString() codeChallenge?: string; } export class OAuthAuthorizeResponseDto { + @ApiProperty({ description: 'OAuth authorization URL' }) url!: string; } export class AuthStatusResponseDto { + @ApiProperty({ description: 'Has PIN code set' }) pinCode!: boolean; + @ApiProperty({ description: 'Has password set' }) password!: boolean; + @ApiProperty({ description: 'Is elevated session' }) isElevated!: boolean; + @ApiPropertyOptional({ description: 'Session expiration date' }) expiresAt?: string; + @ApiPropertyOptional({ description: 'PIN expiration date' }) pinExpiresAt?: string; } diff --git a/server/src/dtos/bbox.dto.ts b/server/src/dtos/bbox.dto.ts new file mode 100644 index 0000000000..1afe9f53ba --- /dev/null +++ b/server/src/dtos/bbox.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsLatitude, IsLongitude } from 'class-validator'; +import { IsGreaterThanOrEqualTo } from 'src/validation'; + +export class BBoxDto { + @ApiProperty({ format: 'double', description: 'West longitude (-180 to 180)' }) + @IsLongitude() + west!: number; + + @ApiProperty({ format: 'double', description: 'South latitude (-90 to 90)' }) + @IsLatitude() + south!: number; + + @ApiProperty({ + format: 'double', + description: 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.', + }) + @IsLongitude() + east!: number; + + @ApiProperty({ format: 'double', description: 'North latitude (-90 to 90). Must be >= south.' }) + @IsLatitude() + @IsGreaterThanOrEqualTo('south') + north!: number; +} diff --git a/server/src/dtos/database-backup.dto.ts b/server/src/dtos/database-backup.dto.ts new file mode 100644 index 0000000000..dc06cdc6ec --- /dev/null +++ b/server/src/dtos/database-backup.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class DatabaseBackupDto { + filename!: string; + filesize!: number; +} + +export class DatabaseBackupListResponseDto { + backups!: DatabaseBackupDto[]; +} + +export class DatabaseBackupUploadDto { + @ApiProperty({ type: 'string', format: 'binary', required: false }) + file?: any; +} + +export class DatabaseBackupDeleteDto { + @IsString({ each: true }) + backups!: string[]; +} diff --git a/server/src/dtos/download.dto.ts b/server/src/dtos/download.dto.ts index e6588a9944..ef52a72bd0 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -1,32 +1,40 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsInt, IsPositive } from 'class-validator'; -import { Optional, ValidateUUID } from 'src/validation'; +import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class DownloadInfoDto { - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, optional: true, description: 'Asset IDs to download' }) assetIds?: string[]; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Album ID to download' }) albumId?: string; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'User ID to download assets from' }) userId?: string; + @ApiPropertyOptional({ type: 'integer', description: 'Archive size limit in bytes' }) @IsInt() @IsPositive() @Optional() - @ApiProperty({ type: 'integer' }) archiveSize?: number; } export class DownloadResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total size in bytes' }) totalSize!: number; + @ApiProperty({ description: 'Archive information' }) archives!: DownloadArchiveInfo[]; } export class DownloadArchiveInfo { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Archive size in bytes' }) size!: number; + @ApiProperty({ description: 'Asset IDs in this archive' }) assetIds!: string[]; } + +export class DownloadArchiveDto extends AssetIdsDto { + @ValidateBoolean({ optional: true, description: 'Download edited asset if available' }) + edited?: boolean; +} diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 166f18ce8f..9cd9147ec5 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,6 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; export class DuplicateResponseDto { + @ApiProperty({ description: 'Duplicate group ID' }) duplicateId!: string; + @ApiProperty({ description: 'Duplicate assets' }) assets!: AssetResponseDto[]; } diff --git a/server/src/dtos/editing.dto.ts b/server/src/dtos/editing.dto.ts new file mode 100644 index 0000000000..8217fec41c --- /dev/null +++ b/server/src/dtos/editing.dto.ts @@ -0,0 +1,111 @@ +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; +import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; + +export enum AssetEditAction { + Crop = 'crop', + Rotate = 'rotate', + Mirror = 'mirror', +} + +export enum MirrorAxis { + Horizontal = 'horizontal', + Vertical = 'vertical', +} + +export class CropParameters { + @IsInt() + @Min(0) + @ApiProperty({ description: 'Top-Left X coordinate of crop' }) + x!: number; + + @IsInt() + @Min(0) + @ApiProperty({ description: 'Top-Left Y coordinate of crop' }) + y!: number; + + @IsInt() + @Min(1) + @ApiProperty({ description: 'Width of the crop' }) + width!: number; + + @IsInt() + @Min(1) + @ApiProperty({ description: 'Height of the crop' }) + height!: number; +} + +export class RotateParameters { + @IsAxisAlignedRotation() + @ApiProperty({ description: 'Rotation angle in degrees' }) + angle!: number; +} + +export class MirrorParameters { + @IsEnum(MirrorAxis) + @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) + axis!: MirrorAxis; +} + +export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; +export type AssetEditActionItem = + | { + action: AssetEditAction.Crop; + parameters: CropParameters; + } + | { + action: AssetEditAction.Rotate; + parameters: RotateParameters; + } + | { + action: AssetEditAction.Mirror; + parameters: MirrorParameters; + }; + +@ApiExtraModels(CropParameters, RotateParameters, MirrorParameters) +export class AssetEditActionItemDto { + @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) + action!: AssetEditAction; + + @ApiProperty({ + description: 'List of edit actions to apply (crop, rotate, or mirror)', + anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ + $ref: getSchemaPath(type), + })), + }) + @ValidateNested() + @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) + parameters!: AssetEditActionItem['parameters']; +} + +export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { + @ValidateUUID() + id!: string; +} + +export type AssetEditActionParameter = typeof actionParameterMap; +const actionParameterMap = { + [AssetEditAction.Crop]: CropParameters, + [AssetEditAction.Rotate]: RotateParameters, + [AssetEditAction.Mirror]: MirrorParameters, +}; + +export class AssetEditsCreateDto { + @ArrayMinSize(1) + @IsUniqueEditActions() + @ValidateNested({ each: true }) + @Type(() => AssetEditActionItemDto) + @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) + edits!: AssetEditActionItemDto[]; +} + +export class AssetEditsResponseDto { + @ValidateUUID({ description: 'Asset ID these edits belong to' }) + assetId!: string; + + @ApiProperty({ + description: 'List of edit actions applied to the asset', + }) + edits!: AssetEditActionItemResponseDto[]; +} diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index 2a9dd8b662..b04366c273 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,8 +1,17 @@ import { Transform, Type } from 'class-transformer'; import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { DatabaseSslMode, ImmichEnvironment, LogLevel } from 'src/enum'; +import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; +// TODO import from sql-tools once the swagger plugin supports external enums +enum DatabaseSslMode { + Disable = 'disable', + Allow = 'allow', + Prefer = 'prefer', + Require = 'require', + VerifyFull = 'verify-full', +} + export class EnvDto { @IsInt() @Optional() @@ -48,6 +57,10 @@ export class EnvDto { @Optional() IMMICH_LOG_LEVEL?: LogLevel; + @IsEnum(LogFormat) + @Optional() + IMMICH_LOG_FORMAT?: LogFormat; + @Optional() @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) IMMICH_MEDIA_LOCATION?: string; @@ -58,7 +71,7 @@ export class EnvDto { IMMICH_MICROSERVICES_METRICS_PORT?: number; @ValidateBoolean({ optional: true }) - IMMICH_PLUGINS_ENABLED?: boolean; + IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean; @Optional() @Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' }) @@ -113,6 +126,9 @@ export class EnvDto { @Optional() IMMICH_THIRD_PARTY_SUPPORT_URL?: string; + @ValidateBoolean({ optional: true }) + IMMICH_ALLOW_SETUP?: boolean; + @IsIPRange({ requireCIDR: false }, { each: true }) @Transform(({ value }) => value && typeof value === 'string' diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 9fa61d93c8..0052b95b6e 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,30 +1,51 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Exif } from 'src/database'; export class ExifResponseDto { + @ApiPropertyOptional({ description: 'Camera make' }) make?: string | null = null; + @ApiPropertyOptional({ description: 'Camera model' }) model?: string | null = null; + @ApiPropertyOptional({ type: 'number', description: 'Image width in pixels' }) exifImageWidth?: number | null = null; + @ApiPropertyOptional({ type: 'number', description: 'Image height in pixels' }) exifImageHeight?: number | null = null; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'File size in bytes' }) fileSizeInByte?: number | null = null; + @ApiPropertyOptional({ description: 'Image orientation' }) orientation?: string | null = null; + @ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' }) dateTimeOriginal?: Date | null = null; + @ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' }) modifyDate?: Date | null = null; + @ApiPropertyOptional({ description: 'Time zone' }) timeZone?: string | null = null; + @ApiPropertyOptional({ description: 'Lens model' }) lensModel?: string | null = null; + @ApiPropertyOptional({ type: 'number', description: 'F-number (aperture)' }) fNumber?: number | null = null; + @ApiPropertyOptional({ type: 'number', description: 'Focal length in mm' }) focalLength?: number | null = null; + @ApiPropertyOptional({ type: 'number', description: 'ISO sensitivity' }) iso?: number | null = null; + @ApiPropertyOptional({ description: 'Exposure time' }) exposureTime?: string | null = null; + @ApiPropertyOptional({ type: 'number', description: 'GPS latitude' }) latitude?: number | null = null; + @ApiPropertyOptional({ type: 'number', description: 'GPS longitude' }) longitude?: number | null = null; + @ApiPropertyOptional({ description: 'City name' }) city?: string | null = null; + @ApiPropertyOptional({ description: 'State/province name' }) state?: string | null = null; + @ApiPropertyOptional({ description: 'Country name' }) country?: string | null = null; + @ApiPropertyOptional({ description: 'Image description' }) description?: string | null = null; + @ApiPropertyOptional({ description: 'Projection type' }) projectionType?: string | null = null; + @ApiPropertyOptional({ type: 'number', description: 'Rating' }) rating?: number | null = null; } diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index 794af6e5e0..ef34a41720 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -2,6 +2,6 @@ import { ManualJobName } from 'src/enum'; import { ValidateEnum } from 'src/validation'; export class JobCreateDto { - @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' }) + @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName', description: 'Job name' }) name!: ManualJobName; } diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index a0aaace13d..3f71b8a0ed 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,17 +1,19 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; import { Library } from 'src/database'; import { Optional, ValidateUUID } from 'src/validation'; export class CreateLibraryDto { - @ValidateUUID() + @ValidateUUID({ description: 'Owner user ID' }) ownerId!: string; + @ApiPropertyOptional({ description: 'Library name' }) @IsString() @Optional() @IsNotEmpty() name?: string; + @ApiPropertyOptional({ description: 'Import paths (max 128)' }) @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @@ -19,6 +21,7 @@ export class CreateLibraryDto { @ArrayMaxSize(128) importPaths?: string[]; + @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @@ -28,11 +31,13 @@ export class CreateLibraryDto { } export class UpdateLibraryDto { + @ApiPropertyOptional({ description: 'Library name' }) @Optional() @IsString() @IsNotEmpty() name?: string; + @ApiPropertyOptional({ description: 'Import paths (max 128)' }) @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @@ -40,6 +45,7 @@ export class UpdateLibraryDto { @ArrayMaxSize(128) importPaths?: string[]; + @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) @Optional() @IsNotEmpty({ each: true }) @IsString({ each: true }) @@ -59,6 +65,7 @@ export interface WalkOptionsDto extends CrawlOptionsDto { } export class ValidateLibraryDto { + @ApiPropertyOptional({ description: 'Import paths to validate (max 128)' }) @Optional() @IsString({ each: true }) @IsNotEmpty({ each: true }) @@ -66,6 +73,7 @@ export class ValidateLibraryDto { @ArrayMaxSize(128) importPaths?: string[]; + @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) @Optional() @IsNotEmpty({ each: true }) @IsString({ each: true }) @@ -75,48 +83,60 @@ export class ValidateLibraryDto { } export class ValidateLibraryResponseDto { + @ApiPropertyOptional({ description: 'Validation results for import paths' }) importPaths?: ValidateLibraryImportPathResponseDto[]; } export class ValidateLibraryImportPathResponseDto { + @ApiProperty({ description: 'Import path' }) importPath!: string; + @ApiProperty({ description: 'Is valid' }) isValid: boolean = false; + @ApiPropertyOptional({ description: 'Validation message' }) message?: string; } export class LibrarySearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by user ID' }) userId?: string; } export class LibraryResponseDto { + @ApiProperty({ description: 'Library ID' }) id!: string; + @ApiProperty({ description: 'Owner user ID' }) ownerId!: string; + @ApiProperty({ description: 'Library name' }) name!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets' }) assetCount!: number; + @ApiProperty({ description: 'Import paths' }) importPaths!: string[]; + @ApiProperty({ description: 'Exclusion patterns' }) exclusionPatterns!: string[]; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; + @ApiProperty({ description: 'Last refresh date' }) refreshedAt!: Date | null; } export class LibraryStatsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of photos' }) photos = 0; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of videos' }) videos = 0; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of assets' }) total = 0; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) usage = 0; } diff --git a/server/src/dtos/license.dto.ts b/server/src/dtos/license.dto.ts index 6020d06b6f..14232940b6 100644 --- a/server/src/dtos/license.dto.ts +++ b/server/src/dtos/license.dto.ts @@ -1,16 +1,20 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, Matches } from 'class-validator'; export class LicenseKeyDto { + @ApiProperty({ description: 'License key (format: IM(SV|CL)(-XXXX){8})' }) @IsString() @IsNotEmpty() @Matches(/IM(SV|CL)(-[\dA-Za-z]{4}){8}/) licenseKey!: string; + @ApiProperty({ description: 'Activation key' }) @IsString() @IsNotEmpty() activationKey!: string; } export class LicenseResponseDto extends LicenseKeyDto { + @ApiProperty({ description: 'Activation date' }) activatedAt!: Date; } diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index fe6960c0a4..f31d9ffa23 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,16 +1,49 @@ -import { MaintenanceAction } from 'src/enum'; -import { ValidateEnum, ValidateString } from 'src/validation'; +import { ApiProperty } from '@nestjs/swagger'; +import { ValidateIf } from 'class-validator'; +import { MaintenanceAction, StorageFolder } from 'src/enum'; +import { ValidateBoolean, ValidateEnum, ValidateString } from 'src/validation'; export class SetMaintenanceModeDto { - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' }) + @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) action!: MaintenanceAction; + + @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) + @ValidateString({ description: 'Restore backup filename' }) + restoreBackupFilename?: string; } export class MaintenanceLoginDto { - @ValidateString({ optional: true }) + @ValidateString({ optional: true, description: 'Maintenance token' }) token?: string; } export class MaintenanceAuthDto { + @ApiProperty({ description: 'Maintenance username' }) username!: string; } + +export class MaintenanceStatusResponseDto { + active!: boolean; + + @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) + action!: MaintenanceAction; + + progress?: number; + task?: string; + error?: string; +} + +export class MaintenanceDetectInstallStorageFolderDto { + @ValidateEnum({ enum: StorageFolder, name: 'StorageFolder', description: 'Storage folder' }) + folder!: StorageFolder; + @ValidateBoolean({ description: 'Whether the folder is readable' }) + readable!: boolean; + @ValidateBoolean({ description: 'Whether the folder is writable' }) + writable!: boolean; + @ApiProperty({ description: 'Number of files in the folder' }) + files!: number; +} + +export class MaintenanceDetectInstallResponseDto { + storage!: MaintenanceDetectInstallStorageFolderDto[]; +} diff --git a/server/src/dtos/map.dto.ts b/server/src/dtos/map.dto.ts index 1d0b84a4d0..d8db175c28 100644 --- a/server/src/dtos/map.dto.ts +++ b/server/src/dtos/map.dto.ts @@ -4,64 +4,64 @@ import { IsLatitude, IsLongitude } from 'class-validator'; import { ValidateBoolean, ValidateDate } from 'src/validation'; export class MapReverseGeocodeDto { - @ApiProperty({ format: 'double' }) + @ApiProperty({ format: 'double', description: 'Latitude (-90 to 90)' }) @Type(() => Number) @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) lat!: number; - @ApiProperty({ format: 'double' }) + @ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' }) @Type(() => Number) @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) lon!: number; } export class MapReverseGeocodeResponseDto { - @ApiProperty() + @ApiProperty({ description: 'City name' }) city!: string | null; - @ApiProperty() + @ApiProperty({ description: 'State/Province name' }) state!: string | null; - @ApiProperty() + @ApiProperty({ description: 'Country name' }) country!: string | null; } export class MapMarkerDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by archived status' }) isArchived?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) isFavorite?: boolean; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter assets created after this date' }) fileCreatedAfter?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter assets created before this date' }) fileCreatedBefore?: Date; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include partner assets' }) withPartners?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include shared album assets' }) withSharedAlbums?: boolean; } export class MapMarkerResponseDto { - @ApiProperty() + @ApiProperty({ description: 'Asset ID' }) id!: string; - @ApiProperty({ format: 'double' }) + @ApiProperty({ format: 'double', description: 'Latitude' }) lat!: number; - @ApiProperty({ format: 'double' }) + @ApiProperty({ format: 'double', description: 'Longitude' }) lon!: number; - @ApiProperty() + @ApiProperty({ description: 'City name' }) city!: string | null; - @ApiProperty() + @ApiProperty({ description: 'State/Province name' }) state!: string | null; - @ApiProperty() + @ApiProperty({ description: 'Country name' }) country!: string | null; } diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index 8e7320f831..edf65ef583 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,30 +2,31 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { Memory } from 'src/database'; +import { HistoryBuilder } from 'src/decorators'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetOrderWithRandom, MemoryType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; class MemoryBaseDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Is memory saved' }) isSaved?: boolean; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Date when memory was seen' }) seenAt?: Date; } export class MemorySearchDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type', optional: true }) type?: MemoryType; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by date' }) for?: Date; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include trashed memories' }) isTrashed?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by saved status' }) isSaved?: boolean; @IsInt() @@ -35,11 +36,12 @@ export class MemorySearchDto { @ApiProperty({ type: 'integer', description: 'Number of memories to return' }) size?: number; - @ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', optional: true }) + @ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', description: 'Sort order', optional: true }) order?: AssetOrderWithRandom; } class OnThisDayDto { + @ApiProperty({ type: 'number', description: 'Year for on this day memory', minimum: 1 }) @IsInt() @IsPositive() year!: number; @@ -48,14 +50,16 @@ class OnThisDayDto { type MemoryData = OnThisDayDto; export class MemoryUpdateDto extends MemoryBaseDto { - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Memory date' }) memoryAt?: Date; } export class MemoryCreateDto extends MemoryBaseDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) type!: MemoryType; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @IsObject() @ValidateNested() @Type((options) => { @@ -71,32 +75,60 @@ export class MemoryCreateDto extends MemoryBaseDto { }) data!: MemoryData; - @ValidateDate() + @ValidateDate({ description: 'Memory date' }) memoryAt!: Date; - @ValidateUUID({ optional: true, each: true }) + @ValidateDate({ + optional: true, + description: 'Date when memory should be shown', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + showAt?: Date; + + @ValidateDate({ + optional: true, + description: 'Date when memory should be hidden', + history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + }) + hideAt?: Date; + + @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) assetIds?: string[]; } export class MemoryStatisticsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of memories' }) total!: number; } export class MemoryResponseDto { + @ApiProperty({ description: 'Memory ID' }) id!: string; + @ValidateDate({ description: 'Creation date' }) createdAt!: Date; + @ValidateDate({ description: 'Last update date' }) updatedAt!: Date; + @ValidateDate({ optional: true, description: 'Deletion date' }) deletedAt?: Date; + @ValidateDate({ description: 'Memory date' }) memoryAt!: Date; + @ValidateDate({ optional: true, description: 'Date when memory was seen' }) seenAt?: Date; + @ValidateDate({ optional: true, description: 'Date when memory should be shown' }) showAt?: Date; + @ValidateDate({ optional: true, description: 'Date when memory should be hidden' }) hideAt?: Date; + @ApiProperty({ description: 'Owner user ID' }) ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) type!: MemoryType; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) data!: MemoryData; + @ApiProperty({ description: 'Is memory saved' }) isSaved!: boolean; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) assets!: AssetResponseDto[]; } diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index 527317346a..a75808f95a 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -4,11 +4,12 @@ import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; import { ValidateBoolean } from 'src/validation'; export class TaskConfig { - @ValidateBoolean() + @ValidateBoolean({ description: 'Whether the task is enabled' }) enabled!: boolean; } export class ModelConfig extends TaskConfig { + @ApiProperty({ description: 'Name of the model to use' }) @IsString() @IsNotEmpty() modelName!: string; @@ -21,7 +22,11 @@ export class DuplicateDetectionConfig extends TaskConfig { @Min(0.001) @Max(0.1) @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ + type: 'number', + format: 'double', + description: 'Maximum distance threshold for duplicate detection', + }) maxDistance!: number; } @@ -30,20 +35,24 @@ export class FacialRecognitionConfig extends ModelConfig { @Min(0.1) @Max(1) @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for face detection' }) minScore!: number; @IsNumber() @Min(0.1) @Max(2) @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ + type: 'number', + format: 'double', + description: 'Maximum distance threshold for face recognition', + }) maxDistance!: number; @IsNumber() @Min(1) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' }) minFaces!: number; } @@ -51,20 +60,24 @@ export class OcrConfig extends ModelConfig { @IsNumber() @Min(1) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' }) maxResolution!: number; @IsNumber() @Min(0.1) @Max(1) @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' }) minDetectionScore!: number; @IsNumber() @Min(0.1) @Max(1) @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ + type: 'number', + format: 'double', + description: 'Minimum confidence score for text recognition', + }) minRecognitionScore!: number; } diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index e83ba7315f..87a15f29e3 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,86 +1,114 @@ -import { IsString } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ArrayMinSize, IsString } from 'class-validator'; import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; export class TestEmailResponseDto { + @ApiProperty({ description: 'Email message ID' }) messageId!: string; } export class TemplateResponseDto { + @ApiProperty({ description: 'Template name' }) name!: string; + @ApiProperty({ description: 'Template HTML content' }) html!: string; } + export class TemplateDto { + @ApiProperty({ description: 'Template name' }) @IsString() template!: string; } export class NotificationDto { + @ApiProperty({ description: 'Notification ID' }) id!: string; - @ValidateDate() + @ValidateDate({ description: 'Creation date' }) createdAt!: Date; - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' }) + @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', description: 'Notification level' }) level!: NotificationLevel; - @ValidateEnum({ enum: NotificationType, name: 'NotificationType' }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', description: 'Notification type' }) type!: NotificationType; + @ApiProperty({ description: 'Notification title' }) title!: string; + @ApiPropertyOptional({ description: 'Notification description' }) description?: string; + @ApiPropertyOptional({ description: 'Additional notification data' }) data?: any; + @ApiPropertyOptional({ description: 'Date when notification was read', format: 'date-time' }) readAt?: Date; } export class NotificationSearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by notification ID' }) id?: string; - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) + @ValidateEnum({ + enum: NotificationLevel, + name: 'NotificationLevel', + optional: true, + description: 'Filter by notification level', + }) level?: NotificationLevel; - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) + @ValidateEnum({ + enum: NotificationType, + name: 'NotificationType', + optional: true, + description: 'Filter by notification type', + }) type?: NotificationType; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by unread status' }) unread?: boolean; } export class NotificationCreateDto { - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true }) + @ValidateEnum({ + enum: NotificationLevel, + name: 'NotificationLevel', + optional: true, + description: 'Notification level', + }) level?: NotificationLevel; - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true }) + @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' }) type?: NotificationType; - @IsString() + @ValidateString({ description: 'Notification title' }) title!: string; - @IsString() - @Optional({ nullable: true }) + @ValidateString({ optional: true, nullable: true, description: 'Notification description' }) description?: string | null; + @ApiPropertyOptional({ description: 'Additional notification data' }) @Optional({ nullable: true }) data?: any; - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) readAt?: Date | null; - @ValidateUUID() + @ValidateUUID({ description: 'User ID to send notification to' }) userId!: string; } export class NotificationUpdateDto { - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) readAt?: Date | null; } export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, description: 'Notification IDs to update' }) + @ArrayMinSize(1) ids!: string[]; - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' }) readAt?: Date | null; } export class NotificationDeleteAllDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) + @ArrayMinSize(1) ids!: string[]; } diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index 47a3992784..d2781c6b90 100644 --- a/server/src/dtos/onboarding.dto.ts +++ b/server/src/dtos/onboarding.dto.ts @@ -1,7 +1,7 @@ import { ValidateBoolean } from 'src/validation'; export class OnboardingDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Is user onboarded' }) isOnboarded!: boolean; } diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 599213f662..5b949326a4 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,23 +1,26 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsNotEmpty } from 'class-validator'; import { UserResponseDto } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; import { ValidateEnum, ValidateUUID } from 'src/validation'; export class PartnerCreateDto { - @ValidateUUID() + @ValidateUUID({ description: 'User ID to share with' }) sharedWithId!: string; } export class PartnerUpdateDto { + @ApiProperty({ description: 'Show partner assets in timeline' }) @IsNotEmpty() inTimeline!: boolean; } export class PartnerSearchDto { - @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' }) + @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection', description: 'Partner direction' }) direction!: PartnerDirection; } export class PartnerResponseDto extends UserResponseDto { + @ApiPropertyOptional({ description: 'Show in timeline' }) inTimeline?: boolean; } diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 3c90cfdc59..983062afcf 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -6,9 +6,12 @@ import { DateTime } from 'luxon'; import { AssetFace, Person } from 'src/database'; import { HistoryBuilder, Property } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { SourceType } from 'src/enum'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { ImageDimensions } from 'src/types'; import { asDateString } from 'src/utils/date'; +import { transformFaceBoundingBox } from 'src/utils/transform'; import { IsDateStringFormat, MaxDateString, @@ -20,46 +23,37 @@ import { } from 'src/validation'; export class PersonCreateDto { - /** - * Person name. - */ + @ApiPropertyOptional({ description: 'Person name' }) @Optional() @IsString() name?: string; - /** - * Person date of birth. - * Note: the mobile app cannot currently set the birth date to null. - */ - @ApiProperty({ format: 'date' }) + // Note: the mobile app cannot currently set the birth date to null. + @ApiProperty({ format: 'date', description: 'Person date of birth', required: false }) @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) @IsDateStringFormat('yyyy-MM-dd') @Optional({ nullable: true, emptyToNull: true }) birthDate?: Date | null; - /** - * Person visibility - */ - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' }) isHidden?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) isFavorite?: boolean; + @ApiPropertyOptional({ description: 'Person color (hex)' }) @Optional({ emptyToNull: true, nullable: true }) @ValidateHexColor() color?: string | null; } export class PersonUpdateDto extends PersonCreateDto { - /** - * Asset is used to get the feature face thumbnail. - */ - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Asset ID used for feature face thumbnail' }) featureFaceAssetId?: string; } export class PeopleUpdateDto { + @ApiProperty({ description: 'People to update' }) @IsArray() @ValidateNested({ each: true }) @Type(() => PeopleUpdateItem) @@ -67,36 +61,32 @@ export class PeopleUpdateDto { } export class PeopleUpdateItem extends PersonUpdateDto { - /** - * Person id. - */ + @ApiProperty({ description: 'Person ID' }) @IsString() @IsNotEmpty() id!: string; } export class MergePersonDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Person IDs to merge' }) ids!: string[]; } export class PersonSearchDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include hidden people' }) withHidden?: boolean; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Closest person ID for similarity search' }) closestPersonId?: string; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Closest asset ID for similarity search' }) closestAssetId?: string; - /** Page number for pagination */ - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Page number for pagination', default: 1 }) @IsInt() @Min(1) @Type(() => Number) page: number = 1; - /** Number of items per page */ - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Number of items per page', default: 500 }) @IsInt() @Min(1) @Max(1000) @@ -105,48 +95,55 @@ export class PersonSearchDto { } export class PersonResponseDto { + @ApiProperty({ description: 'Person ID' }) id!: string; + @ApiProperty({ description: 'Person name' }) name!: string; - @ApiProperty({ format: 'date' }) + @ApiProperty({ format: 'date', description: 'Person date of birth' }) birthDate!: string | null; + @ApiProperty({ description: 'Thumbnail path' }) thumbnailPath!: string; + @ApiProperty({ description: 'Is hidden' }) isHidden!: boolean; - @Property({ history: new HistoryBuilder().added('v1.107.0').stable('v2') }) + @Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') }) updatedAt?: Date; - @Property({ history: new HistoryBuilder().added('v1.126.0').stable('v2') }) + @Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) isFavorite?: boolean; - @Property({ history: new HistoryBuilder().added('v1.126.0').stable('v2') }) + @Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) color?: string; } export class PersonWithFacesResponseDto extends PersonResponseDto { + @ApiProperty({ description: 'Face detections' }) faces!: AssetFaceWithoutPersonResponseDto[]; } export class AssetFaceWithoutPersonResponseDto { - @ValidateUUID() + @ValidateUUID({ description: 'Face ID' }) id!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) imageHeight!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) imageWidth!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' }) boundingBoxX1!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' }) boundingBoxX2!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' }) boundingBoxY1!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' }) boundingBoxY2!: number; - @ValidateEnum({ enum: SourceType, name: 'SourceType' }) + @ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' }) sourceType?: SourceType; } export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { + @ApiProperty({ description: 'Person associated with face' }) person!: PersonResponseDto | null; } export class AssetFaceUpdateDto { + @ApiProperty({ description: 'Face update items' }) @IsArray() @ValidateNested({ each: true }) @Type(() => AssetFaceUpdateItem) @@ -154,69 +151,74 @@ export class AssetFaceUpdateDto { } export class FaceDto { - @ValidateUUID() + @ValidateUUID({ description: 'Face ID' }) id!: string; } export class AssetFaceUpdateItem { - @ValidateUUID() + @ValidateUUID({ description: 'Person ID' }) personId!: string; - @ValidateUUID() + @ValidateUUID({ description: 'Asset ID' }) assetId!: string; } export class AssetFaceCreateDto extends AssetFaceUpdateItem { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) @IsNotEmpty() @IsNumber() imageWidth!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) @IsNotEmpty() @IsNumber() imageHeight!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' }) @IsNotEmpty() @IsNumber() x!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' }) @IsNotEmpty() @IsNumber() y!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Face bounding box width' }) @IsNotEmpty() @IsNumber() width!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Face bounding box height' }) @IsNotEmpty() @IsNumber() height!: number; } export class AssetFaceDeleteDto { + @ApiProperty({ description: 'Force delete even if person has other faces' }) @IsNotEmpty() force!: boolean; } export class PersonStatisticsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets' }) assets!: number; } export class PeopleResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of people' }) total!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of hidden people' }) hidden!: number; + @ApiProperty({ description: 'List of people' }) people!: PersonResponseDto[]; // TODO: make required after a few versions - @Property({ history: new HistoryBuilder().added('v1.110.0').stable('v2') }) + @Property({ + description: 'Whether there are more pages', + history: new HistoryBuilder().added('v1.110.0').stable('v2'), + }) hasNextPage?: boolean; } @@ -233,29 +235,37 @@ export function mapPerson(person: Person): PersonResponseDto { }; } -export function mapFacesWithoutPerson(face: Selectable): AssetFaceWithoutPersonResponseDto { +export function mapFacesWithoutPerson( + face: Selectable, + edits?: AssetEditActionItem[], + assetDimensions?: ImageDimensions, +): AssetFaceWithoutPersonResponseDto { return { id: face.id, - imageHeight: face.imageHeight, - imageWidth: face.imageWidth, - boundingBoxX1: face.boundingBoxX1, - boundingBoxX2: face.boundingBoxX2, - boundingBoxY1: face.boundingBoxY1, - boundingBoxY2: face.boundingBoxY2, + ...transformFaceBoundingBox( + { + boundingBoxX1: face.boundingBoxX1, + boundingBoxY1: face.boundingBoxY1, + boundingBoxX2: face.boundingBoxX2, + boundingBoxY2: face.boundingBoxY2, + imageWidth: face.imageWidth, + imageHeight: face.imageHeight, + }, + edits ?? [], + assetDimensions ?? { width: face.imageWidth, height: face.imageHeight }, + ), sourceType: face.sourceType, }; } -export function mapFaces(face: AssetFace, auth: AuthDto): AssetFaceResponseDto { +export function mapFaces( + face: AssetFace, + auth: AuthDto, + edits?: AssetEditActionItem[], + assetDimensions?: ImageDimensions, +): AssetFaceResponseDto { return { - id: face.id, - imageHeight: face.imageHeight, - imageWidth: face.imageWidth, - boundingBoxX1: face.boundingBoxX1, - boundingBoxX2: face.boundingBoxX2, - boundingBoxY1: face.boundingBoxY1, - boundingBoxY2: face.boundingBoxY2, - sourceType: face.sourceType, + ...mapFacesWithoutPerson(face, edits, assetDimensions), person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null, }; } diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index fcb3ad4a22..d5d1c52997 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { ArrayMinSize, @@ -16,58 +17,68 @@ import { JSONSchema } from 'src/types/plugin-schema.types'; import { ValidateEnum } from 'src/validation'; class PluginManifestWasmDto { + @ApiProperty({ description: 'WASM file path' }) @IsString() @IsNotEmpty() path!: string; } class PluginManifestFilterDto { + @ApiProperty({ description: 'Filter method name' }) @IsString() @IsNotEmpty() methodName!: string; + @ApiProperty({ description: 'Filter title' }) @IsString() @IsNotEmpty() title!: string; + @ApiProperty({ description: 'Filter description' }) @IsString() @IsNotEmpty() description!: string; + @ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true }) @IsArray() @ArrayMinSize(1) @IsEnum(PluginContext, { each: true }) supportedContexts!: PluginContext[]; + @ApiPropertyOptional({ description: 'Filter schema' }) @IsObject() @IsOptional() schema?: JSONSchema; } class PluginManifestActionDto { + @ApiProperty({ description: 'Action method name' }) @IsString() @IsNotEmpty() methodName!: string; + @ApiProperty({ description: 'Action title' }) @IsString() @IsNotEmpty() title!: string; + @ApiProperty({ description: 'Action description' }) @IsString() @IsNotEmpty() description!: string; - @IsArray() @ArrayMinSize(1) - @ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true }) + @ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true, description: 'Supported contexts' }) supportedContexts!: PluginContext[]; + @ApiPropertyOptional({ description: 'Action schema' }) @IsObject() @IsOptional() schema?: JSONSchema; } export class PluginManifestDto { + @ApiProperty({ description: 'Plugin name (lowercase, numbers, hyphens only)' }) @IsString() @IsNotEmpty() @Matches(/^[a-z0-9-]+[a-z0-9]$/, { @@ -75,33 +86,40 @@ export class PluginManifestDto { }) name!: string; + @ApiProperty({ description: 'Plugin version (semver)' }) @IsString() @IsNotEmpty() @IsSemVer() version!: string; + @ApiProperty({ description: 'Plugin title' }) @IsString() @IsNotEmpty() title!: string; + @ApiProperty({ description: 'Plugin description' }) @IsString() @IsNotEmpty() description!: string; + @ApiProperty({ description: 'Plugin author' }) @IsString() @IsNotEmpty() author!: string; + @ApiProperty({ description: 'WASM configuration' }) @ValidateNested() @Type(() => PluginManifestWasmDto) wasm!: PluginManifestWasmDto; + @ApiPropertyOptional({ description: 'Plugin filters' }) @IsArray() @ValidateNested({ each: true }) @Type(() => PluginManifestFilterDto) @IsOptional() filters?: PluginManifestFilterDto[]; + @ApiPropertyOptional({ description: 'Plugin actions' }) @IsArray() @ValidateNested({ each: true }) @Type(() => PluginManifestActionDto) diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index ce80eccd65..de1f1b28d4 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,47 +1,78 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext } from 'src/enum'; +import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; import type { JSONSchema } from 'src/types/plugin-schema.types'; import { ValidateEnum } from 'src/validation'; +export class PluginTriggerResponseDto { + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' }) + type!: PluginTriggerType; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' }) + contextType!: PluginContextType; +} + export class PluginResponseDto { + @ApiProperty({ description: 'Plugin ID' }) id!: string; + @ApiProperty({ description: 'Plugin name' }) name!: string; + @ApiProperty({ description: 'Plugin title' }) title!: string; + @ApiProperty({ description: 'Plugin description' }) description!: string; + @ApiProperty({ description: 'Plugin author' }) author!: string; + @ApiProperty({ description: 'Plugin version' }) version!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: string; + @ApiProperty({ description: 'Last update date' }) updatedAt!: string; + @ApiProperty({ description: 'Plugin filters' }) filters!: PluginFilterResponseDto[]; + @ApiProperty({ description: 'Plugin actions' }) actions!: PluginActionResponseDto[]; } export class PluginFilterResponseDto { + @ApiProperty({ description: 'Filter ID' }) id!: string; + @ApiProperty({ description: 'Plugin ID' }) pluginId!: string; + @ApiProperty({ description: 'Method name' }) methodName!: string; + @ApiProperty({ description: 'Filter title' }) title!: string; + @ApiProperty({ description: 'Filter description' }) description!: string; - @ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) - supportedContexts!: PluginContext[]; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) + supportedContexts!: PluginContextType[]; + @ApiProperty({ description: 'Filter schema' }) schema!: JSONSchema | null; } export class PluginActionResponseDto { + @ApiProperty({ description: 'Action ID' }) id!: string; + @ApiProperty({ description: 'Plugin ID' }) pluginId!: string; + @ApiProperty({ description: 'Method name' }) methodName!: string; + @ApiProperty({ description: 'Action title' }) title!: string; + @ApiProperty({ description: 'Action description' }) description!: string; - @ValidateEnum({ enum: PluginContext, name: 'PluginContext' }) - supportedContexts!: PluginContext[]; + @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) + supportedContexts!: PluginContextType[]; + @ApiProperty({ description: 'Action schema' }) schema!: JSONSchema | null; } export class PluginInstallDto { + @ApiProperty({ description: 'Path to plugin manifest file' }) @IsString() @IsNotEmpty() manifestPath!: string; diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts index 79155e3f74..993160a03b 100644 --- a/server/src/dtos/queue-legacy.dto.ts +++ b/server/src/dtos/queue-legacy.dto.ts @@ -3,15 +3,19 @@ import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; import { QueueName } from 'src/enum'; export class QueueStatusLegacyDto { + @ApiProperty({ description: 'Whether the queue is currently active (has running jobs)' }) isActive!: boolean; + @ApiProperty({ description: 'Whether the queue is paused' }) isPaused!: boolean; } export class QueueResponseLegacyDto { - @ApiProperty({ type: QueueStatusLegacyDto }) + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) queueStatus!: QueueStatusLegacyDto; - @ApiProperty({ type: QueueStatisticsDto }) + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) jobCounts!: QueueStatisticsDto; } @@ -66,6 +70,9 @@ export class QueuesResponseLegacyDto implements Record { diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index 38a4a4ac6b..7893581444 100644 --- a/server/src/dtos/queue.dto.ts +++ b/server/src/dtos/queue.dto.ts @@ -1,29 +1,29 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { HistoryBuilder } from 'src/decorators'; import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum'; import { ValidateBoolean, ValidateEnum } from 'src/validation'; export class QueueNameParamDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName' }) + @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) name!: QueueName; } export class QueueCommandDto { - @ValidateEnum({ enum: QueueCommand, name: 'QueueCommand' }) + @ValidateEnum({ enum: QueueCommand, name: 'QueueCommand', description: 'Queue command to execute' }) command!: QueueCommand; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Force the command execution (if applicable)' }) force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit } export class QueueUpdateDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether to pause the queue' }) isPaused?: boolean; } export class QueueDeleteDto { - @ValidateBoolean({ optional: true }) - @Property({ + @ValidateBoolean({ + optional: true, description: 'If true, will also remove failed jobs from the queue.', history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), }) @@ -31,42 +31,52 @@ export class QueueDeleteDto { } export class QueueJobSearchDto { - @ValidateEnum({ enum: QueueJobStatus, name: 'QueueJobStatus', optional: true, each: true }) + @ValidateEnum({ + enum: QueueJobStatus, + name: 'QueueJobStatus', + optional: true, + each: true, + description: 'Filter jobs by status', + }) status?: QueueJobStatus[]; } export class QueueJobResponseDto { + @ApiPropertyOptional({ description: 'Job ID' }) id?: string; - @ValidateEnum({ enum: JobName, name: 'JobName' }) + @ValidateEnum({ enum: JobName, name: 'JobName', description: 'Job name' }) name!: JobName; + @ApiProperty({ description: 'Job data payload', type: Object }) data!: object; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Job creation timestamp' }) timestamp!: number; } -export class QueueResponseDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName' }) - name!: QueueName; - - @ValidateBoolean() - isPaused!: boolean; - - statistics!: QueueStatisticsDto; -} - export class QueueStatisticsDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of active jobs' }) active!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of completed jobs' }) completed!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of failed jobs' }) failed!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of delayed jobs' }) delayed!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of waiting jobs' }) waiting!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of paused jobs' }) paused!: number; } + +export class QueueResponseDto { + @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) + name!: QueueName; + + @ValidateBoolean({ description: 'Whether the queue is paused' }) + isPaused!: boolean; + + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) + statistics!: QueueStatisticsDto; +} diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 068cd6630c..f72ecdf8b6 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { Place } from 'src/database'; @@ -9,99 +9,117 @@ import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; class BaseSearchDto { - @ValidateUUID({ optional: true, nullable: true }) + @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) libraryId?: string | null; + @ApiPropertyOptional({ description: 'Device ID to filter by' }) @IsString() @IsNotEmpty() @Optional() deviceId?: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' }) type?: AssetType; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by encoded status' }) isEncoded?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) isFavorite?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by motion photo status' }) isMotion?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter by offline status' }) isOffline?: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Filter by visibility' }) visibility?: AssetVisibility; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by creation date (before)' }) createdBefore?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by creation date (after)' }) createdAfter?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by update date (before)' }) updatedBefore?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by update date (after)' }) updatedAfter?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by trash date (before)' }) trashedBefore?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by trash date (after)' }) trashedAfter?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by taken date (before)' }) takenBefore?: Date; - @ValidateDate({ optional: true }) + @ValidateDate({ optional: true, description: 'Filter by taken date (after)' }) takenAfter?: Date; + @ApiPropertyOptional({ description: 'Filter by city name' }) @IsString() @Optional({ nullable: true, emptyToNull: true }) city?: string | null; + @ApiPropertyOptional({ description: 'Filter by state/province name' }) @IsString() @Optional({ nullable: true, emptyToNull: true }) state?: string | null; + @ApiPropertyOptional({ description: 'Filter by country name' }) @IsString() @IsNotEmpty() @Optional({ nullable: true, emptyToNull: true }) country?: string | null; + @ApiPropertyOptional({ description: 'Filter by camera make' }) @IsString() @Optional({ nullable: true, emptyToNull: true }) make?: string; + @ApiPropertyOptional({ description: 'Filter by camera model' }) @IsString() @Optional({ nullable: true, emptyToNull: true }) model?: string | null; + @ApiPropertyOptional({ description: 'Filter by lens model' }) @IsString() @Optional({ nullable: true, emptyToNull: true }) lensModel?: string | null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Filter assets not in any album' }) isNotInAlbum?: boolean; - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, optional: true, description: 'Filter by person IDs' }) personIds?: string[]; - @ValidateUUID({ each: true, optional: true, nullable: true }) + @ValidateUUID({ each: true, optional: true, nullable: true, description: 'Filter by tag IDs' }) tagIds?: string[] | null; - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' }) albumIds?: string[]; - @Optional() + @Property({ + type: 'number', + description: 'Filter by rating [1-5], or null for unrated', + minimum: -1, + maximum: 5, + history: new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), + }) + @Optional({ nullable: true }) @IsInt() @Max(5) @Min(-1) - rating?: number; + rating?: number | null; + @ApiPropertyOptional({ description: 'Filter by OCR text content' }) @IsString() @IsNotEmpty() @Optional() @@ -109,12 +127,13 @@ class BaseSearchDto { } class BaseSearchWithResultsDto extends BaseSearchDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include deleted assets' }) withDeleted?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include EXIF data in response' }) withExif?: boolean; + @ApiPropertyOptional({ type: 'number', description: 'Number of results to return', minimum: 1, maximum: 1000 }) @IsInt() @Min(1) @Max(1000) @@ -124,65 +143,78 @@ class BaseSearchWithResultsDto extends BaseSearchDto { } export class RandomSearchDto extends BaseSearchWithResultsDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include stacked assets' }) withStacked?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include assets with people' }) withPeople?: boolean; } export class LargeAssetSearchDto extends BaseSearchWithResultsDto { + @ApiPropertyOptional({ type: 'integer', description: 'Minimum file size in bytes', minimum: 0 }) @Optional() @IsInt() @Min(0) @Type(() => Number) - @ApiProperty({ type: 'integer' }) minFileSize?: number; } export class MetadataSearchDto extends RandomSearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by asset ID' }) id?: string; + @ApiPropertyOptional({ description: 'Filter by device asset ID' }) @IsString() @IsNotEmpty() @Optional() deviceAssetId?: string; - @ValidateString({ optional: true, trim: true }) + @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) description?: string; + @ApiPropertyOptional({ description: 'Filter by file checksum' }) @IsString() @IsNotEmpty() @Optional() checksum?: string; - @ValidateString({ optional: true, trim: true }) + @ValidateString({ optional: true, trim: true, description: 'Filter by original file name' }) originalFileName?: string; + @ApiPropertyOptional({ description: 'Filter by original file path' }) @IsString() @IsNotEmpty() @Optional() originalPath?: string; + @ApiPropertyOptional({ description: 'Filter by preview file path' }) @IsString() @IsNotEmpty() @Optional() previewPath?: string; + @ApiPropertyOptional({ description: 'Filter by thumbnail file path' }) @IsString() @IsNotEmpty() @Optional() thumbnailPath?: string; + @ApiPropertyOptional({ description: 'Filter by encoded video file path' }) @IsString() @IsNotEmpty() @Optional() encodedVideoPath?: string; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.Desc }) + @ValidateEnum({ + enum: AssetOrder, + name: 'AssetOrder', + optional: true, + default: AssetOrder.Desc, + description: 'Sort order', + }) order?: AssetOrder; + @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) @IsInt() @Min(1) @Type(() => Number) @@ -191,23 +223,24 @@ export class MetadataSearchDto extends RandomSearchDto { } export class StatisticsSearchDto extends BaseSearchDto { - @ValidateString({ optional: true, trim: true }) + @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) description?: string; } export class SmartSearchDto extends BaseSearchWithResultsDto { - @ValidateString({ optional: true, trim: true }) + @ValidateString({ optional: true, trim: true, description: 'Natural language search query' }) query?: string; - @ValidateUUID({ optional: true }) - @Optional() + @ValidateUUID({ optional: true, description: 'Asset ID to use as search reference' }) queryAssetId?: string; + @ApiPropertyOptional({ description: 'Search language code' }) @IsString() @IsNotEmpty() @Optional() language?: string; + @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) @IsInt() @Min(1) @Type(() => Number) @@ -216,25 +249,32 @@ export class SmartSearchDto extends BaseSearchWithResultsDto { } export class SearchPlacesDto { + @ApiProperty({ description: 'Place name to search for' }) @IsString() @IsNotEmpty() name!: string; } export class SearchPeopleDto { + @ApiProperty({ description: 'Person name to search for' }) @IsString() @IsNotEmpty() name!: string; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include hidden people' }) withHidden?: boolean; } export class PlacesResponseDto { + @ApiProperty({ description: 'Place name' }) name!: string; + @ApiProperty({ type: 'number', description: 'Latitude coordinate' }) latitude!: number; + @ApiProperty({ type: 'number', description: 'Longitude coordinate' }) longitude!: number; + @ApiPropertyOptional({ description: 'Administrative level 1 name (state/province)' }) admin1name?: string; + @ApiPropertyOptional({ description: 'Administrative level 2 name (county/district)' }) admin2name?: string; } @@ -258,96 +298,126 @@ export enum SearchSuggestionType { } export class SearchSuggestionRequestDto { - @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' }) + @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType', description: 'Suggestion type' }) type!: SearchSuggestionType; + @ApiPropertyOptional({ description: 'Filter by country' }) @IsString() @Optional() country?: string; + @ApiPropertyOptional({ description: 'Filter by state/province' }) @IsString() @Optional() state?: string; + @ApiPropertyOptional({ description: 'Filter by camera make' }) @IsString() @Optional() make?: string; + @ApiPropertyOptional({ description: 'Filter by camera model' }) @IsString() @Optional() model?: string; + @ApiPropertyOptional({ description: 'Filter by lens model' }) @IsString() @Optional() lensModel?: string; - @ValidateBoolean({ optional: true }) - @Property({ history: new HistoryBuilder().added('v1.111.0').stable('v2') }) + @ValidateBoolean({ + optional: true, + description: 'Include null values in suggestions', + history: new HistoryBuilder().added('v1.111.0').stable('v2'), + }) includeNull?: boolean; } class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' }) count!: number; + @ApiProperty({ description: 'Facet value' }) value!: string; } class SearchFacetResponseDto { + @ApiProperty({ description: 'Facet field name' }) fieldName!: string; + @ApiProperty({ description: 'Facet counts' }) counts!: SearchFacetCountResponseDto[]; } class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of matching albums' }) total!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of albums in this page' }) count!: number; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) items!: AlbumResponseDto[]; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) facets!: SearchFacetResponseDto[]; } class SearchAssetResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) total!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets in this page' }) count!: number; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) items!: AssetResponseDto[]; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) facets!: SearchFacetResponseDto[]; + @ApiProperty({ description: 'Next page token' }) nextPage!: string | null; } export class SearchResponseDto { + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) albums!: SearchAlbumResponseDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) assets!: SearchAssetResponseDto; } export class SearchStatisticsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) total!: number; } class SearchExploreItem { + @ApiProperty({ description: 'Explore value' }) value!: string; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) data!: AssetResponseDto; } export class SearchExploreResponseDto { + @ApiProperty({ description: 'Explore field name' }) fieldName!: string; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) items!: SearchExploreItem[]; } export class MemoryLaneDto { + @ApiProperty({ type: 'integer', description: 'Day of month' }) @IsInt() @Type(() => Number) @Max(31) @Min(1) - @ApiProperty({ type: 'integer' }) day!: number; + @ApiProperty({ type: 'integer', description: 'Month' }) @IsInt() @Type(() => Number) @Max(12) @Min(1) - @ApiProperty({ type: 'integer' }) month!: number; } diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index e98cb2edf6..626c94e40a 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional, ApiResponseProperty } from '@nestjs/swagger'; import { SemVer } from 'semver'; import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; @@ -8,66 +8,94 @@ export class ServerPingResponse { } export class ServerAboutResponseDto { + @ApiProperty({ description: 'Server version' }) version!: string; + @ApiProperty({ description: 'URL to version information' }) versionUrl!: string; + @ApiPropertyOptional({ description: 'Repository name' }) repository?: string; + @ApiPropertyOptional({ description: 'Repository URL' }) repositoryUrl?: string; + @ApiPropertyOptional({ description: 'Source reference (branch/tag)' }) sourceRef?: string; + @ApiPropertyOptional({ description: 'Source commit hash' }) sourceCommit?: string; + @ApiPropertyOptional({ description: 'Source URL' }) sourceUrl?: string; + @ApiPropertyOptional({ description: 'Build identifier' }) build?: string; + @ApiPropertyOptional({ description: 'Build URL' }) buildUrl?: string; + @ApiPropertyOptional({ description: 'Build image name' }) buildImage?: string; + @ApiPropertyOptional({ description: 'Build image URL' }) buildImageUrl?: string; + @ApiPropertyOptional({ description: 'Node.js version' }) nodejs?: string; + @ApiPropertyOptional({ description: 'FFmpeg version' }) ffmpeg?: string; + @ApiPropertyOptional({ description: 'ImageMagick version' }) imagemagick?: string; + @ApiPropertyOptional({ description: 'libvips version' }) libvips?: string; + @ApiPropertyOptional({ description: 'ExifTool version' }) exiftool?: string; + @ApiProperty({ description: 'Whether the server is licensed' }) licensed!: boolean; + @ApiPropertyOptional({ description: 'Third-party source URL' }) thirdPartySourceUrl?: string; + @ApiPropertyOptional({ description: 'Third-party bug/feature URL' }) thirdPartyBugFeatureUrl?: string; + @ApiPropertyOptional({ description: 'Third-party documentation URL' }) thirdPartyDocumentationUrl?: string; + @ApiPropertyOptional({ description: 'Third-party support URL' }) thirdPartySupportUrl?: string; } export class ServerApkLinksDto { + @ApiProperty({ description: 'APK download link for ARM64 v8a architecture' }) arm64v8a!: string; + @ApiProperty({ description: 'APK download link for ARM EABI v7a architecture' }) armeabiv7a!: string; + @ApiProperty({ description: 'APK download link for universal architecture' }) universal!: string; + @ApiProperty({ description: 'APK download link for x86_64 architecture' }) x86_64!: string; } export class ServerStorageResponseDto { + @ApiProperty({ description: 'Total disk size (human-readable format)' }) diskSize!: string; + @ApiProperty({ description: 'Used disk space (human-readable format)' }) diskUse!: string; + @ApiProperty({ description: 'Available disk space (human-readable format)' }) diskAvailable!: string; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' }) diskSizeRaw!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' }) diskUseRaw!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' }) diskAvailableRaw!: number; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Disk usage percentage (0-100)' }) diskUsagePercentage!: number; } export class ServerVersionResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Major version number' }) major!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Minor version number' }) minor!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Patch version number' }) patch!: number; static fromSemVer(value: SemVer) { @@ -76,44 +104,52 @@ export class ServerVersionResponseDto { } export class ServerVersionHistoryResponseDto { + @ApiProperty({ description: 'Version history entry ID' }) id!: string; + @ApiProperty({ description: 'When this version was first seen', format: 'date-time' }) createdAt!: Date; + @ApiProperty({ description: 'Version string' }) version!: string; } export class UsageByUserDto { - @ApiProperty({ type: 'string' }) + @ApiProperty({ type: 'string', description: 'User ID' }) userId!: string; - @ApiProperty({ type: 'string' }) + @ApiProperty({ type: 'string', description: 'User name' }) userName!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of photos' }) photos!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of videos' }) videos!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) usage!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) usagePhotos!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) usageVideos!: number; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ + type: 'integer', + format: 'int64', + nullable: true, + description: 'User quota size in bytes (null if unlimited)', + }) quotaSizeInBytes!: number | null; } export class ServerStatsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of photos' }) photos = 0; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Total number of videos' }) videos = 0; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) usage = 0; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) usagePhotos = 0; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) usageVideos = 0; @ApiProperty({ @@ -134,44 +170,71 @@ export class ServerStatsResponseDto { } export class ServerMediaTypesResponseDto { + @ApiProperty({ description: 'Supported video MIME types' }) video!: string[]; + @ApiProperty({ description: 'Supported image MIME types' }) image!: string[]; + @ApiProperty({ description: 'Supported sidecar MIME types' }) sidecar!: string[]; } export class ServerThemeDto extends SystemConfigThemeDto {} export class ServerConfigDto { + @ApiProperty({ description: 'OAuth button text' }) oauthButtonText!: string; + @ApiProperty({ description: 'Login page message' }) loginPageMessage!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of days before trashed assets are permanently deleted' }) trashDays!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Delay in days before deleted users are permanently removed' }) userDeleteDelay!: number; + @ApiProperty({ description: 'Whether the server has been initialized' }) isInitialized!: boolean; + @ApiProperty({ description: 'Whether the admin has completed onboarding' }) isOnboarded!: boolean; + @ApiProperty({ description: 'External domain URL' }) externalDomain!: string; + @ApiProperty({ description: 'Whether public user registration is enabled' }) publicUsers!: boolean; + @ApiProperty({ description: 'Map dark style URL' }) mapDarkStyleUrl!: string; + @ApiProperty({ description: 'Map light style URL' }) mapLightStyleUrl!: string; + @ApiProperty({ description: 'Whether maintenance mode is active' }) maintenanceMode!: boolean; } export class ServerFeaturesDto { + @ApiProperty({ description: 'Whether smart search is enabled' }) smartSearch!: boolean; + @ApiProperty({ description: 'Whether duplicate detection is enabled' }) duplicateDetection!: boolean; + @ApiProperty({ description: 'Whether config file is available' }) configFile!: boolean; + @ApiProperty({ description: 'Whether facial recognition is enabled' }) facialRecognition!: boolean; + @ApiProperty({ description: 'Whether map feature is enabled' }) map!: boolean; + @ApiProperty({ description: 'Whether trash feature is enabled' }) trash!: boolean; + @ApiProperty({ description: 'Whether reverse geocoding is enabled' }) reverseGeocoding!: boolean; + @ApiProperty({ description: 'Whether face import is enabled' }) importFaces!: boolean; + @ApiProperty({ description: 'Whether OAuth is enabled' }) oauth!: boolean; + @ApiProperty({ description: 'Whether OAuth auto-launch is enabled' }) oauthAutoLaunch!: boolean; + @ApiProperty({ description: 'Whether password login is enabled' }) passwordLogin!: boolean; + @ApiProperty({ description: 'Whether sidecar files are supported' }) sidecar!: boolean; + @ApiProperty({ description: 'Whether search is enabled' }) search!: boolean; + @ApiProperty({ description: 'Whether email notifications are enabled' }) email!: boolean; + @ApiProperty({ description: 'Whether OCR is enabled' }) ocr!: boolean; } diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index 49351eda52..f918f0b3bb 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,44 +1,55 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Equals, IsInt, IsPositive, IsString } from 'class-validator'; import { Session } from 'src/database'; import { Optional, ValidateBoolean } from 'src/validation'; export class SessionCreateDto { - /** - * session duration, in seconds - */ + @ApiPropertyOptional({ type: 'number', description: 'Session duration in seconds' }) @IsInt() @IsPositive() @Optional() duration?: number; + @ApiPropertyOptional({ description: 'Device type' }) @IsString() @Optional() deviceType?: string; + @ApiPropertyOptional({ description: 'Device OS' }) @IsString() @Optional() deviceOS?: string; } export class SessionUpdateDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Reset pending sync state' }) @Equals(true) isPendingSyncReset?: true; } export class SessionResponseDto { + @ApiProperty({ description: 'Session ID' }) id!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: string; + @ApiProperty({ description: 'Last update date' }) updatedAt!: string; + @ApiPropertyOptional({ description: 'Expiration date' }) expiresAt?: string; + @ApiProperty({ description: 'Is current session' }) current!: boolean; + @ApiProperty({ description: 'Device type' }) deviceType!: string; + @ApiProperty({ description: 'Device OS' }) deviceOS!: string; + @ApiProperty({ description: 'App version' }) appVersion!: string | null; + @ApiProperty({ description: 'Is pending sync reset' }) isPendingSyncReset!: boolean; } export class SessionCreateResponseDto extends SessionResponseDto { + @ApiProperty({ description: 'Session token' }) token!: string; } diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 011707f1f7..b2ecc70a3a 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,122 +1,160 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -import _ from 'lodash'; import { SharedLink } from 'src/database'; +import { HistoryBuilder, Property } from 'src/decorators'; 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, ValidateEnum, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; export class SharedLinkSearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by album ID' }) albumId?: string; + + @ValidateUUID({ + optional: true, + description: 'Filter by shared link ID', + history: new HistoryBuilder().added('v2.5.0'), + }) + id?: string; } export class SharedLinkCreateDto { - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) type!: SharedLinkType; - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, optional: true, description: 'Asset IDs (for individual assets)' }) assetIds?: string[]; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Album ID (for album sharing)' }) albumId?: string; + @ApiPropertyOptional({ description: 'Link description' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() description?: string | null; + @ApiPropertyOptional({ description: 'Link password' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() password?: string | null; + @ApiPropertyOptional({ description: 'Custom URL slug' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() slug?: string | null; - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: true, nullable: true, description: 'Expiration date' }) expiresAt?: Date | null = null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Allow uploads' }) allowUpload?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Allow downloads', default: true }) allowDownload?: boolean = true; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Show metadata', default: true }) showMetadata?: boolean = true; } export class SharedLinkEditDto { + @ApiPropertyOptional({ description: 'Link description' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() description?: string | null; + @ApiPropertyOptional({ description: 'Link password' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() password?: string | null; + @ApiPropertyOptional({ description: 'Custom URL slug' }) @Optional({ nullable: true, emptyToNull: true }) @IsString() slug?: string | null; + @ApiPropertyOptional({ description: 'Expiration date' }) @Optional({ nullable: true }) expiresAt?: Date | null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Allow uploads' }) allowUpload?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Allow downloads' }) allowDownload?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Show metadata' }) showMetadata?: boolean; - /** - * Few clients cannot send null to set the expiryTime to never. - * Setting this flag and not sending expiryAt is considered as null instead. - * Clients that can send null values can ignore this. - */ - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ + optional: true, + description: + 'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.', + }) changeExpiryTime?: boolean; } +export class SharedLinkLoginDto { + @ValidateString({ description: 'Shared link password', example: 'password' }) + password!: string; +} + export class SharedLinkPasswordDto { + @ApiPropertyOptional({ example: 'password', description: 'Link password' }) @IsString() @Optional() - @ApiProperty({ example: 'password' }) password?: string; + @ApiPropertyOptional({ description: 'Access token' }) @IsString() @Optional() token?: string; } export class SharedLinkResponseDto { + @ApiProperty({ description: 'Shared link ID' }) id!: string; + @ApiProperty({ description: 'Link description' }) description!: string | null; + @ApiProperty({ description: 'Has password' }) password!: string | null; + @Property({ + description: 'Access token', + history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), + }) token?: string | null; + @ApiProperty({ description: 'Owner user ID' }) userId!: string; + @ApiProperty({ description: 'Encryption key (base64url)' }) key!: string; - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' }) + @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) type!: SharedLinkType; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Expiration date' }) expiresAt!: Date | null; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) assets!: AssetResponseDto[]; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) album?: AlbumResponseDto; + @ApiProperty({ description: 'Allow uploads' }) allowUpload!: boolean; + @ApiProperty({ description: 'Allow downloads' }) allowDownload!: boolean; + @ApiProperty({ description: 'Show metadata' }) showMetadata!: boolean; + @ApiProperty({ description: 'Custom URL slug' }) slug!: string | null; } -export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto { - const linkAssets = sharedLink.assets || []; +export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { + const assets = sharedLink.assets || []; - return { + const response = { id: sharedLink.id, description: sharedLink.description, password: sharedLink.password, @@ -125,35 +163,19 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto { type: sharedLink.type, createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, - assets: linkAssets.map((asset) => mapAsset(asset)), - album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, - allowUpload: sharedLink.allowUpload, - allowDownload: sharedLink.allowDownload, - showMetadata: sharedLink.showExif, - slug: sharedLink.slug, - }; -} - -export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto { - const linkAssets = sharedLink.assets || []; - const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); - - const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); - - return { - id: sharedLink.id, - description: sharedLink.description, - password: sharedLink.password, - userId: sharedLink.userId, - key: sharedLink.key.toString('base64url'), - type: sharedLink.type, - createdAt: sharedLink.createdAt, - expiresAt: sharedLink.expiresAt, - assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })), + assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, slug: sharedLink.slug, }; + + // unless we select sharedLink.album.sharedLinks this will be wrong + if (response.album) { + response.album.hasSharedLink = true; + response.album.shared = true; + } + + return response; } diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index 17037dd892..a76b35e08e 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { ArrayMinSize } from 'class-validator'; import { Stack } from 'src/database'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; @@ -5,25 +6,27 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { ValidateUUID } from 'src/validation'; export class StackCreateDto { - /** first asset becomes the primary */ - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Asset IDs (first becomes primary, min 2)' }) @ArrayMinSize(2) assetIds!: string[]; } export class StackSearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by primary asset ID' }) primaryAssetId?: string; } export class StackUpdateDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Primary asset ID' }) primaryAssetId?: string; } export class StackResponseDto { + @ApiProperty({ description: 'Stack ID' }) id!: string; + @ApiProperty({ description: 'Primary asset ID' }) primaryAssetId!: string; + @ApiProperty({ description: 'Stack assets' }) assets!: AssetResponseDto[]; } diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index d6a557e2c5..9a1332d303 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { AssetEditAction } from 'src/dtos/editing.dto'; import { AlbumUserRole, - AssetMetadataKey, AssetOrder, AssetType, AssetVisibility, @@ -18,32 +18,35 @@ import { UserMetadata } from 'src/types'; import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; export class AssetFullSyncDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) lastId?: string; - @ValidateDate() + @ValidateDate({ description: 'Sync assets updated until this date' }) updatedUntil!: Date; + @ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' }) @IsInt() @IsPositive() - @ApiProperty({ type: 'integer' }) limit!: number; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by user ID' }) userId?: string; } export class AssetDeltaSyncDto { - @ValidateDate() + @ValidateDate({ description: 'Sync assets updated after this date' }) updatedAfter!: Date; - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'User IDs to sync' }) userIds!: string[]; } export class AssetDeltaSyncResponseDto { + @ApiProperty({ description: 'Whether full sync is needed' }) needsFullSync!: boolean; + @ApiProperty({ description: 'Upserted assets' }) upserted!: AssetResponseDto[]; + @ApiProperty({ description: 'Deleted asset IDs' }) deleted!: string[]; } @@ -58,21 +61,31 @@ export const ExtraModel = (): ClassDecorator => { @ExtraModel() export class SyncUserV1 { + @ApiProperty({ description: 'User ID' }) id!: string; + @ApiProperty({ description: 'User name' }) name!: string; + @ApiProperty({ description: 'User email' }) email!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'User avatar color' }) avatarColor!: UserAvatarColor | null; + @ApiProperty({ description: 'User deleted at' }) deletedAt!: Date | null; + @ApiProperty({ description: 'User has profile image' }) hasProfileImage!: boolean; + @ApiProperty({ description: 'User profile changed at' }) profileChangedAt!: Date; } @ExtraModel() export class SyncAuthUserV1 extends SyncUserV1 { + @ApiProperty({ description: 'User is admin' }) isAdmin!: boolean; + @ApiProperty({ description: 'User pin code' }) pinCode!: string | null; + @ApiProperty({ description: 'User OAuth ID' }) oauthId!: string; + @ApiProperty({ description: 'User storage label' }) storageLabel!: string | null; @ApiProperty({ type: 'integer' }) quotaSizeInBytes!: number | null; @@ -82,131 +95,207 @@ export class SyncAuthUserV1 extends SyncUserV1 { @ExtraModel() export class SyncUserDeleteV1 { + @ApiProperty({ description: 'User ID' }) userId!: string; } @ExtraModel() export class SyncPartnerV1 { + @ApiProperty({ description: 'Shared by ID' }) sharedById!: string; + @ApiProperty({ description: 'Shared with ID' }) sharedWithId!: string; + @ApiProperty({ description: 'In timeline' }) inTimeline!: boolean; } @ExtraModel() export class SyncPartnerDeleteV1 { + @ApiProperty({ description: 'Shared by ID' }) sharedById!: string; + @ApiProperty({ description: 'Shared with ID' }) sharedWithId!: string; } @ExtraModel() export class SyncAssetV1 { + @ApiProperty({ description: 'Asset ID' }) id!: string; + @ApiProperty({ description: 'Owner ID' }) ownerId!: string; + @ApiProperty({ description: 'Original file name' }) originalFileName!: string; + @ApiProperty({ description: 'Thumbhash' }) thumbhash!: string | null; + @ApiProperty({ description: 'Checksum' }) checksum!: string; + @ApiProperty({ description: 'File created at' }) fileCreatedAt!: Date | null; + @ApiProperty({ description: 'File modified at' }) fileModifiedAt!: Date | null; + @ApiProperty({ description: 'Local date time' }) localDateTime!: Date | null; + @ApiProperty({ description: 'Duration' }) duration!: string | null; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' }) + @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) type!: AssetType; + @ApiProperty({ description: 'Deleted at' }) deletedAt!: Date | null; + @ApiProperty({ description: 'Is favorite' }) isFavorite!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' }) + @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) visibility!: AssetVisibility; + @ApiProperty({ description: 'Live photo video ID' }) livePhotoVideoId!: string | null; + @ApiProperty({ description: 'Stack ID' }) stackId!: string | null; + @ApiProperty({ description: 'Library ID' }) libraryId!: string | null; + @ApiProperty({ type: 'integer', description: 'Asset width' }) + width!: number | null; + @ApiProperty({ type: 'integer', description: 'Asset height' }) + height!: number | null; + @ApiProperty({ description: 'Is edited' }) + isEdited!: boolean; } @ExtraModel() export class SyncAssetDeleteV1 { + @ApiProperty({ description: 'Asset ID' }) assetId!: string; } @ExtraModel() export class SyncAssetExifV1 { + @ApiProperty({ description: 'Asset ID' }) assetId!: string; + @ApiProperty({ description: 'Description' }) description!: string | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Exif image width' }) exifImageWidth!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Exif image height' }) exifImageHeight!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'File size in byte' }) fileSizeInByte!: number | null; + @ApiProperty({ description: 'Orientation' }) orientation!: string | null; + @ApiProperty({ description: 'Date time original' }) dateTimeOriginal!: Date | null; + @ApiProperty({ description: 'Modify date' }) modifyDate!: Date | null; + @ApiProperty({ description: 'Time zone' }) timeZone!: string | null; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Latitude' }) latitude!: number | null; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Longitude' }) longitude!: number | null; + @ApiProperty({ description: 'Projection type' }) projectionType!: string | null; + @ApiProperty({ description: 'City' }) city!: string | null; + @ApiProperty({ description: 'State' }) state!: string | null; + @ApiProperty({ description: 'Country' }) country!: string | null; + @ApiProperty({ description: 'Make' }) make!: string | null; + @ApiProperty({ description: 'Model' }) model!: string | null; + @ApiProperty({ description: 'Lens model' }) lensModel!: string | null; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'F number' }) fNumber!: number | null; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'Focal length' }) focalLength!: number | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'ISO' }) iso!: number | null; + @ApiProperty({ description: 'Exposure time' }) exposureTime!: string | null; + @ApiProperty({ description: 'Profile description' }) profileDescription!: string | null; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Rating' }) rating!: number | null; - @ApiProperty({ type: 'number', format: 'double' }) + @ApiProperty({ type: 'number', format: 'double', description: 'FPS' }) fps!: number | null; } @ExtraModel() -export class SyncAssetMetadataV1 { +export class SyncAssetEditV1 { + id!: string; assetId!: string; - @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) - key!: AssetMetadataKey; + + @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) + action!: AssetEditAction; + parameters!: object; + + @ApiProperty({ type: 'integer' }) + sequence!: number; +} + +@ExtraModel() +export class SyncAssetEditDeleteV1 { + editId!: string; +} + +@ExtraModel() +export class SyncAssetMetadataV1 { + @ApiProperty({ description: 'Asset ID' }) + assetId!: string; + @ApiProperty({ description: 'Key' }) + key!: string; + @ApiProperty({ description: 'Value' }) value!: object; } @ExtraModel() export class SyncAssetMetadataDeleteV1 { + @ApiProperty({ description: 'Asset ID' }) assetId!: string; - @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) - key!: AssetMetadataKey; + @ApiProperty({ description: 'Key' }) + key!: string; } @ExtraModel() export class SyncAlbumDeleteV1 { + @ApiProperty({ description: 'Album ID' }) albumId!: string; } @ExtraModel() export class SyncAlbumUserDeleteV1 { + @ApiProperty({ description: 'Album ID' }) albumId!: string; + @ApiProperty({ description: 'User ID' }) userId!: string; } @ExtraModel() export class SyncAlbumUserV1 { + @ApiProperty({ description: 'Album ID' }) albumId!: string; + @ApiProperty({ description: 'User ID' }) userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' }) + @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) role!: AlbumUserRole; } @ExtraModel() export class SyncAlbumV1 { + @ApiProperty({ description: 'Album ID' }) id!: string; + @ApiProperty({ description: 'Owner ID' }) ownerId!: string; + @ApiProperty({ description: 'Album name' }) name!: string; + @ApiProperty({ description: 'Album description' }) description!: string; + @ApiProperty({ description: 'Created at' }) createdAt!: Date; + @ApiProperty({ description: 'Updated at' }) updatedAt!: Date; + @ApiProperty({ description: 'Thumbnail asset ID' }) thumbnailAssetId!: string | null; + @ApiProperty({ description: 'Is activity enabled' }) isActivityEnabled!: boolean; @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) order!: AssetOrder; @@ -214,87 +303,127 @@ export class SyncAlbumV1 { @ExtraModel() export class SyncAlbumToAssetV1 { + @ApiProperty({ description: 'Album ID' }) albumId!: string; + @ApiProperty({ description: 'Asset ID' }) assetId!: string; } @ExtraModel() export class SyncAlbumToAssetDeleteV1 { + @ApiProperty({ description: 'Album ID' }) albumId!: string; + @ApiProperty({ description: 'Asset ID' }) assetId!: string; } @ExtraModel() export class SyncMemoryV1 { + @ApiProperty({ description: 'Memory ID' }) id!: string; + @ApiProperty({ description: 'Created at' }) createdAt!: Date; + @ApiProperty({ description: 'Updated at' }) updatedAt!: Date; + @ApiProperty({ description: 'Deleted at' }) deletedAt!: Date | null; + @ApiProperty({ description: 'Owner ID' }) ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType' }) + @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) type!: MemoryType; + @ApiProperty({ description: 'Data' }) data!: object; + @ApiProperty({ description: 'Is saved' }) isSaved!: boolean; + @ApiProperty({ description: 'Memory at' }) memoryAt!: Date; + @ApiProperty({ description: 'Seen at' }) seenAt!: Date | null; + @ApiProperty({ description: 'Show at' }) showAt!: Date | null; + @ApiProperty({ description: 'Hide at' }) hideAt!: Date | null; } @ExtraModel() export class SyncMemoryDeleteV1 { + @ApiProperty({ description: 'Memory ID' }) memoryId!: string; } @ExtraModel() export class SyncMemoryAssetV1 { + @ApiProperty({ description: 'Memory ID' }) memoryId!: string; + @ApiProperty({ description: 'Asset ID' }) assetId!: string; } @ExtraModel() export class SyncMemoryAssetDeleteV1 { + @ApiProperty({ description: 'Memory ID' }) memoryId!: string; + @ApiProperty({ description: 'Asset ID' }) assetId!: string; } @ExtraModel() export class SyncStackV1 { + @ApiProperty({ description: 'Stack ID' }) id!: string; + @ApiProperty({ description: 'Created at' }) createdAt!: Date; + @ApiProperty({ description: 'Updated at' }) updatedAt!: Date; + @ApiProperty({ description: 'Primary asset ID' }) primaryAssetId!: string; + @ApiProperty({ description: 'Owner ID' }) ownerId!: string; } @ExtraModel() export class SyncStackDeleteV1 { + @ApiProperty({ description: 'Stack ID' }) stackId!: string; } @ExtraModel() export class SyncPersonV1 { + @ApiProperty({ description: 'Person ID' }) id!: string; + @ApiProperty({ description: 'Created at' }) createdAt!: Date; + @ApiProperty({ description: 'Updated at' }) updatedAt!: Date; + @ApiProperty({ description: 'Owner ID' }) ownerId!: string; + @ApiProperty({ description: 'Person name' }) name!: string; + @ApiProperty({ description: 'Birth date' }) birthDate!: Date | null; + @ApiProperty({ description: 'Is hidden' }) isHidden!: boolean; + @ApiProperty({ description: 'Is favorite' }) isFavorite!: boolean; + @ApiProperty({ description: 'Color' }) color!: string | null; + @ApiProperty({ description: 'Face asset ID' }) faceAssetId!: string | null; } @ExtraModel() export class SyncPersonDeleteV1 { + @ApiProperty({ description: 'Person ID' }) personId!: string; } @ExtraModel() export class SyncAssetFaceV1 { + @ApiProperty({ description: 'Asset face ID' }) id!: string; + @ApiProperty({ description: 'Asset ID' }) assetId!: string; + @ApiProperty({ description: 'Person ID' }) personId!: string | null; @ApiProperty({ type: 'integer' }) imageWidth!: number; @@ -308,26 +437,45 @@ export class SyncAssetFaceV1 { boundingBoxX2!: number; @ApiProperty({ type: 'integer' }) boundingBoxY2!: number; + @ApiProperty({ description: 'Source type' }) sourceType!: string; } +@ExtraModel() +export class SyncAssetFaceV2 extends SyncAssetFaceV1 { + @ApiProperty({ description: 'Face deleted at' }) + deletedAt!: Date | null; + @ApiProperty({ description: 'Is the face visible in the asset' }) + isVisible!: boolean; +} + +export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 { + const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2; + + return faceV1; +} + @ExtraModel() export class SyncAssetFaceDeleteV1 { + @ApiProperty({ description: 'Asset face ID' }) assetFaceId!: string; } @ExtraModel() export class SyncUserMetadataV1 { + @ApiProperty({ description: 'User ID' }) userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' }) + @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) key!: UserMetadataKey; + @ApiProperty({ description: 'User metadata value' }) value!: UserMetadata[UserMetadataKey]; } @ExtraModel() export class SyncUserMetadataDeleteV1 { + @ApiProperty({ description: 'User ID' }) userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' }) + @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) key!: UserMetadataKey; } @@ -351,6 +499,8 @@ export type SyncItem = { [SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1; [SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1; [SyncEntityType.AssetExifV1]: SyncAssetExifV1; + [SyncEntityType.AssetEditV1]: SyncAssetEditV1; + [SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1; [SyncEntityType.PartnerAssetV1]: SyncAssetV1; [SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1; [SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1; @@ -382,6 +532,7 @@ export type SyncItem = { [SyncEntityType.PersonV1]: SyncPersonV1; [SyncEntityType.PersonDeleteV1]: SyncPersonDeleteV1; [SyncEntityType.AssetFaceV1]: SyncAssetFaceV1; + [SyncEntityType.AssetFaceV2]: SyncAssetFaceV2; [SyncEntityType.AssetFaceDeleteV1]: SyncAssetFaceDeleteV1; [SyncEntityType.UserMetadataV1]: SyncUserMetadataV1; [SyncEntityType.UserMetadataDeleteV1]: SyncUserMetadataDeleteV1; @@ -391,26 +542,34 @@ export type SyncItem = { }; export class SyncStreamDto { - @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true }) + @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true, description: 'Sync request types' }) types!: SyncRequestType[]; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Reset sync state' }) reset?: boolean; } export class SyncAckDto { - @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' }) + @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', description: 'Sync entity type' }) type!: SyncEntityType; + @ApiProperty({ description: 'Acknowledgment ID' }) ack!: string; } export class SyncAckSetDto { + @ApiProperty({ description: 'Acknowledgment IDs (max 1000)' }) @ArrayMaxSize(1000) @IsString({ each: true }) acks!: string[]; } export class SyncAckDeleteDto { - @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true }) + @ValidateEnum({ + enum: SyncEntityType, + name: 'SyncEntityType', + optional: true, + each: true, + description: 'Sync entity types to delete acks for', + }) types?: SyncEntityType[]; } diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index c835073c31..7a0dcb6f3a 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -40,18 +40,20 @@ const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabl const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled; export class DatabaseBackupConfig { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @ValidateIf(isDatabaseBackupEnabled) @IsNotEmpty() @IsCronExpression() @IsString() + @ApiProperty({ description: 'Cron expression' }) cronExpression!: string; @IsInt() @IsPositive() @IsNotEmpty() + @ApiProperty({ description: 'Keep last amount' }) keepLastAmount!: number; } @@ -67,173 +69,187 @@ export class SystemConfigFFmpegDto { @Min(0) @Max(51) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'CRF' }) crf!: number; @IsInt() @Min(0) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Threads' }) threads!: number; @IsString() + @ApiProperty({ description: 'Preset' }) preset!: string; - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' }) targetVideoCodec!: VideoCodec; - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true }) + @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' }) acceptedVideoCodecs!: VideoCodec[]; - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' }) targetAudioCodec!: AudioCodec; - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true }) + @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true, description: 'Accepted audio codecs' }) acceptedAudioCodecs!: AudioCodec[]; - @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true }) + @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' }) acceptedContainers!: VideoContainer[]; @IsString() + @ApiProperty({ description: 'Target resolution' }) targetResolution!: string; @IsString() + @ApiProperty({ description: 'Max bitrate' }) maxBitrate!: string; @IsInt() @Min(-1) @Max(16) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'B-frames' }) bframes!: number; @IsInt() @Min(0) @Max(6) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'References' }) refs!: number; @IsInt() @Min(0) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'GOP size' }) gopSize!: number; - @ValidateBoolean() + @ValidateBoolean({ description: 'Temporal AQ' }) temporalAQ!: boolean; - @ValidateEnum({ enum: CQMode, name: 'CQMode' }) + @ValidateEnum({ enum: CQMode, name: 'CQMode', description: 'CQ mode' }) cqMode!: CQMode; - @ValidateBoolean() + @ValidateBoolean({ description: 'Two pass' }) twoPass!: boolean; + @ApiProperty({ description: 'Preferred hardware device' }) @IsString() preferredHwDevice!: string; - @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' }) + @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy', description: 'Transcode policy' }) transcode!: TranscodePolicy; - @ValidateEnum({ enum: TranscodeHardwareAcceleration, name: 'TranscodeHWAccel' }) + @ValidateEnum({ + enum: TranscodeHardwareAcceleration, + name: 'TranscodeHWAccel', + description: 'Transcode hardware acceleration', + }) accel!: TranscodeHardwareAcceleration; - @ValidateBoolean() + @ValidateBoolean({ description: 'Accelerated decode' }) accelDecode!: boolean; - @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' }) + @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping', description: 'Tone mapping' }) tonemap!: ToneMapping; } class JobSettingsDto { @IsInt() @IsPositive() - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Concurrency' }) concurrency!: number; } class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.ThumbnailGeneration]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.MetadataExtraction]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.VideoConversion]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.SmartSearch]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Migration]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.BackgroundTask]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Search]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.FaceDetection]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Ocr]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Sidecar]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Library]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Notification]!: JobSettingsDto; - @ApiProperty({ type: JobSettingsDto }) + @ApiProperty({ type: JobSettingsDto, description: undefined }) @ValidateNested() @IsObject() @Type(() => JobSettingsDto) [QueueName.Workflow]!: JobSettingsDto; + + @ApiProperty({ type: JobSettingsDto, description: undefined }) + @ValidateNested() + @IsObject() + @Type(() => JobSettingsDto) + [QueueName.Editor]!: JobSettingsDto; } class SystemConfigLibraryScanDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @ValidateIf(isLibraryScanEnabled) @@ -244,7 +260,7 @@ class SystemConfigLibraryScanDto { } class SystemConfigLibraryWatchDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; } @@ -261,7 +277,7 @@ class SystemConfigLibraryDto { } class SystemConfigLoggingDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) @@ -269,7 +285,7 @@ class SystemConfigLoggingDto { } class MachineLearningAvailabilityChecksDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @IsInt() @@ -280,7 +296,7 @@ class MachineLearningAvailabilityChecksDto { } class SystemConfigMachineLearningDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) @@ -326,7 +342,7 @@ export class MapThemeDto { } class SystemConfigMapDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @IsNotEmpty() @@ -339,7 +355,7 @@ class SystemConfigMapDto { } class SystemConfigNewVersionCheckDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; } @@ -347,72 +363,82 @@ class SystemConfigNightlyTasksDto { @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) startTime!: string; - @ValidateBoolean() + @ValidateBoolean({ description: 'Database cleanup' }) databaseCleanup!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Missing thumbnails' }) missingThumbnails!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Cluster new faces' }) clusterNewFaces!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Generate memories' }) generateMemories!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Sync quota usage' }) syncQuotaUsage!: boolean; } class SystemConfigOAuthDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Auto launch' }) autoLaunch!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Auto register' }) autoRegister!: boolean; @IsString() + @ApiProperty({ description: 'Button text' }) buttonText!: string; @ValidateIf(isOAuthEnabled) @IsNotEmpty() @IsString() + @ApiProperty({ description: 'Client ID' }) clientId!: string; @ValidateIf(isOAuthEnabled) @IsString() + @ApiProperty({ description: 'Client secret' }) clientSecret!: string; - @ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' }) + @ValidateEnum({ + enum: OAuthTokenEndpointAuthMethod, + name: 'OAuthTokenEndpointAuthMethod', + description: 'Token endpoint auth method', + }) tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; @IsInt() @IsPositive() @Optional() - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Timeout' }) timeout!: number; @IsNumber() @Min(0) @Optional({ nullable: true }) - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Default storage quota' }) defaultStorageQuota!: number | null; - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @ValidateIf(isOAuthEnabled) @IsNotEmpty() @IsString() + @ApiProperty({ description: 'Issuer URL' }) issuerUrl!: string; - @ValidateBoolean() + @ValidateBoolean({ description: 'Mobile override enabled' }) mobileOverrideEnabled!: boolean; @ValidateIf(isOAuthOverrideEnabled) @IsUrl() + @ApiProperty({ description: 'Mobile redirect URI' }) mobileRedirectUri!: string; @IsString() + @ApiProperty({ description: 'Scope' }) scope!: string; @IsString() @@ -421,30 +447,34 @@ class SystemConfigOAuthDto { @IsString() @IsNotEmpty() + @ApiProperty({ description: 'Profile signing algorithm' }) profileSigningAlgorithm!: string; @IsString() + @ApiProperty({ description: 'Storage label claim' }) storageLabelClaim!: string; @IsString() + @ApiProperty({ description: 'Storage quota claim' }) storageQuotaClaim!: string; @IsString() + @ApiProperty({ description: 'Role claim' }) roleClaim!: string; } class SystemConfigPasswordLoginDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; } class SystemConfigReverseGeocodingDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; } class SystemConfigFacesDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Import' }) import!: boolean; } @@ -458,51 +488,61 @@ class SystemConfigMetadataDto { class SystemConfigServerDto { @ValidateIf((_, value: string) => value !== '') @IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] }) + @ApiProperty({ description: 'External domain' }) externalDomain!: string; @IsString() + @ApiProperty({ description: 'Login page message' }) loginPageMessage!: string; - @ValidateBoolean() + @ValidateBoolean({ description: 'Public users' }) publicUsers!: boolean; } class SystemConfigSmtpTransportDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Whether to ignore SSL certificate errors' }) ignoreCert!: boolean; + @ApiProperty({ description: 'SMTP server hostname' }) @IsNotEmpty() @IsString() host!: string; + @ApiProperty({ description: 'SMTP server port', type: Number, minimum: 0, maximum: 65_535 }) @IsNumber() @Min(0) @Max(65_535) port!: number; - @ValidateBoolean() + @ValidateBoolean({ description: 'Whether to use secure connection (TLS/SSL)' }) secure!: boolean; + @ApiProperty({ description: 'SMTP username' }) @IsString() username!: string; + @ApiProperty({ description: 'SMTP password' }) @IsString() password!: string; } export class SystemConfigSmtpDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Whether SMTP email notifications are enabled' }) enabled!: boolean; + @ApiProperty({ description: 'Email address to send from' }) @ValidateIf(isEmailNotificationEnabled) @IsNotEmpty() @IsString() @IsNotEmpty() from!: string; + @ApiProperty({ description: 'Email address for replies' }) @IsString() replyTo!: string; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @ValidateIf(isEmailNotificationEnabled) @Type(() => SystemConfigSmtpTransportDto) @ValidateNested() @@ -536,97 +576,119 @@ class SystemConfigTemplatesDto { } class SystemConfigStorageTemplateDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; - @ValidateBoolean() + @ValidateBoolean({ description: 'Hash verification enabled' }) hashVerificationEnabled!: boolean; @IsNotEmpty() @IsString() + @ApiProperty({ description: 'Template' }) template!: string; } export class SystemConfigTemplateStorageOptionDto { + @ApiProperty({ description: 'Available year format options for storage template' }) yearOptions!: string[]; + @ApiProperty({ description: 'Available month format options for storage template' }) monthOptions!: string[]; + @ApiProperty({ description: 'Available week format options for storage template' }) weekOptions!: string[]; + @ApiProperty({ description: 'Available day format options for storage template' }) dayOptions!: string[]; + @ApiProperty({ description: 'Available hour format options for storage template' }) hourOptions!: string[]; + @ApiProperty({ description: 'Available minute format options for storage template' }) minuteOptions!: string[]; + @ApiProperty({ description: 'Available second format options for storage template' }) secondOptions!: string[]; + @ApiProperty({ description: 'Available preset template options' }) presetOptions!: string[]; } export class SystemConfigThemeDto { + @ApiProperty({ description: 'Custom CSS for theming' }) @IsString() customCss!: string; } class SystemConfigGeneratedImageDto { - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) format!: ImageFormat; @IsInt() @Min(1) @Max(100) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Quality' }) quality!: number; @IsInt() @Min(1) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Size' }) size!: number; + + @ValidateBoolean({ optional: true, default: false }) + progressive?: boolean; } class SystemConfigGeneratedFullsizeImageDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' }) + @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) format!: ImageFormat; @IsInt() @Min(1) @Max(100) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Quality' }) quality!: number; + + @ValidateBoolean({ optional: true, default: false, description: 'Progressive' }) + progressive?: boolean; } export class SystemConfigImageDto { @Type(() => SystemConfigGeneratedImageDto) @ValidateNested() @IsObject() + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) thumbnail!: SystemConfigGeneratedImageDto; @Type(() => SystemConfigGeneratedImageDto) @ValidateNested() @IsObject() + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) preview!: SystemConfigGeneratedImageDto; @Type(() => SystemConfigGeneratedFullsizeImageDto) @ValidateNested() @IsObject() + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) fullsize!: SystemConfigGeneratedFullsizeImageDto; - @ValidateEnum({ enum: Colorspace, name: 'Colorspace' }) + @ValidateEnum({ enum: Colorspace, name: 'Colorspace', description: 'Colorspace' }) colorspace!: Colorspace; - @ValidateBoolean() + @ValidateBoolean({ description: 'Extract embedded' }) extractEmbedded!: boolean; } class SystemConfigTrashDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Enabled' }) enabled!: boolean; @IsInt() @Min(0) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Days' }) days!: number; } @@ -634,111 +696,153 @@ class SystemConfigUserDto { @IsInt() @Min(1) @Type(() => Number) - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Delete delay' }) deleteDelay!: number; } export class SystemConfigDto implements SystemConfig { + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigBackupsDto) @ValidateNested() @IsObject() backup!: SystemConfigBackupsDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigFFmpegDto) @ValidateNested() @IsObject() ffmpeg!: SystemConfigFFmpegDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigLoggingDto) @ValidateNested() @IsObject() logging!: SystemConfigLoggingDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigMachineLearningDto) @ValidateNested() @IsObject() machineLearning!: SystemConfigMachineLearningDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigMapDto) @ValidateNested() @IsObject() map!: SystemConfigMapDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigNewVersionCheckDto) @ValidateNested() @IsObject() newVersionCheck!: SystemConfigNewVersionCheckDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigNightlyTasksDto) @ValidateNested() @IsObject() nightlyTasks!: SystemConfigNightlyTasksDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigOAuthDto) @ValidateNested() @IsObject() oauth!: SystemConfigOAuthDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigPasswordLoginDto) @ValidateNested() @IsObject() passwordLogin!: SystemConfigPasswordLoginDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigReverseGeocodingDto) @ValidateNested() @IsObject() reverseGeocoding!: SystemConfigReverseGeocodingDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigMetadataDto) @ValidateNested() @IsObject() metadata!: SystemConfigMetadataDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigStorageTemplateDto) @ValidateNested() @IsObject() storageTemplate!: SystemConfigStorageTemplateDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigJobDto) @ValidateNested() @IsObject() job!: SystemConfigJobDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigImageDto) @ValidateNested() @IsObject() image!: SystemConfigImageDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigTrashDto) @ValidateNested() @IsObject() trash!: SystemConfigTrashDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigThemeDto) @ValidateNested() @IsObject() theme!: SystemConfigThemeDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigLibraryDto) @ValidateNested() @IsObject() library!: SystemConfigLibraryDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigNotificationsDto) @ValidateNested() @IsObject() notifications!: SystemConfigNotificationsDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigTemplatesDto) @ValidateNested() @IsObject() templates!: SystemConfigTemplatesDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigServerDto) @ValidateNested() @IsObject() server!: SystemConfigServerDto; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) @Type(() => SystemConfigUserDto) @ValidateNested() @IsObject() diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index 0005aee7eb..0a4d55c970 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -1,21 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; import { ValidateBoolean } from 'src/validation'; export class AdminOnboardingUpdateDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Is admin onboarded' }) isOnboarded!: boolean; } export class AdminOnboardingResponseDto { - @ValidateBoolean() + @ValidateBoolean({ description: 'Is admin onboarded' }) isOnboarded!: boolean; } export class ReverseGeocodingStateResponseDto { + @ApiProperty({ description: 'Last update timestamp' }) lastUpdate!: string | null; + @ApiProperty({ description: 'Last import file name' }) lastImportFileName!: string | null; } export class VersionCheckStateResponseDto { + @ApiProperty({ description: 'Last check timestamp' }) checkedAt!: string | null; + @ApiProperty({ description: 'Release version' }) releaseVersion!: string | null; } diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index a35801d07e..bb33659bfe 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,53 +1,64 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; import { Tag } from 'src/database'; import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; export class TagCreateDto { + @ApiProperty({ description: 'Tag name' }) @IsString() @IsNotEmpty() name!: string; - @ValidateUUID({ optional: true, nullable: true }) + @ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' }) parentId?: string | null; + @ApiPropertyOptional({ description: 'Tag color (hex)' }) @IsHexColor() @Optional({ nullable: true, emptyToNull: true }) color?: string; } export class TagUpdateDto { - @Optional({ emptyToNull: true, nullable: true }) + @ApiPropertyOptional({ description: 'Tag color (hex)' }) + @Optional({ nullable: true, emptyToNull: true }) @ValidateHexColor() color?: string | null; } export class TagUpsertDto { + @ApiProperty({ description: 'Tag names to upsert' }) @IsString({ each: true }) @IsNotEmpty({ each: true }) tags!: string[]; } export class TagBulkAssetsDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Tag IDs' }) tagIds!: string[]; - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Asset IDs' }) assetIds!: string[]; } export class TagBulkAssetsResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of assets tagged' }) count!: number; } export class TagResponseDto { + @ApiProperty({ description: 'Tag ID' }) id!: string; + @ApiPropertyOptional({ description: 'Parent tag ID' }) parentId?: string; + @ApiProperty({ description: 'Tag name' }) name!: string; + @ApiProperty({ description: 'Tag value (full path)' }) value!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; + @ApiPropertyOptional({ description: 'Tag color (hex)' }) color?: string; } diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 58772da00b..9ea9dc49ae 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; - import { IsString } from 'class-validator'; +import type { BBoxDto } from 'src/dtos/bbox.dto'; import { AssetOrder, AssetVisibility } from 'src/enum'; +import { ValidateBBox } from 'src/utils/bbox'; import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class TimeBucketDto { @@ -59,6 +60,9 @@ export class TimeBucketDto { description: 'Include location data in the response', }) withCoordinates?: boolean; + + @ValidateBBox({ optional: true }) + bbox?: BBoxDto; } export class TimeBucketAssetDto extends TimeBucketDto { @@ -132,7 +136,7 @@ export class TimeBucketAssetResponseDto { @ApiProperty({ type: 'array', items: { type: 'string' }, - description: 'Array of file creation timestamps in UTC (ISO 8601 format, without timezone)', + description: 'Array of file creation timestamps in UTC', }) fileCreatedAt!: string[]; diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts index d8e139bff2..f1d1f109f6 100644 --- a/server/src/dtos/trash.dto.ts +++ b/server/src/dtos/trash.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; export class TrashResponseDto { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Number of items in trash' }) count!: number; } diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 452384b423..cce1994007 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { AssetOrder, UserAvatarColor } from 'src/enum'; @@ -6,71 +6,72 @@ import { UserPreferences } from 'src/types'; import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; class AvatarUpdate { - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' }) color?: UserAvatarColor; } class MemoriesUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether memories are enabled' }) enabled?: boolean; @Optional() @IsInt() @IsPositive() - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) duration?: number; } class RatingsUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' }) enabled?: boolean; } +@ApiSchema({ description: 'Album preferences' }) class AlbumsUpdate { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' }) defaultAssetOrder?: AssetOrder; } class FoldersUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether folders are enabled' }) enabled?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' }) sidebarWeb?: boolean; } class PeopleUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether people are enabled' }) enabled?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' }) sidebarWeb?: boolean; } class SharedLinksUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' }) enabled?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' }) sidebarWeb?: boolean; } class TagsUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether tags are enabled' }) enabled?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' }) sidebarWeb?: boolean; } class EmailNotificationsUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' }) enabled?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' }) albumInvite?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' }) albumUpdate?: boolean; } @@ -78,83 +79,108 @@ class DownloadUpdate implements Partial { @Optional() @IsInt() @IsPositive() - @ApiProperty({ type: 'integer' }) + @ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' }) archiveSize?: number; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' }) includeEmbeddedVideos?: boolean; } class PurchaseUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether to show support badge' }) showSupportBadge?: boolean; + @ApiPropertyOptional({ description: 'Date until which to hide buy button' }) @IsDateString() @Optional() hideBuyButtonUntil?: string; } class CastUpdate { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' }) gCastEnabled?: boolean; } export class UserPreferencesUpdateDto { + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => AlbumsUpdate) albums?: AlbumsUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => FoldersUpdate) folders?: FoldersUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => MemoriesUpdate) memories?: MemoriesUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => PeopleUpdate) people?: PeopleUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => RatingsUpdate) ratings?: RatingsUpdate; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined, required: false }) @Optional() @ValidateNested() @Type(() => SharedLinksUpdate) sharedLinks?: SharedLinksUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => TagsUpdate) tags?: TagsUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => AvatarUpdate) avatar?: AvatarUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => EmailNotificationsUpdate) emailNotifications?: EmailNotificationsUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => DownloadUpdate) download?: DownloadUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => PurchaseUpdate) purchase?: PurchaseUpdate; + // Description lives on schema to avoid duplication + @ApiPropertyOptional({ description: undefined }) @Optional() @ValidateNested() @Type(() => CastUpdate) @@ -162,74 +188,113 @@ export class UserPreferencesUpdateDto { } class AlbumsResponse { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) + @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Default asset order for albums' }) defaultAssetOrder: AssetOrder = AssetOrder.Desc; } class RatingsResponse { + @ApiProperty({ description: 'Whether ratings are enabled' }) enabled: boolean = false; } class MemoriesResponse { + @ApiProperty({ description: 'Whether memories are enabled' }) enabled: boolean = true; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) duration: number = 5; } class FoldersResponse { + @ApiProperty({ description: 'Whether folders are enabled' }) enabled: boolean = false; + @ApiProperty({ description: 'Whether folders appear in web sidebar' }) sidebarWeb: boolean = false; } class PeopleResponse { + @ApiProperty({ description: 'Whether people are enabled' }) enabled: boolean = true; + @ApiProperty({ description: 'Whether people appear in web sidebar' }) sidebarWeb: boolean = false; } class TagsResponse { + @ApiProperty({ description: 'Whether tags are enabled' }) enabled: boolean = true; + @ApiProperty({ description: 'Whether tags appear in web sidebar' }) sidebarWeb: boolean = true; } class SharedLinksResponse { + @ApiProperty({ description: 'Whether shared links are enabled' }) enabled: boolean = true; + @ApiProperty({ description: 'Whether shared links appear in web sidebar' }) sidebarWeb: boolean = false; } class EmailNotificationsResponse { + @ApiProperty({ description: 'Whether email notifications are enabled' }) enabled!: boolean; + @ApiProperty({ description: 'Whether to receive email notifications for album invites' }) albumInvite!: boolean; + @ApiProperty({ description: 'Whether to receive email notifications for album updates' }) albumUpdate!: boolean; } class DownloadResponse { - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', description: 'Maximum archive size in bytes' }) archiveSize!: number; + @ApiProperty({ description: 'Whether to include embedded videos in downloads' }) includeEmbeddedVideos: boolean = false; } class PurchaseResponse { + @ApiProperty({ description: 'Whether to show support badge' }) showSupportBadge!: boolean; + @ApiProperty({ description: 'Date until which to hide buy button' }) hideBuyButtonUntil!: string; } class CastResponse { + @ApiProperty({ description: 'Whether Google Cast is enabled' }) gCastEnabled: boolean = false; } export class UserPreferencesResponseDto implements UserPreferences { + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) albums!: AlbumsResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) folders!: FoldersResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) memories!: MemoriesResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) people!: PeopleResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) ratings!: RatingsResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) sharedLinks!: SharedLinksResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) tags!: TagsResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) emailNotifications!: EmailNotificationsResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) download!: DownloadResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) purchase!: PurchaseResponse; + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) cast!: CastResponse; } diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 16eea373e3..6559dd052c 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -2,12 +2,15 @@ import { ApiProperty } from '@nestjs/swagger'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; export class CreateProfileImageDto { - @ApiProperty({ type: 'string', format: 'binary' }) + @ApiProperty({ type: 'string', format: 'binary', description: 'Profile image file' }) [UploadFieldName.PROFILE_DATA]!: Express.Multer.File; } export class CreateProfileImageResponseDto { + @ApiProperty({ description: 'User ID' }) userId!: string; + @ApiProperty({ description: 'Profile image change date', format: 'date-time' }) profileChangedAt!: Date; + @ApiProperty({ description: 'Profile image file path' }) profileImagePath!: string; } diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index c5067f3e8d..2d4fc3934f 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; import { User, UserAdmin } from 'src/database'; @@ -7,39 +7,56 @@ import { UserMetadataItem } from 'src/types'; import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; export class UserUpdateMeDto { + @ApiPropertyOptional({ description: 'User email' }) @Optional() @IsEmail({ require_tld: false }) @Transform(toEmail) email?: string; // TODO: migrate to the other change password endpoint + @ApiPropertyOptional({ description: 'User password (deprecated, use change password endpoint)' }) @Optional() @IsNotEmpty() @IsString() password?: string; + @ApiPropertyOptional({ description: 'User name' }) @Optional() @IsString() @IsNotEmpty() name?: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) + @ValidateEnum({ + enum: UserAvatarColor, + name: 'UserAvatarColor', + optional: true, + nullable: true, + description: 'Avatar color', + }) avatarColor?: UserAvatarColor | null; } export class UserResponseDto { + @ApiProperty({ description: 'User ID' }) id!: string; + @ApiProperty({ description: 'User name' }) name!: string; + @ApiProperty({ description: 'User email' }) email!: string; + @ApiProperty({ description: 'Profile image path' }) profileImagePath!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' }) + @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' }) avatarColor!: UserAvatarColor; + @ApiProperty({ description: 'Profile change date' }) profileChangedAt!: Date; } export class UserLicense { + @ApiProperty({ description: 'License key' }) licenseKey!: string; + @ApiProperty({ description: 'Activation key' }) activationKey!: string; + @ApiProperty({ description: 'Activation date' }) activatedAt!: Date; } @@ -63,108 +80,141 @@ export const mapUser = (entity: User | UserAdmin): UserResponseDto => { }; export class UserAdminSearchDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Include deleted users' }) withDeleted?: boolean; - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'User ID filter' }) id?: string; } export class UserAdminCreateDto { + @ApiProperty({ description: 'User email' }) @IsEmail({ require_tld: false }) @Transform(toEmail) email!: string; + @ApiProperty({ description: 'User password' }) @IsString() password!: string; + @ApiProperty({ description: 'User name' }) @IsNotEmpty() @IsString() name!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) + @ValidateEnum({ + enum: UserAvatarColor, + name: 'UserAvatarColor', + optional: true, + nullable: true, + description: 'Avatar color', + }) avatarColor?: UserAvatarColor | null; + @ApiPropertyOptional({ description: 'PIN code' }) + @PinCode({ optional: true, nullable: true, emptyToNull: true }) + pinCode?: string | null; + + @ApiPropertyOptional({ description: 'Storage label' }) @Optional({ nullable: true }) @IsString() @Transform(toSanitized) storageLabel?: string | null; + @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) @Optional({ nullable: true }) @IsInt() @Min(0) - @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) shouldChangePassword?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Send notification email' }) notify?: boolean; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) isAdmin?: boolean; } export class UserAdminUpdateDto { + @ApiPropertyOptional({ description: 'User email' }) @Optional() @IsEmail({ require_tld: false }) @Transform(toEmail) email?: string; + @ApiPropertyOptional({ description: 'User password' }) @Optional() @IsNotEmpty() @IsString() password?: string; + @ApiPropertyOptional({ description: 'PIN code' }) @PinCode({ optional: true, nullable: true, emptyToNull: true }) pinCode?: string | null; + @ApiPropertyOptional({ description: 'User name' }) @Optional() @IsString() @IsNotEmpty() name?: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true }) + @ValidateEnum({ + enum: UserAvatarColor, + name: 'UserAvatarColor', + optional: true, + nullable: true, + description: 'Avatar color', + }) avatarColor?: UserAvatarColor | null; + @ApiPropertyOptional({ description: 'Storage label' }) @Optional({ nullable: true }) @IsString() @Transform(toSanitized) storageLabel?: string | null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) shouldChangePassword?: boolean; + @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) @Optional({ nullable: true }) @IsInt() @Min(0) - @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) isAdmin?: boolean; } export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) force?: boolean; } export class UserAdminResponseDto extends UserResponseDto { + @ApiProperty({ description: 'Storage label' }) storageLabel!: string | null; + @ApiProperty({ description: 'Require password change on next login' }) shouldChangePassword!: boolean; + @ApiProperty({ description: 'Is admin user' }) isAdmin!: boolean; + @ApiProperty({ description: 'Creation date' }) createdAt!: Date; + @ApiProperty({ description: 'Deletion date' }) deletedAt!: Date | null; + @ApiProperty({ description: 'Last update date' }) updatedAt!: Date; + @ApiProperty({ description: 'OAuth ID' }) oauthId!: string; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64' }) + @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) quotaUsageInBytes!: number | null; - @ValidateEnum({ enum: UserStatus, name: 'UserStatus' }) + @ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' }) status!: string; + @ApiProperty({ description: 'User license' }) license!: UserLicense | null; } diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index 7bfb90e11f..c4e5ac9c4c 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsNotEmpty, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator'; import { WorkflowAction, WorkflowFilter } from 'src/database'; @@ -6,65 +7,85 @@ import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; export class WorkflowFilterItemDto { + @ApiProperty({ description: 'Plugin filter ID' }) @IsUUID() pluginFilterId!: string; + @ApiPropertyOptional({ description: 'Filter configuration' }) @IsObject() @Optional() filterConfig?: FilterConfig; } export class WorkflowActionItemDto { + @ApiProperty({ description: 'Plugin action ID' }) @IsUUID() pluginActionId!: string; + @ApiPropertyOptional({ description: 'Action configuration' }) @IsObject() @Optional() actionConfig?: ActionConfig; } export class WorkflowCreateDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) triggerType!: PluginTriggerType; + @ApiProperty({ description: 'Workflow name' }) @IsString() @IsNotEmpty() name!: string; + @ApiPropertyOptional({ description: 'Workflow description' }) @IsString() @Optional() description?: string; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) enabled?: boolean; + @ApiProperty({ description: 'Workflow filters' }) @ValidateNested({ each: true }) @Type(() => WorkflowFilterItemDto) filters!: WorkflowFilterItemDto[]; + @ApiProperty({ description: 'Workflow actions' }) @ValidateNested({ each: true }) @Type(() => WorkflowActionItemDto) actions!: WorkflowActionItemDto[]; } export class WorkflowUpdateDto { + @ValidateEnum({ + enum: PluginTriggerType, + name: 'PluginTriggerType', + optional: true, + description: 'Workflow trigger type', + }) + triggerType?: PluginTriggerType; + + @ApiPropertyOptional({ description: 'Workflow name' }) @IsString() @IsNotEmpty() @Optional() name?: string; + @ApiPropertyOptional({ description: 'Workflow description' }) @IsString() @Optional() description?: string; - @ValidateBoolean({ optional: true }) + @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) enabled?: boolean; + @ApiPropertyOptional({ description: 'Workflow filters' }) @ValidateNested({ each: true }) @Type(() => WorkflowFilterItemDto) @Optional() filters?: WorkflowFilterItemDto[]; + @ApiPropertyOptional({ description: 'Workflow actions' }) @ValidateNested({ each: true }) @Type(() => WorkflowActionItemDto) @Optional() @@ -72,30 +93,49 @@ export class WorkflowUpdateDto { } export class WorkflowResponseDto { + @ApiProperty({ description: 'Workflow ID' }) id!: string; + @ApiProperty({ description: 'Owner user ID' }) ownerId!: string; + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) triggerType!: PluginTriggerType; + @ApiProperty({ description: 'Workflow name' }) name!: string | null; + @ApiProperty({ description: 'Workflow description' }) description!: string; + @ApiProperty({ description: 'Creation date' }) createdAt!: string; + @ApiProperty({ description: 'Workflow enabled' }) enabled!: boolean; + @ApiProperty({ description: 'Workflow filters' }) filters!: WorkflowFilterResponseDto[]; + @ApiProperty({ description: 'Workflow actions' }) actions!: WorkflowActionResponseDto[]; } export class WorkflowFilterResponseDto { + @ApiProperty({ description: 'Filter ID' }) id!: string; + @ApiProperty({ description: 'Workflow ID' }) workflowId!: string; + @ApiProperty({ description: 'Plugin filter ID' }) pluginFilterId!: string; + @ApiProperty({ description: 'Filter configuration' }) filterConfig!: FilterConfig | null; + @ApiProperty({ description: 'Filter order', type: 'number' }) order!: number; } export class WorkflowActionResponseDto { + @ApiProperty({ description: 'Action ID' }) id!: string; + @ApiProperty({ description: 'Workflow ID' }) workflowId!: string; + @ApiProperty({ description: 'Plugin action ID' }) pluginActionId!: string; + @ApiProperty({ description: 'Action configuration' }) actionConfig!: ActionConfig | null; + @ApiProperty({ description: 'Action order', type: 'number' }) order!: number; } diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx index c84246bf87..324d7dc003 100644 --- a/server/src/emails/components/footer.template.tsx +++ b/server/src/emails/components/footer.template.tsx @@ -7,14 +7,22 @@ export const ImmichFooter = () => (
- + Get it on Google Play
- Immich + Download on the App Store
diff --git a/server/src/enum.ts b/server/src/enum.ts index 77b10585c9..82bc107b7e 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -108,6 +108,11 @@ export enum Permission { AssetUpload = 'asset.upload', AssetReplace = 'asset.replace', AssetCopy = 'asset.copy', + AssetDerive = 'asset.derive', + + AssetEditGet = 'asset.edit.get', + AssetEditCreate = 'asset.edit.create', + AssetEditDelete = 'asset.edit.delete', AlbumCreate = 'album.create', AlbumRead = 'album.read', @@ -130,6 +135,11 @@ export enum Permission { ArchiveRead = 'archive.read', + BackupList = 'backup.list', + BackupDownload = 'backup.download', + BackupUpload = 'backup.upload', + BackupDelete = 'backup.delete', + DuplicateRead = 'duplicate.read', DuplicateDelete = 'duplicate.delete', @@ -138,6 +148,8 @@ export enum Permission { FaceUpdate = 'face.update', FaceDelete = 'face.delete', + FolderRead = 'folder.read', + JobCreate = 'job.create', JobRead = 'job.read', @@ -152,6 +164,9 @@ export enum Permission { Maintenance = 'maintenance', + MapRead = 'map.read', + MapSearch = 'map.search', + MemoryCreate = 'memory.create', MemoryRead = 'memory.read', MemoryUpdate = 'memory.update', @@ -364,7 +379,6 @@ export enum AssetPathType { /** Folder structure containing tiles of the image */ Tiles = 'tiles', EncodedVideo = 'encoded_video', - Sidecar = 'sidecar', } export enum PersonPathType { @@ -375,7 +389,7 @@ export enum UserPathType { Profile = 'profile', } -export type PathType = AssetPathType | PersonPathType | UserPathType; +export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { All = 'all', @@ -449,6 +463,10 @@ export enum RawExtractedFormat { Jxl = 'jxl', } +export enum TilesFormat { + Dz = 'dz', +} + export enum LogLevel { Verbose = 'verbose', Debug = 'debug', @@ -458,6 +476,11 @@ export enum LogLevel { Fatal = 'fatal', } +export enum LogFormat { + Console = 'console', + Json = 'json', +} + export enum ApiCustomExtension { Permission = 'x-immich-permission', AdminOnly = 'x-immich-admin-only', @@ -554,6 +577,7 @@ export enum QueueName { BackupDatabase = 'backupDatabase', Ocr = 'ocr', Workflow = 'workflow', + Editor = 'editor', } export enum QueueJobStatus { @@ -572,6 +596,7 @@ export enum JobName { AssetDetectFaces = 'AssetDetectFaces', AssetDetectDuplicatesQueueAll = 'AssetDetectDuplicatesQueueAll', AssetDetectDuplicates = 'AssetDetectDuplicates', + AssetEditThumbnailGeneration = 'AssetEditThumbnailGeneration', AssetEncodeVideoQueueAll = 'AssetEncodeVideoQueueAll', AssetEncodeVideo = 'AssetEncodeVideo', AssetEmptyTrash = 'AssetEmptyTrash', @@ -683,12 +708,15 @@ export enum DatabaseLock { MediaLocation = 700, GetSystemConfig = 69, BackupDatabase = 42, + MaintenanceOperation = 621, MemoryCreation = 777, } export enum MaintenanceAction { Start = 'start', End = 'end', + SelectDatabaseRestore = 'select_database_restore', + RestoreDatabase = 'restore_database', } export enum ExitCode { @@ -703,6 +731,7 @@ export enum SyncRequestType { AlbumAssetExifsV1 = 'AlbumAssetExifsV1', AssetsV1 = 'AssetsV1', AssetExifsV1 = 'AssetExifsV1', + AssetEditsV1 = 'AssetEditsV1', AssetMetadataV1 = 'AssetMetadataV1', AuthUsersV1 = 'AuthUsersV1', MemoriesV1 = 'MemoriesV1', @@ -715,6 +744,7 @@ export enum SyncRequestType { UsersV1 = 'UsersV1', PeopleV1 = 'PeopleV1', AssetFacesV1 = 'AssetFacesV1', + AssetFacesV2 = 'AssetFacesV2', UserMetadataV1 = 'UserMetadataV1', } @@ -727,6 +757,8 @@ export enum SyncEntityType { AssetV1 = 'AssetV1', AssetDeleteV1 = 'AssetDeleteV1', AssetExifV1 = 'AssetExifV1', + AssetEditV1 = 'AssetEditV1', + AssetEditDeleteV1 = 'AssetEditDeleteV1', AssetMetadataV1 = 'AssetMetadataV1', AssetMetadataDeleteV1 = 'AssetMetadataDeleteV1', @@ -773,6 +805,7 @@ export enum SyncEntityType { PersonDeleteV1 = 'PersonDeleteV1', AssetFaceV1 = 'AssetFaceV1', + AssetFaceV2 = 'AssetFaceV2', AssetFaceDeleteV1 = 'AssetFaceDeleteV1', UserMetadataV1 = 'UserMetadataV1', @@ -804,14 +837,6 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretBasic = 'client_secret_basic', } -export enum DatabaseSslMode { - Disable = 'disable', - Allow = 'allow', - Prefer = 'prefer', - Require = 'require', - VerifyFull = 'verify-full', -} - export enum AssetVisibility { Archive = 'archive', Timeline = 'timeline', @@ -835,6 +860,7 @@ export enum ApiTag { Authentication = 'Authentication', AuthenticationAdmin = 'Authentication (admin)', Assets = 'Assets', + DatabaseBackups = 'Database Backups (admin)', Deprecated = 'Deprecated', Download = 'Download', Duplicates = 'Duplicates', diff --git a/server/src/main.ts b/server/src/main.ts index 47185e846f..f2491f07bc 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,11 +1,11 @@ -import { Kysely } from 'kysely'; +import { Kysely, sql } from 'kysely'; import { CommandFactory } from 'nest-commander'; import { ChildProcess, fork } from 'node:child_process'; import { dirname, join } from 'node:path'; import { Worker } from 'node:worker_threads'; import { PostgresError } from 'postgres'; import { ImmichAdminModule } from 'src/app.module'; -import { ExitCode, ImmichWorker, LogLevel, SystemMetadataKey } from 'src/enum'; +import { DatabaseLock, ExitCode, ImmichWorker, LogLevel, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { type DB } from 'src/schema'; @@ -35,27 +35,26 @@ class Workers { if (isMaintenanceMode) { this.startWorker(ImmichWorker.Maintenance); } else { + await this.waitForFreeLock(); + for (const worker of workers) { this.startWorker(worker); } } } - /** - * Initialise a short-lived Nest application to build configuration - * @returns System configuration - */ private async isMaintenanceMode(): Promise { const { database } = new ConfigRepository().getEnv(); - const kysely = new Kysely(getKyselyConfig(database.config)); + const { log: _, ...kyselyConfig } = getKyselyConfig(database.config); + const kysely = new Kysely(kyselyConfig); const systemMetadataRepository = new SystemMetadataRepository(kysely); try { const value = await systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode); return value?.isMaintenanceMode || false; - } catch (error) { + } catch (error: Error | any) { // Table doesn't exist (migrations haven't run yet) - if (error instanceof PostgresError && error.code === '42P01') { + if ((error as PostgresError).code === '42P01') { return false; } @@ -65,6 +64,32 @@ class Workers { } } + private async waitForFreeLock() { + const { database } = new ConfigRepository().getEnv(); + const kysely = new Kysely(getKyselyConfig(database.config)); + + let locked = false; + while (!locked) { + locked = await kysely.connection().execute(async (conn) => { + const { rows } = await sql<{ + pg_try_advisory_lock: boolean; + }>`SELECT pg_try_advisory_lock(${DatabaseLock.MaintenanceOperation})`.execute(conn); + + const isLocked = rows[0].pg_try_advisory_lock; + + if (isLocked) { + await sql`SELECT pg_advisory_unlock(${DatabaseLock.MaintenanceOperation})`.execute(conn); + } + + return isLocked; + }); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + await kysely.destroy(); + } + /** * Start an individual worker * @param name Worker diff --git a/server/src/maintenance/maintenance-health.repository.ts b/server/src/maintenance/maintenance-health.repository.ts new file mode 100644 index 0000000000..aeef93ec51 --- /dev/null +++ b/server/src/maintenance/maintenance-health.repository.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { fork } from 'node:child_process'; +import { dirname, join } from 'node:path'; + +@Injectable() +export class MaintenanceHealthRepository { + checkApiHealth(): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line unicorn/prefer-module + const basePath = dirname(__filename); + const workerFile = join(basePath, '..', 'workers', `api.js`); + + const worker = fork(workerFile, [], { + execArgv: process.execArgv.filter((arg) => !arg.startsWith('--inspect')), + env: { + ...process.env, + IMMICH_HOST: '127.0.0.1', + IMMICH_PORT: '33001', + }, + stdio: ['ignore', 'pipe', 'ignore', 'ipc'], + }); + + async function checkHealth() { + try { + const response = await fetch('http://127.0.0.1:33001/api/server/config'); + const { isOnboarded } = await response.json(); + if (isOnboarded) { + resolve(); + } else { + reject(new Error('Server health check failed, no admin exists.')); + } + } catch (error) { + reject(error); + } finally { + if (worker.exitCode === null) { + worker.kill('SIGTERM'); + } + } + } + + let output = '', + alive = false; + + worker.stdout?.on('data', (data) => { + if (alive) { + return; + } + + output += data; + + if (output.includes('Immich Server is listening')) { + alive = true; + void checkHealth(); + } + }); + + worker.on('exit', reject); + worker.on('error', reject); + + setTimeout(() => { + if (worker.exitCode === null) { + worker.kill('SIGTERM'); + } + }, 20_000); + }); + } +} diff --git a/server/src/maintenance/maintenance-websocket.repository.ts b/server/src/maintenance/maintenance-websocket.repository.ts index 5d8368cf69..d13ceb083f 100644 --- a/server/src/maintenance/maintenance-websocket.repository.ts +++ b/server/src/maintenance/maintenance-websocket.repository.ts @@ -7,17 +7,24 @@ import { WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; +import { MaintenanceAuthDto, MaintenanceStatusResponseDto } from 'src/dtos/maintenance.dto'; import { AppRepository } from 'src/repositories/app.repository'; import { AppRestartEvent, ArgsOf } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -export const serverEvents = ['AppRestart'] as const; -export type ServerEvents = (typeof serverEvents)[number]; - -export interface ClientEventMap { - AppRestartV1: [AppRestartEvent]; +interface ServerEventMap { + AppRestart: [AppRestartEvent]; + MaintenanceStatus: [MaintenanceStatusResponseDto]; } +interface ClientEventMap { + AppRestartV1: [AppRestartEvent]; + MaintenanceStatusV1: [MaintenanceStatusResponseDto]; +} + +type AuthFn = (client: Socket) => Promise; +type StatusUpdateFn = (status: MaintenanceStatusResponseDto) => void; + @WebSocketGateway({ cors: true, path: '/api/socket.io', @@ -25,8 +32,11 @@ export interface ClientEventMap { }) @Injectable() export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit { + private authFn?: AuthFn; + private statusUpdateFn?: StatusUpdateFn; + @WebSocketServer() - private websocketServer?: Server; + private server?: Server; constructor( private logger: LoggingRepository, @@ -35,25 +45,51 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa this.logger.setContext(MaintenanceWebsocketRepository.name); } - afterInit(websocketServer: Server) { + afterInit(server: Server) { this.logger.log('Initialized websocket server'); - websocketServer.on('AppRestart', () => this.appRepository.exitApp()); + server.on('MaintenanceStatus', (status) => this.statusUpdateFn?.(status)); + server.on('AppRestart', (event: ArgsOf<'AppRestart'>, ack?: (ok: 'ok') => void) => { + this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`); + + ack?.('ok'); + this.appRepository.exitApp(); + }); + } + + clientSend(event: T, room: string, ...data: ClientEventMap[T]) { + this.server?.to(room).emit(event, ...data); } clientBroadcast(event: T, ...data: ClientEventMap[T]) { - this.websocketServer?.emit(event, ...data); + this.server?.emit(event, ...data); } - serverSend(event: T, ...args: ArgsOf): void { + serverSend(event: T, ...args: ServerEventMap[T]): void { this.logger.debug(`Server event: ${event} (send)`); - this.websocketServer?.serverSideEmit(event, ...args); + this.server?.serverSideEmit(event, ...args); } - handleConnection(client: Socket) { - this.logger.log(`Websocket Connect: ${client.id}`); + async handleConnection(client: Socket) { + try { + await this.authFn!(client); + await client.join('private'); + this.logger.log(`Websocket Connect: ${client.id} (private)`); + } catch { + await client.join('public'); + this.logger.log(`Websocket Connect: ${client.id} (public)`); + } } - handleDisconnect(client: Socket) { + async handleDisconnect(client: Socket) { this.logger.log(`Websocket Disconnect: ${client.id}`); + await Promise.allSettled([client.leave('private'), client.leave('public')]); + } + + setAuthFn(fn: (client: Socket) => Promise) { + this.authFn = fn; + } + + setStatusUpdateFn(fn: (status: MaintenanceStatusResponseDto) => void) { + this.statusUpdateFn = fn; } } diff --git a/server/src/maintenance/maintenance-worker.controller.ts b/server/src/maintenance/maintenance-worker.controller.ts index e6143b771a..162fa27257 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -1,23 +1,116 @@ -import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common'; -import { Request, Response } from 'express'; -import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; -import { ServerConfigDto } from 'src/dtos/server.dto'; -import { ImmichCookie, MaintenanceAction } from 'src/enum'; +import { + Body, + Controller, + Delete, + Get, + Next, + Param, + Post, + Req, + Res, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { NextFunction, Request, Response } from 'express'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceLoginDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { ServerConfigDto, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { ImmichCookie } from 'src/enum'; import { MaintenanceRoute } from 'src/maintenance/maintenance-auth.guard'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; import { GetLoginDetails } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoginDetails } from 'src/services/auth.service'; +import { sendFile } from 'src/utils/file'; import { respondWithCookie } from 'src/utils/response'; +import { FilenameParamDto } from 'src/validation'; + +import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller'; +import type { ServerController as _ServerController } from 'src/controllers/server.controller'; +import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; @Controller() export class MaintenanceWorkerController { - constructor(private service: MaintenanceWorkerService) {} + constructor( + private logger: LoggingRepository, + private service: MaintenanceWorkerService, + private databaseBackupService: DatabaseBackupService, + ) {} + /** + * {@link _ServerController.getServerConfig } + */ @Get('server/config') - getServerConfig(): Promise { + getServerConfig(): ServerConfigDto { return this.service.getSystemConfig(); } + @Get('server/version') + getServerVersion(): ServerVersionResponseDto { + return this.service.getVersion(); + } + + /** + * {@link _DatabaseBackupController.listDatabaseBackups} + */ + @Get('admin/database-backups') + @MaintenanceRoute() + listDatabaseBackups(): Promise { + return this.databaseBackupService.listBackups(); + } + + /** + * {@link _DatabaseBackupController.downloadDatabaseBackup} + */ + @Get('admin/database-backups/:filename') + @MaintenanceRoute() + async downloadDatabaseBackup( + @Param() { filename }: FilenameParamDto, + @Res() res: Response, + @Next() next: NextFunction, + ) { + await sendFile(res, next, () => this.databaseBackupService.downloadBackup(filename), this.logger); + } + + /** + * {@link _DatabaseBackupController.deleteDatabaseBackup} + */ + @Delete('admin/database-backups') + @MaintenanceRoute() + async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { + return this.databaseBackupService.deleteBackup(dto.backups); + } + + /** + * {@link _DatabaseBackupController.uploadDatabaseBackup} + */ + @Post('admin/database-backups/upload') + @MaintenanceRoute() + @UseInterceptors(FileInterceptor('file')) + uploadDatabaseBackup( + @UploadedFile() + file: Express.Multer.File, + ): Promise { + return this.databaseBackupService.uploadBackup(file); + } + + @Get('admin/maintenance/status') + maintenanceStatus(@Req() request: Request): Promise { + return this.service.status(request.cookies[ImmichCookie.MaintenanceToken]); + } + + @Get('admin/maintenance/detect-install') + detectPriorInstall(): Promise { + return this.service.detectPriorInstall(); + } + @Post('admin/maintenance/login') async maintenanceLogin( @Req() request: Request, @@ -35,9 +128,7 @@ export class MaintenanceWorkerController { @Post('admin/maintenance') @MaintenanceRoute() - async setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): Promise { - if (dto.action === MaintenanceAction.End) { - await this.service.endMaintenance(); - } + setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): void { + void this.service.setAction(dto); } } diff --git a/server/src/maintenance/maintenance-worker.service.spec.ts b/server/src/maintenance/maintenance-worker.service.spec.ts index dd5b984214..1d5bee62b0 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -1,25 +1,61 @@ import { UnauthorizedException } from '@nestjs/common'; import { SignJWT } from 'jose'; -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service'; -import { automock, getMocks, ServiceMocks } from 'test/utils'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { automock, AutoMocked, getMocks, ServiceMocks } from 'test/utils'; describe(MaintenanceWorkerService.name, () => { let sut: MaintenanceWorkerService; let mocks: ServiceMocks; - let maintenanceWorkerRepositoryMock: MaintenanceWebsocketRepository; + let maintenanceWebsocketRepositoryMock: AutoMocked; + let maintenanceHealthRepositoryMock: AutoMocked; + let databaseBackupServiceMock: AutoMocked; beforeEach(() => { mocks = getMocks(); - maintenanceWorkerRepositoryMock = automock(MaintenanceWebsocketRepository, { args: [mocks.logger], strict: false }); + maintenanceWebsocketRepositoryMock = automock(MaintenanceWebsocketRepository, { + args: [mocks.logger], + strict: false, + }); + maintenanceHealthRepositoryMock = automock(MaintenanceHealthRepository, { + args: [mocks.logger], + strict: false, + }); + databaseBackupServiceMock = automock(DatabaseBackupService, { + args: [ + mocks.logger, + mocks.storage, + mocks.config, + mocks.systemMetadata, + mocks.process, + mocks.database, + mocks.cron, + mocks.job, + maintenanceHealthRepositoryMock, + ], + strict: false, + }); + sut = new MaintenanceWorkerService( mocks.logger as never, mocks.app, mocks.config, mocks.systemMetadata as never, - maintenanceWorkerRepositoryMock, + maintenanceWebsocketRepositoryMock, + maintenanceHealthRepositoryMock, + mocks.storage as never, + mocks.process, + mocks.database as never, + databaseBackupServiceMock, ); + + sut.mock({ + active: true, + action: MaintenanceAction.Start, + }); }); it('should work', () => { @@ -27,14 +63,43 @@ describe(MaintenanceWorkerService.name, () => { }); describe('getSystemConfig', () => { - it('should respond the server is in maintenance mode', async () => { - await expect(sut.getSystemConfig()).resolves.toMatchObject( + it('should respond the server is in maintenance mode', () => { + expect(sut.getSystemConfig()).toMatchObject( expect.objectContaining({ maintenanceMode: true, }), ); - expect(mocks.systemMetadata.get).toHaveBeenCalled(); + expect(mocks.systemMetadata.get).toHaveBeenCalledTimes(0); + }); + }); + + describe.skip('ssr'); + describe.skip('detectMediaLocation'); + + describe('setStatus', () => { + it('should broadcast status', () => { + sut.setStatus({ + active: true, + action: MaintenanceAction.Start, + task: 'abc', + error: 'def', + }); + + expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalled(); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledTimes(2); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: 'start', + task: 'abc', + error: 'def', + }); + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'public', { + active: true, + action: 'start', + task: 'abc', + error: 'Something went wrong, see logs!', + }); }); }); @@ -42,7 +107,14 @@ describe(MaintenanceWorkerService.name, () => { const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/; it('should log a valid login URL', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.logSecret()).resolves.toBeUndefined(); expect(mocks.logger.log).toHaveBeenCalledWith(expect.stringMatching(RE_LOGIN_URL)); @@ -63,7 +135,13 @@ describe(MaintenanceWorkerService.name, () => { }); it('should parse cookie properly', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); await expect( sut.authenticate({ @@ -73,13 +151,102 @@ describe(MaintenanceWorkerService.name, () => { }); }); + describe('status', () => { + beforeEach(() => { + sut.mock({ + active: true, + action: MaintenanceAction.Start, + error: 'secret value!', + }); + }); + + it('generates private status', async () => { + const jwt = await new SignJWT({ _mockValue: true }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('4h') + .sign(new TextEncoder().encode('secret')); + + await expect(sut.status(jwt)).resolves.toEqual( + expect.objectContaining({ + error: 'secret value!', + }), + ); + }); + + it('generates public status', async () => { + await expect(sut.status()).resolves.toEqual( + expect.objectContaining({ + error: 'Something went wrong, see logs!', + }), + ); + }); + }); + + describe('detectPriorInstall', () => { + it('generate report about prior installation', async () => { + mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']); + mocks.storage.readFile.mockResolvedValue(undefined as never); + mocks.storage.overwriteFile.mockRejectedValue(undefined as never); + + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` + { + "storage": [ + { + "files": 2, + "folder": "encoded-video", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "library", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "upload", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "profile", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "thumbs", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "backups", + "readable": true, + "writable": false, + }, + ], + } + `); + }); + }); + describe('login', () => { it('should fail without token', async () => { await expect(sut.login()).rejects.toThrowError(new UnauthorizedException('Missing JWT Token')); }); it('should fail with expired JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const jwt = await new SignJWT({}) .setProtectedHeader({ alg: 'HS256' }) @@ -91,7 +258,13 @@ describe(MaintenanceWorkerService.name, () => { }); it('should succeed with valid JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const jwt = await new SignJWT({ _mockValue: true }) .setProtectedHeader({ alg: 'HS256' }) @@ -107,22 +280,114 @@ describe(MaintenanceWorkerService.name, () => { }); }); - describe('endMaintenance', () => { + describe.skip('setAction'); // just calls setStatus+runAction + + /** + * Actions + */ + + describe('action: start', () => { + it('should not do anything', async () => { + await sut.runAction({ + action: MaintenanceAction.Start, + }); + + expect(mocks.logger.log).toHaveBeenCalledTimes(0); + }); + }); + + describe('action: end', () => { it('should set maintenance mode', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.endMaintenance()).resolves.toBeUndefined(); + await sut.runAction({ + action: MaintenanceAction.End, + }); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: false, }); - expect(maintenanceWorkerRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', { + expect(maintenanceWebsocketRepositoryMock.clientBroadcast).toHaveBeenCalledWith('AppRestartV1', { isMaintenanceMode: false, }); - expect(maintenanceWorkerRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', { + expect(maintenanceWebsocketRepositoryMock.serverSend).toHaveBeenCalledWith('AppRestart', { isMaintenanceMode: false, }); }); }); + + describe('action: restore database', () => { + beforeEach(() => { + mocks.database.tryLock.mockResolvedValueOnce(true); + }); + + it('should update maintenance mode state', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'filename', + }); + + expect(mocks.database.tryLock).toHaveBeenCalled(); + expect(mocks.logger.log).toHaveBeenCalledWith('Running maintenance action restore_database'); + + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: 'secret', + action: { + action: 'start', + }, + }); + }); + + it('should defer to database backup service', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + { + active: true, + action: MaintenanceAction.RestoreDatabase, + task: 'ready', + progress: expect.any(Number), + }, + ); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + { + active: true, + action: 'end', + }, + ); + }); + + it('should forward errors from database backup service', async () => { + databaseBackupServiceMock.restoreDatabaseBackup.mockRejectedValue('Sample error'); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Sample error', + task: 'error', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'public', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Something went wrong, see logs!', + task: 'error', + }); + }); + }); }); diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index c03231c274..9ceb3caa43 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -4,19 +4,33 @@ import { NextFunction, Request, Response } from 'express'; import { jwtVerify } from 'jose'; import { readFileSync } from 'node:fs'; import { IncomingHttpHeaders } from 'node:http'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { ImmichCookie, SystemMetadataKey } from 'src/enum'; +import { serverVersion } from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { ServerConfigDto, ServerVersionResponseDto } from 'src/dtos/server.dto'; +import { DatabaseLock, ImmichCookie, MaintenanceAction, SystemMetadataKey } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository'; import { AppRepository } from 'src/repositories/app.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { type ApiService as _ApiService } from 'src/services/api.service'; import { type BaseService as _BaseService } from 'src/services/base.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { type ServerService as _ServerService } from 'src/services/server.service'; +import { type VersionService as _VersionService } from 'src/services/version.service'; import { MaintenanceModeState } from 'src/types'; import { getConfig } from 'src/utils/config'; -import { createMaintenanceLoginUrl } from 'src/utils/maintenance'; +import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -24,16 +38,55 @@ import { getExternalDomain } from 'src/utils/misc'; */ @Injectable() export class MaintenanceWorkerService { + #secret: string | null = null; + #status: MaintenanceStatusResponseDto = { + active: true, + action: MaintenanceAction.Start, + }; + constructor( protected logger: LoggingRepository, private appRepository: AppRepository, private configRepository: ConfigRepository, private systemMetadataRepository: SystemMetadataRepository, - private maintenanceWorkerRepository: MaintenanceWebsocketRepository, + private maintenanceWebsocketRepository: MaintenanceWebsocketRepository, + private maintenanceHealthRepository: MaintenanceHealthRepository, + private storageRepository: StorageRepository, + private processRepository: ProcessRepository, + private databaseRepository: DatabaseRepository, + private databaseBackupService: DatabaseBackupService, ) { this.logger.setContext(this.constructor.name); } + mock(status: MaintenanceStatusResponseDto) { + this.#secret = 'secret'; + this.#status = status; + } + + async init() { + const state = (await this.systemMetadataRepository.get( + SystemMetadataKey.MaintenanceMode, + )) as MaintenanceModeState & { isMaintenanceMode: true }; + + this.#secret = state.secret; + this.#status = { + active: true, + action: state.action?.action ?? MaintenanceAction.Start, + }; + + StorageCore.setMediaLocation(this.detectMediaLocation()); + + this.maintenanceWebsocketRepository.setAuthFn(async (client) => this.authenticate(client.request.headers)); + this.maintenanceWebsocketRepository.setStatusUpdateFn((status) => (this.#status = status)); + + await this.logSecret(); + + if (state.action) { + void this.runAction(state.action); + } + } + /** * {@link _BaseService.configRepos} */ @@ -55,22 +108,17 @@ export class MaintenanceWorkerService { /** * {@link _ServerService.getSystemConfig} */ - async getSystemConfig() { - const config = await this.getConfig({ withCache: false }); - + getSystemConfig() { return { - loginPageMessage: config.server.loginPageMessage, - trashDays: config.trash.days, - userDeleteDelay: config.user.deleteDelay, - oauthButtonText: config.oauth.buttonText, - isInitialized: true, - isOnboarded: true, - externalDomain: config.server.externalDomain, - publicUsers: config.server.publicUsers, - mapDarkStyleUrl: config.map.darkStyle, - mapLightStyleUrl: config.map.lightStyle, maintenanceMode: true, - }; + } as ServerConfigDto; + } + + /** + * {@link _VersionService.getVersion} + */ + getVersion() { + return ServerVersionResponseDto.fromSemVer(serverVersion); } /** @@ -106,12 +154,70 @@ export class MaintenanceWorkerService { }; } - private async secret(): Promise { - const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) as { - secret: string; - }; + /** + * {@link _StorageService.detectMediaLocation} + */ + detectMediaLocation(): string { + const envData = this.configRepository.getEnv(); + if (envData.storage.mediaLocation) { + return envData.storage.mediaLocation; + } - return state.secret; + const targets: string[] = []; + const candidates = ['/data', '/usr/src/app/upload']; + + for (const candidate of candidates) { + const exists = this.storageRepository.existsSync(candidate); + if (exists) { + targets.push(candidate); + } + } + + if (targets.length === 1) { + return targets[0]; + } + + return '/usr/src/app/upload'; + } + + private get secret() { + if (!this.#secret) { + throw new Error('Secret is not initialised yet.'); + } + + return this.#secret; + } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + health: this.maintenanceHealthRepository, + }; + } + + private getStatus(): MaintenanceStatusResponseDto { + return this.#status; + } + + private getPublicStatus(): MaintenanceStatusResponseDto { + const state = structuredClone(this.#status); + + if (state.error) { + state.error = 'Something went wrong, see logs!'; + } + + return state; + } + + setStatus(status: MaintenanceStatusResponseDto): void { + this.#status = status; + this.maintenanceWebsocketRepository.serverSend('MaintenanceStatus', status); + this.maintenanceWebsocketRepository.clientSend('MaintenanceStatusV1', 'private', status); + this.maintenanceWebsocketRepository.clientSend('MaintenanceStatusV1', 'public', this.getPublicStatus()); } async logSecret(): Promise { @@ -123,7 +229,7 @@ export class MaintenanceWorkerService { { username: 'immich-admin', }, - await this.secret(), + this.secret, ); this.logger.log(`\n\n🚧 Immich is in maintenance mode, you can log in using the following URL:\n${url}\n`); @@ -134,28 +240,115 @@ export class MaintenanceWorkerService { return this.login(jwtToken); } + async status(potentiallyJwt?: string): Promise { + try { + await this.login(potentiallyJwt); + return this.getStatus(); + } catch { + return this.getPublicStatus(); + } + } + + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); + } + async login(jwt?: string): Promise { if (!jwt) { throw new UnauthorizedException('Missing JWT Token'); } - const secret = await this.secret(); - try { - const result = await jwtVerify(jwt, new TextEncoder().encode(secret)); + const result = await jwtVerify(jwt, new TextEncoder().encode(this.secret)); return result.payload; } catch { throw new UnauthorizedException('Invalid JWT Token'); } } - async endMaintenance(): Promise { + async setAction(action: SetMaintenanceModeDto) { + this.setStatus({ + active: true, + action: action.action, + }); + + await this.runAction(action); + } + + async runAction(action: SetMaintenanceModeDto) { + switch (action.action) { + case MaintenanceAction.Start: { + return; + } + case MaintenanceAction.End: { + return this.endMaintenance(); + } + case MaintenanceAction.SelectDatabaseRestore: { + return; + } + } + + const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation); + if (!lock) { + return; + } + + this.logger.log(`Running maintenance action ${action.action}`); + + await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: this.secret, + action: { + action: MaintenanceAction.Start, + }, + }); + + try { + if (!action.restoreBackupFilename) { + throw new Error("Expected restoreBackupFilename but it's missing!"); + } + + await this.restoreBackup(action.restoreBackupFilename); + } catch (error) { + this.logger.error(`Encountered error running action: ${error}`); + this.setStatus({ + active: true, + action: action.action, + task: 'error', + error: '' + error, + }); + } + } + + private async restoreBackup(filename: string): Promise { + this.setStatus({ + active: true, + action: MaintenanceAction.RestoreDatabase, + task: 'ready', + progress: 0, + }); + + await this.databaseBackupService.restoreDatabaseBackup(filename, (task, progress) => + this.setStatus({ + active: true, + action: MaintenanceAction.RestoreDatabase, + progress, + task, + }), + ); + + await this.setAction({ + action: MaintenanceAction.End, + }); + } + + private async endMaintenance(): Promise { const state: MaintenanceModeState = { isMaintenanceMode: false as const }; await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state); // => corresponds to notification.service.ts#onAppRestart - this.maintenanceWorkerRepository.clientBroadcast('AppRestartV1', state); - this.maintenanceWorkerRepository.serverSend('AppRestart', state); + this.maintenanceWebsocketRepository.clientBroadcast('AppRestartV1', state); + this.maintenanceWebsocketRepository.serverSend('AppRestart', state); this.appRepository.exitApp(); } } diff --git a/server/src/plugins.ts b/server/src/plugins.ts index 0c69483696..77f35e79f6 100644 --- a/server/src/plugins.ts +++ b/server/src/plugins.ts @@ -1,37 +1,17 @@ import { PluginContext, PluginTriggerType } from 'src/enum'; -import { JSONSchema } from 'src/types/plugin-schema.types'; export type PluginTrigger = { - name: string; type: PluginTriggerType; - description: string; - context: PluginContext; - schema: JSONSchema | null; + contextType: PluginContext; }; export const pluginTriggers: PluginTrigger[] = [ { - name: 'Asset Uploaded', type: PluginTriggerType.AssetCreate, - description: 'Triggered when a new asset is uploaded', - context: PluginContext.Asset, - schema: { - type: 'object', - properties: { - assetType: { - type: 'string', - description: 'Type of the asset', - default: 'ALL', - enum: ['Image', 'Video', 'All'], - }, - }, - }, + contextType: PluginContext.Asset, }, { - name: 'Person Recognized', type: PluginTriggerType.PersonRecognized, - description: 'Triggered when a person is detected in an asset', - context: PluginContext.Person, - schema: null, + contextType: PluginContext.Person, }, ]; diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index f62e769a17..e3d7436c30 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -58,7 +58,7 @@ select from ( select - * + "shared_link".* from "shared_link" where @@ -243,7 +243,7 @@ select from ( select - * + "shared_link".* from "shared_link" where @@ -316,7 +316,7 @@ select from ( select - * + "shared_link".* from "shared_link" where diff --git a/server/src/queries/album.user.repository.sql b/server/src/queries/album.user.repository.sql index a758ba1cf4..fc4a52bae2 100644 --- a/server/src/queries/album.user.repository.sql +++ b/server/src/queries/album.user.repository.sql @@ -17,8 +17,6 @@ set where "userId" = $2 and "albumId" = $3 -returning - * -- AlbumUserRepository.delete delete from "album_user" diff --git a/server/src/queries/asset.edit.repository.sql b/server/src/queries/asset.edit.repository.sql new file mode 100644 index 0000000000..44dca38031 --- /dev/null +++ b/server/src/queries/asset.edit.repository.sql @@ -0,0 +1,34 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AssetEditRepository.replaceAll +begin +delete from "asset_edit" +where + "assetId" = $1 +rollback + +-- AssetEditRepository.getAll +select + "id", + "action", + "parameters" +from + "asset_edit" +where + "assetId" = $1 +order by + "sequence" asc + +-- AssetEditRepository.getWithSyncInfo +select + "asset_edit"."id", + "asset_edit"."assetId", + "asset_edit"."sequence", + "asset_edit"."action", + "asset_edit"."parameters" +from + "asset_edit" +where + "assetId" = $1 +order by + "sequence" asc diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index b736998e28..a9c407782b 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -29,7 +29,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -37,20 +38,6 @@ select and "asset_file"."type" = $1 ) as agg ) as "files", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "tag"."value" - from - "tag" - inner join "tag_asset" on "tag"."id" = "tag_asset"."tagId" - where - "asset"."id" = "tag_asset"."assetId" - ) as agg - ) as "tags", to_json("asset_exif") as "exifInfo" from "asset" @@ -72,7 +59,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -90,32 +78,97 @@ limit -- AssetJobRepository.streamForThumbnailJob select "asset"."id", - "asset"."thumbhash", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "asset_file"."id", - "asset_file"."path", - "asset_file"."type" - from - "asset_file" - where - "asset_file"."assetId" = "asset"."id" - ) as agg - ) as "files" + "asset"."isEdited" from "asset" inner join "asset_job_status" on "asset_job_status"."assetId" = "asset"."id" where "asset"."deletedAt" is null - and "asset"."visibility" != $1 + and "asset"."visibility" != 'hidden' and ( - "asset_job_status"."previewAt" is null - or "asset_job_status"."thumbnailAt" is null + not exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "type" = 'thumbnail' + ) + or not exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "type" = 'preview' + ) + or ( + "asset"."isEdited" = true + and not exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "type" = 'fullsize' + and "asset_file"."isEdited" = true + ) + ) or "asset"."thumbhash" is null + or ( + not exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "type" = 'fullsize' + ) + and f_unaccent (asset."originalFileName") like any ( + array[ + '%.3fr', + '%.ari', + '%.arw', + '%.cap', + '%.cin', + '%.cr2', + '%.cr3', + '%.crw', + '%.dcr', + '%.dng', + '%.erf', + '%.fff', + '%.iiq', + '%.k25', + '%.kdc', + '%.mrw', + '%.nef', + '%.nrw', + '%.orf', + '%.ori', + '%.pef', + '%.psd', + '%.raf', + '%.raw', + '%.rw2', + '%.rwl', + '%.sr2', + '%.srf', + '%.srw', + '%.x3f', + '%.heic', + '%.heif', + '%.hif', + '%.insp', + '%.jp2', + '%.jpe', + '%.jxl', + '%.svg', + '%.tif', + '%.tiff' + ]::text[] + ) + ) ) -- AssetJobRepository.getForMigrationJob @@ -131,7 +184,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -160,19 +214,37 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited", + "asset_file"."isProgressive", + "asset_file"."isTransparent" from "asset_file" where "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" in ($1, $2, $3) ) as agg ) as "files", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_edit"."action", + "asset_edit"."parameters" + from + "asset_edit" + where + "asset_edit"."assetId" = "asset"."id" + ) as agg + ) as "edits", to_json("asset_exif") as "exifInfo" from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where - "asset"."id" = $1 + "asset"."id" = $4 -- AssetJobRepository.getForMetadataExtraction select @@ -191,6 +263,8 @@ select "asset"."originalPath", "asset"."ownerId", "asset"."type", + "asset"."width", + "asset"."height", ( select coalesce(json_agg(agg), '[]') @@ -203,6 +277,7 @@ select where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" = $1 ) as agg ) as "faces", ( @@ -213,18 +288,19 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where "asset_file"."assetId" = "asset"."id" - and "asset_file"."type" = $1 + and "asset_file"."type" = $2 ) as agg ) as "files" from "asset" where - "asset"."id" = $2 + "asset"."id" = $3 -- AssetJobRepository.getLockedPropertiesForMetadataExtraction select @@ -238,7 +314,8 @@ where select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -266,7 +343,14 @@ from where "asset"."visibility" != $1 and "asset"."deletedAt" is null - and "job_status"."previewAt" is not null + and exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "asset_file"."type" = $2 + ) and not exists ( select from @@ -287,7 +371,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -326,7 +411,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -381,29 +467,6 @@ select "asset"."originalPath", "asset"."isOffline", to_json("asset_exif") as "exifInfo", - ( - select - coalesce(json_agg(agg), '[]') - from - ( - select - "asset_face".*, - "person" as "person" - from - "asset_face" - left join lateral ( - select - "person".* - from - "person" - where - "asset_face"."personId" = "person"."id" - ) as "person" on true - where - "asset_face"."assetId" = "asset"."id" - and "asset_face"."deletedAt" is null - ) as agg - ) as "faces", ( select coalesce(json_agg(agg), '[]') @@ -412,34 +475,45 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where "asset_file"."assetId" = "asset"."id" ) as agg ) as "files", - to_json("stacked_assets") as "stack" + to_json("stack_result") as "stack" from "asset" left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" - left join "stack" on "stack"."id" = "asset"."stackId" left join lateral ( select "stack"."id", "stack"."primaryAssetId", - array_agg("stacked") as "assets" + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "stack_asset"."id" + from + "asset" as "stack_asset" + where + "stack_asset"."stackId" = "stack"."id" + and "stack_asset"."id" != "stack"."primaryAssetId" + and "stack_asset"."visibility" = $1 + and "stack_asset"."status" != $2 + ) as agg + ) as "assets" from - "asset" as "stacked" + "stack" where - "stacked"."deletedAt" is not null - and "stacked"."visibility" = $1 - and "stacked"."stackId" = "stack"."id" - group by - "stack"."id" - ) as "stacked_assets" on "stack"."id" is not null + "stack"."id" = "asset"."stackId" + ) as "stack_result" on true where - "asset"."id" = $2 + "asset"."id" = $3 -- AssetJobRepository.streamForVideoConversion select @@ -488,11 +562,15 @@ select "asset"."checksum", "asset"."originalPath", "asset"."isExternal", + "asset"."visibility", "asset"."originalFileName", "asset"."livePhotoVideoId", "asset"."fileCreatedAt", "asset_exif"."timeZone", "asset_exif"."fileSizeInByte", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", ( select coalesce(json_agg(agg), '[]') @@ -501,7 +579,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -515,6 +594,7 @@ from where "asset"."deletedAt" is null and "asset"."id" = $2 + and "asset"."visibility" != $3 -- AssetJobRepository.streamForStorageTemplateJob select @@ -524,11 +604,15 @@ select "asset"."checksum", "asset"."originalPath", "asset"."isExternal", + "asset"."visibility", "asset"."originalFileName", "asset"."livePhotoVideoId", "asset"."fileCreatedAt", "asset_exif"."timeZone", "asset_exif"."fileSizeInByte", + "asset_exif"."make", + "asset_exif"."model", + "asset_exif"."lensModel", ( select coalesce(json_agg(agg), '[]') @@ -537,7 +621,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -550,6 +635,7 @@ from inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where "asset"."deletedAt" is null + and "asset"."visibility" != $2 -- AssetJobRepository.streamForDeletedJob select @@ -585,7 +671,14 @@ from where "asset"."visibility" != $1 and "asset"."deletedAt" is null - and "job_status"."previewAt" is not null + and exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "asset_file"."type" = $2 + ) order by "asset"."fileCreatedAt" desc diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index f25a0798d2..632fb823c6 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -49,6 +49,23 @@ returning "dateTimeOriginal", "timeZone" +-- AssetRepository.unlockProperties +update "asset_exif" +set + "lockedProperties" = nullif( + array( + select distinct + property + from + unnest("asset_exif"."lockedProperties") property + where + not property = any ($1) + ), + '{}' + ) +where + "assetId" = $2 + -- AssetRepository.getMetadata select "key", @@ -76,6 +93,14 @@ where "assetId" = $1 and "key" = $2 +-- AssetRepository.deleteBulkMetadata +begin +delete from "asset_metadata" +where + "assetId" = $1 + and "key" = $2 +commit + -- AssetRepository.getByDayOfYear with "res" as ( @@ -98,19 +123,18 @@ with ) as "year" ) select - "a".*, - to_json("asset_exif") as "exifInfo" + "a".* from "today" inner join lateral ( select - "asset".* + "asset"."id", + "asset"."localDateTime" from "asset" inner join "asset_job_status" on "asset"."id" = "asset_job_status"."assetId" where - "asset_job_status"."previewAt" is not null - and (asset."localDateTime" at time zone 'UTC')::date = today.date + (asset."localDateTime" at time zone 'UTC')::date = today.date and "asset"."ownerId" = any ($4::uuid[]) and "asset"."visibility" = $5 and exists ( @@ -127,7 +151,6 @@ with limit $7 ) as "a" on true - inner join "asset_exif" on "a"."id" = "asset_exif"."assetId" ) select date_part( @@ -174,6 +197,7 @@ select where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true ) as agg ) as "faces", ( @@ -260,7 +284,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -375,14 +400,10 @@ with "asset_exif"."projectionType", coalesce( case - when asset_exif."exifImageHeight" = 0 - or asset_exif."exifImageWidth" = 0 then 1 - when "asset_exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round( - asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric, - 3 - ) + when asset."height" = 0 + or asset."width" = 0 then 1 else round( - asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric, + asset."width"::numeric / asset."height"::numeric, 3 ) end, @@ -562,3 +583,150 @@ where and "libraryId" = $2::uuid and "isExternal" = $3 ) + +-- AssetRepository.getForOriginal +select + "asset"."id", + "originalFileName", + "asset_file"."path" as "editedPath", + "originalPath" +from + "asset" + left join "asset_file" on "asset"."id" = "asset_file"."assetId" + and "asset_file"."isEdited" = $1 + and "asset_file"."type" = $2 +where + "asset"."id" in ($3) + +-- AssetRepository.getForOriginals +select + "asset"."id", + "originalFileName", + "asset_file"."path" as "editedPath", + "originalPath" +from + "asset" + left join "asset_file" on "asset"."id" = "asset_file"."assetId" + and "asset_file"."isEdited" = $1 + and "asset_file"."type" = $2 +where + "asset"."id" in ($3) + +-- AssetRepository.getForThumbnail +select + "asset"."originalPath", + "asset"."originalFileName", + "asset_file"."path" as "path" +from + "asset" + left join "asset_file" on "asset"."id" = "asset_file"."assetId" + and "asset_file"."type" = $1 +where + "asset"."id" = $2 +order by + "asset_file"."isEdited" desc + +-- AssetRepository.getForVideo +select + "asset"."encodedVideoPath", + "asset"."originalPath" +from + "asset" +where + "asset"."id" = $1 + and "asset"."type" = $2 + +-- AssetRepository.getForOcr +select + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_edit"."action", + "asset_edit"."parameters" + from + "asset_edit" + where + "asset_edit"."assetId" = "asset"."id" + ) as agg + ) as "edits", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."orientation" +from + "asset" + inner join "asset_exif" on "asset_exif"."assetId" = "asset"."id" +where + "asset"."id" = $1 + +-- AssetRepository.getForEdit +select + "asset"."type", + "asset"."livePhotoVideoId", + "asset"."originalPath", + "asset"."originalFileName", + "asset_exif"."exifImageWidth", + "asset_exif"."exifImageHeight", + "asset_exif"."orientation", + "asset_exif"."projectionType" +from + "asset" + inner join "asset_exif" on "asset_exif"."assetId" = "asset"."id" +where + "asset"."id" = $1 + +-- AssetRepository.getForMetadataExtractionTags +select + "asset_exif"."tags" +from + "asset_exif" +where + "asset_exif"."assetId" = $1 + +-- AssetRepository.getForFaces +select + "asset_exif"."exifImageHeight", + "asset_exif"."exifImageWidth", + "asset_exif"."orientation", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_edit"."action", + "asset_edit"."parameters" + from + "asset_edit" + where + "asset_edit"."assetId" = "asset"."id" + ) as agg + ) as "edits" +from + "asset" + inner join "asset_exif" on "asset_exif"."assetId" = "asset"."id" +where + "asset"."id" = $1 + +-- AssetRepository.getForUpdateTags +select + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "tag"."value" + from + "tag" + inner join "tag_asset" on "tag"."id" = "tag_asset"."tagId" + where + "asset"."id" = "tag_asset"."assetId" + ) as agg + ) as "tags" +from + "asset" +where + "asset"."id" = $1 diff --git a/server/src/queries/ocr.repository.sql b/server/src/queries/ocr.repository.sql index d9fe049031..fc8991dea0 100644 --- a/server/src/queries/ocr.repository.sql +++ b/server/src/queries/ocr.repository.sql @@ -15,6 +15,7 @@ from "asset_ocr" where "asset_ocr"."assetId" = $1 + and "asset_ocr"."isVisible" = $2 -- OcrRepository.upsert with @@ -66,3 +67,12 @@ with ) select 1 as "dummy" + +-- OcrRepository.updateOcrVisibilities +begin +update "ocr_search" +set + "text" = $1 +where + "assetId" = $2 +commit diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 8ad5b96bbc..964aaaccee 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -35,6 +35,7 @@ from where "person"."ownerId" = $1 and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true and "person"."isHidden" = $2 group by "person"."id" @@ -63,6 +64,7 @@ from left join "asset_face" on "asset_face"."personId" = "person"."id" where "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true group by "person"."id" having @@ -89,6 +91,7 @@ from where "asset_face"."assetId" = $1 and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" = $2 order by "asset_face"."boundingBoxX1" asc @@ -173,6 +176,7 @@ select where "asset_file"."assetId" = "asset"."id" and "asset_file"."type" = 'preview' + and "asset_file"."isEdited" = $1 ) as "previewPath" from "person" @@ -180,7 +184,7 @@ from inner join "asset" on "asset_face"."assetId" = "asset"."id" left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "person"."id" = $1 + "person"."id" = $2 and "asset_face"."deletedAt" is null -- PersonRepository.reassignFace @@ -229,6 +233,7 @@ from and "asset"."deletedAt" is null where "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true -- PersonRepository.getNumberOfPeople select @@ -250,6 +255,7 @@ where where "asset_face"."personId" = "person"."id" and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" = $2 and exists ( select from @@ -260,7 +266,7 @@ where and "asset"."deletedAt" is null ) ) - and "person"."ownerId" = $2 + and "person"."ownerId" = $3 -- PersonRepository.refreshFaces with @@ -280,19 +286,6 @@ from -- PersonRepository.getFacesByIds select "asset_face".*, - ( - select - to_json(obj) - from - ( - select - "asset".* - from - "asset" - where - "asset"."id" = "asset_face"."assetId" - ) as obj - ) as "asset", ( select to_json(obj) @@ -321,6 +314,7 @@ from where "asset_face"."personId" = $1 and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true -- PersonRepository.getLatestFaceDate select @@ -348,3 +342,14 @@ from "person" where "id" in ($1) + +-- PersonRepository.getForFeatureFaceUpdate +select + "asset_face"."id" +from + "asset_face" + inner join "asset" on "asset"."id" = "asset_face"."assetId" + and "asset"."isOffline" = $1 +where + "asset_face"."assetId" = $2 + and "asset_face"."personId" = $3 diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 8540da91c8..cdd20ef85d 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -102,22 +102,30 @@ order by "shared_link"."createdAt" desc -- SharedLinkRepository.getAll -select distinct - on ("shared_link"."createdAt") "shared_link".*, - "assets"."assets", +select + "shared_link".*, + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset".* + from + "shared_link_asset" + inner join "asset" on "asset"."id" = "shared_link_asset"."assetId" + where + "shared_link"."id" = "shared_link_asset"."sharedLinkId" + and "asset"."deletedAt" is null + order by + "asset"."fileCreatedAt" asc + limit + $1 + ) as agg + ) as "assets", to_json("album") as "album" from "shared_link" - left join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "shared_link"."id" - left join lateral ( - select - json_agg("asset") as "assets" - from - "asset" - where - "asset"."id" = "shared_link_asset"."assetId" - and "asset"."deletedAt" is null - ) as "assets" on true left join lateral ( select "album".*, @@ -152,12 +160,12 @@ from and "album"."deletedAt" is null ) as "album" on true where - "shared_link"."userId" = $1 + "shared_link"."userId" = $2 and ( - "shared_link"."type" = $2 + "shared_link"."type" = $3 or "album"."id" is not null ) - and "shared_link"."albumId" = $3 + and "shared_link"."albumId" = $4 order by "shared_link"."createdAt" desc diff --git a/server/src/queries/stack.repository.sql b/server/src/queries/stack.repository.sql index 64714e5665..b5f1dc7d18 100644 --- a/server/src/queries/stack.repository.sql +++ b/server/src/queries/stack.repository.sql @@ -43,6 +43,7 @@ select "asset_exif"."projectionType", "asset_exif"."rating", "asset_exif"."state", + "asset_exif"."tags", "asset_exif"."timeZone" from "asset_exif" @@ -127,6 +128,7 @@ select "asset_exif"."projectionType", "asset_exif"."rating", "asset_exif"."state", + "asset_exif"."tags", "asset_exif"."timeZone" from "asset_exif" diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 7c1dc3b6b4..43c6a380bf 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -69,6 +69,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "album_asset"."updateId" from "album_asset" as "album_asset" @@ -99,6 +102,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -134,7 +140,10 @@ select "asset"."duration", "asset"."livePhotoVideoId", "asset"."stackId", - "asset"."libraryId" + "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited" from "album_asset" as "album_asset" inner join "asset" on "asset"."id" = "album_asset"."assetId" @@ -448,6 +457,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -502,6 +514,38 @@ where order by "asset_exif"."updateId" asc +-- SyncRepository.assetEdit.getDeletes +select + "asset_edit_audit"."id", + "editId" +from + "asset_edit_audit" as "asset_edit_audit" + inner join "asset" on "asset"."id" = "asset_edit_audit"."assetId" +where + "asset_edit_audit"."id" < $1 + and "asset_edit_audit"."id" > $2 + and "asset"."ownerId" = $3 +order by + "asset_edit_audit"."id" asc + +-- SyncRepository.assetEdit.getUpserts +select + "asset_edit"."id", + "asset_edit"."assetId", + "asset_edit"."sequence", + "asset_edit"."action", + "asset_edit"."parameters", + "asset_edit"."updateId" +from + "asset_edit" as "asset_edit" + inner join "asset" on "asset"."id" = "asset_edit"."assetId" +where + "asset_edit"."updateId" < $1 + and "asset_edit"."updateId" > $2 + and "asset"."ownerId" = $3 +order by + "asset_edit"."updateId" asc + -- SyncRepository.assetFace.getDeletes select "asset_face_audit"."id", @@ -528,6 +572,8 @@ select "boundingBoxX2", "boundingBoxY2", "sourceType", + "isVisible", + "asset_face"."deletedAt", "asset_face"."updateId" from "asset_face" as "asset_face" @@ -536,6 +582,7 @@ where "asset_face"."updateId" < $1 and "asset_face"."updateId" > $2 and "asset"."ownerId" = $3 + and "asset_face"."isVisible" = $4 order by "asset_face"."updateId" asc @@ -740,6 +787,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -789,6 +839,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" diff --git a/server/src/queries/workflow.repository.sql b/server/src/queries/workflow.repository.sql index 3797c5bb06..27dc21dffe 100644 --- a/server/src/queries/workflow.repository.sql +++ b/server/src/queries/workflow.repository.sql @@ -7,6 +7,8 @@ from "workflow" where "id" = $1 +order by + "createdAt" desc -- WorkflowRepository.getWorkflowsByOwner select @@ -16,7 +18,7 @@ from where "ownerId" = $1 order by - "name" + "createdAt" desc -- WorkflowRepository.getWorkflowsByTrigger select diff --git a/server/src/repositories/album-user.repository.ts b/server/src/repositories/album-user.repository.ts index 1a1e58a77d..558a0c05d7 100644 --- a/server/src/repositories/album-user.repository.ts +++ b/server/src/repositories/album-user.repository.ts @@ -25,14 +25,13 @@ export class AlbumUserRepository { } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }, { role: AlbumUserRole.Viewer }] }) - update({ userId, albumId }: AlbumPermissionId, dto: Updateable) { - return this.db + async update({ userId, albumId }: AlbumPermissionId, dto: Updateable) { + await this.db .updateTable('album_user') .set(dto) .where('userId', '=', userId) .where('albumId', '=', albumId) - .returningAll() - .executeTakeFirstOrThrow(); + .execute(); } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] }) diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 100ab908c0..cf132a023d 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -44,9 +44,9 @@ const withAlbumUsers = (eb: ExpressionBuilder) => { }; const withSharedLink = (eb: ExpressionBuilder) => { - return jsonArrayFrom(eb.selectFrom('shared_link').selectAll().whereRef('shared_link.albumId', '=', 'album.id')).as( - 'sharedLinks', - ); + return jsonArrayFrom( + eb.selectFrom('shared_link').selectAll('shared_link').whereRef('shared_link.albumId', '=', 'album.id'), + ).as('sharedLinks'); }; const withAssets = (eb: ExpressionBuilder) => { @@ -283,7 +283,7 @@ export class AlbumRepository { return tx .selectFrom('album') - .selectAll() + .selectAll('album') .where('id', '=', newAlbum.id) .select(withOwner) .select(withAssets) diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index 28307d7c83..1e0d13f16c 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -31,7 +31,7 @@ export class ApiKeyRepository { } @GenerateSql({ params: [DummyValue.STRING] }) - getKey(hashedToken: string) { + getKey(hashedToken: Buffer) { return this.db .selectFrom('api_key') .select((eb) => [ diff --git a/server/src/repositories/app.repository.ts b/server/src/repositories/app.repository.ts index e6181ef7f3..96e413232f 100644 --- a/server/src/repositories/app.repository.ts +++ b/server/src/repositories/app.repository.ts @@ -1,5 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; +import { Server as SocketIO } from 'socket.io'; import { ExitCode } from 'src/enum'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { AppRestartEvent } from 'src/repositories/event.repository'; @Injectable() export class AppRepository { @@ -17,4 +22,26 @@ export class AppRepository { setCloseFn(fn: () => Promise) { this.closeFn = fn; } + + async sendOneShotAppRestart(state: AppRestartEvent): Promise { + const server = new SocketIO(); + const { redis } = new ConfigRepository().getEnv(); + const pubClient = new Redis({ ...redis, lazyConnect: true }); + const subClient = pubClient.duplicate(); + + await Promise.all([pubClient.connect(), subClient.connect()]); + + server.adapter(createAdapter(pubClient, subClient)); + + // => corresponds to notification.service.ts#onAppRestart + server.emit('AppRestartV1', state, async () => { + const responses = await server.serverSideEmitWithAck('AppRestart', state); + if (responses.some((response) => response !== 'ok')) { + throw new Error("One or more node(s) returned a non-'ok' response to our restart request!"); + } + + pubClient.disconnect(); + subClient.disconnect(); + }); + } } diff --git a/server/src/repositories/asset-edit.repository.ts b/server/src/repositories/asset-edit.repository.ts new file mode 100644 index 0000000000..164ebec6b6 --- /dev/null +++ b/server/src/repositories/asset-edit.repository.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEditActionItem, AssetEditActionItemResponseDto } from 'src/dtos/editing.dto'; +import { DB } from 'src/schema'; + +@Injectable() +export class AssetEditRepository { + constructor(@InjectKysely() private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID] }) + replaceAll(assetId: string, edits: AssetEditActionItem[]): Promise { + return this.db.transaction().execute(async (trx) => { + await trx.deleteFrom('asset_edit').where('assetId', '=', assetId).execute(); + + if (edits.length > 0) { + return trx + .insertInto('asset_edit') + .values(edits.map((edit, i) => ({ assetId, sequence: i, ...edit }))) + .returning(['id', 'action', 'parameters']) + .execute(); + } + + return []; + }); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getAll(assetId: string): Promise { + return this.db + .selectFrom('asset_edit') + .select(['id', 'action', 'parameters']) + .where('assetId', '=', assetId) + .orderBy('sequence', 'asc') + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getWithSyncInfo(assetId: string) { + return this.db + .selectFrom('asset_edit') + .select(columns.syncAssetEdit) + .where('assetId', '=', assetId) + .orderBy('sequence', 'asc') + .execute(); + } +} diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 214d42747f..df9b50791f 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -1,23 +1,24 @@ import { Injectable } from '@nestjs/common'; -import { Kysely } from 'kysely'; +import { Kysely, sql } from 'kysely'; import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; -import { Asset, columns } from 'src/database'; +import { columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { AssetFileType, AssetType, AssetVisibility } from 'src/enum'; +import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; import { anyUuid, asUuid, toJson, withDefaultVisibility, + withEdits, withExif, withExifInner, withFaces, - withFacesAndPeople, withFilePath, withFiles, } from 'src/utils/database'; +import { mimeTypes } from 'src/utils/mime-types'; @Injectable() export class AssetJobRepository { @@ -41,15 +42,6 @@ export class AssetJobRepository { .where('asset.id', '=', asUuid(id)) .select(['id', 'originalPath']) .select((eb) => withFiles(eb, AssetFileType.Sidecar)) - .select((eb) => - jsonArrayFrom( - eb - .selectFrom('tag') - .select(['tag.value']) - .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagId') - .whereRef('asset.id', '=', 'tag_asset.assetId'), - ).as('tags'), - ) .$call(withExifInner) .limit(1) .executeTakeFirst(); @@ -66,25 +58,45 @@ export class AssetJobRepository { .executeTakeFirst(); } - @GenerateSql({ params: [false], stream: true }) - streamForThumbnailJob(force: boolean) { + @GenerateSql({ params: [{ force: false, fullsizeEnabled: true }], stream: true }) + streamForThumbnailJob(options: { force: boolean | undefined; fullsizeEnabled: boolean }) { return this.db .selectFrom('asset') - .select(['asset.id', 'asset.thumbhash']) - .select(withFiles) + .select(['asset.id', 'asset.isEdited']) .where('asset.deletedAt', 'is', null) - .where('asset.visibility', '!=', AssetVisibility.Hidden) - .$if(!force, (qb) => + .where('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)) + .$if(!options.force, (qb) => qb // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails .innerJoin('asset_job_status', 'asset_job_status.assetId', 'asset.id') - .where((eb) => - eb.or([ - eb('asset_job_status.previewAt', 'is', null), - eb('asset_job_status.thumbnailAt', 'is', null), + .where(({ and, eb, exists, not, or, selectFrom }) => { + const file = (type: AssetFileType) => + selectFrom('asset_file').whereRef('assetId', '=', 'asset.id').where('type', '=', sql.lit(type)); + + const conditions = [ + not(exists(file(AssetFileType.Thumbnail))), + not(exists(file(AssetFileType.Preview))), + and([ + eb('asset.isEdited', '=', sql.lit(true)), + not(exists(file(AssetFileType.FullSize).where('asset_file.isEdited', '=', sql.lit(true)))), + ]), eb('asset.thumbhash', 'is', null), - ]), - ), + ]; + + if (options.fullsizeEnabled) { + const isWebUnsupported = sql.join( + Object.keys(mimeTypes.webUnsupportedImage).map((ext) => sql.lit(`%${ext}`)), + ); + conditions.push( + and([ + not(exists(file(AssetFileType.FullSize))), + eb(sql`f_unaccent(asset."originalFileName")`, 'like', sql`any(array[${isWebUnsupported}]::text[])`), + ]), + ); + } + + return or(conditions); + }), ) .stream(); } @@ -112,7 +124,16 @@ export class AssetJobRepository { 'asset.thumbhash', 'asset.type', ]) - .select(withFiles) + .select((eb) => + jsonArrayFrom( + eb + .selectFrom('asset_file') + .select(columns.assetFilesForThumbnail) + .whereRef('asset_file.assetId', '=', 'asset.id') + .where('asset_file.type', 'in', [AssetFileType.Thumbnail, AssetFileType.Preview, AssetFileType.FullSize]), + ).as('files'), + ) + .select(withEdits) .$call(withExifInner) .where('asset.id', '=', id) .executeTakeFirst(); @@ -155,7 +176,14 @@ export class AssetJobRepository { .where('asset.visibility', '!=', AssetVisibility.Hidden) .where('asset.deletedAt', 'is', null) .innerJoin('asset_job_status as job_status', 'assetId', 'asset.id') - .where('job_status.previewAt', 'is not', null); + .where((eb) => + eb.exists((qb) => + qb + .selectFrom('asset_file') + .whereRef('assetId', '=', 'asset.id') + .where('asset_file.type', '=', AssetFileType.Preview), + ), + ); } @GenerateSql({ params: [], stream: true }) @@ -200,7 +228,7 @@ export class AssetJobRepository { .selectFrom('asset') .select(['asset.id', 'asset.visibility']) .$call(withExifInner) - .select((eb) => withFaces(eb, true)) + .select((eb) => withFaces(eb, true, true)) .select((eb) => withFiles(eb, AssetFileType.Preview)) .where('asset.id', '=', id) .executeTakeFirst(); @@ -246,23 +274,29 @@ export class AssetJobRepository { 'asset.isOffline', ]) .$call(withExif) - .select(withFacesAndPeople) .select(withFiles) - .leftJoin('stack', 'stack.id', 'asset.stackId') .leftJoinLateral( (eb) => eb - .selectFrom('asset as stacked') - .select(['stack.id', 'stack.primaryAssetId']) - .select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets')) - .where('stacked.deletedAt', 'is not', null) - .where('stacked.visibility', '=', AssetVisibility.Timeline) - .whereRef('stacked.stackId', '=', 'stack.id') - .groupBy('stack.id') - .as('stacked_assets'), - (join) => join.on('stack.id', 'is not', null), + .selectFrom('stack') + .whereRef('stack.id', '=', 'asset.stackId') + .select((eb) => [ + 'stack.id', + 'stack.primaryAssetId', + jsonArrayFrom( + eb + .selectFrom('asset as stack_asset') + .select(['stack_asset.id']) + .whereRef('stack_asset.stackId', '=', 'stack.id') + .whereRef('stack_asset.id', '!=', 'stack.primaryAssetId') + .where('stack_asset.visibility', '=', sql.val(AssetVisibility.Timeline)) + .where('stack_asset.status', '!=', sql.val(AssetStatus.Deleted)), + ).as('assets'), + ]) + .as('stack_result'), + (join) => join.onTrue(), ) - .select((eb) => toJson(eb, 'stacked_assets').as('stack')) + .select((eb) => toJson(eb, 'stack_result').as('stack')) .where('asset.id', '=', id) .executeTakeFirst(); } @@ -319,24 +353,31 @@ export class AssetJobRepository { 'asset.checksum', 'asset.originalPath', 'asset.isExternal', + 'asset.visibility', 'asset.originalFileName', 'asset.livePhotoVideoId', 'asset.fileCreatedAt', 'asset_exif.timeZone', 'asset_exif.fileSizeInByte', + 'asset_exif.make', + 'asset_exif.model', + 'asset_exif.lensModel', ]) .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .where('asset.deletedAt', 'is', null); } @GenerateSql({ params: [DummyValue.UUID] }) - getForStorageTemplateJob(id: string) { - return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst(); + getForStorageTemplateJob(id: string, options?: { includeHidden?: boolean }) { + return this.storageTemplateAssetQuery() + .where('asset.id', '=', id) + .$if(!options?.includeHidden, (qb) => qb.where('asset.visibility', '!=', AssetVisibility.Hidden)) + .executeTakeFirst(); } @GenerateSql({ params: [], stream: true }) streamForStorageTemplateJob() { - return this.storageTemplateAssetQuery().stream(); + return this.storageTemplateAssetQuery().where('asset.visibility', '!=', AssetVisibility.Hidden).stream(); } @GenerateSql({ params: [DummyValue.DATE], stream: true }) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 7db3a76f12..e971a995e6 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,15 +1,27 @@ import { Injectable } from '@nestjs/common'; -import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely'; +import { + ExpressionBuilder, + Insertable, + Kysely, + NotNull, + Selectable, + SelectQueryBuilder, + sql, + Updateable, + UpdateResult, +} from 'kysely'; +import { jsonArrayFrom } from 'kysely/helpers/postgres'; import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { LockableProperty, Stack } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; +import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetTable } from 'src/schema/tables/asset.table'; import { anyUuid, @@ -19,6 +31,7 @@ import { truncatedDate, unnest, withDefaultVisibility, + withEdits, withExif, withFaces, withFacesAndPeople, @@ -33,6 +46,13 @@ import { globToSqlPattern } from 'src/utils/misc'; export type AssetStats = Record; +export interface BoundingBox { + west: number; + south: number; + east: number; + north: number; +} + interface AssetStatsOptions { isFavorite?: boolean; isTrashed?: boolean; @@ -61,6 +81,7 @@ interface AssetBuilderOptions { assetType?: AssetType; visibility?: AssetVisibility; withCoordinates?: boolean; + bbox?: BoundingBox; } export interface TimeBucketOptions extends AssetBuilderOptions { @@ -111,11 +132,40 @@ interface GetByIdsRelations { smartSearch?: boolean; stack?: { assets?: boolean }; tags?: boolean; + edits?: boolean; } const distinctLocked = (eb: ExpressionBuilder, columns: T) => sql`nullif(array(select distinct unnest(${eb.ref('asset_exif.lockedProperties')} || ${columns})), '{}')`; +const getBoundingCircle = (bbox: BoundingBox) => { + const { west, south, east, north } = bbox; + const eastUnwrapped = west <= east ? east : east + 360; + const centerLongitude = (((west + eastUnwrapped) / 2 + 540) % 360) - 180; + const centerLatitude = (south + north) / 2; + const radius = sql`greatest( + earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${west})), + earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${south}, ${east})), + earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${west})), + earth_distance(ll_to_earth_public(${centerLatitude}, ${centerLongitude}), ll_to_earth_public(${north}, ${east})) + )`; + + return { centerLatitude, centerLongitude, radius }; +}; + +const withBoundingBox = (qb: SelectQueryBuilder, bbox: BoundingBox) => { + const { west, south, east, north } = bbox; + const withLatitude = qb.where('asset_exif.latitude', '>=', south).where('asset_exif.latitude', '<=', north); + + if (west <= east) { + return withLatitude.where('asset_exif.longitude', '>=', west).where('asset_exif.longitude', '<=', east); + } + + return withLatitude.where((eb) => + eb.or([eb('asset_exif.longitude', '>=', west), eb('asset_exif.longitude', '<=', east)]), + ); +}; + @Injectable() export class AssetRepository { constructor(@InjectKysely() private db: Kysely) {} @@ -175,6 +225,7 @@ export class AssetRepository { bitsPerSample: ref('bitsPerSample'), rating: ref('rating'), fps: ref('fps'), + tags: ref('tags'), lockedProperties: lockedPropertiesBehavior === 'append' ? distinctLocked(eb, exif.lockedProperties ?? null) @@ -220,6 +271,17 @@ export class AssetRepository { .execute(); } + @GenerateSql({ params: [DummyValue.UUID, ['description']] }) + unlockProperties(assetId: string, properties: LockableProperty[]) { + return this.db + .updateTable('asset_exif') + .where('assetId', '=', assetId) + .set((eb) => ({ + lockedProperties: sql`nullif(array(select distinct property from unnest(${eb.ref('asset_exif.lockedProperties')}) property where not property = any(${properties})), '{}')`, + })) + .execute(); + } + async upsertJobStatus(...jobStatus: Insertable[]): Promise { if (jobStatus.length === 0) { return; @@ -236,8 +298,6 @@ export class AssetRepository { duplicatesDetectedAt: eb.ref('excluded.duplicatesDetectedAt'), facesRecognizedAt: eb.ref('excluded.facesRecognizedAt'), metadataExtractedAt: eb.ref('excluded.metadataExtractedAt'), - previewAt: eb.ref('excluded.previewAt'), - thumbnailAt: eb.ref('excluded.thumbnailAt'), ocrAt: eb.ref('excluded.ocrAt'), }, values[0], @@ -256,7 +316,11 @@ export class AssetRepository { .execute(); } - upsertMetadata(id: string, items: Array<{ key: AssetMetadataKey; value: object }>) { + upsertMetadata(id: string, items: Array<{ key: string; value: object }>) { + if (items.length === 0) { + return []; + } + return this.db .insertInto('asset_metadata') .values(items.map((item) => ({ assetId: id, ...item }))) @@ -269,8 +333,21 @@ export class AssetRepository { .execute(); } + upsertBulkMetadata(items: Insertable[]) { + return this.db + .insertInto('asset_metadata') + .values(items) + .onConflict((oc) => + oc + .columns(['assetId', 'key']) + .doUpdateSet((eb) => ({ key: eb.ref('excluded.key'), value: eb.ref('excluded.value') })), + ) + .returning(['assetId', 'key', 'value', 'updatedAt']) + .execute(); + } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - getMetadataByKey(assetId: string, key: AssetMetadataKey) { + getMetadataByKey(assetId: string, key: string) { return this.db .selectFrom('asset_metadata') .select(['key', 'value', 'updatedAt']) @@ -280,10 +357,23 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async deleteMetadataByKey(id: string, key: AssetMetadataKey) { + async deleteMetadataByKey(id: string, key: string) { await this.db.deleteFrom('asset_metadata').where('assetId', '=', id).where('key', '=', key).execute(); } + @GenerateSql({ params: [[{ assetId: DummyValue.UUID, key: DummyValue.STRING }]] }) + async deleteBulkMetadata(items: Array<{ assetId: string; key: string }>) { + if (items.length === 0) { + return; + } + + await this.db.transaction().execute(async (tx) => { + for (const { assetId, key } of items) { + await tx.deleteFrom('asset_metadata').where('assetId', '=', assetId).where('key', '=', key).execute(); + } + }); + } + create(asset: Insertable) { return this.db.insertInto('asset').values(asset).returningAll().executeTakeFirstOrThrow(); } @@ -314,9 +404,8 @@ export class AssetRepository { (qb) => qb .selectFrom('asset') - .selectAll('asset') + .select(['asset.id', 'asset.localDateTime']) .innerJoin('asset_job_status', 'asset.id', 'asset_job_status.assetId') - .where('asset_job_status.previewAt', 'is not', null) .where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) .where('asset.ownerId', '=', anyUuid(ownerIds)) .where('asset.visibility', '=', AssetVisibility.Timeline) @@ -334,9 +423,7 @@ export class AssetRepository { .as('a'), (join) => join.onTrue(), ) - .innerJoin('asset_exif', 'a.id', 'asset_exif.assetId') - .selectAll('a') - .select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo')), + .selectAll('a'), ) .selectFrom('res') .select(sql`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year')) @@ -441,7 +528,10 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) { + getById( + id: string, + { exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {}, + ) { return this.db .selectFrom('asset') .selectAll('asset') @@ -478,6 +568,7 @@ export class AssetRepository { ) .$if(!!files, (qb) => qb.select(withFiles)) .$if(!!tags, (qb) => qb.select(withTags)) + .$if(!!edits, (qb) => qb.select(withEdits)) .limit(1) .executeTakeFirst(); } @@ -505,10 +596,11 @@ export class AssetRepository { .selectAll('asset') .$call(withExif) .$call((qb) => qb.select(withFacesAndPeople)) + .$call((qb) => qb.select(withEdits)) .executeTakeFirst(); } - return this.getById(asset.id, { exifInfo: true, faces: { person: true } }); + return this.getById(asset.id, { exifInfo: true, faces: { person: true }, edits: true }); } async remove(asset: { id: string }): Promise { @@ -603,6 +695,20 @@ export class AssetRepository { .select(truncatedDate().as('timeBucket')) .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) + .$if(!!options.bbox, (qb) => { + const bbox = options.bbox!; + const circle = getBoundingCircle(bbox); + + const withBoundingCircle = qb + .innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId') + .where( + sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`, + '@>', + sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`, + ); + + return withBoundingBox(withBoundingCircle, bbox); + }) .$if(options.visibility === undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .$if(!!options.albumId, (qb) => @@ -665,11 +771,9 @@ export class AssetRepository { .coalesce( eb .case() - .when(sql`asset_exif."exifImageHeight" = 0 or asset_exif."exifImageWidth" = 0`) + .when(sql`asset."height" = 0 or asset."width" = 0`) .then(eb.lit(1)) - .when('asset_exif.orientation', 'in', sql`('5', '6', '7', '8', '-90', '90')`) - .then(sql`round(asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric, 3)`) - .else(sql`round(asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric, 3)`) + .else(sql`round(asset."width"::numeric / asset."height"::numeric, 3)`) .end(), eb.lit(1), ) @@ -679,6 +783,18 @@ export class AssetRepository { .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .$if(options.visibility == undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) + .$if(!!options.bbox, (qb) => { + const bbox = options.bbox!; + const circle = getBoundingCircle(bbox); + + const withBoundingCircle = qb.where( + sql`earth_box(ll_to_earth_public(${circle.centerLatitude}, ${circle.centerLongitude}), ${circle.radius})`, + '@>', + sql`ll_to_earth_public(asset_exif.latitude, asset_exif.longitude)`, + ); + + return withBoundingBox(withBoundingCircle, bbox); + }) .where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, '')) .$if(!!options.albumId, (qb) => qb.where((eb) => @@ -856,31 +972,41 @@ export class AssetRepository { .execute(); } - async upsertFile(file: Pick, 'assetId' | 'path' | 'type'>): Promise { - const value = { ...file, assetId: asUuid(file.assetId) }; + async upsertFile( + file: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >, + ): Promise { await this.db .insertInto('asset_file') - .values(value) + .values(file) .onConflict((oc) => - oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ + oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), })), ) .execute(); } - async upsertFiles(files: Pick, 'assetId' | 'path' | 'type'>[]): Promise { + async upsertFiles( + files: Pick< + Insertable, + 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive' | 'isTransparent' + >[], + ): Promise { if (files.length === 0) { return; } - const values = files.map((row) => ({ ...row, assetId: asUuid(row.assetId) })); await this.db .insertInto('asset_file') - .values(values) + .values(files) .onConflict((oc) => - oc.columns(['assetId', 'type']).doUpdateSet((eb) => ({ + oc.columns(['assetId', 'type', 'isEdited']).doUpdateSet((eb) => ({ path: eb.ref('excluded.path'), + isProgressive: eb.ref('excluded.isProgressive'), + isTransparent: eb.ref('excluded.isTransparent'), })), ) .execute(); @@ -959,4 +1085,120 @@ export class AssetRepository { return count; } + + private buildGetForOriginal(ids: string[], isEdited: boolean) { + return this.db + .selectFrom('asset') + .select('asset.id') + .select('originalFileName') + .where('asset.id', 'in', ids) + .$if(isEdited, (qb) => + qb + .leftJoin('asset_file', (join) => + join + .onRef('asset.id', '=', 'asset_file.assetId') + .on('asset_file.isEdited', '=', true) + .on('asset_file.type', '=', AssetFileType.FullSize), + ) + .select('asset_file.path as editedPath'), + ) + .select('originalPath'); + } + + @GenerateSql({ params: [DummyValue.UUID, true] }) + getForOriginal(id: string, isEdited: boolean) { + return this.buildGetForOriginal([id], isEdited).executeTakeFirstOrThrow(); + } + + @GenerateSql({ params: [[DummyValue.UUID], true] }) + getForOriginals(ids: string[], isEdited: boolean) { + return this.buildGetForOriginal(ids, isEdited).execute(); + } + + @GenerateSql({ params: [DummyValue.UUID, AssetFileType.Preview, true] }) + async getForThumbnail(id: string, type: AssetFileType, isEdited: boolean) { + return this.db + .selectFrom('asset') + .where('asset.id', '=', id) + .leftJoin('asset_file', (join) => + join.onRef('asset.id', '=', 'asset_file.assetId').on('asset_file.type', '=', type), + ) + .select(['asset.originalPath', 'asset.originalFileName', 'asset_file.path as path']) + .orderBy('asset_file.isEdited', isEdited ? 'desc' : 'asc') + .executeTakeFirstOrThrow(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForVideo(id: string) { + return this.db + .selectFrom('asset') + .select(['asset.encodedVideoPath', 'asset.originalPath']) + .where('asset.id', '=', id) + .where('asset.type', '=', AssetType.Video) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForOcr(id: string) { + return this.db + .selectFrom('asset') + .where('asset.id', '=', id) + .select(withEdits) + .innerJoin('asset_exif', (join) => join.onRef('asset_exif.assetId', '=', 'asset.id')) + .select(['asset_exif.exifImageWidth', 'asset_exif.exifImageHeight', 'asset_exif.orientation']) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForEdit(id: string) { + return this.db + .selectFrom('asset') + .select(['asset.type', 'asset.livePhotoVideoId', 'asset.originalPath', 'asset.originalFileName']) + .where('asset.id', '=', id) + .innerJoin('asset_exif', (join) => join.onRef('asset_exif.assetId', '=', 'asset.id')) + .select([ + 'asset_exif.exifImageWidth', + 'asset_exif.exifImageHeight', + 'asset_exif.orientation', + 'asset_exif.projectionType', + ]) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForMetadataExtractionTags(id: string) { + return this.db + .selectFrom('asset_exif') + .select('asset_exif.tags') + .where('asset_exif.assetId', '=', id) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForFaces(id: string) { + return this.db + .selectFrom('asset') + .innerJoin('asset_exif', (join) => join.onRef('asset_exif.assetId', '=', 'asset.id')) + .select(['asset_exif.exifImageHeight', 'asset_exif.exifImageWidth', 'asset_exif.orientation']) + .select(withEdits) + .where('asset.id', '=', id) + .executeTakeFirstOrThrow(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async getForUpdateTags(id: string) { + return this.db + .selectFrom('asset') + .select((eb) => + jsonArrayFrom( + eb + .selectFrom('tag') + .select('tag.value') + .innerJoin('tag_asset', 'tag.id', 'tag_asset.tagId') + .whereRef('asset.id', '=', 'tag_asset.assetId'), + ).as('tags'), + ) + .where('asset.id', '=', id) + .executeTakeFirstOrThrow(); + } } diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 1641850583..a3dc8ba5cb 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -8,6 +8,8 @@ const getEnv = () => { const resetEnv = () => { for (const env of [ + 'IMMICH_ALLOW_EXTERNAL_PLUGINS', + 'IMMICH_ALLOW_SETUP', 'IMMICH_ENV', 'IMMICH_WORKERS_INCLUDE', 'IMMICH_WORKERS_EXCLUDE', @@ -75,6 +77,9 @@ describe('getEnv', () => { configFile: undefined, logLevel: undefined, }); + + expect(config.plugins.external).toEqual({ allow: false }); + expect(config.setup).toEqual({ allow: true }); }); describe('IMMICH_MEDIA_LOCATION', () => { @@ -84,6 +89,32 @@ describe('getEnv', () => { }); }); + describe('IMMICH_ALLOW_EXTERNAL_PLUGINS', () => { + it('should disable plugins', () => { + process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'false'; + const config = getEnv(); + expect(config.plugins.external).toEqual({ allow: false }); + }); + + it('should throw an error for invalid value', () => { + process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'invalid'; + expect(() => getEnv()).toThrowError('IMMICH_ALLOW_EXTERNAL_PLUGINS must be a boolean value'); + }); + }); + + describe('IMMICH_ALLOW_SETUP', () => { + it('should disable setup', () => { + process.env.IMMICH_ALLOW_SETUP = 'false'; + const { setup } = getEnv(); + expect(setup).toEqual({ allow: false }); + }); + + it('should throw an error for invalid value', () => { + process.env.IMMICH_ALLOW_SETUP = 'invalid'; + expect(() => getEnv()).toThrowError('IMMICH_ALLOW_SETUP must be a boolean value'); + }); + }); + describe('database', () => { it('should use defaults', () => { const { database } = getEnv(); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 60ec021b3b..7e8082a582 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -1,3 +1,4 @@ +import { DatabaseConnectionParams } from '@immich/sql-tools'; import { RegisterQueueOptions } from '@nestjs/bullmq'; import { Inject, Injectable, Optional } from '@nestjs/common'; import { QueueOptions } from 'bullmq'; @@ -17,10 +18,11 @@ import { ImmichHeader, ImmichTelemetry, ImmichWorker, + LogFormat, LogLevel, QueueName, } from 'src/enum'; -import { DatabaseConnectionParams, VectorExtension } from 'src/types'; +import { VectorExtension } from 'src/types'; import { setDifference } from 'src/utils/set'; export interface EnvData { @@ -29,6 +31,7 @@ export interface EnvData { environment: ImmichEnvironment; configFile?: string; logLevel?: LogLevel; + logFormat?: LogFormat; buildMetadata: { build?: string; @@ -90,6 +93,10 @@ export interface EnvData { redis: RedisOptions; + setup: { + allow: boolean; + }; + telemetry: { apiPort: number; microservicesPort: number; @@ -104,8 +111,10 @@ export interface EnvData { workers: ImmichWorker[]; plugins: { - enabled: boolean; - installFolder?: string; + external: { + allow: boolean; + installFolder?: string; + }; }; noColor: boolean; @@ -176,7 +185,7 @@ const getEnv = (): EnvData => { try { redisConfig = JSON.parse(Buffer.from(redisUrl.slice(10), 'base64').toString()); } catch (error) { - throw new Error(`Failed to decode redis options: ${error}`); + throw new Error('Failed to decode redis options', { cause: error }); } } @@ -227,6 +236,7 @@ const getEnv = (): EnvData => { environment, configFile: dto.IMMICH_CONFIG_FILE, logLevel: dto.IMMICH_LOG_LEVEL, + logFormat: dto.IMMICH_LOG_FORMAT || LogFormat.Console, buildMetadata: { build: dto.IMMICH_BUILD, @@ -313,6 +323,10 @@ const getEnv = (): EnvData => { corePlugin: join(buildFolder, 'corePlugin'), }, + setup: { + allow: dto.IMMICH_ALLOW_SETUP ?? true, + }, + storage: { ignoreMountCheckErrors: !!dto.IMMICH_IGNORE_MOUNT_CHECK_ERRORS, mediaLocation: dto.IMMICH_MEDIA_LOCATION, @@ -327,8 +341,10 @@ const getEnv = (): EnvData => { workers, plugins: { - enabled: !!dto.IMMICH_PLUGINS_ENABLED, - installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER, + external: { + allow: dto.IMMICH_ALLOW_EXTERNAL_PLUGINS ?? false, + installFolder: dto.IMMICH_PLUGINS_INSTALL_FOLDER, + }, }, noColor: !!dto.NO_COLOR, diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts index bcd791ade2..9b093f6d79 100644 --- a/server/src/repositories/crypto.repository.ts +++ b/server/src/repositories/crypto.repository.ts @@ -23,7 +23,7 @@ export class CryptoRepository { } hashSha256(value: string) { - return createHash('sha256').update(value).digest('base64'); + return createHash('sha256').update(value).digest(); } verifySha256(value: string, encryptedValue: string, publicKey: string) { diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 0fbaabf930..4ffb37c79c 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -1,3 +1,4 @@ +import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools'; import { Injectable } from '@nestjs/common'; import AsyncLock from 'async-lock'; import { FileMigrationProvider, Kysely, Migrator, sql, Transaction } from 'kysely'; @@ -19,7 +20,9 @@ import { GenerateSql } from 'src/decorators'; import { DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import 'src/schema'; // make sure all schema definitions are imported for schemaFromCode import { DB } from 'src/schema'; +import { immich_uuid_v7 } from 'src/schema/functions'; import { ExtensionVersion, VectorExtension, VectorUpdateResult } from 'src/types'; import { vectorIndexQuery } from 'src/utils/database'; import { isValidInteger } from 'src/validation'; @@ -246,11 +249,11 @@ export class DatabaseRepository { } const dimSize = await this.getDimensionSize(table); lists ||= this.targetListCount(await this.getRowCount(table)); - await this.db.schema.dropIndex(indexName).ifExists().execute(); - if (table === 'smart_search') { - await this.db.schema.alterTable(table).dropConstraint('dim_size_constraint').ifExists().execute(); - } await this.db.transaction().execute(async (tx) => { + await sql`DROP INDEX IF EXISTS ${sql.raw(indexName)}`.execute(tx); + if (table === 'smart_search') { + await sql`ALTER TABLE ${sql.raw(table)} DROP CONSTRAINT IF EXISTS dim_size_constraint`.execute(tx); + } if (!rows.some((row) => row.columnName === 'embedding')) { this.logger.warn(`Column 'embedding' does not exist in table '${table}', truncating and adding column.`); await sql`TRUNCATE TABLE ${sql.raw(table)}`.execute(tx); @@ -281,6 +284,32 @@ export class DatabaseRepository { return rows[0].db; } + getMigrations() { + return this.db.selectFrom('kysely_migrations').select(['name', 'timestamp']).orderBy('name', 'asc').execute(); + } + + async getSchemaDrift() { + const source = schemaFromCode({ + overrides: true, + namingStrategy: 'default', + uuidFunction: (version) => (version === 7 ? `${immich_uuid_v7.name}()` : 'uuid_generate_v4()'), + }); + const { database } = this.configRepository.getEnv(); + const target = await schemaFromDatabase({ connection: database.config }); + + const drift = schemaDiff(source, target, { + tables: { ignoreExtra: true }, + constraints: { ignoreExtra: false }, + indexes: { ignoreExtra: true }, + triggers: { ignoreExtra: true }, + columns: { ignoreExtra: true }, + functions: { ignoreExtra: false }, + parameters: { ignoreExtra: true }, + }); + + return drift; + } + async getDimensionSize(table: string, column = 'embedding'): Promise { const { rows } = await sql<{ dimsize: number }>` SELECT atttypmod as dimsize @@ -358,7 +387,7 @@ export class DatabaseRepository { } async runMigrations(): Promise { - this.logger.debug('Running migrations'); + this.logger.log('Running migrations'); const migrator = this.createMigrator(); @@ -379,7 +408,7 @@ export class DatabaseRepository { throw error; } - this.logger.debug('Finished running migrations'); + this.logger.log('Finished running migrations'); } async migrateFilePaths(sourceFolder: string, targetFolder: string): Promise { diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index c59110d674..361a2e7179 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -4,6 +4,7 @@ import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AppRepository } from 'src/repositories/app.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; @@ -59,6 +60,7 @@ export const repositories = [ ApiKeyRepository, AppRepository, AssetRepository, + AssetEditRepository, AssetJobRepository, ConfigRepository, CronRepository, diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 576ee6c810..39867b14d0 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -2,7 +2,7 @@ import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; import { ClsService } from 'nestjs-cls'; import { Telemetry } from 'src/decorators'; -import { LogLevel } from 'src/enum'; +import { LogFormat, LogLevel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; type LogDetails = any; @@ -27,10 +27,12 @@ export class MyConsoleLogger extends ConsoleLogger { constructor( private cls: ClsService | undefined, - options?: { color?: boolean; context?: string }, + options?: { json?: boolean; color?: boolean; context?: string }, ) { - super(options?.context || MyConsoleLogger.name); - this.isColorEnabled = options?.color || false; + super(options?.context || MyConsoleLogger.name, { + json: options?.json ?? false, + }); + this.isColorEnabled = !options?.json && (options?.color || false); } isLevelEnabled(level: LogLevel) { @@ -79,10 +81,17 @@ export class LoggingRepository { @Inject(ConfigRepository) configRepository: ConfigRepository | undefined, ) { let noColor = false; + let logFormat = LogFormat.Console; if (configRepository) { - noColor = configRepository.getEnv().noColor; + const env = configRepository.getEnv(); + noColor = env.noColor; + logFormat = env.logFormat ?? logFormat; } - this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor }); + this.logger = new MyConsoleLogger(cls, { + context: LoggingRepository.name, + json: logFormat === LogFormat.Json, + color: !noColor, + }); } static create(context?: string) { diff --git a/server/src/repositories/media.repository.spec.ts b/server/src/repositories/media.repository.spec.ts new file mode 100644 index 0000000000..a5380852ee --- /dev/null +++ b/server/src/repositories/media.repository.spec.ts @@ -0,0 +1,667 @@ +import sharp from 'sharp'; +import { AssetFace } from 'src/database'; +import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; +import { SourceType } from 'src/enum'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { BoundingBox } from 'src/repositories/machine-learning.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; +import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor'; +import { automock } from 'test/utils'; + +const getPixelColor = async (buffer: Buffer, x: number, y: number) => { + const metadata = await sharp(buffer).metadata(); + const width = metadata.width!; + const { data } = await sharp(buffer).raw().toBuffer({ resolveWithObject: true }); + const idx = (y * width + x) * 4; + return { + r: data[idx], + g: data[idx + 1], + b: data[idx + 2], + }; +}; + +const buildTestQuadImage = async () => { + // build a 4 quadrant image for testing mirroring + const base = sharp({ + create: { width: 1000, height: 1000, channels: 3, background: { r: 0, g: 0, b: 0 } }, + }).png(); + + const tl = await sharp({ + create: { width: 500, height: 500, channels: 3, background: { r: 255, g: 0, b: 0 } }, + }) + .png() + .toBuffer(); + + const tr = await sharp({ + create: { width: 500, height: 500, channels: 3, background: { r: 0, g: 255, b: 0 } }, + }) + .png() + .toBuffer(); + + const bl = await sharp({ + create: { width: 500, height: 500, channels: 3, background: { r: 0, g: 0, b: 255 } }, + }) + .png() + .toBuffer(); + + const br = await sharp({ + create: { width: 500, height: 500, channels: 3, background: { r: 255, g: 255, b: 0 } }, + }) + .png() + .toBuffer(); + + const image = base.composite([ + { input: tl, left: 0, top: 0 }, // top-left + { input: tr, left: 500, top: 0 }, // top-right + { input: bl, left: 0, top: 500 }, // bottom-left + { input: br, left: 500, top: 500 }, // bottom-right + ]); + + return image.png().toBuffer(); +}; + +describe(MediaRepository.name, () => { + let sut: MediaRepository; + + beforeEach(() => { + // eslint-disable-next-line no-sparse-arrays + sut = new MediaRepository(automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false })); + }); + + describe('applyEdits (single actions)', () => { + it('should apply crop edit correctly', async () => { + const result = await sut['applyEdits']( + sharp({ + create: { + width: 1000, + height: 1000, + channels: 4, + background: { r: 255, g: 0, b: 0, alpha: 0.5 }, + }, + }).png(), + [ + { + action: AssetEditAction.Crop, + parameters: { + x: 100, + y: 200, + width: 700, + height: 300, + }, + }, + ], + ); + + const metadata = await result.toBuffer().then((buf) => sharp(buf).metadata()); + expect(metadata.width).toBe(700); + expect(metadata.height).toBe(300); + }); + it('should apply rotate edit correctly', async () => { + const result = await sut['applyEdits']( + sharp({ + create: { + width: 500, + height: 1000, + channels: 4, + background: { r: 255, g: 0, b: 0, alpha: 0.5 }, + }, + }).png(), + [ + { + action: AssetEditAction.Rotate, + parameters: { + angle: 90, + }, + }, + ], + ); + + const metadata = await result.toBuffer().then((buf) => sharp(buf).metadata()); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(500); + }); + + it('should apply mirror edit correctly', async () => { + const resultHorizontal = await sut['applyEdits'](sharp(await buildTestQuadImage()), [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + ]); + + const bufferHorizontal = await resultHorizontal.toBuffer(); + const metadataHorizontal = await resultHorizontal.metadata(); + expect(metadataHorizontal.width).toBe(1000); + expect(metadataHorizontal.height).toBe(1000); + + expect(await getPixelColor(bufferHorizontal, 10, 10)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(bufferHorizontal, 990, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(bufferHorizontal, 10, 990)).toEqual({ r: 255, g: 255, b: 0 }); + expect(await getPixelColor(bufferHorizontal, 990, 990)).toEqual({ r: 0, g: 0, b: 255 }); + + const resultVertical = await sut['applyEdits'](sharp(await buildTestQuadImage()), [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Vertical, + }, + }, + ]); + + const bufferVertical = await resultVertical.toBuffer(); + const metadataVertical = await resultVertical.metadata(); + expect(metadataVertical.width).toBe(1000); + expect(metadataVertical.height).toBe(1000); + + // top-left should now be bottom-left (blue) + expect(await getPixelColor(bufferVertical, 10, 10)).toEqual({ r: 0, g: 0, b: 255 }); + // top-right should now be bottom-right (yellow) + expect(await getPixelColor(bufferVertical, 990, 10)).toEqual({ r: 255, g: 255, b: 0 }); + // bottom-left should now be top-left (red) + expect(await getPixelColor(bufferVertical, 10, 990)).toEqual({ r: 255, g: 0, b: 0 }); + // bottom-right should now be top-right (blue) + expect(await getPixelColor(bufferVertical, 990, 990)).toEqual({ r: 0, g: 255, b: 0 }); + }); + }); + + describe('applyEdits (multiple sequential edits)', () => { + it('should apply horizontal mirror then vertical mirror (equivalent to 180° rotation)', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 0, b: 255 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 990)).toEqual({ r: 255, g: 0, b: 0 }); + }); + + it('should apply rotate 90° then horizontal mirror', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 0, b: 255 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 990)).toEqual({ r: 255, g: 255, b: 0 }); + }); + + it('should apply 180° rotation', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Rotate, parameters: { angle: 180 } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 0, b: 255 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 990)).toEqual({ r: 255, g: 0, b: 0 }); + }); + + it('should apply 270° rotations', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Rotate, parameters: { angle: 270 } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 255, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 990, 990)).toEqual({ r: 0, g: 0, b: 255 }); + }); + + it('should apply crop then rotate 90°', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 1000, height: 500 } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(500); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 0, g: 255, b: 0 }); + }); + + it('should apply rotate 90° then crop', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 1000 } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(500); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 0, g: 0, b: 255 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 255, g: 0, b: 0 }); + }); + + it('should apply vertical mirror then horizontal mirror then rotate 90°', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(1000); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 0, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 255, g: 255, b: 0 }); + expect(await getPixelColor(buffer, 10, 990)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 990, 990)).toEqual({ r: 0, g: 0, b: 255 }); + }); + + it('should apply crop to single quadrant then mirror', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 500 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(500); + expect(metadata.height).toBe(500); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 490, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 10, 490)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 490, 490)).toEqual({ r: 255, g: 0, b: 0 }); + }); + + it('should apply all operations: crop, rotate, mirror', async () => { + const imageBuffer = await buildTestQuadImage(); + const result = await sut['applyEdits'](sharp(imageBuffer), [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 1000 } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + ]); + + const buffer = await result.png().toBuffer(); + const metadata = await sharp(buffer).metadata(); + expect(metadata.width).toBe(1000); + expect(metadata.height).toBe(500); + + expect(await getPixelColor(buffer, 10, 10)).toEqual({ r: 255, g: 0, b: 0 }); + expect(await getPixelColor(buffer, 990, 10)).toEqual({ r: 0, g: 0, b: 255 }); + }); + }); + + describe('checkFaceVisibility', () => { + const baseFace: AssetFace = { + id: 'face-1', + assetId: 'asset-1', + personId: 'person-1', + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + sourceType: SourceType.MachineLearning, + isVisible: true, + updatedAt: new Date(), + deletedAt: null, + updateId: '', + }; + + const assetDimensions = { width: 1000, height: 800 }; + + describe('with no crop edit', () => { + it('should return only currently invisible faces when no crop is provided', () => { + const visibleFace = { ...baseFace, id: 'face-visible', isVisible: true }; + const invisibleFace = { ...baseFace, id: 'face-invisible', isVisible: false }; + const faces = [visibleFace, invisibleFace]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toEqual([invisibleFace]); + expect(result.hidden).toEqual([]); + }); + + it('should return empty arrays when all faces are already visible and no crop is provided', () => { + const faces = [baseFace]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual([]); + }); + + it('should return all faces when all are invisible and no crop is provided', () => { + const face1 = { ...baseFace, id: 'face-1', isVisible: false }; + const face2 = { ...baseFace, id: 'face-2', isVisible: false }; + const faces = [face1, face2]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toEqual([face1, face2]); + expect(result.hidden).toEqual([]); + }); + }); + + describe('with crop edit', () => { + it('should mark face as visible when fully inside crop area', () => { + const crop: BoundingBox = { x1: 0, y1: 0, x2: 500, y2: 400 }; + const faces = [baseFace]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual(faces); + expect(result.hidden).toEqual([]); + }); + + it('should mark face as visible when more than 50% inside crop area', () => { + const crop: BoundingBox = { x1: 150, y1: 150, x2: 650, y2: 550 }; + // Face at (100,100)-(200,200), crop starts at (150,150) + // Overlap: (150,150)-(200,200) = 50x50 = 2500 + // Face area: 100x100 = 10000 + // Overlap percentage: 25% - should be hidden + const faces = [baseFace]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual(faces); + }); + + it('should mark face as hidden when less than 50% inside crop area', () => { + const crop: BoundingBox = { x1: 250, y1: 250, x2: 750, y2: 650 }; + // Face completely outside crop area + const faces = [baseFace]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual(faces); + }); + + it('should mark face as hidden when completely outside crop area', () => { + const crop: BoundingBox = { x1: 500, y1: 500, x2: 700, y2: 700 }; + const faces = [baseFace]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual(faces); + }); + + it('should handle multiple faces with mixed visibility', () => { + const crop: BoundingBox = { x1: 0, y1: 0, x2: 300, y2: 300 }; + const faceInside: AssetFace = { + ...baseFace, + id: 'face-inside', + boundingBoxX1: 50, + boundingBoxY1: 50, + boundingBoxX2: 150, + boundingBoxY2: 150, + }; + const faceOutside: AssetFace = { + ...baseFace, + id: 'face-outside', + boundingBoxX1: 400, + boundingBoxY1: 400, + boundingBoxX2: 500, + boundingBoxY2: 500, + }; + const faces = [faceInside, faceOutside]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual([faceInside]); + expect(result.hidden).toEqual([faceOutside]); + }); + + it('should handle face at exactly 50% overlap threshold', () => { + // Face at (0,0)-(100,100), crop at (50,0)-(150,100) + // Overlap: (50,0)-(100,100) = 50x100 = 5000 + // Face area: 100x100 = 10000 + // Overlap percentage: 50% - exactly at threshold, should be visible + const faceAtEdge: AssetFace = { + ...baseFace, + id: 'face-edge', + boundingBoxX1: 0, + boundingBoxY1: 0, + boundingBoxX2: 100, + boundingBoxY2: 100, + }; + const crop: BoundingBox = { x1: 50, y1: 0, x2: 150, y2: 100 }; + const faces = [faceAtEdge]; + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toEqual([faceAtEdge]); + expect(result.hidden).toEqual([]); + }); + }); + + describe('with scaled dimensions', () => { + it('should handle faces when asset dimensions differ from face image dimensions', () => { + // Face stored at 1000x800 resolution, but displaying at 500x400 + const scaledDimensions = { width: 500, height: 400 }; + const crop: BoundingBox = { x1: 0, y1: 0, x2: 250, y2: 200 }; + // Face at (100,100)-(200,200) on 1000x800 + // Scaled to 500x400: (50,50)-(100,100) + // Crop at (0,0)-(250,200) - face is fully inside + const faces = [baseFace]; + const result = checkFaceVisibility(faces, scaledDimensions, crop); + + expect(result.visible).toEqual(faces); + expect(result.hidden).toEqual([]); + }); + }); + }); + + describe('checkOcrVisibility', () => { + const baseOcr: AssetOcrResponseDto & { isVisible: boolean } = { + id: 'ocr-1', + assetId: 'asset-1', + x1: 0.1, + y1: 0.1, + x2: 0.2, + y2: 0.1, + x3: 0.2, + y3: 0.2, + x4: 0.1, + y4: 0.2, + boxScore: 0.9, + textScore: 0.85, + text: 'Test OCR', + isVisible: false, + }; + + const assetDimensions = { width: 1000, height: 800 }; + + describe('with no crop edit', () => { + it('should return only currently invisible OCR items when no crop is provided', () => { + const visibleOcr = { ...baseOcr, id: 'ocr-visible', isVisible: true }; + const invisibleOcr = { ...baseOcr, id: 'ocr-invisible', isVisible: false }; + const ocrs = [visibleOcr, invisibleOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toEqual([invisibleOcr]); + expect(result.hidden).toEqual([]); + }); + + it('should return empty arrays when all OCR items are already visible and no crop is provided', () => { + const visibleOcr = { ...baseOcr, isVisible: true }; + const ocrs = [visibleOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual([]); + }); + + it('should return all OCR items when all are invisible and no crop is provided', () => { + const ocr1 = { ...baseOcr, id: 'ocr-1', isVisible: false }; + const ocr2 = { ...baseOcr, id: 'ocr-2', isVisible: false }; + const ocrs = [ocr1, ocr2]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toEqual([ocr1, ocr2]); + expect(result.hidden).toEqual([]); + }); + }); + + describe('with crop edit', () => { + it('should mark OCR as visible when fully inside crop area', () => { + const crop: BoundingBox = { x1: 0, y1: 0, x2: 500, y2: 400 }; + // OCR box: (0.1,0.1)-(0.2,0.2) on 1000x800 = (100,80)-(200,160) + // Crop: (0,0)-(500,400) - OCR fully inside + const ocrs = [baseOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toEqual(ocrs); + expect(result.hidden).toEqual([]); + }); + + it('should mark OCR as hidden when completely outside crop area', () => { + const crop: BoundingBox = { x1: 500, y1: 500, x2: 700, y2: 700 }; + // OCR box: (100,80)-(200,160) - completely outside crop + const ocrs = [baseOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual(ocrs); + }); + + it('should mark OCR as hidden when less than 50% inside crop area', () => { + const crop: BoundingBox = { x1: 150, y1: 120, x2: 650, y2: 520 }; + // OCR box: (100,80)-(200,160) + // Crop: (150,120)-(650,520) + // Overlap: (150,120)-(200,160) = 50x40 = 2000 + // OCR area: 100x80 = 8000 + // Overlap percentage: 25% - should be hidden + const ocrs = [baseOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toEqual([]); + expect(result.hidden).toEqual(ocrs); + }); + + it('should handle multiple OCR items with mixed visibility', () => { + const crop: BoundingBox = { x1: 0, y1: 0, x2: 300, y2: 300 }; + const ocrInside = { + ...baseOcr, + id: 'ocr-inside', + }; + const ocrOutside = { + ...baseOcr, + id: 'ocr-outside', + x1: 0.5, + y1: 0.5, + x2: 0.6, + y2: 0.5, + x3: 0.6, + y3: 0.6, + x4: 0.5, + y4: 0.6, + }; + const ocrs = [ocrInside, ocrOutside]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toEqual([ocrInside]); + expect(result.hidden).toEqual([ocrOutside]); + }); + + it('should handle OCR boxes with rotated/skewed polygons', () => { + // OCR with a rotated bounding box (not axis-aligned) + const rotatedOcr = { + ...baseOcr, + id: 'ocr-rotated', + x1: 0.15, + y1: 0.1, + x2: 0.25, + y2: 0.15, + x3: 0.2, + y3: 0.25, + x4: 0.1, + y4: 0.2, + }; + const crop: BoundingBox = { x1: 0, y1: 0, x2: 300, y2: 300 }; + const ocrs = [rotatedOcr]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toEqual([rotatedOcr]); + expect(result.hidden).toEqual([]); + }); + }); + + describe('visibility is only affected by crop (not rotate or mirror)', () => { + it('should keep all OCR items visible when there is no crop regardless of other transforms', () => { + // Rotate and mirror edits don't affect visibility - only crop does + // The visibility functions only take an optional crop parameter + const ocrs = [baseOcr]; + + // Without any crop, all OCR items remain visible + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toEqual(ocrs); + expect(result.hidden).toEqual([]); + }); + + it('should only consider crop for visibility calculation', () => { + // Even if the image will be rotated/mirrored, visibility is determined + // solely by whether the OCR box overlaps with the crop area + const crop: BoundingBox = { x1: 0, y1: 0, x2: 300, y2: 300 }; + + const ocrInsideCrop = { + ...baseOcr, + id: 'ocr-inside', + // OCR at (0.1,0.1)-(0.2,0.2) = (100,80)-(200,160) on 1000x800, inside crop + }; + + const ocrOutsideCrop = { + ...baseOcr, + id: 'ocr-outside', + x1: 0.5, + y1: 0.5, + x2: 0.6, + y2: 0.5, + x3: 0.6, + y3: 0.6, + x4: 0.5, + y4: 0.6, + // OCR at (500,400)-(600,480) on 1000x800, outside crop + }; + + const ocrs = [ocrInsideCrop, ocrOutsideCrop]; + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + // OCR inside crop area is visible, OCR outside is hidden + // This is true regardless of any subsequent rotate/mirror operations + expect(result.visible).toEqual([ocrInsideCrop]); + expect(result.hidden).toEqual([ocrOutsideCrop]); + }); + }); + }); +}); diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 1aa248c895..710985a4be 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -7,6 +7,7 @@ import { Writable } from 'node:stream'; import sharp from 'sharp'; import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants'; import { Exif } from 'src/database'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { Colorspace, LogLevel, RawExtractedFormat } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { @@ -19,6 +20,7 @@ import { VideoInfo, } from 'src/types'; import { handlePromiseError } from 'src/utils/misc'; +import { createAffineMatrix } from 'src/utils/transform'; const probe = (input: string, options: string[]): Promise => new Promise((resolve, reject) => @@ -105,7 +107,7 @@ export class MediaRepository { ExposureTime: tags.exposureTime, ProfileDescription: tags.profileDescription, ColorSpace: tags.colorspace, - Rating: tags.rating, + Rating: tags.rating === null ? 0 : tags.rating, // specially convert Orientation to numeric Orientation# for exiftool 'Orientation#': tags.orientation ? Number(tags.orientation) : undefined, }; @@ -121,42 +123,54 @@ export class MediaRepository { } } - async copyTagGroup(tagGroup: string, source: string, target: string): Promise { - try { - await exiftool.write( - target, - {}, - { - ignoreMinorErrors: true, - writeArgs: ['-TagsFromFile', source, `-${tagGroup}:all>${tagGroup}:all`, '-overwrite_original'], - }, - ); - return true; - } catch (error: any) { - this.logger.warn(`Could not copy tag data to image: ${error.message}`); - return false; - } + async decodeImage(input: string | Buffer, options: DecodeToBufferOptions) { + const pipeline = await this.getImageDecodingPipeline(input, options); + return pipeline.raw().toBuffer({ resolveWithObject: true }); } - decodeImage(input: string | Buffer, options: DecodeToBufferOptions) { - return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true }); + private async applyEdits(pipeline: sharp.Sharp, edits: AssetEditActionItem[]): Promise { + const affineEditOperations = edits.filter((edit) => edit.action !== 'crop'); + const matrix = createAffineMatrix(affineEditOperations); + + const crop = edits.find((edit) => edit.action === 'crop'); + const dimensions = await pipeline.metadata(); + + if (crop) { + pipeline = pipeline.extract({ + left: crop ? Math.round(crop.parameters.x) : 0, + top: crop ? Math.round(crop.parameters.y) : 0, + width: crop ? Math.round(crop.parameters.width) : dimensions.width || 0, + height: crop ? Math.round(crop.parameters.height) : dimensions.height || 0, + }); + } + + const { a, b, c, d } = matrix; + pipeline = pipeline.affine([ + [a, b], + [c, d], + ]); + + return pipeline; } async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise { - await this.getImageDecodingPipeline(input, options) - .toFormat(options.format, { - quality: options.quality, - // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp - chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0', - }) - .toFile(output); + const pipeline = await this.getImageDecodingPipeline(input, options); + const decoded = pipeline.toFormat(options.format, { + quality: options.quality, + // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp + chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0', + progressive: options.progressive, + }); + + await decoded.toFile(output); } /** * For output file path 'output.dz', this creates an 'output.dzi' file and 'output_files' directory containing tiles */ async generateTiles(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise { - await this.getImageDecodingPipeline(input, options) + const pipeline = await this.getImageDecodingPipeline(input, options); + await pipeline .toFormat(options.format) .tile({ depth: 'one', @@ -165,7 +179,7 @@ export class MediaRepository { .toFile(output); } - private getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) { + private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) { let pipeline = sharp(input, { // some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes failOn: options.processInvalidImages ? 'none' : 'error', @@ -188,8 +202,8 @@ export class MediaRepository { } } - if (options.crop) { - pipeline = pipeline.extract(options.crop); + if (options.edits && options.edits.length > 0) { + pipeline = await this.applyEdits(pipeline, options.edits); } if (options.size !== undefined) { @@ -199,14 +213,20 @@ export class MediaRepository { } async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise { - const [{ rgbaToThumbHash }, { data, info }] = await Promise.all([ + const [{ rgbaToThumbHash }, decodingPipeline] = await Promise.all([ import('thumbhash'), - sharp(input, options) - .resize(100, 100, { fit: 'inside', withoutEnlargement: true }) - .raw() - .ensureAlpha() - .toBuffer({ resolveWithObject: true }), + this.getImageDecodingPipeline(input, { + colorspace: options.colorspace, + processInvalidImages: options.processInvalidImages, + raw: options.raw, + edits: options.edits, + }), ]); + + const pipeline = decodingPipeline.resize(100, 100, { fit: 'inside', withoutEnlargement: true }).raw().ensureAlpha(); + + const { data, info } = await pipeline.toBuffer({ resolveWithObject: true }); + return Buffer.from(rgbaToThumbHash(info.width, info.height, data)); } @@ -220,23 +240,26 @@ export class MediaRepository { bitrate: this.parseInt(results.format.bit_rate), }, videoStreams: results.streams - .filter((stream) => stream.codec_type === 'video') - .filter((stream) => !stream.disposition?.attached_pic) - .map((stream) => ({ - index: stream.index, - height: this.parseInt(stream.height), - width: this.parseInt(stream.width), - codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name, - codecType: stream.codec_type, - frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames), - rotation: this.parseInt(stream.rotation), - isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67', - bitrate: this.parseInt(stream.bit_rate), - pixelFormat: stream.pix_fmt || 'yuv420p', - colorPrimaries: stream.color_primaries, - colorSpace: stream.color_space, - colorTransfer: stream.color_transfer, - })), + .filter((stream) => stream.codec_type === 'video' && !stream.disposition?.attached_pic) + .map((stream) => { + const height = this.parseInt(stream.height); + const dar = this.getDar(stream.display_aspect_ratio); + return { + index: stream.index, + height, + width: dar ? Math.round(height * dar) : this.parseInt(stream.width), + codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name, + codecType: stream.codec_type, + frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames), + rotation: this.parseInt(stream.rotation), + isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67', + bitrate: this.parseInt(stream.bit_rate), + pixelFormat: stream.pix_fmt || 'yuv420p', + colorPrimaries: stream.color_primaries, + colorSpace: stream.color_space, + colorTransfer: stream.color_transfer, + }; + }), audioStreams: results.streams .filter((stream) => stream.codec_type === 'audio') .map((stream) => ({ @@ -286,9 +309,9 @@ export class MediaRepository { }); } - async getImageDimensions(input: string | Buffer): Promise { - const { width = 0, height = 0 } = await sharp(input).metadata(); - return { width, height }; + async getImageMetadata(input: string | Buffer): Promise { + const { width = 0, height = 0, hasAlpha = false } = await sharp(input).metadata(); + return { width, height, isTransparent: hasAlpha }; } private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) { @@ -329,4 +352,15 @@ export class MediaRepository { private parseFloat(value: string | number | undefined): number { return Number.parseFloat(value as string) || 0; } + + private getDar(dar: string | undefined): number { + if (dar) { + const [darW, darH] = dar.split(':').map(Number); + if (darW && darH) { + return darW / darH; + } + } + + return 0; + } } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 1334d1220f..3c36bf62db 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -106,7 +106,7 @@ export class MetadataRepository { readTags(path: string): Promise { const args = mimeTypes.isVideo(path) ? ['-ee'] : []; - return this.exiftool.read(path, args).catch((error) => { + return this.exiftool.read(path, { readArgs: args }).catch((error) => { this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`); return {}; }) as Promise; diff --git a/server/src/repositories/ocr.repository.ts b/server/src/repositories/ocr.repository.ts index a39f0d368c..63375cf57d 100644 --- a/server/src/repositories/ocr.repository.ts +++ b/server/src/repositories/ocr.repository.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { DB } from 'src/schema'; import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table'; @@ -15,8 +16,15 @@ export class OcrRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - getByAssetId(id: string) { - return this.db.selectFrom('asset_ocr').selectAll('asset_ocr').where('asset_ocr.assetId', '=', id).execute(); + getByAssetId(id: string, options?: { isVisible?: boolean }) { + const isVisible = options === undefined ? true : options.isVisible; + + return this.db + .selectFrom('asset_ocr') + .selectAll('asset_ocr') + .where('asset_ocr.assetId', '=', id) + .$if(isVisible !== undefined, (qb) => qb.where('asset_ocr.isVisible', '=', isVisible!)) + .execute(); } deleteAll() { @@ -65,4 +73,40 @@ export class OcrRepository { return query.selectNoFrom(sql`1`.as('dummy')).execute(); } + + @GenerateSql({ params: [DummyValue.UUID, [], []] }) + async updateOcrVisibilities( + assetId: string, + visible: AssetOcrResponseDto[], + hidden: AssetOcrResponseDto[], + ): Promise { + await this.db.transaction().execute(async (trx) => { + if (visible.length > 0) { + await trx + .updateTable('asset_ocr') + .set({ isVisible: true }) + .where( + 'asset_ocr.id', + 'in', + visible.map((i) => i.id), + ) + .execute(); + } + + if (hidden.length > 0) { + await trx + .updateTable('asset_ocr') + .set({ isVisible: false }) + .where( + 'asset_ocr.id', + 'in', + hidden.map((i) => i.id), + ) + .execute(); + } + + const searchText = visible.map((item) => item.text.trim()).join(' '); + await trx.updateTable('ocr_search').set({ text: searchText }).where('assetId', '=', assetId).execute(); + }); + } } diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 725304938c..00156a2492 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable } from 'kysely'; +import { ExpressionBuilder, Insertable, Kysely, Selectable, sql, Updateable } from 'kysely'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { InjectKysely } from 'nestjs-kysely'; +import { AssetFace } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFileType, AssetVisibility, SourceType } from 'src/enum'; import { DB } from 'src/schema'; @@ -121,6 +122,7 @@ export class PersonRepository { .$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!)) .$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!)) .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) .stream(); } @@ -160,6 +162,7 @@ export class PersonRepository { ) .where('person.ownerId', '=', userId) .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) .orderBy('person.isHidden', 'asc') .orderBy('person.isFavorite', 'desc') .having((eb) => @@ -208,19 +211,23 @@ export class PersonRepository { .selectAll('person') .leftJoin('asset_face', 'asset_face.personId', 'person.id') .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) .having((eb) => eb.fn.count('asset_face.assetId'), '=', 0) .groupBy('person.id') .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) - getFaces(assetId: string) { + getFaces(assetId: string, options?: { isVisible?: boolean }) { + const isVisible = options === undefined ? true : options.isVisible; + return this.db .selectFrom('asset_face') .selectAll('asset_face') .select(withPerson) .where('asset_face.assetId', '=', assetId) .where('asset_face.deletedAt', 'is', null) + .$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!)) .orderBy('asset_face.boundingBoxX1', 'asc') .execute(); } @@ -281,6 +288,7 @@ export class PersonRepository { .select('asset_file.path') .whereRef('asset_file.assetId', '=', 'asset.id') .where('asset_file.type', '=', sql.lit(AssetFileType.Preview)) + .where('asset_file.isEdited', '=', false) .as('previewPath'), ) .where('person.id', '=', id) @@ -350,6 +358,7 @@ export class PersonRepository { ) .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) .executeTakeFirst(); return { @@ -368,6 +377,7 @@ export class PersonRepository { .selectFrom('asset_face') .whereRef('asset_face.personId', '=', 'person.id') .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', '=', true) .where((eb) => eb.exists((eb) => eb @@ -475,12 +485,6 @@ export class PersonRepository { return this.db .selectFrom('asset_face') .selectAll('asset_face') - .select((eb) => - jsonObjectFrom(eb.selectFrom('asset').selectAll('asset').whereRef('asset.id', '=', 'asset_face.assetId')).as( - 'asset', - ), - ) - .$narrowType<{ asset: NotNull }>() .select(withPerson) .where('asset_face.assetId', 'in', assetIds) .where('asset_face.personId', 'in', personIds) @@ -495,6 +499,7 @@ export class PersonRepository { .selectAll('asset_face') .where('asset_face.personId', '=', personId) .where('asset_face.deletedAt', 'is', null) + .where('asset_face.isVisible', 'is', true) .executeTakeFirst(); } @@ -539,4 +544,48 @@ export class PersonRepository { } return this.db.selectFrom('person').select(['id', 'thumbnailPath']).where('id', 'in', ids).execute(); } + + @GenerateSql({ params: [[], []] }) + async updateVisibility(visible: AssetFace[], hidden: AssetFace[]): Promise { + if (visible.length === 0 && hidden.length === 0) { + return; + } + + await this.db.transaction().execute(async (trx) => { + if (visible.length > 0) { + await trx + .updateTable('asset_face') + .set({ isVisible: true }) + .where( + 'asset_face.id', + 'in', + visible.map(({ id }) => id), + ) + .execute(); + } + + if (hidden.length > 0) { + await trx + .updateTable('asset_face') + .set({ isVisible: false }) + .where( + 'asset_face.id', + 'in', + hidden.map(({ id }) => id), + ) + .execute(); + } + }); + } + + @GenerateSql({ params: [{ personId: DummyValue.UUID, assetId: DummyValue.UUID }] }) + getForFeatureFaceUpdate({ personId, assetId }: { personId: string; assetId: string }) { + return this.db + .selectFrom('asset_face') + .select('asset_face.id') + .where('asset_face.assetId', '=', assetId) + .where('asset_face.personId', '=', personId) + .innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false)) + .executeTakeFirst(); + } } diff --git a/server/src/repositories/process.repository.spec.ts b/server/src/repositories/process.repository.spec.ts new file mode 100644 index 0000000000..a3f44bd78b --- /dev/null +++ b/server/src/repositories/process.repository.spec.ts @@ -0,0 +1,85 @@ +import { ChildProcessWithoutNullStreams } from 'node:child_process'; +import { Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { ProcessRepository } from 'src/repositories/process.repository'; + +function* data() { + yield 'Hello, world!'; +} + +describe(ProcessRepository.name, () => { + let sut: ProcessRepository; + let sink: Writable; + + beforeAll(() => { + sut = new ProcessRepository(); + }); + + beforeEach(() => { + sink = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, + + final(callback) { + callback(); + }, + }); + }); + + describe('createSpawnDuplexStream', () => { + it('should work (drain to stdout)', async () => { + const process = sut.spawnDuplexStream('bash', ['-c', 'exit 0']); + await pipeline(process, sink); + }); + + it('should throw on non-zero exit code', async () => { + const process = sut.spawnDuplexStream('bash', ['-c', 'echo "error message" >&2; exit 1']); + await expect(pipeline(process, sink)).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: bash non-zero exit code (1) + error message + ] + `); + }); + + it('should accept stdin / output stdout', async () => { + let output = ''; + const sink = new Writable({ + write(chunk, _encoding, callback) { + output += chunk; + callback(); + }, + + final(callback) { + callback(); + }, + }); + + const echoProcess = sut.spawnDuplexStream('cat'); + await pipeline(Readable.from(data()), echoProcess, sink); + expect(output).toBe('Hello, world!'); + }); + + it('should drain stdin on process exit', async () => { + let resolve1: () => void; + let resolve2: () => void; + const promise1 = new Promise((r) => (resolve1 = r)); + const promise2 = new Promise((r) => (resolve2 = r)); + + async function* data() { + yield 'Hello, world!'; + await promise1; + await promise2; + yield 'Write after stdin close / process exit!'; + } + + const process = sut.spawnDuplexStream('bash', ['-c', 'exit 0']); + + const realProcess = (process as never as { _process: ChildProcessWithoutNullStreams })._process; + realProcess.on('close', () => setImmediate(() => resolve1())); + realProcess.stdin.on('close', () => setImmediate(() => resolve2())); + + await pipeline(Readable.from(data()), process); + }); + }); +}); diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts index 5055c4f3b5..9d8cac1f40 100644 --- a/server/src/repositories/process.repository.ts +++ b/server/src/repositories/process.repository.ts @@ -1,9 +1,110 @@ import { Injectable } from '@nestjs/common'; -import { ChildProcessWithoutNullStreams, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { ChildProcessWithoutNullStreams, fork, spawn, SpawnOptionsWithoutStdio } from 'node:child_process'; +import { Duplex } from 'node:stream'; @Injectable() export class ProcessRepository { - spawn(command: string, args: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { + spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { return spawn(command, args, options); } + + spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex { + let stdinClosed = false; + let drainCallback: undefined | (() => void); + + const process = this.spawn(command, args, options); + const duplex = new Duplex({ + // duplex -> stdin + write(chunk, encoding, callback) { + // drain the input if process dies + if (stdinClosed) { + return callback(); + } + + // handle stream backpressure + if (process.stdin.write(chunk, encoding)) { + callback(); + } else { + drainCallback = callback; + process.stdin.once('drain', () => { + drainCallback = undefined; + callback(); + }); + } + }, + + read() { + // no-op + }, + + final(callback) { + if (stdinClosed) { + callback(); + } else { + process.stdin.end(callback); + } + }, + }); + + // stdout -> duplex + process.stdout.on('data', (chunk) => { + // handle stream backpressure + if (!duplex.push(chunk)) { + process.stdout.pause(); + } + }); + + duplex.on('resume', () => process.stdout.resume()); + + // end handling + let stdoutClosed = false; + function close(error?: Error) { + stdinClosed = true; + + if (error) { + duplex.destroy(error); + } else if (stdoutClosed && typeof process.exitCode === 'number') { + duplex.push(null); + } + } + + process.stdout.on('close', () => { + stdoutClosed = true; + close(); + }); + + // error handling + process.on('error', close); + process.stdout.on('error', close); + process.stdin.on('error', (error) => { + if ((error as { code?: 'EPIPE' })?.code === 'EPIPE') { + try { + drainCallback!(); + } catch (error) { + close(error as Error); + } + } else { + close(error); + } + }); + + let stderr = ''; + process.stderr.on('data', (chunk) => (stderr += chunk)); + + process.on('exit', (code) => { + console.info(`${command} exited (${code})`); + + if (code === 0) { + close(); + } else { + close(new Error(`${command} non-zero exit code (${code})\n${stderr}`)); + } + }); + + return Object.assign(duplex, { _process: process }); + } + + fork(...args: Parameters): ReturnType { + return fork(...args); + } } diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 4500094899..934706d5e1 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -69,7 +69,7 @@ export class ServerInfoRepository { return response.json(); } catch (error) { - throw new Error(`Failed to fetch GitHub release: ${error}`); + throw new Error('Failed to fetch GitHub release', { cause: error }); } } diff --git a/server/src/repositories/session.repository.ts b/server/src/repositories/session.repository.ts index 52292b8e4a..e008943f21 100644 --- a/server/src/repositories/session.repository.ts +++ b/server/src/repositories/session.repository.ts @@ -48,7 +48,7 @@ export class SessionRepository { } @GenerateSql({ params: [DummyValue.STRING] }) - getByToken(token: string) { + getByToken(token: Buffer) { return this.db .selectFrom('session') .select((eb) => [ diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 7bfa9ac6ae..2a8acd6377 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely'; -import { jsonObjectFrom } from 'kysely/helpers/postgres'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; +import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import _ from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { Album, columns } from 'src/database'; @@ -12,6 +12,7 @@ import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; export type SharedLinkSearchOptions = { userId: string; + id?: string; albumId?: string; }; @@ -118,24 +119,25 @@ export class SharedLinkRepository { } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] }) - getAll({ userId, albumId }: SharedLinkSearchOptions) { + getAll({ userId, id, albumId }: SharedLinkSearchOptions) { return this.db .selectFrom('shared_link') .selectAll('shared_link') .where('shared_link.userId', '=', userId) - .leftJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'shared_link.id') - .leftJoinLateral( - (eb) => + .select((eb) => + jsonArrayFrom( eb - .selectFrom('asset') - .select((eb) => eb.fn.jsonAgg('asset').as('assets')) - .whereRef('asset.id', '=', 'shared_link_asset.assetId') + .selectFrom('shared_link_asset') + .whereRef('shared_link.id', '=', 'shared_link_asset.sharedLinkId') + .innerJoin('asset', 'asset.id', 'shared_link_asset.assetId') .where('asset.deletedAt', 'is', null) - .as('assets'), - (join) => join.onTrue(), + .selectAll('asset') + .orderBy('asset.fileCreatedAt', 'asc') + .limit(1), + ) + .$castTo() + .as('assets'), ) - .select('assets.assets') - .$narrowType<{ assets: NotNull }>() .leftJoinLateral( (eb) => eb @@ -176,8 +178,8 @@ export class SharedLinkRepository { .select((eb) => eb.fn.toJson('album').$castTo().as('album')) .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)])) .$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!)) + .$if(!!id, (eb) => eb.where('shared_link.id', '=', id!)) .orderBy('shared_link.createdAt', 'desc') - .distinctOn(['shared_link.createdAt']) .execute(); } @@ -258,7 +260,7 @@ export class SharedLinkRepository { .selectAll('asset') .innerJoinLateral( (eb) => - eb.selectFrom('asset_exif').whereRef('asset_exif.assetId', '=', 'asset.id').selectAll().as('exif'), + eb.selectFrom('asset_exif').whereRef('asset_exif.assetId', '=', 'asset.id').selectAll().as('exifInfo'), (join) => join.onTrue(), ) .as('assets'), diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index e901273b57..5a1a936e77 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -5,7 +5,8 @@ import { escapePath, glob, globStream } from 'fast-glob'; import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, ReadOptionsWithBuffer } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { Readable, Writable } from 'node:stream'; +import { PassThrough, Readable, Writable } from 'node:stream'; +import { createGunzip, createGzip } from 'node:zlib'; import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { mimeTypes } from 'src/utils/mime-types'; @@ -93,6 +94,18 @@ export class StorageRepository { return { stream: archive, addFile, finalize }; } + createGzip(): PassThrough { + return createGzip(); + } + + createGunzip(): PassThrough { + return createGunzip(); + } + + createPlainReadStream(filepath: string): Readable { + return createReadStream(filepath); + } + async createReadStream(filepath: string, mimeType?: string | null): Promise { const { size } = await fs.stat(filepath); await fs.access(filepath, constants.R_OK); @@ -139,7 +152,7 @@ export class StorageRepository { } async unlinkDir(folder: string, options: { recursive?: boolean; force?: boolean }) { - await fs.rm(folder, options); + await fs.rm(folder, { ...options, maxRetries: 5, retryDelay: 100 }); } async removeEmptyDirs(directory: string, self: boolean = false) { @@ -155,7 +168,13 @@ export class StorageRepository { if (self) { const updated = await fs.readdir(directory); if (updated.length === 0) { - await fs.rmdir(directory); + try { + await fs.rmdir(directory); + } catch (error: Error | any) { + if (error.code !== 'ENOTEMPTY') { + this.logger.warn(`Attempted to remove directory, but failed: ${error}`); + } + } } } } diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 437e32da16..b2fa144ca4 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -53,6 +53,7 @@ export class SyncRepository { albumUser: AlbumUserSync; asset: AssetSync; assetExif: AssetExifSync; + assetEdit: AssetEditSync; assetFace: AssetFaceSync; assetMetadata: AssetMetadataSync; authUser: AuthUserSync; @@ -75,6 +76,7 @@ export class SyncRepository { this.albumUser = new AlbumUserSync(this.db); this.asset = new AssetSync(this.db); this.assetExif = new AssetExifSync(this.db); + this.assetEdit = new AssetEditSync(this.db); this.assetFace = new AssetFaceSync(this.db); this.assetMetadata = new AssetMetadataSync(this.db); this.authUser = new AuthUserSync(this.db); @@ -91,7 +93,7 @@ export class SyncRepository { } } -class BaseSync { +export class BaseSync { constructor(protected db: Kysely) {} protected backfillQuery(t: T, { nowId, beforeUpdateId, afterUpdateId }: SyncBackfillOptions) { @@ -479,10 +481,13 @@ class AssetFaceSync extends BaseSync { 'boundingBoxX2', 'boundingBoxY2', 'sourceType', + 'isVisible', + 'asset_face.deletedAt', 'asset_face.updateId', ]) .leftJoin('asset', 'asset.id', 'asset_face.assetId') .where('asset.ownerId', '=', options.userId) + .where('asset_face.isVisible', '=', true) .stream(); } } @@ -498,6 +503,30 @@ class AssetExifSync extends BaseSync { } } +class AssetEditSync extends BaseSync { + @GenerateSql({ params: [dummyQueryOptions], stream: true }) + getDeletes(options: SyncQueryOptions) { + return this.auditQuery('asset_edit_audit', options) + .select(['asset_edit_audit.id', 'editId']) + .innerJoin('asset', 'asset.id', 'asset_edit_audit.assetId') + .where('asset.ownerId', '=', options.userId) + .stream(); + } + + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('asset_edit_audit', daysAgo); + } + + @GenerateSql({ params: [dummyQueryOptions], stream: true }) + getUpserts(options: SyncQueryOptions) { + return this.upsertQuery('asset_edit', options) + .select([...columns.syncAssetEdit, 'asset_edit.updateId']) + .innerJoin('asset', 'asset.id', 'asset_edit.assetId') + .where('asset.ownerId', '=', options.userId) + .stream(); + } +} + class MemorySync extends BaseSync { @GenerateSql({ params: [dummyQueryOptions], stream: true }) getDeletes(options: SyncQueryOptions) { diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index d87bf76351..235d2f2a84 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -11,7 +11,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { NotificationDto } from 'src/dtos/notification.dto'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto'; +import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto'; import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { handlePromiseError } from 'src/utils/misc'; @@ -37,6 +37,7 @@ export interface ClientEventMap { AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; AppRestartV1: [AppRestartEvent]; + AssetEditReadyV1: [{ asset: SyncAssetV1; edit: SyncAssetEditV1[] }]; } export type AuthFn = (client: Socket) => Promise; diff --git a/server/src/repositories/workflow.repository.ts b/server/src/repositories/workflow.repository.ts index 4ae657cfbf..deaf2aa2fc 100644 --- a/server/src/repositories/workflow.repository.ts +++ b/server/src/repositories/workflow.repository.ts @@ -12,12 +12,22 @@ export class WorkflowRepository { @GenerateSql({ params: [DummyValue.UUID] }) getWorkflow(id: string) { - return this.db.selectFrom('workflow').selectAll().where('id', '=', id).executeTakeFirst(); + return this.db + .selectFrom('workflow') + .selectAll() + .where('id', '=', id) + .orderBy('createdAt', 'desc') + .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID] }) getWorkflowsByOwner(ownerId: string) { - return this.db.selectFrom('workflow').selectAll().where('ownerId', '=', ownerId).orderBy('name').execute(); + return this.db + .selectFrom('workflow') + .selectAll() + .where('ownerId', '=', ownerId) + .orderBy('createdAt', 'desc') + .execute(); } @GenerateSql({ params: [PluginTriggerType.AssetCreate] }) diff --git a/server/src/schema/enums.ts b/server/src/schema/enums.ts index a1134df6bc..c68f152779 100644 --- a/server/src/schema/enums.ts +++ b/server/src/schema/enums.ts @@ -1,5 +1,5 @@ +import { registerEnum } from '@immich/sql-tools'; import { AssetStatus, AssetVisibility, SourceType } from 'src/enum'; -import { registerEnum } from 'src/sql-tools'; export const assets_status_enum = registerEnum({ name: 'assets_status_enum', diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 385db37cf8..6dbbd28b1a 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -1,4 +1,4 @@ -import { registerFunction } from 'src/sql-tools'; +import { registerFunction } from '@immich/sql-tools'; export const immich_uuid_v7 = registerFunction({ name: 'immich_uuid_v7', @@ -255,3 +255,47 @@ export const asset_face_audit = registerFunction({ RETURN NULL; END`, }); + +export const asset_edit_insert = registerFunction({ + name: 'asset_edit_insert', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + UPDATE asset + SET "isEdited" = true + FROM inserted_edit + WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited"; + RETURN NULL; + END + `, +}); + +export const asset_edit_delete = registerFunction({ + name: 'asset_edit_delete', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + `, +}); + +export const asset_edit_audit = registerFunction({ + name: 'asset_edit_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO asset_edit_audit ("editId", "assetId") + SELECT "id", "assetId" + FROM OLD; + RETURN NULL; + END`, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 9e206826e6..2426c2aab7 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -1,3 +1,4 @@ +import { Database, Extensions, Generated, Int8 } from '@immich/sql-tools'; import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; import { album_delete_audit, @@ -28,6 +29,8 @@ import { AlbumUserTable } from 'src/schema/tables/album-user.table'; import { AlbumTable } from 'src/schema/tables/album.table'; import { ApiKeyTable } from 'src/schema/tables/api-key.table'; import { AssetAuditTable } from 'src/schema/tables/asset-audit.table'; +import { AssetEditAuditTable } from 'src/schema/tables/asset-edit-audit.table'; +import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceAuditTable } from 'src/schema/tables/asset-face-audit.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; @@ -71,7 +74,6 @@ import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; import { UserTable } from 'src/schema/tables/user.table'; import { VersionHistoryTable } from 'src/schema/tables/version-history.table'; import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table'; -import { Database, Extensions, Generated, Int8 } from 'src/sql-tools'; @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql']) @Database({ name: 'immich' }) @@ -86,6 +88,8 @@ export class ImmichDatabase { AlbumTable, ApiKeyTable, AssetAuditTable, + AssetEditTable, + AssetEditAuditTable, AssetFaceTable, AssetFaceAuditTable, AssetMetadataTable, @@ -166,6 +170,8 @@ export interface Migrations { } export interface DB { + kysely_migrations: { timestamp: string; name: string }; + activity: ActivityTable; album: AlbumTable; @@ -179,6 +185,8 @@ export interface DB { asset: AssetTable; asset_audit: AssetAuditTable; + asset_edit: AssetEditTable; + asset_edit_audit: AssetEditAuditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; diff --git a/server/src/schema/migrations/1744910873969-InitialMigration.ts b/server/src/schema/migrations/1744910873969-InitialMigration.ts index b703a47536..530b084f83 100644 --- a/server/src/schema/migrations/1744910873969-InitialMigration.ts +++ b/server/src/schema/migrations/1744910873969-InitialMigration.ts @@ -1,4 +1,5 @@ import { Kysely, sql } from 'kysely'; +import { ErrorMessages } from 'src/constants'; import { DatabaseExtension } from 'src/enum'; import { getVectorExtension } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -16,9 +17,7 @@ export async function up(db: Kysely): Promise { rows: [lastMigration], } = await lastMigrationSql.execute(db); if (lastMigration?.name !== 'AddMissingIndex1744910873956') { - throw new Error( - 'Invalid upgrade path. For more information, see https://docs.immich.app/errors/#typeorm-upgrade', - ); + throw new Error(ErrorMessages.TypeOrmUpgrade); } logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration'); return; diff --git a/server/src/schema/migrations/1768336661963-AddAssetWidthHeight.ts b/server/src/schema/migrations/1768336661963-AddAssetWidthHeight.ts new file mode 100644 index 0000000000..90ae32bebf --- /dev/null +++ b/server/src/schema/migrations/1768336661963-AddAssetWidthHeight.ts @@ -0,0 +1,28 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset" ADD COLUMN "width" integer;`.execute(db); + await sql`ALTER TABLE "asset" ADD COLUMN "height" integer;`.execute(db); + + // Populate width and height from exif data with orientation-aware swapping + await sql` + UPDATE "asset" + SET + "width" = CASE + WHEN "asset_exif"."orientation" IN ('5', '6', '7', '8', '-90', '90') THEN "asset_exif"."exifImageHeight" + ELSE "asset_exif"."exifImageWidth" + END, + "height" = CASE + WHEN "asset_exif"."orientation" IN ('5', '6', '7', '8', '-90', '90') THEN "asset_exif"."exifImageWidth" + ELSE "asset_exif"."exifImageHeight" + END + FROM "asset_exif" + WHERE "asset"."id" = "asset_exif"."assetId" + AND ("asset_exif"."exifImageWidth" IS NOT NULL OR "asset_exif"."exifImageHeight" IS NOT NULL) + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset" DROP COLUMN "width";`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "height";`.execute(db); +} diff --git a/server/src/schema/migrations/1768336671610-CreateAssetEditTable.ts b/server/src/schema/migrations/1768336671610-CreateAssetEditTable.ts new file mode 100644 index 0000000000..ef2ef74726 --- /dev/null +++ b/server/src/schema/migrations/1768336671610-CreateAssetEditTable.ts @@ -0,0 +1,22 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql` + CREATE TABLE "asset_edit" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "assetId" uuid NOT NULL, + "action" varchar NOT NULL, + "parameters" jsonb NOT NULL + ); + `.execute(db); + + await sql`ALTER TABLE "asset_edit" ADD CONSTRAINT "asset_edit_pkey" PRIMARY KEY ("id");`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD CONSTRAINT "asset_edit_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "asset" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + await sql`CREATE INDEX "asset_edit_assetId_idx" ON "asset_edit" ("assetId")`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TABLE IF EXISTS "asset_edit";`.execute(db); +} diff --git a/server/src/schema/migrations/1768336694315-CreateIsVisibleColumns.ts b/server/src/schema/migrations/1768336694315-CreateIsVisibleColumns.ts new file mode 100644 index 0000000000..74e4d3bf17 --- /dev/null +++ b/server/src/schema/migrations/1768336694315-CreateIsVisibleColumns.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_ocr" ADD COLUMN "isVisible" boolean NOT NULL DEFAULT TRUE`.execute(db); + await sql`ALTER TABLE "asset_face" ADD COLUMN "isVisible" boolean NOT NULL DEFAULT TRUE`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_ocr" DROP COLUMN "isVisible";`.execute(db); + await sql`ALTER TABLE "asset_face" DROP COLUMN "isVisible";`.execute(db); +} diff --git a/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts b/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts new file mode 100644 index 0000000000..3dd60ccda0 --- /dev/null +++ b/server/src/schema/migrations/1768587436457-AddEditCountToAsset.ts @@ -0,0 +1,53 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" + 1 + WHERE "id" = NEW."assetId"; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" - 1 + WHERE "id" = OLD."assetId"; + RETURN NULL; + END + $$;`.execute(db); + await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + FOR EACH ROW + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_insert', '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_delete', '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_delete', '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_insert', '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_edit_delete" ON "asset_edit";`.execute(db); + await sql`DROP TRIGGER "asset_edit_insert" ON "asset_edit";`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db); + await sql`DROP FUNCTION asset_edit_insert;`.execute(db); + await sql`DROP FUNCTION asset_edit_delete;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} diff --git a/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts b/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts new file mode 100644 index 0000000000..0660b7303d --- /dev/null +++ b/server/src/schema/migrations/1768757482271-SwitchToIsEdited.ts @@ -0,0 +1,89 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_insert() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = true + FROM inserted_edit + WHERE asset.id = inserted_edit."assetId" AND NOT asset."isEdited"; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $$;`.execute(db); + await sql`ALTER TABLE "asset" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "deleted_edit" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + REFERENCING NEW TABLE AS "inserted_edit" + FOR EACH STATEMENT + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = true\\n FROM inserted_edit\\n WHERE asset.id = inserted_edit.\\"assetId\\" AND NOT asset.\\"isEdited\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"deleted_edit\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n REFERENCING NEW TABLE AS \\"inserted_edit\\"\\n FOR EACH STATEMENT\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_insert() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" + 1 + WHERE "id" = NEW."assetId"; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "editCount" = "editCount" - 1 + WHERE "id" = OLD."assetId"; + RETURN NULL; + END + $function$ +`.execute(db); + await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + WHEN ((pg_trigger_depth() = 0)) + EXECUTE FUNCTION asset_edit_delete();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert" + AFTER INSERT ON "asset_edit" + FOR EACH ROW + EXECUTE FUNCTION asset_edit_insert();`.execute(db); + await sql`ALTER TABLE "asset" DROP COLUMN "isEdited";`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_insert","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_insert';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();","name":"asset_edit_delete","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_delete';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();","name":"asset_edit_insert","type":"trigger"}'::jsonb WHERE "name" = 'trigger_asset_edit_insert';`.execute(db); +} diff --git a/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts b/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts new file mode 100644 index 0000000000..b1daa3d72f --- /dev/null +++ b/server/src/schema/migrations/1768828334807-AddIsEditedToAssetFile.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_uq";`.execute(db); + await sql`ALTER TABLE "asset_file" ADD "isEdited" boolean NOT NULL DEFAULT false;`.execute(db); + await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_isEdited_uq" UNIQUE ("assetId", "type", "isEdited");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP CONSTRAINT "asset_file_assetId_type_isEdited_uq";`.execute(db); + await sql`ALTER TABLE "asset_file" ADD CONSTRAINT "asset_file_assetId_type_uq" UNIQUE ("assetId", "type");`.execute(db); + await sql`ALTER TABLE "asset_file" DROP COLUMN "isEdited";`.execute(db); +} diff --git a/server/src/schema/migrations/1768847456553-AddTagsToExif.ts b/server/src/schema/migrations/1768847456553-AddTagsToExif.ts new file mode 100644 index 0000000000..6839468cae --- /dev/null +++ b/server/src/schema/migrations/1768847456553-AddTagsToExif.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_exif" ADD "tags" character varying[];`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_exif" DROP COLUMN "tags";`.execute(db); +} diff --git a/server/src/schema/migrations/1769105700133-AddAssetEditSequence.ts b/server/src/schema/migrations/1769105700133-AddAssetEditSequence.ts new file mode 100644 index 0000000000..40c1723cd6 --- /dev/null +++ b/server/src/schema/migrations/1769105700133-AddAssetEditSequence.ts @@ -0,0 +1,14 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DELETE FROM "asset_edit";`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD "sequence" integer NOT NULL;`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD CONSTRAINT "asset_edit_assetId_sequence_uq" UNIQUE ("assetId", "sequence");`.execute( + db, + ); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_edit" DROP CONSTRAINT "asset_edit_assetId_sequence_uq";`.execute(db); + await sql`ALTER TABLE "asset_edit" DROP COLUMN "sequence";`.execute(db); +} diff --git a/server/src/schema/migrations/1769441657564-AddIsProgressiveColumn.ts b/server/src/schema/migrations/1769441657564-AddIsProgressiveColumn.ts new file mode 100644 index 0000000000..6377dc1059 --- /dev/null +++ b/server/src/schema/migrations/1769441657564-AddIsProgressiveColumn.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" ADD "isProgressive" boolean NOT NULL DEFAULT false;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP COLUMN "isProgressive";`.execute(db); +} diff --git a/server/src/schema/migrations/1769635093204-DropThumbnailJobStatusColumns.ts b/server/src/schema/migrations/1769635093204-DropThumbnailJobStatusColumns.ts new file mode 100644 index 0000000000..9cd2f91b47 --- /dev/null +++ b/server/src/schema/migrations/1769635093204-DropThumbnailJobStatusColumns.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_job_status" DROP COLUMN "previewAt";`.execute(db); + await sql`ALTER TABLE "asset_job_status" DROP COLUMN "thumbnailAt";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_job_status" ADD "previewAt" timestamp with time zone;`.execute(db); + await sql`ALTER TABLE "asset_job_status" ADD "thumbnailAt" timestamp with time zone;`.execute(db); +} diff --git a/server/src/schema/migrations/1771478781948-PeopleSearchIndex.ts b/server/src/schema/migrations/1771478781948-PeopleSearchIndex.ts new file mode 100644 index 0000000000..f09257a3ce --- /dev/null +++ b/server/src/schema/migrations/1771478781948-PeopleSearchIndex.ts @@ -0,0 +1,15 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE INDEX "asset_id_timeline_notDeleted_idx" ON "asset" ("id") WHERE visibility = 'timeline' AND "deletedAt" IS NULL;`.execute(db); + await sql`CREATE INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("personId", "assetId") WHERE "deletedAt" IS NULL AND "isVisible" IS TRUE;`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_id_timeline_notDeleted_idx', '{"type":"index","name":"asset_id_timeline_notDeleted_idx","sql":"CREATE INDEX \\"asset_id_timeline_notDeleted_idx\\" ON \\"asset\\" (\\"id\\") WHERE visibility = ''timeline'' AND \\"deletedAt\\" IS NULL;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_personId_assetId_notDeleted_isVisible_idx', '{"type":"index","name":"asset_face_personId_assetId_notDeleted_isVisible_idx","sql":"CREATE INDEX \\"asset_face_personId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"personId\\", \\"assetId\\") WHERE \\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE;"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "asset_id_timeline_notDeleted_idx";`.execute(db); + await sql`DROP INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx";`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_id_timeline_notDeleted_idx';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_personId_assetId_notDeleted_isVisible_idx';`.execute(db); +} diff --git a/server/src/schema/migrations/1771535611395-ConvertRating0ToNull.ts b/server/src/schema/migrations/1771535611395-ConvertRating0ToNull.ts new file mode 100644 index 0000000000..8faebb250e --- /dev/null +++ b/server/src/schema/migrations/1771535611395-ConvertRating0ToNull.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = 0;`.execute(db); +} + +export async function down(): Promise { + // not supported +} diff --git a/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts new file mode 100644 index 0000000000..a19d102edb --- /dev/null +++ b/server/src/schema/migrations/1771639515206-AddIsTransparentColumn.ts @@ -0,0 +1,9 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" ADD "isTransparent" boolean NOT NULL DEFAULT false;`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset_file" DROP COLUMN "isTransparent";`.execute(db); +} diff --git a/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts b/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts new file mode 100644 index 0000000000..b0ed28a55c --- /dev/null +++ b/server/src/schema/migrations/1771873044511-ChangesTokensToBuffers.ts @@ -0,0 +1,15 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "api_key" ALTER COLUMN "key" TYPE bytea USING decode("key", 'base64');`.execute(db); + await sql`ALTER TABLE "session" ALTER COLUMN "token" TYPE bytea USING decode("token", 'base64');`.execute(db); + await sql`CREATE INDEX "api_key_key_idx" ON "api_key" ("key");`.execute(db); + await sql`CREATE INDEX "session_token_idx" ON "session" ("token");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "api_key_key_idx";`.execute(db); + await sql`DROP INDEX "session_token_idx";`.execute(db); + await sql`ALTER TABLE "api_key" ALTER COLUMN "key" TYPE character varying USING encode("key", 'base64');`.execute(db); + await sql`ALTER TABLE "session" ALTER COLUMN "token" TYPE character varying USING encode("token", 'base64');`.execute(db); +} diff --git a/server/src/schema/migrations/1771873813973-AssetEditSync.ts b/server/src/schema/migrations/1771873813973-AssetEditSync.ts new file mode 100644 index 0000000000..4f5be1ddcd --- /dev/null +++ b/server/src/schema/migrations/1771873813973-AssetEditSync.ts @@ -0,0 +1,53 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO asset_edit_audit ("editId", "assetId") + SELECT "id", "assetId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "asset_edit_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "editId" uuid NOT NULL, + "assetId" uuid NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "asset_edit_audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "asset_edit_audit_assetId_idx" ON "asset_edit_audit" ("assetId");`.execute(db); + await sql`CREATE INDEX "asset_edit_audit_deletedAt_idx" ON "asset_edit_audit" ("deletedAt");`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD "updatedAt" timestamp with time zone NOT NULL DEFAULT now();`.execute(db); + await sql`ALTER TABLE "asset_edit" ADD "updateId" uuid NOT NULL DEFAULT immich_uuid_v7();`.execute(db); + await sql`CREATE INDEX "asset_edit_updateId_idx" ON "asset_edit" ("updateId");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_audit" + AFTER DELETE ON "asset_edit" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION asset_edit_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_edit_updatedAt" + BEFORE UPDATE ON "asset_edit" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_audit', '{"type":"function","name":"asset_edit_audit","sql":"CREATE OR REPLACE FUNCTION asset_edit_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO asset_edit_audit (\\"editId\\", \\"assetId\\")\\n SELECT \\"id\\", \\"assetId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_audit', '{"type":"trigger","name":"asset_edit_audit","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_audit\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_updatedAt', '{"type":"trigger","name":"asset_edit_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_updatedAt\\"\\n BEFORE UPDATE ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_edit_audit" ON "asset_edit";`.execute(db); + await sql`DROP TRIGGER "asset_edit_updatedAt" ON "asset_edit";`.execute(db); + await sql`DROP INDEX "asset_edit_updateId_idx";`.execute(db); + await sql`ALTER TABLE "asset_edit" DROP COLUMN "updatedAt";`.execute(db); + await sql`ALTER TABLE "asset_edit" DROP COLUMN "updateId";`.execute(db); + await sql`DROP TABLE "asset_edit_audit";`.execute(db); + await sql`DROP FUNCTION asset_edit_audit;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_updatedAt';`.execute(db); +} diff --git a/server/src/schema/migrations/1772121424533-AddAssetExifGistEarthcoord.ts b/server/src/schema/migrations/1772121424533-AddAssetExifGistEarthcoord.ts new file mode 100644 index 0000000000..f86529142d --- /dev/null +++ b/server/src/schema/migrations/1772121424533-AddAssetExifGistEarthcoord.ts @@ -0,0 +1,11 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE INDEX "IDX_asset_exif_gist_earthcoord" ON "asset_exif" USING gist (ll_to_earth_public(latitude, longitude));`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_IDX_asset_exif_gist_earthcoord', '{"type":"index","name":"IDX_asset_exif_gist_earthcoord","sql":"CREATE INDEX \\"IDX_asset_exif_gist_earthcoord\\" ON \\"asset_exif\\" USING gist (ll_to_earth_public(latitude, longitude));"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "IDX_asset_exif_gist_earthcoord";`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_IDX_asset_exif_gist_earthcoord';`.execute(db); +} diff --git a/server/src/schema/migrations/1772129818245-FixStupidWhiteSpace.ts b/server/src/schema/migrations/1772129818245-FixStupidWhiteSpace.ts new file mode 100644 index 0000000000..7dc2c5c72f --- /dev/null +++ b/server/src/schema/migrations/1772129818245-FixStupidWhiteSpace.ts @@ -0,0 +1,36 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION asset_edit_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $$;`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\"\\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION public.asset_edit_delete() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ + BEGIN + UPDATE asset + SET "isEdited" = false + FROM deleted_edit + WHERE asset.id = deleted_edit."assetId" AND asset."isEdited" + AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit."assetId" = asset.id); + RETURN NULL; + END + $function$ +`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"isEdited\\" = false\\n FROM deleted_edit\\n WHERE asset.id = deleted_edit.\\"assetId\\" AND asset.\\"isEdited\\" \\n AND NOT EXISTS (SELECT FROM asset_edit edit WHERE edit.\\"assetId\\" = asset.id);\\n RETURN NULL;\\n END\\n $$;","name":"asset_edit_delete","type":"function"}'::jsonb WHERE "name" = 'function_asset_edit_delete';`.execute(db); +} diff --git a/server/src/schema/tables/activity.table.ts b/server/src/schema/tables/activity.table.ts index dfa7c98e42..4a3cc196ee 100644 --- a/server/src/schema/tables/activity.table.ts +++ b/server/src/schema/tables/activity.table.ts @@ -1,8 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Check, Column, @@ -15,7 +10,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AlbumAssetTable } from 'src/schema/tables/album-asset.table'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('activity') @UpdatedAtTrigger('activity_updatedAt') diff --git a/server/src/schema/tables/album-asset-audit.table.ts b/server/src/schema/tables/album-asset-audit.table.ts index ab8fd9ae89..176d32575a 100644 --- a/server/src/schema/tables/album-asset-audit.table.ts +++ b/server/src/schema/tables/album-asset-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { AlbumTable } from 'src/schema/tables/album.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_asset_audit') export class AlbumAssetAuditTable { diff --git a/server/src/schema/tables/album-asset.table.ts b/server/src/schema/tables/album-asset.table.ts index dea271239b..5853e846f1 100644 --- a/server/src/schema/tables/album-asset.table.ts +++ b/server/src/schema/tables/album-asset.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { album_asset_delete_audit } from 'src/schema/functions'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -10,7 +6,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { album_asset_delete_audit } from 'src/schema/functions'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; @Table({ name: 'album_asset' }) @UpdatedAtTrigger('album_asset_updatedAt') diff --git a/server/src/schema/tables/album-audit.table.ts b/server/src/schema/tables/album-audit.table.ts index 432c51c36a..7865f6bfa8 100644 --- a/server/src/schema/tables/album-audit.table.ts +++ b/server/src/schema/tables/album-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_audit') export class AlbumAuditTable { diff --git a/server/src/schema/tables/album-user-audit.table.ts b/server/src/schema/tables/album-user-audit.table.ts index 2259511bdd..d4798761e0 100644 --- a/server/src/schema/tables/album-user-audit.table.ts +++ b/server/src/schema/tables/album-user-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('album_user_audit') export class AlbumUserAuditTable { diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 761aabc1af..2e38041daf 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -1,8 +1,3 @@ -import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AlbumUserRole } from 'src/enum'; -import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, AfterInsertTrigger, @@ -13,7 +8,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AlbumUserRole } from 'src/enum'; +import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'album_user' }) // Pre-existing indices from original album <--> user ManyToMany mapping diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index 5628db3d03..81b846c0f4 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -1,8 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetOrder } from 'src/enum'; -import { album_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -14,7 +9,12 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetOrder } from 'src/enum'; +import { album_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'album' }) @UpdatedAtTrigger('album_updatedAt') diff --git a/server/src/schema/tables/api-key.table.ts b/server/src/schema/tables/api-key.table.ts index efbf18afaa..f0e33a9c71 100644 --- a/server/src/schema/tables/api-key.table.ts +++ b/server/src/schema/tables/api-key.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { Permission } from 'src/enum'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { Permission } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('api_key') @UpdatedAtTrigger('api_key_updatedAt') @@ -21,8 +21,8 @@ export class ApiKeyTable { @Column() name!: string; - @Column() - key!: string; + @Column({ type: 'bytea', index: true }) + key!: Buffer; @ForeignKeyColumn(() => UserTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) userId!: string; diff --git a/server/src/schema/tables/asset-audit.table.ts b/server/src/schema/tables/asset-audit.table.ts index 86c3f6f28b..fee6dde59a 100644 --- a/server/src/schema/tables/asset-audit.table.ts +++ b/server/src/schema/tables/asset-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_audit') export class AssetAuditTable { diff --git a/server/src/schema/tables/asset-edit-audit.table.ts b/server/src/schema/tables/asset-edit-audit.table.ts new file mode 100644 index 0000000000..9c8b29f374 --- /dev/null +++ b/server/src/schema/tables/asset-edit-audit.table.ts @@ -0,0 +1,17 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; + +@Table('asset_edit_audit') +export class AssetEditAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid' }) + editId!: string; + + @Column({ type: 'uuid', index: true }) + assetId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts new file mode 100644 index 0000000000..2e9d2be20d --- /dev/null +++ b/server/src/schema/tables/asset-edit.table.ts @@ -0,0 +1,55 @@ +import { + AfterDeleteTrigger, + AfterInsertTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, + Timestamp, + Unique, + UpdateDateColumn, +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetEditAction, AssetEditParameters } from 'src/dtos/editing.dto'; +import { asset_edit_audit, asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; + +@Table('asset_edit') +@UpdatedAtTrigger('asset_edit_updatedAt') +@AfterInsertTrigger({ scope: 'statement', function: asset_edit_insert, referencingNewTableAs: 'inserted_edit' }) +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_edit_delete, + referencingOldTableAs: 'deleted_edit', + when: 'pg_trigger_depth() = 0', +}) +@AfterDeleteTrigger({ + scope: 'statement', + function: asset_edit_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) +@Unique({ columns: ['assetId', 'sequence'] }) +export class AssetEditTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + assetId!: string; + + @Column() + action!: AssetEditAction; + + @Column({ type: 'jsonb' }) + parameters!: AssetEditParameters; + + @Column({ type: 'integer' }) + sequence!: number; + + @UpdateDateColumn() + updatedAt!: Generated; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; +} diff --git a/server/src/schema/tables/asset-exif.table.ts b/server/src/schema/tables/asset-exif.table.ts index 346098a72c..ae47ecfb10 100644 --- a/server/src/schema/tables/asset-exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -1,9 +1,23 @@ +import { + Column, + ForeignKeyColumn, + Generated, + Index, + Int8, + Table, + Timestamp, + UpdateDateColumn, +} from '@immich/sql-tools'; import { LockableProperty } from 'src/database'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools'; @Table('asset_exif') +@Index({ + name: 'IDX_asset_exif_gist_earthcoord', + using: 'gist', + expression: 'll_to_earth_public(latitude, longitude)', +}) @UpdatedAtTrigger('asset_exif_updatedAt') export class AssetExifTable { @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', primary: true }) @@ -93,6 +107,9 @@ export class AssetExifTable { @Column({ type: 'integer', nullable: true }) rating!: number | null; + @Column({ type: 'character varying', array: true, nullable: true }) + tags!: string[] | null; + @UpdateDateColumn({ default: () => 'clock_timestamp()' }) updatedAt!: Generated; diff --git a/server/src/schema/tables/asset-face-audit.table.ts b/server/src/schema/tables/asset-face-audit.table.ts index 4f03c22aa0..2e61904800 100644 --- a/server/src/schema/tables/asset-face-audit.table.ts +++ b/server/src/schema/tables/asset-face-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_face_audit') export class AssetFaceAuditTable { diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index 5041d945e2..b67e5e5dac 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -1,9 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { SourceType } from 'src/enum'; -import { asset_face_source_type } from 'src/schema/enums'; -import { asset_face_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { PersonTable } from 'src/schema/tables/person.table'; import { AfterDeleteTrigger, Column, @@ -15,7 +9,13 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { SourceType } from 'src/enum'; +import { asset_face_source_type } from 'src/schema/enums'; +import { asset_face_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { PersonTable } from 'src/schema/tables/person.table'; @Table({ name: 'asset_face' }) @UpdatedAtTrigger('asset_face_updatedAt') @@ -27,6 +27,11 @@ import { }) // schemaFromDatabase does not preserve column order @Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] }) +@Index({ + name: 'asset_face_personId_assetId_notDeleted_isVisible_idx', + columns: ['personId', 'assetId'], + where: '"deletedAt" IS NULL AND "isVisible" IS TRUE', +}) @Index({ columns: ['personId', 'assetId'] }) export class AssetFaceTable { @PrimaryGeneratedColumn() @@ -78,4 +83,7 @@ export class AssetFaceTable { @UpdateIdColumn() updateId!: Generated; + + @Column({ type: 'boolean', default: true }) + isVisible!: Generated; } diff --git a/server/src/schema/tables/asset-file.table.ts b/server/src/schema/tables/asset-file.table.ts index 6456d1d535..6285e4d653 100644 --- a/server/src/schema/tables/asset-file.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetFileType } from 'src/enum'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, CreateDateColumn, @@ -11,10 +8,13 @@ import { Timestamp, Unique, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetFileType } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; @Table('asset_file') -@Unique({ columns: ['assetId', 'type'] }) +@Unique({ columns: ['assetId', 'type', 'isEdited'] }) @UpdatedAtTrigger('asset_file_updatedAt') export class AssetFileTable { @PrimaryGeneratedColumn() @@ -37,4 +37,13 @@ export class AssetFileTable { @UpdateIdColumn({ index: true }) updateId!: Generated; + + @Column({ type: 'boolean', default: false }) + isEdited!: Generated; + + @Column({ type: 'boolean', default: false }) + isProgressive!: Generated; + + @Column({ type: 'boolean', default: false }) + isTransparent!: Generated; } diff --git a/server/src/schema/tables/asset-job-status.table.ts b/server/src/schema/tables/asset-job-status.table.ts index d68dbcb761..4d889ade46 100644 --- a/server/src/schema/tables/asset-job-status.table.ts +++ b/server/src/schema/tables/asset-job-status.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Table, Timestamp } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Table, Timestamp } from 'src/sql-tools'; @Table('asset_job_status') export class AssetJobStatusTable { @@ -15,12 +15,6 @@ export class AssetJobStatusTable { @Column({ type: 'timestamp with time zone', nullable: true }) duplicatesDetectedAt!: Timestamp | null; - @Column({ type: 'timestamp with time zone', nullable: true }) - previewAt!: Timestamp | null; - - @Column({ type: 'timestamp with time zone', nullable: true }) - thumbnailAt!: Timestamp | null; - @Column({ type: 'timestamp with time zone', nullable: true }) ocrAt!: Timestamp | null; } diff --git a/server/src/schema/tables/asset-metadata-audit.table.ts b/server/src/schema/tables/asset-metadata-audit.table.ts index 3b94ce6d1a..15c0b47edc 100644 --- a/server/src/schema/tables/asset-metadata-audit.table.ts +++ b/server/src/schema/tables/asset-metadata-audit.table.ts @@ -1,6 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { AssetMetadataKey } from 'src/enum'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('asset_metadata_audit') export class AssetMetadataAuditTable { @@ -11,7 +10,7 @@ export class AssetMetadataAuditTable { assetId!: string; @Column({ index: true }) - key!: AssetMetadataKey; + key!: string; @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) deletedAt!: Generated; diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts index d529d6ad7b..53e3121a41 100644 --- a/server/src/schema/tables/asset-metadata.table.ts +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetMetadataKey } from 'src/enum'; -import { asset_metadata_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; import { AfterDeleteTrigger, Column, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetMetadataKey } from 'src/enum'; +import { asset_metadata_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; @UpdatedAtTrigger('asset_metadata_updated_at') @Table('asset_metadata') @@ -32,7 +32,7 @@ export class AssetMetadataTable { assetId!: string; @PrimaryColumn({ type: 'character varying' }) - key!: AssetMetadataKey; + key!: AssetMetadataKey | string; @Column({ type: 'jsonb' }) value!: object; diff --git a/server/src/schema/tables/asset-ocr.table.ts b/server/src/schema/tables/asset-ocr.table.ts index 6ab159b531..b58224a247 100644 --- a/server/src/schema/tables/asset-ocr.table.ts +++ b/server/src/schema/tables/asset-ocr.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; @Table('asset_ocr') export class AssetOcrTable { @@ -42,4 +42,7 @@ export class AssetOcrTable { @Column({ type: 'text' }) text!: string; + + @Column({ type: 'boolean', default: true }) + isVisible!: Generated; } diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index b28fc99e4a..12e9c36125 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -1,10 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; -import { asset_delete_audit } from 'src/schema/functions'; -import { LibraryTable } from 'src/schema/tables/library.table'; -import { StackTable } from 'src/schema/tables/stack.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -17,7 +10,14 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums'; +import { asset_delete_audit } from 'src/schema/functions'; +import { LibraryTable } from 'src/schema/tables/library.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { UserTable } from 'src/schema/tables/user.table'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; @Table('asset') @@ -55,6 +55,11 @@ import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; using: 'gin', expression: 'f_unaccent("originalFileName") gin_trgm_ops', }) +@Index({ + name: 'asset_id_timeline_notDeleted_idx', + columns: ['id'], + where: `visibility = 'timeline' AND "deletedAt" IS NULL`, +}) // For all assets, each originalpath must be unique per user and library export class AssetTable { @PrimaryGeneratedColumn() @@ -137,4 +142,13 @@ export class AssetTable { @Column({ enum: asset_visibility_enum, default: AssetVisibility.Timeline }) visibility!: Generated; + + @Column({ type: 'integer', nullable: true }) + width!: number | null; + + @Column({ type: 'integer', nullable: true }) + height!: number | null; + + @Column({ type: 'boolean', default: false }) + isEdited!: Generated; } diff --git a/server/src/schema/tables/audit.table.ts b/server/src/schema/tables/audit.table.ts index 15b4990814..78c9a57c09 100644 --- a/server/src/schema/tables/audit.table.ts +++ b/server/src/schema/tables/audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; import { DatabaseAction, EntityType } from 'src/enum'; -import { Column, CreateDateColumn, Generated, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; @Table('audit') @Index({ columns: ['ownerId', 'createdAt'] }) diff --git a/server/src/schema/tables/face-search.table.ts b/server/src/schema/tables/face-search.table.ts index ff63879404..7c585437c8 100644 --- a/server/src/schema/tables/face-search.table.ts +++ b/server/src/schema/tables/face-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table({ name: 'face_search' }) @Index({ diff --git a/server/src/schema/tables/geodata-places.table.ts b/server/src/schema/tables/geodata-places.table.ts index eec2b240d0..101ddb759f 100644 --- a/server/src/schema/tables/geodata-places.table.ts +++ b/server/src/schema/tables/geodata-places.table.ts @@ -1,4 +1,4 @@ -import { Column, Index, PrimaryColumn, Table, Timestamp } from 'src/sql-tools'; +import { Column, Index, PrimaryColumn, Table, Timestamp } from '@immich/sql-tools'; @Table({ name: 'geodata_places', primaryConstraintName: 'geodata_places_pkey' }) @Index({ diff --git a/server/src/schema/tables/library.table.ts b/server/src/schema/tables/library.table.ts index 57ad144c8e..2f79a3e78d 100644 --- a/server/src/schema/tables/library.table.ts +++ b/server/src/schema/tables/library.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +8,9 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('library') @UpdatedAtTrigger('library_updatedAt') diff --git a/server/src/schema/tables/memory-asset-audit.table.ts b/server/src/schema/tables/memory-asset-audit.table.ts index 218c2f19ff..67c434c45a 100644 --- a/server/src/schema/tables/memory-asset-audit.table.ts +++ b/server/src/schema/tables/memory-asset-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { MemoryTable } from 'src/schema/tables/memory.table'; -import { Column, CreateDateColumn, ForeignKeyColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_asset_audit') export class MemoryAssetAuditTable { diff --git a/server/src/schema/tables/memory-asset.table.ts b/server/src/schema/tables/memory-asset.table.ts index b162000ca0..b44c78c3b9 100644 --- a/server/src/schema/tables/memory-asset.table.ts +++ b/server/src/schema/tables/memory-asset.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { memory_asset_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { MemoryTable } from 'src/schema/tables/memory.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -10,7 +6,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { memory_asset_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { MemoryTable } from 'src/schema/tables/memory.table'; @Table('memory_asset') @UpdatedAtTrigger('memory_asset_updatedAt') diff --git a/server/src/schema/tables/memory-audit.table.ts b/server/src/schema/tables/memory-audit.table.ts index 167caf8e6e..6d278676b7 100644 --- a/server/src/schema/tables/memory-audit.table.ts +++ b/server/src/schema/tables/memory-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('memory_audit') export class MemoryAuditTable { diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 408f7bca19..8b9867b4cc 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { MemoryType } from 'src/enum'; -import { memory_delete_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { MemoryType } from 'src/enum'; +import { memory_delete_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('memory') @UpdatedAtTrigger('memory_updatedAt') diff --git a/server/src/schema/tables/move.table.ts b/server/src/schema/tables/move.table.ts index 1afda2767a..c7229431f7 100644 --- a/server/src/schema/tables/move.table.ts +++ b/server/src/schema/tables/move.table.ts @@ -1,5 +1,5 @@ +import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from '@immich/sql-tools'; import { PathType } from 'src/enum'; -import { Column, Generated, PrimaryGeneratedColumn, Table, Unique } from 'src/sql-tools'; @Table('move_history') // path lock (per entity) diff --git a/server/src/schema/tables/natural-earth-countries.table.ts b/server/src/schema/tables/natural-earth-countries.table.ts index c59d15fc21..06f189264e 100644 --- a/server/src/schema/tables/natural-earth-countries.table.ts +++ b/server/src/schema/tables/natural-earth-countries.table.ts @@ -1,4 +1,4 @@ -import { Column, Generated, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; +import { Column, Generated, PrimaryGeneratedColumn, Table } from '@immich/sql-tools'; @Table({ name: 'naturalearth_countries', primaryConstraintName: 'naturalearth_countries_pkey' }) export class NaturalEarthCountriesTable { diff --git a/server/src/schema/tables/notification.table.ts b/server/src/schema/tables/notification.table.ts index 01a93a73e5..6bf65808f1 100644 --- a/server/src/schema/tables/notification.table.ts +++ b/server/src/schema/tables/notification.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { NotificationLevel, NotificationType } from 'src/enum'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -11,7 +8,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { NotificationLevel, NotificationType } from 'src/enum'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('notification') @UpdatedAtTrigger('notification_updatedAt') diff --git a/server/src/schema/tables/ocr-search.table.ts b/server/src/schema/tables/ocr-search.table.ts index 3449725adb..74aefb333b 100644 --- a/server/src/schema/tables/ocr-search.table.ts +++ b/server/src/schema/tables/ocr-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table('ocr_search') @Index({ diff --git a/server/src/schema/tables/partner-audit.table.ts b/server/src/schema/tables/partner-audit.table.ts index fa2f0c27cc..3cfd1854e1 100644 --- a/server/src/schema/tables/partner-audit.table.ts +++ b/server/src/schema/tables/partner-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('partner_audit') export class PartnerAuditTable { diff --git a/server/src/schema/tables/partner.table.ts b/server/src/schema/tables/partner.table.ts index 8fc332cb12..408cac650f 100644 --- a/server/src/schema/tables/partner.table.ts +++ b/server/src/schema/tables/partner.table.ts @@ -1,6 +1,3 @@ -import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { partner_delete_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { partner_delete_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('partner') @UpdatedAtTrigger('partner_updatedAt') diff --git a/server/src/schema/tables/person-audit.table.ts b/server/src/schema/tables/person-audit.table.ts index 8a899a1808..4fb55f1744 100644 --- a/server/src/schema/tables/person-audit.table.ts +++ b/server/src/schema/tables/person-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('person_audit') export class PersonAuditTable { diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index 3b523a39d2..02fb85b757 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { person_delete_audit } from 'src/schema/functions'; -import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Check, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { person_delete_audit } from 'src/schema/functions'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('person') @UpdatedAtTrigger('person_updatedAt') diff --git a/server/src/schema/tables/plugin.table.ts b/server/src/schema/tables/plugin.table.ts index 3de7ca63c9..5f82807f23 100644 --- a/server/src/schema/tables/plugin.table.ts +++ b/server/src/schema/tables/plugin.table.ts @@ -1,4 +1,3 @@ -import { PluginContext } from 'src/enum'; import { Column, CreateDateColumn, @@ -9,7 +8,8 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { PluginContext } from 'src/enum'; import type { JSONSchema } from 'src/types/plugin-schema.types'; @Table('plugin') diff --git a/server/src/schema/tables/session.table.ts b/server/src/schema/tables/session.table.ts index 466152d35d..e57628d6da 100644 --- a/server/src/schema/tables/session.table.ts +++ b/server/src/schema/tables/session.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -9,7 +7,9 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table({ name: 'session' }) @UpdatedAtTrigger('session_updatedAt') @@ -17,9 +17,8 @@ export class SessionTable { @PrimaryGeneratedColumn() id!: Generated; - // TODO convert to byte[] - @Column() - token!: string; + @Column({ type: 'bytea', index: true }) + token!: Buffer; @CreateDateColumn() createdAt!: Generated; diff --git a/server/src/schema/tables/shared-link-asset.table.ts b/server/src/schema/tables/shared-link-asset.table.ts index 37e6a3d9f0..ff96f69980 100644 --- a/server/src/schema/tables/shared-link-asset.table.ts +++ b/server/src/schema/tables/shared-link-asset.table.ts @@ -1,6 +1,6 @@ +import { ForeignKeyColumn, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; -import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('shared_link_asset') export class SharedLinkAssetTable { diff --git a/server/src/schema/tables/shared-link.table.ts b/server/src/schema/tables/shared-link.table.ts index 80e2d7cdf4..d99520388a 100644 --- a/server/src/schema/tables/shared-link.table.ts +++ b/server/src/schema/tables/shared-link.table.ts @@ -1,6 +1,3 @@ -import { SharedLinkType } from 'src/enum'; -import { AlbumTable } from 'src/schema/tables/album.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -9,7 +6,10 @@ import { PrimaryGeneratedColumn, Table, Timestamp, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { SharedLinkType } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('shared_link') export class SharedLinkTable { diff --git a/server/src/schema/tables/smart-search.table.ts b/server/src/schema/tables/smart-search.table.ts index dc140efb2f..31071e6134 100644 --- a/server/src/schema/tables/smart-search.table.ts +++ b/server/src/schema/tables/smart-search.table.ts @@ -1,5 +1,5 @@ +import { Column, ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { Column, ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Table({ name: 'smart_search' }) @Index({ diff --git a/server/src/schema/tables/stack-audit.table.ts b/server/src/schema/tables/stack-audit.table.ts index d46ff95e57..3a62545cd2 100644 --- a/server/src/schema/tables/stack-audit.table.ts +++ b/server/src/schema/tables/stack-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('stack_audit') export class StackAuditTable { diff --git a/server/src/schema/tables/stack.table.ts b/server/src/schema/tables/stack.table.ts index 9c9eb81373..3f903e065a 100644 --- a/server/src/schema/tables/stack.table.ts +++ b/server/src/schema/tables/stack.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { stack_delete_audit } from 'src/schema/functions'; -import { AssetTable } from 'src/schema/tables/asset.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, CreateDateColumn, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { stack_delete_audit } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('stack') @UpdatedAtTrigger('stack_updatedAt') diff --git a/server/src/schema/tables/sync-checkpoint.table.ts b/server/src/schema/tables/sync-checkpoint.table.ts index 6ad4c54a86..d9ada5aed0 100644 --- a/server/src/schema/tables/sync-checkpoint.table.ts +++ b/server/src/schema/tables/sync-checkpoint.table.ts @@ -1,6 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { SyncEntityType } from 'src/enum'; -import { SessionTable } from 'src/schema/tables/session.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { SyncEntityType } from 'src/enum'; +import { SessionTable } from 'src/schema/tables/session.table'; @Table('session_sync_checkpoint') @UpdatedAtTrigger('session_sync_checkpoint_updatedAt') diff --git a/server/src/schema/tables/system-metadata.table.ts b/server/src/schema/tables/system-metadata.table.ts index 8657768db6..9f21172505 100644 --- a/server/src/schema/tables/system-metadata.table.ts +++ b/server/src/schema/tables/system-metadata.table.ts @@ -1,5 +1,5 @@ +import { Column, PrimaryColumn, Table } from '@immich/sql-tools'; import { SystemMetadataKey } from 'src/enum'; -import { Column, PrimaryColumn, Table } from 'src/sql-tools'; import { SystemMetadata } from 'src/types'; @Table('system_metadata') diff --git a/server/src/schema/tables/tag-asset.table.ts b/server/src/schema/tables/tag-asset.table.ts index 3ea2361b4f..9d7ea026c6 100644 --- a/server/src/schema/tables/tag-asset.table.ts +++ b/server/src/schema/tables/tag-asset.table.ts @@ -1,6 +1,6 @@ +import { ForeignKeyColumn, Index, Table } from '@immich/sql-tools'; import { AssetTable } from 'src/schema/tables/asset.table'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ForeignKeyColumn, Index, Table } from 'src/sql-tools'; @Index({ columns: ['assetId', 'tagId'] }) @Table('tag_asset') diff --git a/server/src/schema/tables/tag-closure.table.ts b/server/src/schema/tables/tag-closure.table.ts index aeb8c8cf11..2e1c83a20f 100644 --- a/server/src/schema/tables/tag-closure.table.ts +++ b/server/src/schema/tables/tag-closure.table.ts @@ -1,5 +1,5 @@ +import { ForeignKeyColumn, Table } from '@immich/sql-tools'; import { TagTable } from 'src/schema/tables/tag.table'; -import { ForeignKeyColumn, Table } from 'src/sql-tools'; @Table('tag_closure') export class TagClosureTable { diff --git a/server/src/schema/tables/tag.table.ts b/server/src/schema/tables/tag.table.ts index dc1fa2947b..2a07239d84 100644 --- a/server/src/schema/tables/tag.table.ts +++ b/server/src/schema/tables/tag.table.ts @@ -1,5 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +8,9 @@ import { Timestamp, Unique, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserTable } from 'src/schema/tables/user.table'; @Table('tag') @UpdatedAtTrigger('tag_updatedAt') diff --git a/server/src/schema/tables/user-audit.table.ts b/server/src/schema/tables/user-audit.table.ts index 084b42fb65..36f89dfa7d 100644 --- a/server/src/schema/tables/user-audit.table.ts +++ b/server/src/schema/tables/user-audit.table.ts @@ -1,5 +1,5 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('user_audit') export class UserAuditTable { diff --git a/server/src/schema/tables/user-metadata-audit.table.ts b/server/src/schema/tables/user-metadata-audit.table.ts index 63f503ab85..17dee673b4 100644 --- a/server/src/schema/tables/user-metadata-audit.table.ts +++ b/server/src/schema/tables/user-metadata-audit.table.ts @@ -1,6 +1,6 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; import { UserMetadataKey } from 'src/enum'; -import { Column, CreateDateColumn, Generated, Table, Timestamp } from 'src/sql-tools'; @Table('user_metadata_audit') export class UserMetadataAuditTable { diff --git a/server/src/schema/tables/user-metadata.table.ts b/server/src/schema/tables/user-metadata.table.ts index a453ec6677..6983ed3dda 100644 --- a/server/src/schema/tables/user-metadata.table.ts +++ b/server/src/schema/tables/user-metadata.table.ts @@ -1,7 +1,3 @@ -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserMetadataKey } from 'src/enum'; -import { user_metadata_audit } from 'src/schema/functions'; -import { UserTable } from 'src/schema/tables/user.table'; import { AfterDeleteTrigger, Column, @@ -11,7 +7,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserMetadataKey } from 'src/enum'; +import { user_metadata_audit } from 'src/schema/functions'; +import { UserTable } from 'src/schema/tables/user.table'; import { UserMetadata, UserMetadataItem } from 'src/types'; @UpdatedAtTrigger('user_metadata_updated_at') diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 46d6656382..3a340d976b 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -1,7 +1,3 @@ -import { ColumnType } from 'kysely'; -import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; -import { UserAvatarColor, UserStatus } from 'src/enum'; -import { user_delete_audit } from 'src/schema/functions'; import { AfterDeleteTrigger, Column, @@ -13,7 +9,11 @@ import { Table, Timestamp, UpdateDateColumn, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { ColumnType } from 'kysely'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { UserAvatarColor, UserStatus } from 'src/enum'; +import { user_delete_audit } from 'src/schema/functions'; @Table('user') @UpdatedAtTrigger('user_updatedAt') diff --git a/server/src/schema/tables/version-history.table.ts b/server/src/schema/tables/version-history.table.ts index 143852c527..12eab7fd69 100644 --- a/server/src/schema/tables/version-history.table.ts +++ b/server/src/schema/tables/version-history.table.ts @@ -1,4 +1,4 @@ -import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from 'src/sql-tools'; +import { Column, CreateDateColumn, Generated, PrimaryGeneratedColumn, Table, Timestamp } from '@immich/sql-tools'; @Table('version_history') export class VersionHistoryTable { diff --git a/server/src/schema/tables/workflow.table.ts b/server/src/schema/tables/workflow.table.ts index 62a5531d8e..163518e039 100644 --- a/server/src/schema/tables/workflow.table.ts +++ b/server/src/schema/tables/workflow.table.ts @@ -1,6 +1,3 @@ -import { PluginTriggerType } from 'src/enum'; -import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table'; -import { UserTable } from 'src/schema/tables/user.table'; import { Column, CreateDateColumn, @@ -10,7 +7,10 @@ import { PrimaryGeneratedColumn, Table, Timestamp, -} from 'src/sql-tools'; +} from '@immich/sql-tools'; +import { PluginTriggerType } from 'src/enum'; +import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table'; +import { UserTable } from 'src/schema/tables/user.table'; import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; @Table('workflow') diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 03be834354..d21185bd35 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -3,9 +3,13 @@ import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum'; import { AlbumService } from 'src/services/album.service'; -import { albumStub } from 'test/fixtures/album.stub'; +import { AlbumUserFactory } from 'test/factories/album-user.factory'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(AlbumService.name, () => { @@ -39,17 +43,19 @@ describe(AlbumService.name, () => { describe('getAll', () => { it('gets list of albums for auth user', async () => { - mocks.album.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); + const album = AlbumFactory.from().albumUser().build(); + const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).albumUser().build(); + mocks.album.getOwned.mockResolvedValue([album, sharedWithUserAlbum]); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.empty.id, + albumId: album.id, assetCount: 0, startDate: null, endDate: null, lastModifiedAssetTimestamp: null, }, { - albumId: albumStub.sharedWithUser.id, + albumId: sharedWithUserAlbum.id, assetCount: 0, startDate: null, endDate: null, @@ -57,17 +63,18 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(authStub.admin, {}); + const result = await sut.getAll(AuthFactory.create(album.owner), {}); expect(result).toHaveLength(2); - expect(result[0].id).toEqual(albumStub.empty.id); - expect(result[1].id).toEqual(albumStub.sharedWithUser.id); + expect(result[0].id).toEqual(album.id); + expect(result[1].id).toEqual(sharedWithUserAlbum.id); }); it('gets list of albums that have a specific asset', async () => { - mocks.album.getByAssetId.mockResolvedValue([albumStub.oneAsset]); + const album = AlbumFactory.from().owner({ isAdmin: true }).albumUser().asset().asset().build(); + mocks.album.getByAssetId.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.oneAsset.id, + albumId: album.id, assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), @@ -75,17 +82,18 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id }); + const result = await sut.getAll(AuthFactory.create(album.owner), { assetId: album.assets[0].id }); expect(result).toHaveLength(1); - expect(result[0].id).toEqual(albumStub.oneAsset.id); + expect(result[0].id).toEqual(album.id); expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(1); }); it('gets list of albums that are shared', async () => { - mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]); + const album = AlbumFactory.from().albumUser().build(); + mocks.album.getShared.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.sharedWithUser.id, + albumId: album.id, assetCount: 0, startDate: null, endDate: null, @@ -93,17 +101,18 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(authStub.admin, { shared: true }); + const result = await sut.getAll(AuthFactory.create(album.owner), { shared: true }); expect(result).toHaveLength(1); - expect(result[0].id).toEqual(albumStub.sharedWithUser.id); + expect(result[0].id).toEqual(album.id); expect(mocks.album.getShared).toHaveBeenCalledTimes(1); }); it('gets list of albums that are NOT shared', async () => { - mocks.album.getNotShared.mockResolvedValue([albumStub.empty]); + const album = AlbumFactory.create(); + mocks.album.getNotShared.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.empty.id, + albumId: album.id, assetCount: 0, startDate: null, endDate: null, @@ -111,18 +120,19 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(authStub.admin, { shared: false }); + const result = await sut.getAll(AuthFactory.create(album.owner), { shared: false }); expect(result).toHaveLength(1); - expect(result[0].id).toEqual(albumStub.empty.id); + expect(result[0].id).toEqual(album.id); expect(mocks.album.getNotShared).toHaveBeenCalledTimes(1); }); }); it('counts assets correctly', async () => { - mocks.album.getOwned.mockResolvedValue([albumStub.oneAsset]); + const album = AlbumFactory.create(); + mocks.album.getOwned.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.oneAsset.id, + albumId: album.id, assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), @@ -130,8 +140,7 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(authStub.admin, {}); - + const result = await sut.getAll(AuthFactory.create(album.owner), {}); expect(result).toHaveLength(1); expect(result[0].assetCount).toEqual(1); expect(mocks.album.getOwned).toHaveBeenCalledTimes(1); @@ -139,42 +148,52 @@ describe(AlbumService.name, () => { describe('create', () => { it('creates album', async () => { - mocks.album.create.mockResolvedValue(albumStub.empty); - mocks.user.get.mockResolvedValue(userStub.user1); - mocks.user.getMetadata.mockResolvedValue([]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['123'])); + const assetId = newUuid(); + const albumUser = { userId: newUuid(), role: AlbumUserRole.Editor }; + const album = AlbumFactory.from({ albumName: 'test', description: 'description' }) + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser(albumUser) + .build(); - await sut.create(authStub.admin, { - albumName: 'Empty album', - albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }], - description: '', - assetIds: ['123'], + mocks.album.create.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user)); + mocks.user.getMetadata.mockResolvedValue([]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + + await sut.create(AuthFactory.create(album.owner), { + albumName: 'test', + albumUsers: [albumUser], + description: 'description', + assetIds: [assetId], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: authStub.admin.user.id, - albumName: albumStub.empty.albumName, - description: albumStub.empty.description, - order: 'desc', - albumThumbnailAssetId: '123', + ownerId: album.owner.id, + albumName: 'test', + description: 'description', + order: album.order, + albumThumbnailAssetId: assetId, }, - ['123'], - [{ userId: 'user-id', role: AlbumUserRole.Editor }], + [assetId], + [{ userId: albumUser.userId, role: AlbumUserRole.Editor }], ); - expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); - expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false); - expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { - id: albumStub.empty.id, - userId: 'user-id', - }); + expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {}); + expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false); + expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId }); }); it('creates album with assetOrder from user preferences', async () => { - mocks.album.create.mockResolvedValue(albumStub.empty); - mocks.user.get.mockResolvedValue(userStub.user1); + const assetId = newUuid(); + const albumUser = { userId: newUuid(), role: AlbumUserRole.Editor }; + const album = AlbumFactory.from() + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser(albumUser) + .build(); + mocks.album.create.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(album.albumUsers[0].user); mocks.user.getMetadata.mockResolvedValue([ { key: UserMetadataKey.Preferences, @@ -185,84 +204,87 @@ describe(AlbumService.name, () => { }, }, ]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['123'])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); - await sut.create(authStub.admin, { - albumName: 'Empty album', - albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }], - description: '', - assetIds: ['123'], + await sut.create(AuthFactory.create(album.owner), { + albumName: album.albumName, + albumUsers: [albumUser], + description: album.description, + assetIds: [assetId], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: authStub.admin.user.id, - albumName: albumStub.empty.albumName, - description: albumStub.empty.description, + ownerId: album.owner.id, + albumName: album.albumName, + description: album.description, order: 'asc', - albumThumbnailAssetId: '123', + albumThumbnailAssetId: assetId, }, - ['123'], - [{ userId: 'user-id', role: AlbumUserRole.Editor }], + [assetId], + [albumUser], ); - expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); - expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false); - expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { - id: albumStub.empty.id, - userId: 'user-id', - }); + expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {}); + expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false); + expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId }); }); it('should require valid userIds', async () => { mocks.user.get.mockResolvedValue(void 0); await expect( - sut.create(authStub.admin, { + sut.create(AuthFactory.create(), { albumName: 'Empty album', - albumUsers: [{ userId: 'user-3', role: AlbumUserRole.Editor }], + albumUsers: [{ userId: 'unknown-user', role: AlbumUserRole.Editor }], }), ).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.user.get).toHaveBeenCalledWith('user-3', {}); + expect(mocks.user.get).toHaveBeenCalledWith('unknown-user', {}); expect(mocks.album.create).not.toHaveBeenCalled(); }); it('should only add assets the user is allowed to access', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); - mocks.album.create.mockResolvedValue(albumStub.oneAsset); + const assetId = newUuid(); + const album = AlbumFactory.from() + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser() + .build(); + mocks.user.get.mockResolvedValue(album.albumUsers[0].user); + mocks.album.create.mockResolvedValue(album); mocks.user.getMetadata.mockResolvedValue([]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); - await sut.create(authStub.admin, { - albumName: 'Test album', - description: '', - assetIds: ['asset-1', 'asset-2'], + await sut.create(AuthFactory.create(album.owner), { + albumName: album.albumName, + description: album.description, + assetIds: [assetId, 'asset-2'], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: authStub.admin.user.id, - albumName: 'Test album', - description: '', + ownerId: album.owner.id, + albumName: album.albumName, + description: album.description, order: 'desc', - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: assetId, }, - ['asset-1'], + [assetId], [], ); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set(['asset-1', 'asset-2']), + album.owner.id, + new Set([assetId, 'asset-2']), false, ); }); it('should throw an error if the userId is the ownerId', async () => { - mocks.user.get.mockResolvedValue(userStub.admin); + const album = AlbumFactory.create(); + mocks.user.get.mockResolvedValue(album.owner); await expect( - sut.create(authStub.admin, { + sut.create(AuthFactory.create(album.owner), { albumName: 'Empty album', - albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.Editor }], + albumUsers: [{ userId: album.owner.id, role: AlbumUserRole.Editor }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.create).not.toHaveBeenCalled(); @@ -271,11 +293,12 @@ describe(AlbumService.name, () => { describe('update', () => { it('should prevent updating an album that does not exist', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set()); mocks.album.getById.mockResolvedValue(void 0); await expect( - sut.update(authStub.user1, 'invalid-id', { - albumName: 'new album name', + sut.update(AuthFactory.create(), 'invalid-id', { + albumName: 'Album', }), ).rejects.toBeInstanceOf(BadRequestException); @@ -283,139 +306,138 @@ describe(AlbumService.name, () => { }); it('should prevent updating a not owned album (shared with auth user)', async () => { + const album = AlbumFactory.from().albumUser().build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.update(authStub.admin, albumStub.sharedWithAdmin.id, { - albumName: 'new album name', - }), + sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should require a valid thumbnail asset id', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); - mocks.album.update.mockResolvedValue(albumStub.oneAsset); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set()); await expect( - sut.update(authStub.admin, albumStub.oneAsset.id, { - albumThumbnailAssetId: 'not-in-album', - }), + sut.update(AuthFactory.create(album.owner), album.id, { albumThumbnailAssetId: 'not-in-album' }), ).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.album.getAssetIds).toHaveBeenCalledWith('album-4', ['not-in-album']); + expect(mocks.album.getAssetIds).toHaveBeenCalledWith(album.id, ['not-in-album']); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should allow the owner to update the album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.update.mockResolvedValue(album); - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); - mocks.album.update.mockResolvedValue(albumStub.oneAsset); - - await sut.update(authStub.admin, albumStub.oneAsset.id, { - albumName: 'new album name', - }); + await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' }); expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenCalledWith('album-4', { - id: 'album-4', - albumName: 'new album name', - }); + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { id: album.id, albumName: 'new album name' }); }); }); describe('delete', () => { - it('should throw an error for an album not found', async () => { + it('should require permissions', async () => { + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); - await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.delete).not.toHaveBeenCalled(); }); it('should not let a shared user delete the album', async () => { - mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin); + const album = AlbumFactory.create(); + mocks.album.getById.mockResolvedValue(album); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); - await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( - BadRequestException, - ); + await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.delete).not.toHaveBeenCalled(); }); it('should let the owner delete an album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id])); - mocks.album.getById.mockResolvedValue(albumStub.empty); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); - await sut.delete(authStub.admin, albumStub.empty.id); + await sut.delete(AuthFactory.create(album.owner), album.id); expect(mocks.album.delete).toHaveBeenCalledTimes(1); - expect(mocks.album.delete).toHaveBeenCalledWith(albumStub.empty.id); + expect(mocks.album.delete).toHaveBeenCalledWith(album.id); }); }); describe('addUsers', () => { it('should throw an error if the auth user is not the owner', async () => { + const album = AlbumFactory.create(); + const user = UserFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-1' }] }), + sut.addUsers(AuthFactory.create(user), album.id, { albumUsers: [{ userId: newUuid() }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should throw an error if the userId is already added', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); - mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin); + const userId = newUuid(); + const album = AlbumFactory.from().albumUser({ userId }).build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); await expect( - sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { - albumUsers: [{ userId: authStub.admin.user.id }], - }), + sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); + expect(mocks.user.get).not.toHaveBeenCalled(); }); it('should throw an error if the userId does not exist', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); - mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue(void 0); await expect( - sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { albumUsers: [{ userId: 'user-3' }] }), + sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); + expect(mocks.user.get).toHaveBeenCalledWith('unknown-user', {}); }); it('should throw an error if the userId is the ownerId', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); - mocks.album.getById.mockResolvedValue(albumStub.sharedWithAdmin); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); await expect( - sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { - albumUsers: [{ userId: userStub.user1.id }], + sut.addUsers(AuthFactory.create(album.owner), album.id, { + albumUsers: [{ userId: album.owner.id }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); + expect(mocks.user.get).not.toHaveBeenCalled(); }); it('should add valid shared users', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); - mocks.album.update.mockResolvedValue(albumStub.sharedWithAdmin); - mocks.user.get.mockResolvedValue(userStub.user2); - mocks.albumUser.create.mockResolvedValue({ - userId: userStub.user2.id, - albumId: albumStub.sharedWithAdmin.id, - role: AlbumUserRole.Editor, - }); - await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { - albumUsers: [{ userId: authStub.user2.user.id }], - }); + const album = AlbumFactory.create(); + const user = UserFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.update.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); + mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build()); + + await sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: user.id }] }); + expect(mocks.albumUser.create).toHaveBeenCalledWith({ - userId: authStub.user2.user.id, - albumId: albumStub.sharedWithAdmin.id, + userId: user.id, + albumId: album.id, }); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { - id: albumStub.sharedWithAdmin.id, - userId: userStub.user2.id, + id: album.id, + userId: user.id, }); }); }); @@ -424,71 +446,69 @@ describe(AlbumService.name, () => { it('should require a valid album id', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); mocks.album.getById.mockResolvedValue(void 0); - await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeUser(AuthFactory.create(), 'album-1', 'user-1')).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should remove a shared user from an owned album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id])); - mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + const userId = newUuid(); + const album = AlbumFactory.from().albumUser({ userId }).build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await expect( - sut.removeUser(authStub.admin, albumStub.sharedWithUser.id, userStub.user1.id), - ).resolves.toBeUndefined(); + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined(); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: userStub.user1.id, - }); - expect(mocks.album.getById).toHaveBeenCalledWith(albumStub.sharedWithUser.id, { withAssets: false }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); }); it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { - mocks.album.getById.mockResolvedValue(albumStub.sharedWithMultiple); + const user1 = UserFactory.create(); + const user2 = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user1.id }).albumUser({ userId: user2.id }).build(); + mocks.album.getById.mockResolvedValue(album); - await expect( - sut.removeUser(authStub.user1, albumStub.sharedWithMultiple.id, authStub.user2.user.id), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeUser(AuthFactory.create(user1), album.id, user2.id)).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.albumUser.delete).not.toHaveBeenCalled(); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.user1.user.id, - new Set([albumStub.sharedWithMultiple.id]), - ); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(user1.id, new Set([album.id])); }); it('should allow a shared user to remove themselves', async () => { - mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + const user1 = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user1.id }).build(); + mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, authStub.user1.user.id); + await sut.removeUser(AuthFactory.create(user1), album.id, user1.id); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, - }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId: user1.id }); }); it('should allow a shared user to remove themselves using "me"', async () => { - mocks.album.getById.mockResolvedValue(albumStub.sharedWithUser); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await sut.removeUser(authStub.user1, albumStub.sharedWithUser.id, 'me'); + await sut.removeUser(AuthFactory.create(user), album.id, 'me'); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: albumStub.sharedWithUser.id, - userId: authStub.user1.user.id, - }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId: user.id }); }); it('should not allow the owner to be removed', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + const album = AlbumFactory.from().albumUser().build(); + mocks.album.getById.mockResolvedValue(album); - await expect(sut.removeUser(authStub.admin, albumStub.empty.id, authStub.admin.user.id)).rejects.toBeInstanceOf( + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf( BadRequestException, ); @@ -496,9 +516,10 @@ describe(AlbumService.name, () => { }); it('should throw an error for a user not in the album', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + const album = AlbumFactory.from().albumUser().build(); + mocks.album.getById.mockResolvedValue(album); - await expect(sut.removeUser(authStub.admin, albumStub.empty.id, 'user-3')).rejects.toBeInstanceOf( + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf( BadRequestException, ); @@ -508,26 +529,28 @@ describe(AlbumService.name, () => { describe('updateUser', () => { it('should update user role', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); - mocks.albumUser.update.mockResolvedValue(null as any); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.albumUser.update.mockResolvedValue(); + + await sut.updateUser(AuthFactory.create(album.owner), album.id, user.id, { role: AlbumUserRole.Viewer }); - await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, { - role: AlbumUserRole.Editor, - }); expect(mocks.albumUser.update).toHaveBeenCalledWith( - { albumId: albumStub.sharedWithAdmin.id, userId: userStub.admin.id }, - { role: AlbumUserRole.Editor }, + { albumId: album.id, userId: user.id }, + { role: AlbumUserRole.Viewer }, ); }); }); describe('getAlbumInfo', () => { it('should get a shared album', async () => { - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + const album = AlbumFactory.from().albumUser().build(); + mocks.album.getById.mockResolvedValue(album); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.oneAsset.id, + albumId: album.id, assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), @@ -535,21 +558,19 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(authStub.admin, albumStub.oneAsset.id, {}); + await sut.get(AuthFactory.create(album.owner), album.id, {}); - expect(mocks.album.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([albumStub.oneAsset.id]), - ); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id])); }); it('should get a shared album via a shared link', async () => { - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); - mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); + const album = AlbumFactory.from().albumUser().build(); + mocks.album.getById.mockResolvedValue(album); + mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.oneAsset.id, + albumId: album.id, assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), @@ -557,21 +578,21 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(authStub.adminSharedLink, 'album-123', {}); + const auth = AuthFactory.from().sharedLink().build(); + await sut.get(auth, album.id, {}); - expect(mocks.album.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLink?.id, - new Set(['album-123']), - ); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); + expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink!.id, new Set([album.id])); }); it('should get a shared album via shared with user', async () => { - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); - mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ { - albumId: albumStub.oneAsset.id, + albumId: album.id, assetCount: 1, startDate: new Date('1970-01-01'), endDate: new Date('1970-01-01'), @@ -579,22 +600,23 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(authStub.user1, 'album-123', {}); + await sut.get(AuthFactory.create(user), album.id, {}); - expect(mocks.album.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); + expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( - authStub.user1.user.id, - new Set(['album-123']), + user.id, + new Set([album.id]), AlbumUserRole.Viewer, ); }); it('should throw an error for no access', async () => { - await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-123'])); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123'])); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( - authStub.admin.user.id, + auth.user.id, new Set(['album-123']), AlbumUserRole.Viewer, ); @@ -603,173 +625,189 @@ describe(AlbumService.name, () => { describe('addAssets', () => { it('should allow the owner to add assets', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + const owner = UserFactory.create({ isAdmin: true }); + const album = AlbumFactory.from({ ownerId: owner.id }).owner(owner).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); await expect( - sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), + sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).resolves.toEqual([ - { success: true, id: 'asset-1' }, - { success: true, id: 'asset-2' }, - { success: true, id: 'asset-3' }, + { success: true, id: asset1.id }, + { success: true, id: asset2.id }, + { success: true, id: asset3.id }, ]); - expect(mocks.album.update).toHaveBeenCalledWith('album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { + id: album.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); + expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); }); it('should not set the thumbnail if the album has one already', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })); + const [asset1, asset2] = [AssetFactory.create(), AssetFactory.create()]; + const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ - { success: true, id: 'asset-1' }, + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([ + { success: true, id: asset2.id }, ]); - expect(mocks.album.update).toHaveBeenCalledWith('album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { + id: album.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-id', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIds).toHaveBeenCalled(); }); it('should allow a shared user to add assets', async () => { - mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); await expect( - sut.addAssets(authStub.user1, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), + sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).resolves.toEqual([ - { success: true, id: 'asset-1' }, - { success: true, id: 'asset-2' }, - { success: true, id: 'asset-3' }, + { success: true, id: asset1.id }, + { success: true, id: asset2.id }, + { success: true, id: asset3.id }, ]); - expect(mocks.album.update).toHaveBeenCalledWith('album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { + id: album.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); + expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { - id: 'album-123', - recipientId: 'admin_id', + id: album.id, + recipientId: album.ownerId, }); }); it('should not allow a shared user with viewer access to add assets', async () => { + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set()); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); + mocks.album.getById.mockResolvedValue(album); await expect( - sut.addAssets(authStub.user2, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), + sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should allow a shared link user to add assets', async () => { - mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + const album = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build(); + mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect( - sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), - ).resolves.toEqual([ - { success: true, id: 'asset-1' }, - { success: true, id: 'asset-2' }, - { success: true, id: 'asset-3' }, + await expect(sut.addAssets(auth, album.id, { ids: [asset1.id, asset2.id, asset3.id] })).resolves.toEqual([ + { success: true, id: asset1.id }, + { success: true, id: asset2.id }, + { success: true, id: asset3.id }, ]); - expect(mocks.album.update).toHaveBeenCalledWith('album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { + id: album.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); + expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); - expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLink?.id, - new Set(['album-123']), - ); + expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith(auth.sharedLink?.id, new Set([album.id])); }); it('should allow adding assets shared via partner sharing', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + const album = AlbumFactory.create(); + const asset = AssetFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ - { success: true, id: 'asset-1' }, + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: true, id: asset.id }, ]); - expect(mocks.album.update).toHaveBeenCalledWith('album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { + id: album.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset.id, }); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id])); }); it('should skip duplicate assets', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); - mocks.album.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id])); - await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE }, + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: false, id: asset.id, error: BulkIdErrorReason.DUPLICATE }, ]); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should skip assets not shared with user', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ - { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: false, id: asset.id, error: BulkIdErrorReason.NO_PERMISSION }, ]); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set(['asset-1']), - false, - ); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id]), false); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id])); }); it('should not allow unauthorized access to the album', async () => { - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); + const user = UserFactory.create(); + const album = AlbumFactory.create(); + const asset = AssetFactory.create({ ownerId: user.id }); + mocks.album.getById.mockResolvedValue(album); - await expect( - sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalled(); }); it('should not allow unauthorized shared link access to the album', async () => { - mocks.album.getById.mockResolvedValue(albumStub.oneAsset); + const album = AlbumFactory.create(); + const asset = AssetFactory.create(); + mocks.album.getById.mockResolvedValue(album); await expect( - sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), + sut.addAssets(AuthFactory.from().sharedLink({ allowUpload: true }).build(), album.id, { ids: [asset.id] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled(); @@ -778,133 +816,140 @@ describe(AlbumService.name, () => { describe('addAssetsToAlbums', () => { it('should allow the owner to add assets', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(2); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { + id: album1.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.update).toHaveBeenNthCalledWith(2, 'album-321', { - id: 'album-321', + expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, { + id: album2.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-123', assetId: 'asset-1' }, - { albumId: 'album-123', assetId: 'asset-2' }, - { albumId: 'album-123', assetId: 'asset-3' }, - { albumId: 'album-321', assetId: 'asset-1' }, - { albumId: 'album-321', assetId: 'asset-2' }, - { albumId: 'album-321', assetId: 'asset-3' }, + { albumId: album1.id, assetId: asset1.id }, + { albumId: album1.id, assetId: asset2.id }, + { albumId: album1.id, assetId: asset3.id }, + { albumId: album2.id, assetId: asset1.id }, + { albumId: album2.id, assetId: asset2.id }, + { albumId: album2.id, assetId: asset3.id }, ]); }); it('should not set the thumbnail if the album has one already', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })) - .mockResolvedValueOnce(_.cloneDeep({ ...albumStub.oneAsset, albumThumbnailAssetId: 'asset-id' })); + const asset = AssetFactory.create(); + const album1 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build(); + const album2 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(2); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { + id: album1.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-id', + albumThumbnailAssetId: asset.id, }); - expect(mocks.album.update).toHaveBeenNthCalledWith(2, 'album-321', { - id: 'album-321', + expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, { + id: album2.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-id', + albumThumbnailAssetId: asset.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-123', assetId: 'asset-1' }, - { albumId: 'album-123', assetId: 'asset-2' }, - { albumId: 'album-123', assetId: 'asset-3' }, - { albumId: 'album-321', assetId: 'asset-1' }, - { albumId: 'album-321', assetId: 'asset-2' }, - { albumId: 'album-321', assetId: 'asset-3' }, + { albumId: album1.id, assetId: asset1.id }, + { albumId: album1.id, assetId: asset2.id }, + { albumId: album1.id, assetId: asset3.id }, + { albumId: album2.id, assetId: asset1.id }, + { albumId: album2.id, assetId: asset2.id }, + { albumId: album2.id, assetId: asset3.id }, ]); }); it('should allow a shared user to add assets', async () => { - mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithMultiple)); + const user = UserFactory.create(); + const album1 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const album2 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.user1, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(user), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(2); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { + id: album1.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.update).toHaveBeenNthCalledWith(2, 'album-321', { - id: 'album-321', + expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, { + id: album2.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-123', assetId: 'asset-1' }, - { albumId: 'album-123', assetId: 'asset-2' }, - { albumId: 'album-123', assetId: 'asset-3' }, - { albumId: 'album-321', assetId: 'asset-1' }, - { albumId: 'album-321', assetId: 'asset-2' }, - { albumId: 'album-321', assetId: 'asset-3' }, + { albumId: album1.id, assetId: asset1.id }, + { albumId: album1.id, assetId: asset2.id }, + { albumId: album1.id, assetId: asset3.id }, + { albumId: album2.id, assetId: asset1.id }, + { albumId: album2.id, assetId: asset2.id }, + { albumId: album2.id, assetId: asset3.id }, ]); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { - id: 'album-123', - recipientId: 'admin_id', + id: album1.id, + recipientId: album1.ownerId, }); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { - id: 'album-321', - recipientId: 'admin_id', + id: album2.id, + recipientId: album2.ownerId, }); }); it('should not allow a shared user with viewer access to add assets', async () => { + const user = UserFactory.create(); + const album1 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const album2 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithAdmin)); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.user2, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(user), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: false, @@ -915,125 +960,131 @@ describe(AlbumService.name, () => { }); it('should not allow a shared link user to add assets to multiple albums', async () => { - mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithMultiple)); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); + const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build(); await expect( - sut.addAssetsToAlbums(authStub.adminSharedLink, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(auth, { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { + id: album1.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-123', assetId: 'asset-1' }, - { albumId: 'album-123', assetId: 'asset-2' }, - { albumId: 'album-123', assetId: 'asset-3' }, + { albumId: album1.id, assetId: asset1.id }, + { albumId: album1.id, assetId: asset2.id }, + { albumId: album1.id, assetId: asset3.id }, ]); - expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { - id: 'album-123', - recipientId: 'user-id', - }); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalledWith( - authStub.adminSharedLink.sharedLink?.id, - new Set(['album-123', 'album-321']), + auth.sharedLink?.id, + new Set([album1.id, album2.id]), ); }); it('should allow adding assets shared via partner sharing', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); - mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [ + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + ]; + mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); + mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(2); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-123', { - id: 'album-123', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album1.id, { + id: album1.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); - expect(mocks.album.update).toHaveBeenNthCalledWith(2, 'album-321', { - id: 'album-321', + expect(mocks.album.update).toHaveBeenNthCalledWith(2, album2.id, { + id: album2.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-123', assetId: 'asset-1' }, - { albumId: 'album-123', assetId: 'asset-2' }, - { albumId: 'album-123', assetId: 'asset-3' }, - { albumId: 'album-321', assetId: 'asset-1' }, - { albumId: 'album-321', assetId: 'asset-2' }, - { albumId: 'album-321', assetId: 'asset-3' }, + { albumId: album1.id, assetId: asset1.id }, + { albumId: album1.id, assetId: asset2.id }, + { albumId: album1.id, assetId: asset3.id }, + { albumId: album2.id, assetId: asset1.id }, + { albumId: album2.id, assetId: asset2.id }, + { albumId: album2.id, assetId: asset3.id }, ]); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set(['asset-1', 'asset-2', 'asset-3']), + album1.ownerId, + new Set([asset1.id, asset2.id, asset3.id]), ); }); it('should skip some duplicate assets', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123', 'album-321'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getAssetIds - .mockResolvedValueOnce(new Set(['asset-1', 'asset-2', 'asset-3'])) + .mockResolvedValueOnce(new Set([asset1.id, asset2.id, asset3.id])) .mockResolvedValueOnce(new Set()); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: true, error: undefined }); expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenNthCalledWith(1, 'album-321', { - id: 'album-321', + expect(mocks.album.update).toHaveBeenNthCalledWith(1, album2.id, { + id: album2.id, updatedAt: expect.any(Date), - albumThumbnailAssetId: 'asset-1', + albumThumbnailAssetId: asset1.id, }); expect(mocks.album.addAssetIdsToAlbums).toHaveBeenCalledWith([ - { albumId: 'album-321', assetId: 'asset-1' }, - { albumId: 'album-321', assetId: 'asset-2' }, - { albumId: 'album-321', assetId: 'asset-3' }, + { albumId: album2.id, assetId: asset1.id }, + { albumId: album2.id, assetId: asset2.id }, + { albumId: album2.id, assetId: asset3.id }, ]); }); it('should skip all duplicate assets', async () => { + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); mocks.access.album.checkOwnerAccess - .mockResolvedValueOnce(new Set(['album-123'])) - .mockResolvedValueOnce(new Set(['album-321'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); - mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-1', 'asset-2'])); + .mockResolvedValueOnce(new Set([album1.id])) + .mockResolvedValueOnce(new Set([album2.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); + mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: false, @@ -1045,18 +1096,24 @@ describe(AlbumService.name, () => { }); it('should skip assets not shared with user', async () => { + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [ + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + ]; mocks.access.album.checkSharedAlbumAccess - .mockResolvedValueOnce(new Set(['album-123'])) - .mockResolvedValueOnce(new Set(['album-321'])); - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithMultiple)); + .mockResolvedValueOnce(new Set([album1.id])) + .mockResolvedValueOnce(new Set([album2.id])); + mocks.album.getById.mockResolvedValueOnce(_.cloneDeep(album1)).mockResolvedValueOnce(_.cloneDeep(album2)); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: false, @@ -1066,25 +1123,27 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); expect(mocks.album.addAssetIds).not.toHaveBeenCalled(); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set(['asset-1', 'asset-2', 'asset-3']), + album1.ownerId, + new Set([asset1.id, asset2.id, asset3.id]), false, ); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set(['asset-1', 'asset-2', 'asset-3']), + album1.ownerId, + new Set([asset1.id, asset2.id, asset3.id]), ); }); it('should not allow unauthorized access to the albums', async () => { - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithUser)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.sharedWithMultiple)); + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(authStub.admin, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.create(user), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: false, @@ -1098,14 +1157,15 @@ describe(AlbumService.name, () => { }); it('should not allow unauthorized shared link access to the album', async () => { - mocks.album.getById - .mockResolvedValueOnce(_.cloneDeep(albumStub.empty)) - .mockResolvedValueOnce(_.cloneDeep(albumStub.oneAsset)); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(authStub.adminSharedLink, { - albumIds: ['album-123', 'album-321'], - assetIds: ['asset-1', 'asset-2', 'asset-3'], + sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), { + albumIds: [album1.id, album2.id], + assetIds: [asset1.id, asset2.id, asset3.id], }), ).resolves.toEqual({ success: false, @@ -1118,48 +1178,57 @@ describe(AlbumService.name, () => { describe('removeAssets', () => { it('should allow the owner to remove assets', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); - mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id'])); + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id])); - await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: true, id: 'asset-id' }, + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: true, id: asset.id }, ]); - expect(mocks.album.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']); + expect(mocks.album.removeAssetIds).toHaveBeenCalledWith(album.id, [asset.id]); }); it('should skip assets not in the album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.empty)); + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set()); - await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND }, + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: false, id: asset.id, error: BulkIdErrorReason.NOT_FOUND }, ]); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should allow owner to remove all assets from the album', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); - mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id'])); + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id])); - await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: true, id: 'asset-id' }, + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ + { success: true, id: asset.id }, ]); }); it('should reset the thumbnail if it is removed', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); - mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); - mocks.album.getAssetIds.mockResolvedValue(new Set(['asset-id'])); + const asset1 = AssetFactory.create(); + const asset2 = AssetFactory.create(); + const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id])); + mocks.album.getById.mockResolvedValue(album); + mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id])); - await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: true, id: 'asset-id' }, + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset1.id] })).resolves.toEqual([ + { success: true, id: asset1.id }, ]); expect(mocks.album.updateThumbnails).toHaveBeenCalled(); diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 8d48b47f1e..3a31dbbea1 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -24,7 +24,7 @@ describe(ApiKeyService.name, () => { await sut.create(auth, { name: apiKey.name, permissions: apiKey.permissions }); expect(mocks.apiKey.create).toHaveBeenCalledWith({ - key: 'super-secret (hashed)', + key: Buffer.from('super-secret (hashed)'), name: apiKey.name, permissions: apiKey.permissions, userId: apiKey.userId, @@ -44,7 +44,7 @@ describe(ApiKeyService.name, () => { await sut.create(auth, { permissions: [Permission.All] }); expect(mocks.apiKey.create).toHaveBeenCalledWith({ - key: 'super-secret (hashed)', + key: Buffer.from('super-secret (hashed)'), name: 'API Key', permissions: [Permission.All], userId: auth.user.id, @@ -107,6 +107,78 @@ describe(ApiKeyService.name, () => { permissions: newPermissions, }); }); + + describe('api key auth', () => { + it('should prevent adding Permission.all', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.All] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should prevent adding a new permission', async () => { + const permissions = [Permission.ApiKeyCreate, Permission.ApiKeyUpdate, Permission.AssetRead]; + const auth = factory.auth({ apiKey: { permissions } }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + + await expect(sut.update(auth, apiKey.id, { permissions: [Permission.AssetCopy] })).rejects.toThrow( + 'Cannot grant permissions you do not have', + ); + + expect(mocks.apiKey.update).not.toHaveBeenCalled(); + }); + + it('should allow removing permissions', async () => { + const auth = factory.auth({ apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead] } }); + const apiKey = factory.apiKey({ + userId: auth.user.id, + permissions: [Permission.AssetRead, Permission.AssetDelete], + }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // remove Permission.AssetDelete + await sut.update(auth, apiKey.id, { permissions: [Permission.AssetRead] }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead] }), + ); + }); + + it('should allow adding new permissions', async () => { + const auth = factory.auth({ + apiKey: { permissions: [Permission.ApiKeyUpdate, Permission.AssetRead, Permission.AssetUpdate] }, + }); + const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.AssetRead] }); + + mocks.apiKey.getById.mockResolvedValue(apiKey); + mocks.apiKey.update.mockResolvedValue(apiKey); + + // add Permission.AssetUpdate + await sut.update(auth, apiKey.id, { + name: apiKey.name, + permissions: [Permission.AssetRead, Permission.AssetUpdate], + }); + + expect(mocks.apiKey.update).toHaveBeenCalledWith( + auth.user.id, + apiKey.id, + expect.objectContaining({ permissions: [Permission.AssetRead, Permission.AssetUpdate] }), + ); + }); + }); }); describe('delete', () => { diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 96671daab1..534de69107 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -10,14 +10,14 @@ import { isGranted } from 'src/utils/access'; export class ApiKeyService extends BaseService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { throw new BadRequestException('Cannot grant permissions you do not have'); } const entity = await this.apiKeyRepository.create({ - key: tokenHashed, + key: hashed, name: dto.name || 'API Key', userId: auth.user.id, permissions: dto.permissions, @@ -32,6 +32,14 @@ export class ApiKeyService extends BaseService { throw new BadRequestException('API Key not found'); } + if ( + auth.apiKey && + dto.permissions && + !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions }) + ) { + throw new BadRequestException('Cannot grant permissions you do not have'); + } + const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name, permissions: dto.permissions }); return this.map(key); diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 95eb8b3c97..5fb45690cf 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -9,13 +9,16 @@ import { AssetFile } from 'src/database'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; import { MapAsset } from 'src/dtos/asset-response.dto'; +import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { AssetMediaService } from 'src/services/asset-media.service'; import { UploadBody } from 'src/types'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; import { ImmichFileResponse } from 'src/utils/file'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -107,6 +110,7 @@ const validVideos = [ '.mp4', '.mpg', '.mts', + '.mxf', '.vob', '.webm', '.wmv', @@ -426,56 +430,52 @@ describe(AssetMediaService.name, () => { }); it('should handle a live photo', async () => { - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(authStub.user1.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should hide the linked motion asset', async () => { - mocks.asset.getById.mockResolvedValueOnce({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); - mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.from({ type: AssetType.Video }).owner(authStub.user1.user).build(); + const asset = AssetFactory.create(); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect( - sut.uploadAsset( - authStub.user1, - { ...createDto, livePhotoVideoId: 'live-photo-motion-asset' }, - fileStub.livePhotoStill, - ), + sut.uploadAsset(authStub.user1, { ...createDto, livePhotoVideoId: motionAsset.id }, fileStub.livePhotoStill), ).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: 'live-photo-still-asset', + id: asset.id, }); - expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); + expect(mocks.asset.getById).toHaveBeenCalledWith(motionAsset.id); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: 'live-photo-motion-asset', + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); }); it('should handle a sidecar file', async () => { - mocks.asset.getById.mockResolvedValueOnce(assetStub.image); - mocks.asset.create.mockResolvedValueOnce(assetStub.image); + const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build(); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.create.mockResolvedValueOnce(asset); await expect(sut.uploadAsset(authStub.user1, createDto, fileStub.photo, fileStub.photoSidecar)).resolves.toEqual({ status: AssetMediaStatus.CREATED, - id: assetStub.image.id, + id: asset.id, }); expect(mocks.storage.utimes).toHaveBeenCalledWith( @@ -489,7 +489,7 @@ describe(AssetMediaService.name, () => { describe('downloadOriginal', () => { it('should require the asset.download permission', async () => { - await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, @@ -500,22 +500,94 @@ describe(AssetMediaService.name, () => { expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['asset-1'])); }); - it('should throw an error if the asset is not found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + it('should download a file', async () => { + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForOriginal.mockResolvedValue(asset); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); - - expect(mocks.asset.getById).toHaveBeenCalledWith('asset-1', { files: true }); + await expect(sut.downloadOriginal(authStub.admin, asset.id, {})).resolves.toEqual( + new ImmichFileResponse({ + path: asset.originalPath, + fileName: asset.originalFileName, + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); }); - it('should download a file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + it('should download edited file by default when edits exist', async () => { + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).resolves.toEqual( + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path }); + + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, {})).resolves.toEqual( new ImmichFileResponse({ - path: '/original/path.jpg', - fileName: 'asset-id.jpg', + path: editedAsset.files[3].path, + fileName: editedAsset.originalFileName, + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should download edited file when edited=true', async () => { + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: editedAsset.files[3].path }); + + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: true })).resolves.toEqual( + new ImmichFileResponse({ + path: editedAsset.files[3].path, + fileName: editedAsset.originalFileName, + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should not return the unedited version if requested using a shared link', async () => { + const fullsizeEdited = AssetFileFactory.create({ type: AssetFileType.FullSize, isEdited: true }); + const editedAsset = AssetFactory.from().edit({ action: AssetEditAction.Crop }).file(fullsizeEdited).build(); + + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue({ ...editedAsset, editedPath: fullsizeEdited.path }); + + await expect( + sut.downloadOriginal(AuthFactory.from().sharedLink().build(), editedAsset.id, { edited: false }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: fullsizeEdited.path, + fileName: editedAsset.originalFileName, + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should download original file when edited=false', async () => { + const editedAsset = AssetFactory.from() + .edit() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .file({ type: AssetFileType.FullSize, isEdited: true }) + .build(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([editedAsset.id])); + mocks.asset.getForOriginal.mockResolvedValue(editedAsset); + + await expect(sut.downloadOriginal(AuthFactory.create(), editedAsset.id, { edited: false })).resolves.toEqual( + new ImmichFileResponse({ + path: editedAsset.originalPath, + fileName: editedAsset.originalFileName, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithCache, }), @@ -532,93 +604,119 @@ describe(AssetMediaService.name, () => { expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); }); - it('should throw an error if the asset does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), - ).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should throw an error if the requested thumbnail file does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ ...assetStub.image, files: [] }); - - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should throw an error if the requested preview file does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ - ...assetStub.image, - files: [ - { - id: '42', - path: '/path/to/preview', - type: AssetFileType.Thumbnail, - }, - ], - }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), - ).rejects.toBeInstanceOf(NotFoundException); - }); - it('should fall back to preview if the requested thumbnail file does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ - ...assetStub.image, - files: [ - { - id: '42', - path: '/path/to/preview.jpg', - type: AssetFileType.Preview, - }, - ], - }); + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).resolves.toEqual( + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( new ImmichFileResponse({ - path: '/path/to/preview.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_thumbnail.jpg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, }), ); }); it('should get preview file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ ...assetStub.image }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), - ).resolves.toEqual( + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.PREVIEW })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/thumbs/path.jpg', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'image/jpeg', - fileName: 'asset-id_preview.jpg', + fileName: `IMG_${asset.id}_preview.jpg`, }), ); }); it('should get thumbnail file', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ ...assetStub.image }); - await expect( - sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), - ).resolves.toEqual( + const asset = AssetFactory.from() + .file({ type: AssetFileType.Thumbnail, path: '/uploads/user-id/webp/path.ext' }) + .build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( new ImmichFileResponse({ - path: '/uploads/user-id/webp/path.ext', + path: asset.files[0].path, cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', - fileName: 'asset-id_thumbnail.ext', + fileName: `IMG_${asset.id}_thumbnail.ext`, }), ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); + }); + + it('should get original thumbnail by default', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect(sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL })).resolves.toEqual( + new ImmichFileResponse({ + path: asset.files[0].path, + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); + }); + + it('should get edited thumbnail when edited=true', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail, isEdited: true }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect( + sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: true }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: asset.files[0].path, + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true); + }); + + it('should get original thumbnail when edited=false', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect( + sut.viewThumbnail(authStub.admin, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: false }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: asset.files[0].path, + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, false); + }); + + it('should not return the unedited version if requested using a shared link', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ ...asset, path: asset.files[0].path }); + await expect( + sut.viewThumbnail(authStub.adminSharedLink, asset.id, { + size: AssetMediaSize.THUMBNAIL, + edited: true, + }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: asset.files[0].path, + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: `IMG_${asset.id}_thumbnail.jpg`, + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(asset.id, AssetFileType.Thumbnail, true); }); }); @@ -631,26 +729,21 @@ describe(AssetMediaService.name, () => { expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id'])); }); - it('should throw an error if the asset does not exist', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + it('should throw an error if the video asset could not be found', async () => { + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(NotFoundException); - }); - - it('should throw an error if the asset is not a video', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); - - await expect(sut.playbackVideo(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.playbackVideo(authStub.admin, asset.id)).rejects.toBeInstanceOf(NotFoundException); }); it('should return the encoded video path if available', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.hasEncodedVideo.id])); - mocks.asset.getById.mockResolvedValue(assetStub.hasEncodedVideo); + const asset = AssetFactory.create({ encodedVideoPath: '/path/to/encoded/video.mp4' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForVideo.mockResolvedValue(asset); - await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual( + await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.hasEncodedVideo.encodedVideoPath!, + path: asset.encodedVideoPath!, cacheControl: CacheControl.PrivateWithCache, contentType: 'video/mp4', }), @@ -658,12 +751,13 @@ describe(AssetMediaService.name, () => { }); it('should fall back to the original path', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.video.id])); - mocks.asset.getById.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForVideo.mockResolvedValue(asset); - await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( + await expect(sut.playbackVideo(authStub.admin, asset.id)).resolves.toEqual( new ImmichFileResponse({ - path: assetStub.video.originalPath, + path: asset.originalPath, cacheControl: CacheControl.PrivateWithCache, contentType: 'application/octet-stream', }), diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 9f7c8fef33..4d60bd686b 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -20,11 +20,11 @@ import { CheckExistingAssetsDto, UploadFieldName, } from 'src/dtos/asset-media.dto'; +import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileType, AssetStatus, - AssetType, AssetVisibility, CacheControl, JobName, @@ -35,7 +35,7 @@ import { AuthRequest } from 'src/middleware/auth.guard'; import { BaseService } from 'src/services/base.service'; import { UploadFile, UploadRequest } from 'src/types'; import { requireUploadAccess } from 'src/utils/access'; -import { asUploadRequest, getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; +import { asUploadRequest, onBeforeLink } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; import { getFilenameExtension, getFileNameWithoutExtension, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; @@ -193,15 +193,24 @@ export class AssetMediaService extends BaseService { } } - async downloadOriginal(auth: AuthDto, id: string): Promise { + async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] }); - const asset = await this.findOrFail(id); + if (auth.sharedLink) { + dto.edited = true; + } + + const { originalPath, originalFileName, editedPath } = await this.assetRepository.getForOriginal( + id, + dto.edited ?? false, + ); + + const path = editedPath ?? originalPath!; return new ImmichFileResponse({ - path: asset.originalPath, - fileName: asset.originalFileName, - contentType: mimeTypes.lookup(asset.originalPath), + path, + fileName: getFileNameWithoutExtension(originalFileName) + getFilenameExtension(path), + contentType: mimeTypes.lookup(path), cacheControl: CacheControl.PrivateWithCache, }); } @@ -213,37 +222,42 @@ export class AssetMediaService extends BaseService { ): Promise { await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] }); - const asset = await this.findOrFail(id); - const size = dto.size ?? AssetMediaSize.THUMBNAIL; - - const { thumbnailFile, previewFile, fullsizeFile } = getAssetFiles(asset.files ?? []); - let filepath = previewFile?.path; - if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { - filepath = thumbnailFile.path; - } else if (size === AssetMediaSize.FULLSIZE) { - if (mimeTypes.isWebSupportedImage(asset.originalPath)) { - // use original file for web supported images - return { targetSize: 'original' }; - } - if (!fullsizeFile) { - // downgrade to preview if fullsize is not available. - // e.g. disabled or not yet (re)generated - return { targetSize: AssetMediaSize.PREVIEW }; - } - filepath = fullsizeFile.path; + if (dto.size === AssetMediaSize.Original) { + throw new BadRequestException('May not request original file'); } - if (!filepath) { + if (auth.sharedLink) { + dto.edited = true; + } + + const size = (dto.size ?? AssetMediaSize.THUMBNAIL) as unknown as AssetFileType; + const { originalPath, originalFileName, path } = await this.assetRepository.getForThumbnail( + id, + size, + dto.edited ?? false, + ); + + if (size === AssetFileType.FullSize && mimeTypes.isWebSupportedImage(originalPath) && !dto.edited) { + // use original file for web supported images + return { targetSize: 'original' }; + } + + if (dto.size === AssetMediaSize.FULLSIZE && !path) { + // downgrade to preview if fullsize is not available. + // e.g. disabled or not yet (re)generated + return { targetSize: AssetMediaSize.PREVIEW }; + } + + if (!path) { throw new NotFoundException('Asset media not found'); } - let fileName = getFileNameWithoutExtension(asset.originalFileName); - fileName += `_${size}`; - fileName += getFilenameExtension(filepath); + + const fileName = `${getFileNameWithoutExtension(originalFileName)}_${size}${getFilenameExtension(path)}`; return new ImmichFileResponse({ fileName, - path: filepath, - contentType: mimeTypes.lookup(filepath), + path, + contentType: mimeTypes.lookup(path), cacheControl: CacheControl.PrivateWithCache, }); } @@ -251,8 +265,8 @@ export class AssetMediaService extends BaseService { async getAssetTile(auth: AuthDto, id: string, level: number, col: number, row: number): Promise { await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] }); - const asset = await this.findOrFail(id); - const { tilesPath } = getAssetFiles(asset.files ?? []); + const asset = await this.assetRepository.getForThumbnail(id, AssetFileType.Tiles, false); + let tilesPath = undefined; // TODO if (!tilesPath) { // TODO: placeholder tiles. return new ImmichFileResponse({ @@ -264,6 +278,8 @@ export class AssetMediaService extends BaseService { throw new NotFoundException('Asset tiles not found'); } + tilesPath = { path: 'tmppath' }; + const tileName = getFileNameWithoutExtension(asset.originalFileName) + `_${level}_${col}_${row}.jpg`; const tilePath = tilesPath.path.replace('.dz', '_files') + `/${level}/${col}_${row}.jpg`; @@ -278,10 +294,10 @@ export class AssetMediaService extends BaseService { async playbackVideo(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] }); - const asset = await this.findOrFail(id); + const asset = await this.assetRepository.getForVideo(id); - if (asset.type !== AssetType.Video) { - throw new BadRequestException('Asset is not a video'); + if (!asset) { + throw new NotFoundException('Asset not found or asset is not a video'); } const filepath = asset.encodedVideoPath || asset.originalPath; @@ -460,7 +476,7 @@ export class AssetMediaService extends BaseService { originalFileName: dto.filename || file.originalName, }); - if (dto.metadata) { + if (dto.metadata?.length) { await this.assetRepository.upsertMetadata(asset.id, dto.metadata); } @@ -490,13 +506,4 @@ export class AssetMediaService extends BaseService { throw new BadRequestException('Quota has been exceeded!'); } } - - private async findOrFail(id: string) { - const asset = await this.assetRepository.getById(id, { files: true }); - if (!asset) { - throw new NotFoundException('Asset not found'); - } - - return asset; - } } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 5e1cce2ccf..db895f8321 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,15 +1,14 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; -import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; +import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetFileType, AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; import { AssetService } from 'src/services/asset.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { faceStub } from 'test/fixtures/face.stub'; -import { userStub } from 'test/fixtures/user.stub'; -import { factory } from 'test/small.factory'; +import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; const stats: AssetStats = { @@ -33,54 +32,46 @@ describe(AssetService.name, () => { expect(sut).toBeDefined(); }); - const mockGetById = (assets: MapAsset[]) => { - mocks.asset.getById.mockImplementation((assetId) => Promise.resolve(assets.find((asset) => asset.id === assetId))); - }; - beforeEach(() => { ({ sut, mocks } = newTestService(AssetService)); - - mockGetById([assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]); }); describe('getStatistics', () => { it('should get the statistics for a user, excluding archived assets', async () => { + const auth = AuthFactory.create(); mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Timeline })).resolves.toEqual( - statResponse, - ); - expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { - visibility: AssetVisibility.Timeline, - }); + await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Timeline })).resolves.toEqual(statResponse); + expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { visibility: AssetVisibility.Timeline }); }); it('should get the statistics for a user for archived assets', async () => { + const auth = AuthFactory.create(); mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Archive })).resolves.toEqual( - statResponse, - ); - expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { + await expect(sut.getStatistics(auth, { visibility: AssetVisibility.Archive })).resolves.toEqual(statResponse); + expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { visibility: AssetVisibility.Archive, }); }); it('should get the statistics for a user for favorite assets', async () => { + const auth = AuthFactory.create(); mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, { isFavorite: true })).resolves.toEqual(statResponse); - expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isFavorite: true }); + await expect(sut.getStatistics(auth, { isFavorite: true })).resolves.toEqual(statResponse); + expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, { isFavorite: true }); }); it('should get the statistics for a user for all assets', async () => { + const auth = AuthFactory.create(); mocks.asset.getStatistics.mockResolvedValue(stats); - await expect(sut.getStatistics(authStub.admin, {})).resolves.toEqual(statResponse); - expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {}); + await expect(sut.getStatistics(auth, {})).resolves.toEqual(statResponse); + expect(mocks.asset.getStatistics).toHaveBeenCalledWith(auth.user.id, {}); }); }); describe('getRandom', () => { it('should get own random assets', async () => { mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); await sut.getRandom(authStub.admin, 1); @@ -91,7 +82,7 @@ describe(AssetService.name, () => { const partner = factory.partner({ inTimeline: false }); const auth = factory.auth({ user: { id: partner.sharedWithId } }); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); mocks.partner.getAll.mockResolvedValue([partner]); await sut.getRandom(auth, 1); @@ -103,7 +94,7 @@ describe(AssetService.name, () => { const partner = factory.partner({ inTimeline: true }); const auth = factory.auth({ user: { id: partner.sharedWithId } }); - mocks.asset.getRandom.mockResolvedValue([assetStub.image]); + mocks.asset.getRandom.mockResolvedValue([AssetFactory.create()]); mocks.partner.getAll.mockResolvedValue([partner]); await sut.getRandom(auth, 1); @@ -114,88 +105,90 @@ describe(AssetService.name, () => { describe('get', () => { it('should allow owner access', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), undefined, ); }); it('should allow shared link access', async () => { - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.adminSharedLink, assetStub.image.id); + await sut.get(authStub.adminSharedLink, asset.id); expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLink?.id, - new Set([assetStub.image.id]), + new Set([asset.id]), ); }); it('should strip metadata for shared link if exif is disabled', async () => { - mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.from().exif({ description: 'foo' }).build(); + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); const result = await sut.get( { ...authStub.adminSharedLink, sharedLink: { ...authStub.adminSharedLink.sharedLink!, showExif: false } }, - assetStub.image.id, + asset.id, ); expect(result).toEqual(expect.objectContaining({ hasMetadata: false })); expect(result).not.toHaveProperty('exifInfo'); expect(mocks.access.asset.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLink?.id, - new Set([assetStub.image.id]), + new Set([asset.id]), ); }); it('should allow partner sharing access', async () => { - mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([assetStub.image.id]), - ); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id])); }); it('should allow shared album access', async () => { - mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkAlbumAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); - await sut.get(authStub.admin, assetStub.image.id); + await sut.get(authStub.admin, asset.id); - expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([assetStub.image.id]), - ); + expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([asset.id])); }); it('should throw an error for no access', async () => { - await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.admin, AssetFactory.create().id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.getById).not.toHaveBeenCalled(); }); it('should throw an error for an invalid shared link', async () => { - await expect(sut.get(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.adminSharedLink, AssetFactory.create().id)).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.access.asset.checkOwnerAccess).not.toHaveBeenCalled(); expect(mocks.asset.getById).not.toHaveBeenCalled(); }); it('should throw an error if the asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - await expect(sut.get(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.get(authStub.admin, asset.id)).rejects.toBeInstanceOf(BadRequestException); }); }); @@ -209,38 +202,41 @@ describe(AssetService.name, () => { }); it('should update the asset', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); - mocks.asset.update.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); + mocks.asset.update.mockResolvedValue(asset); - await sut.update(authStub.admin, 'asset-1', { isFavorite: true }); + await sut.update(authStub.admin, asset.id, { isFavorite: true }); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, isFavorite: true }); }); it('should update the exif description', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); - mocks.asset.update.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); + mocks.asset.update.mockResolvedValue(asset); - await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); + await sut.update(authStub.admin, asset.id, { description: 'Test description' }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - { assetId: 'asset-1', description: 'Test description', lockedProperties: ['description'] }, + { assetId: asset.id, description: 'Test description', lockedProperties: ['description'] }, { lockedPropertiesBehavior: 'append' }, ); }); it('should update the exif rating', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValueOnce(assetStub.image); - mocks.asset.update.mockResolvedValueOnce(assetStub.image); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.update.mockResolvedValueOnce(asset); - await sut.update(authStub.admin, 'asset-1', { rating: 3 }); + await sut.update(authStub.admin, asset.id, { rating: 3 }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( { - assetId: 'asset-1', + assetId: asset.id, rating: 3, lockedProperties: ['rating'], }, @@ -249,141 +245,143 @@ describe(AssetService.name, () => { }); it('should fail linking a live video if the motion part could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); + const auth = AuthFactory.create(); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: 'unknown', }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: 'unknown', }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: 'unknown', visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, - userId: userStub.admin.id, + assetId: 'unknown', + userId: auth.user.id, }); }); it('should fail linking a live video if the motion part is not a video', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset); + const auth = AuthFactory.create(); + const motionAsset = AssetFactory.from().owner(auth.user).build(); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(asset); await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(authStub.admin, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, - userId: userStub.admin.id, + assetId: motionAsset.id, + userId: auth.user.id, }); }); it('should fail linking a live video if the motion part has a different owner', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); + const auth = AuthFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValue(motionAsset); await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + sut.update(auth, asset.id, { + livePhotoVideoId: motionAsset.id, }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).not.toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Timeline, }); expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, - userId: userStub.admin.id, + assetId: motionAsset.id, + userId: auth.user.id, }); }); it('should link a live video', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValueOnce({ - ...assetStub.livePhotoMotionAsset, - ownerId: authStub.admin.user.id, - visibility: AssetVisibility.Timeline, - }); - mocks.asset.getById.mockResolvedValueOnce(assetStub.image); - mocks.asset.update.mockResolvedValue(assetStub.image); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Timeline }); + const stillAsset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([stillAsset.id])); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.getById.mockResolvedValueOnce(stillAsset); + mocks.asset.update.mockResolvedValue(stillAsset); + const auth = AuthFactory.from(motionAsset.owner).build(); - await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, - }); + await sut.update(auth, stillAsset.id, { livePhotoVideoId: motionAsset.id }); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, - visibility: AssetVisibility.Hidden, - }); - expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { - assetId: assetStub.livePhotoMotionAsset.id, - userId: userStub.admin.id, - }); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, - }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, visibility: AssetVisibility.Hidden }); + expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { assetId: motionAsset.id, userId: auth.user.id }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, livePhotoVideoId: motionAsset.id }); }); it('should throw an error if asset could not be found after update', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await expect(sut.update(authStub.admin, 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf( + await expect(sut.update(AuthFactory.create(), 'asset-1', { isFavorite: true })).rejects.toBeInstanceOf( BadRequestException, ); }); it('should unlink a live video', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - mocks.asset.getById.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); - mocks.asset.update.mockResolvedValueOnce(assetStub.image); + const auth = AuthFactory.create(); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }) + .owner(auth.user) + .build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + const unlinkedAsset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(asset); + mocks.asset.getById.mockResolvedValueOnce(motionAsset); + mocks.asset.update.mockResolvedValueOnce(unlinkedAsset); - await sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }); + await sut.update(auth, asset.id, { livePhotoVideoId: null }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, livePhotoVideoId: null, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, - visibility: assetStub.livePhotoStillAsset.visibility, + id: motionAsset.id, + visibility: asset.visibility, }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', { - assetId: assetStub.livePhotoMotionAsset.id, - userId: userStub.admin.id, + assetId: motionAsset.id, + userId: auth.user.id, }); }); it('should fail unlinking a live video if the asset could not be found', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.livePhotoStillAsset.id])); - // eslint-disable-next-line unicorn/no-useless-undefined - mocks.asset.getById.mockResolvedValueOnce(undefined); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getById.mockResolvedValueOnce(void 0); - await expect( - sut.update(authStub.admin, assetStub.livePhotoStillAsset.id, { livePhotoVideoId: null }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(authStub.admin, asset.id, { livePhotoVideoId: null })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); @@ -392,17 +390,15 @@ describe(AssetService.name, () => { describe('updateAll', () => { it('should require asset write access for all ids', async () => { - await expect( - sut.updateAll(authStub.admin, { - ids: ['asset-1'], - }), - ).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + await expect(sut.updateAll(auth, { ids: ['asset-1'] })).rejects.toBeInstanceOf(BadRequestException); }); it('should update all assets', async () => { + const auth = AuthFactory.create(); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive }); + await sut.updateAll(auth, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive }); expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { visibility: AssetVisibility.Archive, @@ -410,9 +406,10 @@ describe(AssetService.name, () => { }); it('should not update Assets table if no relevant fields are provided', async () => { + const auth = AuthFactory.create(); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await sut.updateAll(authStub.admin, { + await sut.updateAll(auth, { ids: ['asset-1'], latitude: 0, longitude: 0, @@ -564,126 +561,84 @@ describe(AssetService.name, () => { }); describe('handleAssetDeletion', () => { - it('should remove faces', async () => { - const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] }; + it('should clean up files', async () => { + const asset = AssetFactory.from() + .file({ type: AssetFileType.Thumbnail }) + .file({ type: AssetFileType.Preview }) + .file({ type: AssetFileType.FullSize }) + .build(); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetWithFace); - - await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); expect(mocks.job.queue.mock.calls).toEqual([ [ { name: JobName.FileDelete, data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - assetWithFace.originalPath, - ], + files: [...asset.files.map(({ path }) => path), asset.originalPath], }, }, ], ]); - - expect(mocks.asset.remove).toHaveBeenCalledWith(assetWithFace); - }); - - it('should update stack primary asset if deleted asset was primary asset in a stack', async () => { - mocks.stack.update.mockResolvedValue(factory.stack() as any); - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.primaryImage); - - await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true }); - - expect(mocks.stack.update).toHaveBeenCalledWith('stack-1', { - id: 'stack-1', - primaryAssetId: 'stack-child-asset-1', - }); + expect(mocks.asset.remove).toHaveBeenCalledWith(asset); }); it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => { + const asset = AssetFactory.from() + .stack({}, (builder) => builder.asset()) + .build(); mocks.stack.delete.mockResolvedValue(); mocks.assetJob.getForAssetDeletion.mockResolvedValue({ - ...assetStub.primaryImage, - stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) }, + ...asset, + // TODO the specific query filters out the primary asset from `stack.assets`. This should be in a mapper eventually + stack: { ...asset.stack!, assets: asset.stack!.assets.filter(({ id }) => id !== asset.stack!.primaryAssetId) }, }); - await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); - expect(mocks.stack.delete).toHaveBeenCalledWith('stack-1'); + expect(mocks.stack.delete).toHaveBeenCalledWith(asset.stackId); }); it('should delete a live photo', async () => { - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + const motionAsset = AssetFactory.from({ type: AssetType.Video, visibility: AssetVisibility.Hidden }).build(); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); mocks.asset.getLivePhotoCount.mockResolvedValue(0); await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, + id: asset.id, deleteOnDisk: true, }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.AssetDelete, - data: { - id: assetStub.livePhotoMotionAsset.id, - deleteOnDisk: true, - }, - }, - ], - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.AssetDelete, data: { id: motionAsset.id, deleteOnDisk: true } }], + [{ name: JobName.FileDelete, data: { files: [asset.originalPath] } }], ]); }); it('should not delete a live motion part if it is being used by another asset', async () => { + const asset = AssetFactory.create({ livePhotoVideoId: newUuid() }); mocks.asset.getLivePhotoCount.mockResolvedValue(2); - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); - await sut.handleAssetDeletion({ - id: assetStub.livePhotoStillAsset.id, - deleteOnDisk: true, - }); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); expect(mocks.job.queue.mock.calls).toEqual([ - [ - { - name: JobName.FileDelete, - data: { - files: [ - '/uploads/user-id/webp/path.ext', - '/uploads/user-id/thumbs/path.jpg', - '/uploads/user-id/fullsize/path.webp', - 'fake_path/asset_1.jpeg', - ], - }, - }, - ], + [{ name: JobName.FileDelete, data: { files: [`/data/library/IMG_${asset.id}.jpg`] } }], ]); }); it('should update usage', async () => { - mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.image); - await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000); + const asset = AssetFactory.from().exif({ fileSizeInByte: 5000 }).build(); + mocks.assetJob.getForAssetDeletion.mockResolvedValue(asset); + await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true }); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, -5000); }); it('should fail if asset could not be found', async () => { mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0); - await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe( + await expect(sut.handleAssetDeletion({ id: AssetFactory.create().id, deleteOnDisk: true })).resolves.toBe( JobStatus.Failed, ); }); @@ -701,27 +656,30 @@ describe(AssetService.name, () => { it('should return OCR data for an asset', async () => { const ocr1 = factory.assetOcr({ text: 'Hello World' }); const ocr2 = factory.assetOcr({ text: 'Test Image' }); + const asset = AssetFactory.from().exif().build(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.ocr.getByAssetId.mockResolvedValue([ocr1, ocr2]); + mocks.asset.getForOcr.mockResolvedValue({ edits: [], ...asset.exifInfo }); - await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([ocr1, ocr2]); + await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([ocr1, ocr2]); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set(['asset-1']), + new Set([asset.id]), undefined, ); - expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1'); + expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id); }); it('should return empty array when no OCR data exists', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + const asset = AssetFactory.from().exif().build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.ocr.getByAssetId.mockResolvedValue([]); + mocks.asset.getForOcr.mockResolvedValue({ edits: [], ...asset.exifInfo }); + await expect(sut.getOcr(authStub.admin, asset.id)).resolves.toEqual([]); - await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([]); - - expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1'); + expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith(asset.id); }); }); @@ -765,7 +723,7 @@ describe(AssetService.name, () => { describe('getUserAssetsByDeviceId', () => { it('get assets by device id', async () => { - const assets = [assetStub.image, assetStub.image1]; + const assets = [AssetFactory.create(), AssetFactory.create()]; mocks.asset.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); @@ -776,4 +734,61 @@ describe(AssetService.name, () => { expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); }); }); + + describe('upsertMetadata', () => { + it('should throw a bad request exception if duplicate keys are sent', async () => { + const asset = factory.asset(); + const items = [ + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + { key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + ]; + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + + await expect(sut.upsertMetadata(authStub.admin, asset.id, { items })).rejects.toThrowError( + 'Duplicate items are not allowed:', + ); + + expect(mocks.asset.upsertBulkMetadata).not.toHaveBeenCalled(); + }); + }); + + describe('upsertBulkMetadata', () => { + it('should throw a bad request exception if duplicate keys are sent', async () => { + const asset = factory.asset(); + const items = [ + { assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + { assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + ]; + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + + await expect(sut.upsertBulkMetadata(authStub.admin, { items })).rejects.toThrowError( + 'Duplicate items are not allowed:', + ); + + expect(mocks.asset.upsertBulkMetadata).not.toHaveBeenCalled(); + }); + }); + + describe('editAsset', () => { + it('should enforce crop first', async () => { + await expect( + sut.editAsset(authStub.admin, 'asset-1', { + edits: [ + { + action: AssetEditAction.Rotate, + parameters: { angle: 90 }, + }, + { + action: AssetEditAction.Crop, + parameters: { x: 0, y: 0, width: 100, height: 100 }, + }, + ], + }), + ).rejects.toBeInstanceOf(BadRequestException); + + expect(mocks.assetEdit.replaceAll).not.toHaveBeenCalled(); + }); + }); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 282d74a9b1..387b700f01 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -4,13 +4,16 @@ import { DateTime, Duration } from 'luxon'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { AssetFile } from 'src/database'; import { OnJob } from 'src/decorators'; -import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, AssetBulkUpdateDto, AssetCopyDto, AssetJobName, AssetJobsDto, + AssetMetadataBulkDeleteDto, + AssetMetadataBulkResponseDto, + AssetMetadataBulkUpsertDto, AssetMetadataResponseDto, AssetMetadataUpsertDto, AssetStatsDto, @@ -18,11 +21,12 @@ import { mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditAction, AssetEditActionItem, AssetEditsCreateDto, AssetEditsResponseDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { AssetFileType, - AssetMetadataKey, AssetStatus, + AssetType, AssetVisibility, JobName, JobStatus, @@ -32,8 +36,18 @@ import { import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; import { requireElevatedPermission } from 'src/utils/access'; -import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; +import { + getAssetFiles, + getDimensions, + getMyPartnerIds, + isPanorama, + onAfterUnlink, + onBeforeLink, + onBeforeUnlink, +} from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; +import { extractTimeZone } from 'src/utils/date'; +import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() export class AssetService extends BaseService { @@ -68,6 +82,7 @@ export class AssetService extends BaseService { owner: true, faces: { person: true }, stack: { assets: true }, + edits: true, tags: true, }); @@ -98,7 +113,7 @@ export class AssetService extends BaseService { const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const repos = { asset: this.assetRepository, event: this.eventRepository }; - let previousMotion: MapAsset | null = null; + let previousMotion: { id: string } | null = null; if (rest.livePhotoVideoId) { await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId }); } else if (rest.livePhotoVideoId === null) { @@ -144,14 +159,29 @@ export class AssetService extends BaseService { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids }); const assetDto = _.omitBy({ isFavorite, visibility, duplicateId }, _.isUndefined); - const exifDto = _.omitBy({ latitude, longitude, rating, description, dateTimeOriginal }, _.isUndefined); + const exifDto = _.omitBy( + { + latitude, + longitude, + rating, + description, + dateTimeOriginal, + }, + _.isUndefined, + ); if (Object.keys(exifDto).length > 0) { await this.assetRepository.updateAllExif(ids, exifDto); } - if ((dateTimeRelative !== undefined && dateTimeRelative !== 0) || timeZone !== undefined) { - await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone); + const extractedTimeZone = extractTimeZone(dateTimeOriginal); + + if ( + (dateTimeRelative !== undefined && dateTimeRelative !== 0) || + timeZone !== undefined || + extractedTimeZone?.type === 'fixed' + ) { + await this.assetRepository.updateDateTimeOriginal(ids, dateTimeRelative, timeZone ?? extractedTimeZone?.name); } if (Object.keys(assetDto).length > 0) { @@ -299,10 +329,11 @@ export class AssetService extends BaseService { return JobStatus.Failed; } - // Replace the parent of the stack children with a new asset + // replace the parent of the stack children with a new asset if (asset.stack?.primaryAssetId === id) { - const stackAssetIds = asset.stack?.assets.map((a) => a.id) ?? []; - if (stackAssetIds.length > 2) { + // this only includes timeline visible assets and excludes the primary asset + const stackAssetIds = asset.stack.assets.map((a) => a.id); + if (stackAssetIds.length >= 2) { const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!; await this.stackRepository.update(asset.stack.id, { id: asset.stack.id, @@ -331,11 +362,19 @@ export class AssetService extends BaseService { } } - const { fullsizeFile, previewFile, thumbnailFile, sidecarFile } = getAssetFiles(asset.files ?? []); - const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath]; + const assetFiles = getAssetFiles(asset.files ?? []); + const files = [ + assetFiles.thumbnailFile?.path, + assetFiles.previewFile?.path, + assetFiles.fullsizeFile?.path, + assetFiles.editedFullsizeFile?.path, + assetFiles.editedPreviewFile?.path, + assetFiles.editedThumbnailFile?.path, + asset.encodedVideoPath, + ]; if (deleteOnDisk && !asset.isOffline) { - files.push(sidecarFile?.path, asset.originalPath); + files.push(assetFiles.sidecarFile?.path, asset.originalPath); } await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: files.filter(Boolean) } }); @@ -364,15 +403,54 @@ export class AssetService extends BaseService { async getOcr(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); - return this.ocrRepository.getByAssetId(id); + const ocr = await this.ocrRepository.getByAssetId(id); + const asset = await this.assetRepository.getForOcr(id); + + if (!asset) { + throw new BadRequestException('Asset not found'); + } + + const dimensions = getDimensions({ + exifImageHeight: asset.exifImageHeight, + exifImageWidth: asset.exifImageWidth, + orientation: asset.orientation, + }); + + return ocr.map((item) => transformOcrBoundingBox(item, asset.edits, dimensions)); + } + + async upsertBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkUpsertDto): Promise { + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) }); + + const uniqueKeys = new Set(); + for (const item of dto.items) { + const key = `(${item.assetId}, ${item.key})`; + if (uniqueKeys.has(key)) { + throw new BadRequestException(`Duplicate items are not allowed: "${key}"`); + } + + uniqueKeys.add(key); + } + + return this.assetRepository.upsertBulkMetadata(dto.items); } async upsertMetadata(auth: AuthDto, id: string, dto: AssetMetadataUpsertDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); + + const uniqueKeys = new Set(); + for (const { key } of dto.items) { + if (uniqueKeys.has(key)) { + throw new BadRequestException(`Duplicate items are not allowed: "${key}"`); + } + + uniqueKeys.add(key); + } + return this.assetRepository.upsertMetadata(id, dto.items); } - async getMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise { + async getMetadataByKey(auth: AuthDto, id: string, key: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); const item = await this.assetRepository.getMetadataByKey(id, key); @@ -382,11 +460,16 @@ export class AssetService extends BaseService { return item; } - async deleteMetadataByKey(auth: AuthDto, id: string, key: AssetMetadataKey): Promise { + async deleteMetadataByKey(auth: AuthDto, id: string, key: string): Promise { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); return this.assetRepository.deleteMetadataByKey(id, key); } + async deleteBulkMetadata(auth: AuthDto, dto: AssetMetadataBulkDeleteDto) { + await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.items.map((item) => item.assetId) }); + await this.assetRepository.deleteBulkMetadata(dto.items); + } + async run(auth: AuthDto, dto: AssetJobsDto) { await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds }); @@ -433,10 +516,21 @@ export class AssetService extends BaseService { dateTimeOriginal?: string; latitude?: number; longitude?: number; - rating?: number; + rating?: number | null; }) { const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; - const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); + const writes = _.omitBy( + { + description, + dateTimeOriginal, + timeZone: extractTimeZone(dateTimeOriginal)?.name, + latitude, + longitude, + rating, + }, + _.isUndefined, + ); + if (Object.keys(writes).length > 0) { await this.assetRepository.upsertExif( updateLockedColumns({ @@ -448,4 +542,91 @@ export class AssetService extends BaseService { await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id } }); } } + + async getAssetEdits(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] }); + const edits = await this.assetEditRepository.getAll(id); + + return { + assetId: id, + edits, + }; + } + + async editAsset(auth: AuthDto, id: string, dto: AssetEditsCreateDto): Promise { + await this.requireAccess({ auth, permission: Permission.AssetEditCreate, ids: [id] }); + + const asset = await this.assetRepository.getForEdit(id); + if (!asset) { + throw new BadRequestException('Asset not found'); + } + + if (asset.type !== AssetType.Image) { + throw new BadRequestException('Only images can be edited'); + } + + if (asset.livePhotoVideoId) { + throw new BadRequestException('Editing live photos is not supported'); + } + + if (isPanorama(asset)) { + throw new BadRequestException('Editing panorama images is not supported'); + } + + if (asset.originalPath?.toLowerCase().endsWith('.gif')) { + throw new BadRequestException('Editing GIF images is not supported'); + } + + if (asset.originalPath?.toLowerCase().endsWith('.svg')) { + throw new BadRequestException('Editing SVG images is not supported'); + } + + // check that crop parameters will not go out of bounds + const { width: assetWidth, height: assetHeight } = getDimensions(asset); + + if (!assetWidth || !assetHeight) { + throw new BadRequestException('Asset dimensions are not available for editing'); + } + + const edits = dto.edits as AssetEditActionItem[]; + const crop = edits.find((e) => e.action === AssetEditAction.Crop); + if (crop) { + if (edits[0].action !== AssetEditAction.Crop) { + throw new BadRequestException('Crop action must be the first edit action'); + } + + // check that crop parameters will not go out of bounds + const { width: assetWidth, height: assetHeight } = getDimensions(asset); + + if (!assetWidth || !assetHeight) { + throw new BadRequestException('Asset dimensions are not available for editing'); + } + + const { x, y, width, height } = crop.parameters; + if (x + width > assetWidth || y + height > assetHeight) { + throw new BadRequestException('Crop parameters are out of bounds'); + } + } + + const newEdits = await this.assetEditRepository.replaceAll(id, edits); + await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } }); + + // Return the asset and its applied edits + return { + assetId: id, + edits: newEdits, + }; + } + + async removeAssetEdits(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.AssetEditDelete, ids: [id] }); + + const asset = await this.assetRepository.getById(id); + if (!asset) { + throw new BadRequestException('Asset not found'); + } + + await this.assetEditRepository.replaceAll(id, []); + await this.jobRepository.queue({ name: JobName.AssetEditThumbnailGeneration, data: { id } }); + } } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index a34efedfb0..81f601da0a 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -513,7 +513,7 @@ describe(AuthService.name, () => { metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), ).rejects.toBeInstanceOf(UnauthorizedException); - expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)'); + expect(mocks.apiKey.getKey).toHaveBeenCalledWith(Buffer.from('auth_token (hashed)')); }); it('should throw an error if api key has insufficient permissions', async () => { @@ -574,7 +574,7 @@ describe(AuthService.name, () => { metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, }), ).resolves.toEqual({ user: authUser, apiKey: expect.objectContaining(authApiKey) }); - expect(mocks.apiKey.getKey).toHaveBeenCalledWith('auth_token (hashed)'); + expect(mocks.apiKey.getKey).toHaveBeenCalledWith(Buffer.from('auth_token (hashed)')); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 1a68bbfce7..b8e1b78107 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -165,6 +165,11 @@ export class AuthService extends BaseService { } async adminSignUp(dto: SignUpDto): Promise { + const { setup } = this.configRepository.getEnv(); + if (!setup.allow) { + throw new BadRequestException('Admin setup is disabled'); + } + const adminUser = await this.userRepository.getAdmin(); if (adminUser) { throw new BadRequestException('The server already has an admin'); @@ -451,8 +456,8 @@ export class AuthService extends BaseService { } private async validateApiKey(key: string): Promise { - const hashedKey = this.cryptoRepository.hashSha256(key); - const apiKey = await this.apiKeyRepository.getKey(hashedKey); + const hashed = this.cryptoRepository.hashSha256(key); + const apiKey = await this.apiKeyRepository.getKey(hashed); if (apiKey?.user) { return { user: apiKey.user, @@ -471,9 +476,9 @@ export class AuthService extends BaseService { return this.cryptoRepository.compareBcrypt(inputSecret, existingHash); } - private async validateSession(tokenValue: string, headers: IncomingHttpHeaders): Promise { - const hashedToken = this.cryptoRepository.hashSha256(tokenValue); - const session = await this.sessionRepository.getByToken(hashedToken); + private async validateSession(token: string, headers: IncomingHttpHeaders): Promise { + const hashed = this.cryptoRepository.hashSha256(token); + const session = await this.sessionRepository.getByToken(hashed); if (session?.user) { const { appVersion, deviceOS, deviceType } = getUserAgentDetails(headers); const now = DateTime.now(); @@ -538,10 +543,10 @@ export class AuthService extends BaseService { private async createLoginResponse(user: UserAdmin, loginDetails: LoginDetails) { const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); await this.sessionRepository.create({ - token: tokenHashed, + token: hashed, deviceOS: loginDetails.deviceOS, deviceType: loginDetails.deviceType, appVersion: loginDetails.appVersion, diff --git a/server/src/services/backup.service.spec.ts b/server/src/services/backup.service.spec.ts deleted file mode 100644 index 9e25fbaf2e..0000000000 --- a/server/src/services/backup.service.spec.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { DateTime } from 'luxon'; -import { PassThrough } from 'node:stream'; -import { defaults, SystemConfig } from 'src/config'; -import { StorageCore } from 'src/cores/storage.core'; -import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; -import { BackupService } from 'src/services/backup.service'; -import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { mockSpawn, newTestService, ServiceMocks } from 'test/utils'; -import { describe } from 'vitest'; - -describe(BackupService.name, () => { - let sut: BackupService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(BackupService)); - }); - - it('should work', () => { - expect(sut).toBeDefined(); - }); - - describe('onBootstrapEvent', () => { - it('should init cron job and handle config changes', async () => { - mocks.database.tryLock.mockResolvedValue(true); - mocks.cron.create.mockResolvedValue(); - - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).toHaveBeenCalled(); - }); - - it('should not initialize backup database cron job when lock is taken', async () => { - mocks.database.tryLock.mockResolvedValue(false); - - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).not.toHaveBeenCalled(); - }); - - it('should not initialise backup database job when running on microservices', async () => { - mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); - await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); - - expect(mocks.cron.create).not.toHaveBeenCalled(); - }); - }); - - describe('onConfigUpdateEvent', () => { - beforeEach(async () => { - mocks.database.tryLock.mockResolvedValue(true); - mocks.cron.create.mockResolvedValue(); - - await sut.onConfigInit({ newConfig: defaults }); - }); - - it('should update cron job if backup is enabled', () => { - mocks.cron.update.mockResolvedValue(); - - sut.onConfigUpdate({ - oldConfig: defaults, - newConfig: { - backup: { - database: { - enabled: true, - cronExpression: '0 1 * * *', - }, - }, - } as SystemConfig, - }); - - expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true }); - expect(mocks.cron.update).toHaveBeenCalled(); - }); - - it('should do nothing if instance does not have the backup database lock', async () => { - mocks.database.tryLock.mockResolvedValue(false); - await sut.onConfigInit({ newConfig: defaults }); - sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults }); - expect(mocks.cron.update).not.toHaveBeenCalled(); - }); - }); - - describe('cleanupDatabaseBackups', () => { - it('should do nothing if not reached keepLastAmount', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).not.toHaveBeenCalled(); - }); - - it('should remove failed backup files', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - //`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, - mocks.storage.readdir.mockResolvedValue([ - 'immich-db-backup-123.sql.gz.tmp', - `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - ]); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz.tmp`, - ); - }); - - it('should remove old backup files over keepLastAmount', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`, - ); - }); - - it('should remove old backup files over keepLastAmount and failed backups', async () => { - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.readdir.mockResolvedValue([ - `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, - `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - 'immich-db-backup-1753789649000.sql.gz', - `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, - ]); - await sut.cleanupDatabaseBackups(); - expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1753789649000.sql.gz`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, - ); - expect(mocks.storage.unlink).toHaveBeenCalledWith( - `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz`, - ); - }); - }); - - describe('handleBackupDatabase', () => { - beforeEach(() => { - mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); - mocks.storage.rename.mockResolvedValue(); - mocks.storage.unlink.mockResolvedValue(); - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); - }); - - it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { - // create a service instance with a URL connection that includes libpqcompat - const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; - const configMock = { - getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), - getWorker: () => ImmichWorker.Api, - isDev: () => false, - } as unknown as any; - - ({ sut, mocks } = newTestService(BackupService, { config: configMock })); - - mocks.storage.readdir.mockResolvedValue([]); - mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); - mocks.storage.rename.mockResolvedValue(); - mocks.storage.unlink.mockResolvedValue(); - mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); - mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); - mocks.database.getPostgresVersion.mockResolvedValue('14.10'); - - await sut.handleBackupDatabase(); - - expect(mocks.process.spawn).toHaveBeenCalled(); - const call = mocks.process.spawn.mock.calls[0]; - const args = call[1] as string[]; - // ['--dbname', '', '--clean', '--if-exists'] - expect(args[0]).toBe('--dbname'); - const passedUrl = args[1]; - expect(passedUrl).not.toContain('uselibpqcompat'); - expect(passedUrl).toContain('sslmode=require'); - }); - - it('should run a database backup successfully', async () => { - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.Success); - expect(mocks.storage.createWriteStream).toHaveBeenCalled(); - }); - - it('should rename file on success', async () => { - const result = await sut.handleBackupDatabase(); - expect(result).toBe(JobStatus.Success); - expect(mocks.storage.rename).toHaveBeenCalled(); - }); - - it('should fail if pg_dumpall fails', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); - }); - - it('should not rename file if pgdump fails and gzip succeeds', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); - expect(mocks.storage.rename).not.toHaveBeenCalled(); - }); - - it('should fail if gzip fails', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', '')); - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1'); - }); - - it('should fail if write stream fails', async () => { - mocks.storage.createWriteStream.mockImplementation(() => { - throw new Error('error'); - }); - await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); - }); - - it('should fail if rename fails', async () => { - mocks.storage.rename.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); - }); - - it('should ignore unlink failing and still return failed job status', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - mocks.storage.unlink.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); - expect(mocks.storage.unlink).toHaveBeenCalled(); - }); - - it.each` - postgresVersion | expectedVersion - ${'14.10'} | ${14} - ${'14.10.3'} | ${14} - ${'14.10 (Debian 14.10-1.pgdg120+1)'} | ${14} - ${'15.3.3'} | ${15} - ${'16.4.2'} | ${16} - ${'17.15.1'} | ${17} - ${'18.0.0'} | ${18} - `( - `should use pg_dumpall $expectedVersion with postgres version $postgresVersion`, - async ({ postgresVersion, expectedVersion }) => { - mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); - await sut.handleBackupDatabase(); - expect(mocks.process.spawn).toHaveBeenCalledWith( - `/usr/lib/postgresql/${expectedVersion}/bin/pg_dumpall`, - expect.any(Array), - expect.any(Object), - ); - }, - ); - it.each` - postgresVersion - ${'13.99.99'} - ${'19.0.0'} - `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => { - mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); - const result = await sut.handleBackupDatabase(); - expect(mocks.process.spawn).not.toHaveBeenCalled(); - expect(result).toBe(JobStatus.Failed); - }); - }); -}); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts deleted file mode 100644 index 2ff3e5dd3e..0000000000 --- a/server/src/services/backup.service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DateTime } from 'luxon'; -import path from 'node:path'; -import semver from 'semver'; -import { serverVersion } from 'src/constants'; -import { StorageCore } from 'src/cores/storage.core'; -import { OnEvent, OnJob } from 'src/decorators'; -import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; -import { ArgOf } from 'src/repositories/event.repository'; -import { BaseService } from 'src/services/base.service'; -import { handlePromiseError } from 'src/utils/misc'; - -@Injectable() -export class BackupService extends BaseService { - private backupLock = false; - - @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) - async onConfigInit({ - newConfig: { - backup: { database }, - }, - }: ArgOf<'ConfigInit'>) { - this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase); - - if (this.backupLock) { - this.cronRepository.create({ - name: 'backupDatabase', - expression: database.cronExpression, - onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger), - start: database.enabled, - }); - } - } - - @OnEvent({ name: 'ConfigUpdate', server: true }) - onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) { - if (!this.backupLock) { - return; - } - - this.cronRepository.update({ - name: 'backupDatabase', - expression: backup.database.cronExpression, - start: backup.database.enabled, - }); - } - - async cleanupDatabaseBackups() { - this.logger.debug(`Database Backup Cleanup Started`); - const { - backup: { database: config }, - } = await this.getConfig({ withCache: false }); - - const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); - const files = await this.storageRepository.readdir(backupsFolder); - const failedBackups = files.filter((file) => file.match(/immich-db-backup-.*\.sql\.gz\.tmp$/)); - const backups = files - .filter((file) => { - const oldBackupStyle = file.match(/immich-db-backup-\d+\.sql\.gz$/); - //immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz - const newBackupStyle = file.match(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/); - return oldBackupStyle || newBackupStyle; - }) - .toSorted() - .toReversed(); - - const toDelete = backups.slice(config.keepLastAmount); - toDelete.push(...failedBackups); - - for (const file of toDelete) { - await this.storageRepository.unlink(path.join(backupsFolder, file)); - } - this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); - } - - @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) - async handleBackupDatabase(): Promise { - this.logger.debug(`Database Backup Started`); - const { database } = this.configRepository.getEnv(); - const config = database.config; - - const isUrlConnection = config.connectionType === 'url'; - - let connectionUrl: string = isUrlConnection ? config.url : ''; - if (URL.canParse(connectionUrl)) { - // remove known bad url parameters for pg_dumpall - const url = new URL(connectionUrl); - url.searchParams.delete('uselibpqcompat'); - connectionUrl = url.toString(); - } - - const databaseParams = isUrlConnection - ? ['--dbname', connectionUrl] - : [ - '--username', - config.username, - '--host', - config.host, - '--port', - `${config.port}`, - '--database', - config.database, - ]; - - databaseParams.push('--clean', '--if-exists'); - const databaseVersion = await this.databaseRepository.getPostgresVersion(); - const backupFilePath = path.join( - StorageCore.getBaseFolder(StorageFolder.Backups), - `immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, - ); - const databaseSemver = semver.coerce(databaseVersion); - const databaseMajorVersion = databaseSemver?.major; - - if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { - this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`); - return JobStatus.Failed; - } - - this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); - - try { - await new Promise((resolve, reject) => { - const pgdump = this.processRepository.spawn( - `/usr/lib/postgresql/${databaseMajorVersion}/bin/pg_dumpall`, - databaseParams, - { - env: { - PATH: process.env.PATH, - PGPASSWORD: isUrlConnection ? new URL(connectionUrl).password : config.password, - }, - }, - ); - - // NOTE: `--rsyncable` is only supported in GNU gzip - const gzip = this.processRepository.spawn(`gzip`, ['--rsyncable']); - pgdump.stdout.pipe(gzip.stdin); - - const fileStream = this.storageRepository.createWriteStream(backupFilePath); - - gzip.stdout.pipe(fileStream); - - pgdump.on('error', (err) => { - this.logger.error(`Backup failed with error: ${err}`); - reject(err); - }); - - gzip.on('error', (err) => { - this.logger.error(`Gzip failed with error: ${err}`); - reject(err); - }); - - let pgdumpLogs = ''; - let gzipLogs = ''; - - pgdump.stderr.on('data', (data) => (pgdumpLogs += data)); - gzip.stderr.on('data', (data) => (gzipLogs += data)); - - pgdump.on('exit', (code) => { - if (code !== 0) { - this.logger.error(`Backup failed with code ${code}`); - reject(`Backup failed with code ${code}`); - this.logger.error(pgdumpLogs); - return; - } - if (pgdumpLogs) { - this.logger.debug(`pgdump_all logs\n${pgdumpLogs}`); - } - }); - - gzip.on('exit', (code) => { - if (code !== 0) { - this.logger.error(`Gzip failed with code ${code}`); - reject(`Gzip failed with code ${code}`); - this.logger.error(gzipLogs); - return; - } - if (pgdump.exitCode !== 0) { - this.logger.error(`Gzip exited with code 0 but pgdump exited with ${pgdump.exitCode}`); - return; - } - resolve(); - }); - }); - await this.storageRepository.rename(backupFilePath, backupFilePath.replace('.tmp', '')); - } catch (error) { - this.logger.error(`Database Backup Failure: ${error}`); - await this.storageRepository - .unlink(backupFilePath) - .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); - throw error; - } - - this.logger.log(`Database Backup Success`); - await this.cleanupDatabaseBackups(); - return JobStatus.Success; - } -} diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 9c422818b3..b3a50a07ae 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -11,6 +11,7 @@ import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AppRepository } from 'src/repositories/app.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; @@ -69,6 +70,7 @@ export const BASE_SERVICE_DEPENDENCIES = [ ApiKeyRepository, AppRepository, AssetRepository, + AssetEditRepository, AssetJobRepository, AuditRepository, ConfigRepository, @@ -127,6 +129,7 @@ export class BaseService { protected apiKeyRepository: ApiKeyRepository, protected appRepository: AppRepository, protected assetRepository: AssetRepository, + protected assetEditRepository: AssetEditRepository, protected assetJobRepository: AssetJobRepository, protected auditRepository: AuditRepository, protected configRepository: ConfigRepository, diff --git a/server/src/services/cli.service.spec.ts b/server/src/services/cli.service.spec.ts index 49fa5cf5b8..36a3d2eb2c 100644 --- a/server/src/services/cli.service.spec.ts +++ b/server/src/services/cli.service.spec.ts @@ -1,5 +1,5 @@ import { jwtVerify } from 'jose'; -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { CliService } from 'src/services/cli.service'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -89,16 +89,25 @@ describe(CliService.name, () => { alreadyDisabled: true, }); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0); expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0); expect(mocks.event.emit).toHaveBeenCalledTimes(0); }); it('should disable maintenance mode', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.disableMaintenanceMode()).resolves.toEqual({ alreadyDisabled: false, }); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: false, }); @@ -107,13 +116,21 @@ describe(CliService.name, () => { describe('enableMaintenanceMode', () => { it('should not do anything if in maintenance mode', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); + await expect(sut.enableMaintenanceMode()).resolves.toEqual( expect.objectContaining({ alreadyEnabled: true, }), ); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0); expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0); expect(mocks.event.emit).toHaveBeenCalledTimes(0); }); @@ -126,16 +143,26 @@ describe(CliService.name, () => { }), ); + expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret: expect.stringMatching(/^\w{128}$/), + action: { + action: 'start', + }, }); }); const RE_LOGIN_URL = /https:\/\/my.immich.app\/maintenance\?token=([A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*)/; it('should return a valid login URL', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); const result = await sut.enableMaintenanceMode(); diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 3d248edc7a..22f06e2ed9 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -1,15 +1,59 @@ +import { schemaDiff } from '@immich/sql-tools'; import { Injectable } from '@nestjs/common'; -import { isAbsolute } from 'node:path'; +import { isAbsolute, join } from 'node:path'; import { SALT_ROUNDS } from 'src/constants'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { createMaintenanceLoginUrl, generateMaintenanceSecret, sendOneShotAppRestart } from 'src/utils/maintenance'; +import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; +export type SchemaReport = { + migrations: MigrationStatus[]; + drift: ReturnType; +}; + +type MigrationStatus = { + name: string; + status: 'applied' | 'missing' | 'deleted'; +}; + @Injectable() export class CliService extends BaseService { + async schemaReport(): Promise { + // eslint-disable-next-line unicorn/prefer-module + const allFiles = await this.storageRepository.readdir(join(__dirname, '../schema/migrations')); + const files = allFiles.filter((file) => file.endsWith('.js')).map((file) => file.slice(0, -3)); + const rows = await this.databaseRepository.getMigrations(); + const filesSet = new Set(files); + const rowsSet = new Set(rows.map((item) => item.name)); + const combined = [...filesSet, ...rowsSet].toSorted(); + + const migrations: MigrationStatus[] = []; + + for (const name of combined) { + if (filesSet.has(name) && rowsSet.has(name)) { + migrations.push({ name, status: 'applied' }); + continue; + } + + if (filesSet.has(name) && !rowsSet.has(name)) { + migrations.push({ name, status: 'missing' }); + continue; + } + + if (!filesSet.has(name) && rowsSet.has(name)) { + migrations.push({ name, status: 'deleted' }); + continue; + } + } + + const drift = await this.databaseRepository.getSchemaDrift(); + + return { migrations, drift }; + } + async listUsers(): Promise { const users = await this.userRepository.getList({ withDeleted: true }); return users.map((user) => mapUserAdmin(user)); @@ -55,8 +99,7 @@ export class CliService extends BaseService { const state = { isMaintenanceMode: false as const }; await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state); - - sendOneShotAppRestart(state); + await this.appRepository.sendOneShotAppRestart(state); return { alreadyDisabled: false, @@ -87,9 +130,12 @@ export class CliService extends BaseService { await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret, + action: { + action: MaintenanceAction.Start, + }, }); - sendOneShotAppRestart({ + await this.appRepository.sendOneShotAppRestart({ isMaintenanceMode: true, }); diff --git a/server/src/services/database-backup.service.spec.ts b/server/src/services/database-backup.service.spec.ts new file mode 100644 index 0000000000..429e60aede --- /dev/null +++ b/server/src/services/database-backup.service.spec.ts @@ -0,0 +1,857 @@ +import { BadRequestException } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { PassThrough, Readable } from 'node:stream'; +import { defaults, SystemConfig } from 'src/config'; +import { StorageCore } from 'src/cores/storage.core'; +import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils'; + +describe(DatabaseBackupService.name, () => { + let sut: DatabaseBackupService; + let mocks: ServiceMocks; + let maintenanceHealthRepositoryMock: AutoMocked; + + beforeEach(() => { + mocks = getMocks(); + maintenanceHealthRepositoryMock = automock(MaintenanceHealthRepository, { + args: [mocks.logger], + strict: false, + }); + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + mocks.config, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + maintenanceHealthRepositoryMock as never, + ); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + describe('onBootstrapEvent', () => { + it('should init cron job and handle config changes', async () => { + mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).toHaveBeenCalled(); + }); + + it('should not initialize backup database cron job when lock is taken', async () => { + mocks.database.tryLock.mockResolvedValue(false); + + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).not.toHaveBeenCalled(); + }); + + it('should not initialise backup database job when running on microservices', async () => { + mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); + await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); + + expect(mocks.cron.create).not.toHaveBeenCalled(); + }); + }); + + describe('onConfigUpdateEvent', () => { + beforeEach(async () => { + mocks.database.tryLock.mockResolvedValue(true); + mocks.cron.create.mockResolvedValue(); + + await sut.onConfigInit({ newConfig: defaults }); + }); + + it('should update cron job if backup is enabled', () => { + mocks.cron.update.mockResolvedValue(); + + sut.onConfigUpdate({ + oldConfig: defaults, + newConfig: { + backup: { + database: { + enabled: true, + cronExpression: '0 1 * * *', + }, + }, + } as SystemConfig, + }); + + expect(mocks.cron.update).toHaveBeenCalledWith({ name: 'backupDatabase', expression: '0 1 * * *', start: true }); + expect(mocks.cron.update).toHaveBeenCalled(); + }); + + it('should do nothing if instance does not have the backup database lock', async () => { + mocks.database.tryLock.mockResolvedValue(false); + await sut.onConfigInit({ newConfig: defaults }); + sut.onConfigUpdate({ newConfig: systemConfigStub.backupEnabled as SystemConfig, oldConfig: defaults }); + expect(mocks.cron.update).not.toHaveBeenCalled(); + }); + }); + + describe('cleanupDatabaseBackups', () => { + it('should do nothing if not reached keepLastAmount', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz']); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).not.toHaveBeenCalled(); + }); + + it('should remove failed backup files', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + //`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, + mocks.storage.readdir.mockResolvedValue([ + 'immich-db-backup-123.sql.gz.tmp', + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + ]); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz.tmp`, + ); + }); + + it('should remove old backup files over keepLastAmount', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue(['immich-db-backup-1.sql.gz', 'immich-db-backup-2.sql.gz']); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`, + ); + }); + + it('should remove old backup files over keepLastAmount and failed backups', async () => { + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.readdir.mockResolvedValue([ + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + 'immich-db-backup-1753789649000.sql.gz', + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + ]); + await sut.cleanupDatabaseBackups(); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(3); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1753789649000.sql.gz`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp`, + ); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz`, + ); + }); + }); + + describe('handleBackupDatabase / createDatabaseBackup', () => { + beforeEach(() => { + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + }); + + it('should sanitize DB_URL (remove uselibpqcompat) before calling pg_dumpall', async () => { + // create a service instance with a URL connection that includes libpqcompat + const dbUrl = 'postgresql://postgres:pwd@host:5432/immich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.database.getPostgresVersion.mockResolvedValue('14.10'); + + await sut.handleBackupDatabase(); + + expect(mocks.process.spawnDuplexStream).toHaveBeenCalled(); + const call = mocks.process.spawnDuplexStream.mock.calls[0]; + const args = call[1] as string[]; + expect(args).toMatchInlineSnapshot(` + [ + "postgresql://postgres:pwd@host:5432/immich?sslmode=require", + "--clean", + "--if-exists", + ] + `); + }); + + it('should run a database backup successfully', async () => { + const result = await sut.handleBackupDatabase(); + expect(result).toBe(JobStatus.Success); + expect(mocks.storage.createWriteStream).toHaveBeenCalled(); + }); + + it('should rename file on success', async () => { + const result = await sut.handleBackupDatabase(); + expect(result).toBe(JobStatus.Success); + expect(mocks.storage.rename).toHaveBeenCalled(); + }); + + it('should fail if pg_dump fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + }); + + it('should not rename file if pgdump fails and gzip succeeds', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + expect(mocks.storage.rename).not.toHaveBeenCalled(); + }); + + it('should fail if gzip fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('gzip', 1, '', 'error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('gzip non-zero exit code (1)'); + }); + + it('should fail if write stream fails', async () => { + mocks.storage.createWriteStream.mockImplementation(() => { + throw new Error('error'); + }); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); + }); + + it('should fail if rename fails', async () => { + mocks.storage.rename.mockRejectedValue(new Error('error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('error'); + }); + + it('should ignore unlink failing and still return failed job status', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + mocks.storage.unlink.mockRejectedValue(new Error('error')); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); + expect(mocks.storage.unlink).toHaveBeenCalled(); + }); + + it.each` + postgresVersion | expectedVersion + ${'14.10'} | ${14} + ${'14.10.3'} | ${14} + ${'14.10 (Debian 14.10-1.pgdg120+1)'} | ${14} + ${'15.3.3'} | ${15} + ${'16.4.2'} | ${16} + ${'17.15.1'} | ${17} + ${'18.0.0'} | ${18} + `( + `should use pg_dump $expectedVersion with postgres version $postgresVersion`, + async ({ postgresVersion, expectedVersion }) => { + mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); + await sut.handleBackupDatabase(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledWith( + `/usr/lib/postgresql/${expectedVersion}/bin/pg_dump`, + expect.any(Array), + expect.any(Object), + ); + }, + ); + it.each` + postgresVersion + ${'13.99.99'} + ${'19.0.0'} + `(`should fail if postgres version $postgresVersion is not supported`, async ({ postgresVersion }) => { + mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); + const result = await sut.handleBackupDatabase(); + expect(mocks.process.spawn).not.toHaveBeenCalled(); + expect(result).toBe(JobStatus.Failed); + }); + }); + + describe('buildPostgresLaunchArguments', () => { + describe('default config', () => { + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "immich", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "--dbname", + "immich", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--username", + "postgres", + "--host", + "database", + "--port", + "5432", + "--dbname", + "immich", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "postgres", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using custom parts', () => { + beforeEach(() => { + const configMock = { + getEnv: () => ({ + database: { + config: { + connectionType: 'parts', + host: 'myhost', + port: 1234, + username: 'mypg', + password: 'mypwd', + database: 'myimmich', + }, + skipMigrations: false, + }, + }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--username", + "mypg", + "--host", + "myhost", + "--port", + "1234", + "myimmich", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--username", + "mypg", + "--host", + "myhost", + "--port", + "1234", + "--dbname", + "myimmich", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using URL', () => { + beforeEach(() => { + const dbUrl = 'postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should generate pg_dump arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('pg_dump')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require", + "--clean", + "--if-exists", + ], + "bin": "/usr/lib/postgresql/14/bin/pg_dump", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + + it('should generate psql (single transaction) arguments', async () => { + await expect(sut.buildPostgresLaunchArguments('psql', { singleTransaction: true })).resolves + .toMatchInlineSnapshot(` + { + "args": [ + "--dbname", + "postgresql://mypg:mypwd@myhost:1234/myimmich?sslmode=require", + "--single-transaction", + "--set", + "ON_ERROR_STOP=on", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "mypwd", + "databaseUsername": "mypg", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + + describe('using bad URL', () => { + beforeEach(() => { + const dbUrl = 'post://gresql://mypg:myp@wd@myhos:t:1234/myimmich?sslmode=require&uselibpqcompat=true'; + const configMock = { + getEnv: () => ({ database: { config: { connectionType: 'url', url: dbUrl }, skipMigrations: false } }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + void 0 as never, + ); + }); + + it('should fallback to reasonable defaults', async () => { + await expect(sut.buildPostgresLaunchArguments('psql')).resolves.toMatchInlineSnapshot(` + { + "args": [ + "--dbname", + "post://gresql//mypg:myp@wd@myhos:t:1234/myimmich?sslmode=require", + "--echo-all", + "--output=/dev/null", + ], + "bin": "/usr/lib/postgresql/14/bin/psql", + "databaseMajorVersion": 14, + "databasePassword": "", + "databaseUsername": "postgres", + "databaseVersion": "14.10 (Debian 14.10-1.pgdg120+1)", + } + `); + }); + }); + }); + + describe('uploadBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.uploadBackup({ originalname: 'invalid backup' } as never)).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should write file', async () => { + await sut.uploadBackup({ originalname: 'path.sql.gz', buffer: 'buffer' } as never); + expect(mocks.storage.createOrOverwriteFile).toBeCalledWith('/data/backups/uploaded-path.sql.gz', 'buffer'); + }); + }); + + describe('downloadBackup', () => { + it('should reject invalid file names', () => { + expect(() => sut.downloadBackup('invalid backup')).toThrowError(new BadRequestException('Invalid backup name!')); + }); + + it('should get backup path', () => { + expect(sut.downloadBackup('hello.sql.gz')).toEqual( + expect.objectContaining({ + path: '/data/backups/hello.sql.gz', + }), + ); + }); + }); + + describe('listBackups', () => { + it('should give us all backups', async () => { + mocks.storage.readdir.mockResolvedValue([ + `immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`, + `immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + 'immich-db-backup-1753789649000.sql.gz', + `immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`, + ]); + mocks.storage.stat.mockResolvedValue({ size: 1024 } as any); + + await expect(sut.listBackups()).resolves.toMatchObject({ + backups: [ + { filename: 'immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz', filesize: 1024 }, + { filename: 'immich-db-backup-1753789649000.sql.gz', filesize: 1024 }, + ], + }); + }); + }); + + describe('deleteBackup', () => { + it('should reject invalid file names', async () => { + await expect(sut.deleteBackup(['filename'])).rejects.toThrowError( + new BadRequestException('Invalid backup name!'), + ); + }); + + it('should unlink the target file', async () => { + await sut.deleteBackup(['filename.sql']); + expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); + expect(mocks.storage.unlink).toHaveBeenCalledWith( + `${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename.sql`, + ); + }); + }); + + describe('restoreDatabaseBackup', () => { + beforeEach(() => { + mocks.storage.readdir.mockResolvedValue([]); + mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementation(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.fork.mockImplementation(() => mockSpawn(0, 'Immich Server is listening', '')); + mocks.storage.rename.mockResolvedValue(); + mocks.storage.unlink.mockResolvedValue(); + mocks.storage.createPlainReadStream.mockReturnValue(Readable.from(mockData())); + mocks.storage.createWriteStream.mockReturnValue(new PassThrough()); + mocks.storage.createGzip.mockReturnValue(new PassThrough()); + mocks.storage.createGunzip.mockReturnValue(new PassThrough()); + + const configMock = { + getEnv: () => ({ + database: { + config: { + connectionType: 'parts', + host: 'myhost', + port: 1234, + username: 'mypg', + password: 'mypwd', + database: 'myimmich', + }, + skipMigrations: false, + }, + }), + getWorker: () => ImmichWorker.Api, + isDev: () => false, + } as unknown as any; + + sut = new DatabaseBackupService( + mocks.logger as never, + mocks.storage as never, + configMock as never, + mocks.systemMetadata as never, + mocks.process, + mocks.database as never, + mocks.cron as never, + mocks.job as never, + maintenanceHealthRepositoryMock, + ); + }); + + it('should fail to restore invalid backup', async () => { + await expect(sut.restoreDatabaseBackup('filename')).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Invalid backup file format!]`, + ); + }); + + it('should successfully restore a backup', async () => { + let writtenToPsql = ''; + + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => { + return mockDuplex((chunk) => (writtenToPsql += chunk))('command', 0, 'data', ''); + }); + + const progress = vitest.fn(); + await sut.restoreDatabaseBackup('development-filename.sql', progress); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + + expect(mocks.process.spawnDuplexStream).toHaveBeenLastCalledWith( + expect.stringMatching('/bin/psql'), + [ + '--username', + 'mypg', + '--host', + 'myhost', + '--port', + '1234', + '--dbname', + 'myimmich', + '--single-transaction', + '--set', + 'ON_ERROR_STOP=on', + '--echo-all', + '--output=/dev/null', + ], + expect.objectContaining({ + env: expect.objectContaining({ + PATH: expect.any(String), + PGPASSWORD: 'mypwd', + }), + }), + ); + + expect(writtenToPsql).toMatchInlineSnapshot(` + " + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); + + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO "mypg"; + GRANT ALL ON SCHEMA public TO public; + SELECT 1;" + `); + }); + + it('should generate pg_dumpall specific SQL instructions', async () => { + let writtenToPsql = ''; + + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => mockDuplex()('command', 0, 'data', '')); + mocks.process.spawnDuplexStream.mockImplementationOnce(() => { + return mockDuplex((chunk) => (writtenToPsql += chunk))('command', 0, 'data', ''); + }); + + const progress = vitest.fn(); + await sut.restoreDatabaseBackup('development-v2.4.0-.sql', progress); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + + expect(mocks.process.spawnDuplexStream).toHaveBeenLastCalledWith( + expect.stringMatching('/bin/psql'), + [ + '--username', + 'mypg', + '--host', + 'myhost', + '--port', + '1234', + '--dbname', + 'myimmich', + '--echo-all', + '--output=/dev/null', + ], + expect.objectContaining({ + env: expect.objectContaining({ + PATH: expect.any(String), + PGPASSWORD: 'mypwd', + }), + }), + ); + + expect(writtenToPsql).toMatchInlineSnapshot(String.raw` + " + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); + + \c postgres + SELECT 1;" + `); + }); + + it('should fail if backup creation fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex()('pg_dump', 1, '', 'error')); + + const progress = vitest.fn(); + await expect(sut.restoreDatabaseBackup('development-filename.sql', progress)).rejects + .toThrowErrorMatchingInlineSnapshot(` + [Error: pg_dump non-zero exit code (1) + error] + `); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + }); + + it('should fail if restore itself fails', async () => { + mocks.process.spawnDuplexStream + .mockReturnValueOnce(mockDuplex()('pg_dump', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex()('gzip', 0, 'data', '')) + .mockReturnValueOnce(mockDuplex()('psql', 1, '', 'error')); + + const progress = vitest.fn(); + await expect(sut.restoreDatabaseBackup('development-filename.sql', progress)).rejects + .toThrowErrorMatchingInlineSnapshot(` + [Error: psql non-zero exit code (1) + error] + `); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + }); + + it('should rollback if database migrations fail', async () => { + mocks.database.runMigrations.mockRejectedValue(new Error('Migrations Error')); + + const progress = vitest.fn(); + await expect( + sut.restoreDatabaseBackup('development-filename.sql', progress), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Migrations Error]`); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalledTimes(0); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + + it('should rollback if API healthcheck fails', async () => { + maintenanceHealthRepositoryMock.checkApiHealth.mockRejectedValue(new Error('Health Error')); + + const progress = vitest.fn(); + await expect( + sut.restoreDatabaseBackup('development-filename.sql', progress), + ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Health Error]`); + + expect(progress).toHaveBeenCalledWith('backup', 0.05); + expect(progress).toHaveBeenCalledWith('migrations', 0.9); + expect(progress).toHaveBeenCalledWith('rollback', 0); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + }); +}); + +function* mockData() { + yield 'SELECT 1;'; +} diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts new file mode 100644 index 0000000000..3c964c950c --- /dev/null +++ b/server/src/services/database-backup.service.ts @@ -0,0 +1,561 @@ +import { BadRequestException, Injectable, Optional } from '@nestjs/common'; +import { debounce } from 'lodash'; +import { DateTime } from 'luxon'; +import path, { basename } from 'node:path'; +import { PassThrough, Readable, Writable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import semver from 'semver'; +import { serverVersion } from 'src/constants'; +import { StorageCore } from 'src/cores/storage.core'; +import { OnEvent, OnJob } from 'src/decorators'; +import { DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { CacheControl, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { CronRepository } from 'src/repositories/cron.repository'; +import { DatabaseRepository } from 'src/repositories/database.repository'; +import { ArgOf } from 'src/repositories/event.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { getConfig } from 'src/utils/config'; +import { + findDatabaseBackupVersion, + isFailedDatabaseBackupName, + isValidDatabaseBackupName, + isValidDatabaseRoutineBackupName, + UnsupportedPostgresError, +} from 'src/utils/database-backups'; +import { ImmichFileResponse } from 'src/utils/file'; +import { handlePromiseError } from 'src/utils/misc'; + +@Injectable() +export class DatabaseBackupService { + constructor( + private readonly logger: LoggingRepository, + private readonly storageRepository: StorageRepository, + private readonly configRepository: ConfigRepository, + private readonly systemMetadataRepository: SystemMetadataRepository, + private readonly processRepository: ProcessRepository, + private readonly databaseRepository: DatabaseRepository, + @Optional() + private readonly cronRepository: CronRepository, + @Optional() + private readonly jobRepository: JobRepository, + @Optional() + private readonly maintenanceHealthRepository: MaintenanceHealthRepository, + ) { + this.logger.setContext(this.constructor.name); + } + + private backupLock = false; + + @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] }) + async onConfigInit({ + newConfig: { + backup: { database }, + }, + }: ArgOf<'ConfigInit'>) { + if (!this.cronRepository || !this.jobRepository) { + return; + } + + this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase); + + if (this.backupLock) { + this.cronRepository.create({ + name: 'backupDatabase', + expression: database.cronExpression, + onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.DatabaseBackup }), this.logger), + start: database.enabled, + }); + } + } + + @OnEvent({ name: 'ConfigUpdate', server: true }) + onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) { + if (!this.cronRepository || !this.jobRepository || !this.backupLock) { + return; + } + + this.cronRepository.update({ + name: 'backupDatabase', + expression: backup.database.cronExpression, + start: backup.database.enabled, + }); + } + + @OnJob({ name: JobName.DatabaseBackup, queue: QueueName.BackupDatabase }) + async handleBackupDatabase(): Promise { + try { + await this.createDatabaseBackup(); + } catch (error) { + if (error instanceof UnsupportedPostgresError) { + return JobStatus.Failed; + } + + throw error; + } + + await this.cleanupDatabaseBackups(); + return JobStatus.Success; + } + + async buildPostgresLaunchArguments( + bin: 'pg_dump' | 'pg_dumpall' | 'psql', + options: { + singleTransaction?: boolean; + } = {}, + ): Promise<{ + bin: string; + args: string[]; + databaseUsername: string; + databasePassword: string; + databaseVersion: string; + databaseMajorVersion?: number; + }> { + const { + database: { config: databaseConfig }, + } = this.configRepository.getEnv(); + const isUrlConnection = databaseConfig.connectionType === 'url'; + + const databaseVersion = await this.databaseRepository.getPostgresVersion(); + const databaseSemver = semver.coerce(databaseVersion); + const databaseMajorVersion = databaseSemver?.major; + + const args: string[] = []; + let databaseUsername; + + if (isUrlConnection) { + if (bin !== 'pg_dump') { + args.push('--dbname'); + } + + let url = databaseConfig.url; + if (URL.canParse(databaseConfig.url)) { + const parsedUrl = new URL(databaseConfig.url); + // remove known bad parameters + parsedUrl.searchParams.delete('uselibpqcompat'); + + databaseUsername = parsedUrl.username || parsedUrl.searchParams.get('user'); + + url = parsedUrl.toString(); + } + + // assume typical values if we can't parse URL or not present + databaseUsername ??= 'postgres'; + + args.push(url); + } else { + databaseUsername = databaseConfig.username; + + args.push( + '--username', + databaseUsername, + '--host', + databaseConfig.host, + '--port', + databaseConfig.port.toString(), + ); + + switch (bin) { + case 'pg_dumpall': { + args.push('--database'); + break; + } + case 'psql': { + args.push('--dbname'); + break; + } + } + + args.push(databaseConfig.database); + } + + switch (bin) { + case 'pg_dump': + case 'pg_dumpall': { + args.push('--clean', '--if-exists'); + break; + } + case 'psql': { + if (options.singleTransaction) { + args.push( + // don't commit any transaction on failure + '--single-transaction', + // exit with non-zero code on error + '--set', + 'ON_ERROR_STOP=on', + ); + } + + args.push( + // used for progress monitoring + '--echo-all', + '--output=/dev/null', + ); + break; + } + } + + if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <19.0.0')) { + this.logger.error(`Database Restore Failure: Unsupported PostgreSQL version: ${databaseVersion}`); + throw new UnsupportedPostgresError(databaseVersion); + } + + return { + bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, + args, + databaseUsername, + databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, + databaseVersion, + databaseMajorVersion, + }; + } + + async createDatabaseBackup(filenamePrefix: string = ''): Promise { + this.logger.debug(`Database Backup Started`); + + const { bin, args, databasePassword, databaseVersion, databaseMajorVersion } = + await this.buildPostgresLaunchArguments('pg_dump'); + + this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); + + const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`; + const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + const temporaryFilePath = `${backupFilePath}.tmp`; + + try { + const pgdump = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const gzip = this.processRepository.spawnDuplexStream('gzip', ['--rsyncable']); + const fileStream = this.storageRepository.createWriteStream(temporaryFilePath); + + await pipeline(pgdump, gzip, fileStream); + await this.storageRepository.rename(temporaryFilePath, backupFilePath); + } catch (error) { + this.logger.error(`Database Backup Failure: ${error}`); + await this.storageRepository + .unlink(temporaryFilePath) + .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); + throw error; + } + + this.logger.log(`Database Backup Success`); + return backupFilePath; + } + + async uploadBackup(file: Express.Multer.File): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const fn = basename(file.originalname); + if (!isValidDatabaseBackupName(fn)) { + throw new BadRequestException('Invalid backup name!'); + } + + const filePath = path.join(backupsFolder, `uploaded-${fn}`); + await this.storageRepository.createOrOverwriteFile(filePath, file.buffer); + } + + downloadBackup(fileName: string): ImmichFileResponse { + if (!isValidDatabaseBackupName(fileName)) { + throw new BadRequestException('Invalid backup name!'); + } + + const filePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName); + + return { + path: filePath, + fileName, + cacheControl: CacheControl.PrivateWithoutCache, + contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', + }; + } + + async listBackups(): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await this.storageRepository.readdir(backupsFolder); + + const validFiles = files + .filter((fn) => isValidDatabaseBackupName(fn)) + .toSorted((a, b) => (a.startsWith('uploaded-') === b.startsWith('uploaded-') ? a.localeCompare(b) : 1)) + .toReversed(); + + const backups = await Promise.all( + validFiles.map(async (filename) => { + const stats = await this.storageRepository.stat(path.join(backupsFolder, filename)); + return { filename, filesize: stats.size }; + }), + ); + + return { + backups, + }; + } + + async deleteBackup(files: string[]): Promise { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + + if (files.some((filename) => !isValidDatabaseBackupName(filename))) { + throw new BadRequestException('Invalid backup name!'); + } + + await Promise.all(files.map((filename) => this.storageRepository.unlink(path.join(backupsFolder, filename)))); + } + + async cleanupDatabaseBackups() { + this.logger.debug(`Database Backup Cleanup Started`); + const { + backup: { database: config }, + } = await getConfig( + { + configRepo: this.configRepository, + metadataRepo: this.systemMetadataRepository, + logger: this.logger, + }, + { + withCache: false, + }, + ); + + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await this.storageRepository.readdir(backupsFolder); + const backups = files + .filter((filename) => isValidDatabaseRoutineBackupName(filename)) + .toSorted() + .toReversed(); + const failedBackups = files.filter((filename) => isFailedDatabaseBackupName(filename)); + + const toDelete = backups.slice(config.keepLastAmount); + toDelete.push(...failedBackups); + + for (const file of toDelete) { + await this.storageRepository.unlink(path.join(backupsFolder, file)); + } + + this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); + } + + async restoreDatabaseBackup( + filename: string, + progressCb?: (action: 'backup' | 'restore' | 'migrations' | 'rollback', progress: number) => void, + ): Promise { + this.logger.debug(`Database Restore Started`); + + let complete = false; + try { + if (!isValidDatabaseBackupName(filename)) { + throw new Error('Invalid backup file format!'); + } + + const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + await this.storageRepository.stat(backupFilePath); // => check file exists + + let isPgClusterDump = false; + const version = findDatabaseBackupVersion(filename); + if (version && semver.satisfies(version, '<= 2.4')) { + isPgClusterDump = true; + } + + const { bin, args, databaseUsername, databasePassword, databaseMajorVersion } = + await this.buildPostgresLaunchArguments('psql', { + singleTransaction: !isPgClusterDump, + }); + + progressCb?.('backup', 0.05); + + const restorePointFilePath = await this.createDatabaseBackup('restore-point-'); + + this.logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`); + + let inputStream: Readable; + if (backupFilePath.endsWith('.gz')) { + const fileStream = this.storageRepository.createPlainReadStream(backupFilePath); + const gunzip = this.storageRepository.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + } else { + inputStream = this.storageRepository.createPlainReadStream(backupFilePath); + } + + const sqlStream = Readable.from(sql(inputStream, databaseUsername, isPgClusterDump)); + const psql = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + this.logger.log(`Restore progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('restore', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + try { + progressCb?.('migrations', 0.9); + await this.databaseRepository.runMigrations(); + await this.maintenanceHealthRepository.checkApiHealth(); + } catch (error) { + progressCb?.('rollback', 0); + + const fileStream = this.storageRepository.createPlainReadStream(restorePointFilePath); + const gunzip = this.storageRepository.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + + const sqlStream = Readable.from(sqlRollback(inputStream, databaseUsername)); + const psql = this.processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + this.logger.log(`Rollback progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('rollback', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + throw error; + } + } catch (error) { + this.logger.error(`Database Restore Failure: ${error}`); + throw error; + } finally { + complete = true; + } + + this.logger.log(`Database Restore Success`); + } +} + +const SQL_DROP_CONNECTIONS = ` + -- drop all other database connections + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = current_database() + AND pid <> pg_backend_pid(); +`; + +const SQL_RESET_SCHEMA = (username: string) => ` + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO "${username}"; + GRANT ALL ON SCHEMA public TO public; +`; + +async function* sql(inputStream: Readable, databaseUsername: string, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + yield isPgClusterDump + ? // it is likely the dump contains SQL to try to drop the currently active + // database to ensure we have a fresh slate; if the `postgres` database exists + // then prefer to switch before continuing otherwise this will just silently fail + String.raw` + \c postgres + ` + : SQL_RESET_SCHEMA(databaseUsername); + + for await (const chunk of inputStream) { + yield chunk; + } +} + +async function* sqlRollback(inputStream: Readable, databaseUsername: string) { + yield SQL_DROP_CONNECTIONS; + yield SQL_RESET_SCHEMA(databaseUsername); + + for await (const chunk of inputStream) { + yield chunk; + } +} + +function createSqlProgressStreams(cb: (progress: number) => void) { + const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); + const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); + + let readingStdin = false; + let sequenceIdx = 0; + + let linesSent = 0; + let linesProcessed = 0; + + const startedAt = +Date.now(); + const cbDebounced = debounce( + () => { + const progress = source.writableEnded + ? Math.min(1, linesProcessed / linesSent) + : // progress simulation while we're in an indeterminate state + Math.min(0.3, 0.1 + (Date.now() - startedAt) / 1e4); + cb(progress); + }, + 100, + { + maxWait: 100, + }, + ); + + let lastByte = -1; + const source = new PassThrough({ + transform(chunk, _encoding, callback) { + for (const byte of chunk) { + if (!readingStdin && byte === 10 && lastByte !== 10) { + linesSent += 1; + } + + lastByte = byte; + + const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; + if (sequence[sequenceIdx] === byte) { + sequenceIdx += 1; + + if (sequence.length === sequenceIdx) { + sequenceIdx = 0; + readingStdin = !readingStdin; + } + } else { + sequenceIdx = 0; + } + } + + cbDebounced(); + this.push(chunk); + callback(); + }, + }); + + const sink = new Writable({ + write(chunk, _encoding, callback) { + for (const byte of chunk) { + if (byte === 10) { + linesProcessed++; + } + } + + cbDebounced(); + callback(); + }, + }); + + return [source, sink]; +} diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index e30722d3d7..bae3a705a4 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -21,6 +21,11 @@ describe(DatabaseService.name, () => { extensionRange = '0.2.x'; mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VectorChord); mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange); + mocks.database.getSchemaDrift.mockResolvedValue({ + items: [], + asSql: () => [], + asHuman: () => [], + }); versionBelowRange = '0.1.0'; minVersionInRange = '0.2.0'; diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index 2ff0e0ca27..1b2289e6e3 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import semver from 'semver'; -import { EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants'; +import { ErrorMessages, EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants'; import { OnEvent } from 'src/decorators'; import { BootstrapEventPriority, DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -124,6 +124,17 @@ export class DatabaseService extends BaseService { const { database } = this.configRepository.getEnv(); if (!database.skipMigrations) { await this.databaseRepository.runMigrations(); + + this.logger.log('Checking for schema drift'); + const drift = await this.databaseRepository.getSchemaDrift(); + if (drift.items.length === 0) { + this.logger.log('No schema drift detected'); + } else { + this.logger.warn(`${ErrorMessages.SchemaDrift} or run \`immich-admin schema-check\``); + for (const warning of drift.asHuman()) { + this.logger.warn(` - ${warning}`); + } + } } await Promise.all([ this.databaseRepository.prewarm(VectorIndex.Clip), diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 86d0bda7f8..1ae1b0b4d8 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common'; import { Readable } from 'node:stream'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { DownloadService } from 'src/services/download.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { vitest } from 'vitest'; @@ -36,21 +36,18 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset = AssetFactory.create(); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.noResizePath, id: 'asset-1' }]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id, 'unknown-asset'])); + mocks.asset.getForOriginals.mockResolvedValue([asset]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id, 'unknown-asset'] })).resolves.toEqual({ stream: archiveMock.stream, }); expect(archiveMock.addFile).toHaveBeenCalledTimes(1); - expect(archiveMock.addFile).toHaveBeenNthCalledWith( - 1, - expect.stringContaining('/data/library/IMG_123.jpg'), - 'IMG_123.jpg', - ); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset.originalPath, asset.originalFileName); }); it('should log a warning if the original path could not be resolved', async () => { @@ -60,22 +57,22 @@ describe(DownloadService.name, () => { stream: new Readable(), }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); + const asset1 = AssetFactory.create(); + const asset2 = AssetFactory.create(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); mocks.storage.realpath.mockRejectedValue(new Error('Could not read file')); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1' }, - { ...assetStub.noWebpPath, id: 'asset-2' }, - ]); + mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); expect(mocks.logger.warn).toHaveBeenCalledTimes(2); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName); }); it('should download an archive', async () => { @@ -85,20 +82,20 @@ describe(DownloadService.name, () => { stream: new Readable(), }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1' }, - { ...assetStub.noWebpPath, id: 'asset-2' }, - ]); + const asset1 = AssetFactory.create(); + const asset2 = AssetFactory.create(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); + mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); expect(archiveMock.addFile).toHaveBeenCalledTimes(2); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, '/data/library/IMG_123.jpg', 'IMG_123.jpg'); - expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, '/data/library/IMG_456.jpg', 'IMG_456.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, asset1.originalPath, asset1.originalFileName); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, asset2.originalPath, asset2.originalFileName); }); it('should handle duplicate file names', async () => { @@ -107,15 +104,14 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); + const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1' }, - { ...assetStub.noResizePath, id: 'asset-2' }, - ]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); + mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); @@ -130,15 +126,14 @@ describe(DownloadService.name, () => { finalize: vitest.fn(), stream: new Readable(), }; + const asset1 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); + const asset2 = AssetFactory.create({ originalFileName: 'IMG_123.jpg' }); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-2' }, - { ...assetStub.noResizePath, id: 'asset-1' }, - ]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id])); + mocks.asset.getForOriginals.mockResolvedValue([asset1, asset2]); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset1.id, asset2.id] })).resolves.toEqual({ stream: archiveMock.stream, }); @@ -154,18 +149,17 @@ describe(DownloadService.name, () => { stream: new Readable(), }; - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getByIds.mockResolvedValue([ - { ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' }, - ]); + const asset = AssetFactory.create({ originalPath: '/path/to/symlink.jpg' }); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.asset.getForOriginals.mockResolvedValue([asset]); mocks.storage.realpath.mockResolvedValue('/path/to/realpath.jpg'); mocks.storage.createZipStream.mockReturnValue(archiveMock); - await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({ + await expect(sut.downloadArchive(authStub.admin, { assetIds: [asset.id] })).resolves.toEqual({ stream: archiveMock.stream, }); - expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', asset.originalFileName); }); }); diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index a5f734e59c..8d939e9635 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,9 +1,8 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; +import { DownloadArchiveDto, DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { Permission } from 'src/enum'; import { ImmichReadStream } from 'src/repositories/storage.repository'; import { BaseService } from 'src/services/base.service'; @@ -80,11 +79,11 @@ export class DownloadService extends BaseService { return { totalSize, archives }; } - async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { + async downloadArchive(auth: AuthDto, dto: DownloadArchiveDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); - const assets = await this.assetRepository.getByIds(dto.assetIds); + const assets = await this.assetRepository.getForOriginals(dto.assetIds, dto.edited ?? false); const assetMap = new Map(assets.map((asset) => [asset.id, asset])); const paths: Record = {}; @@ -94,7 +93,7 @@ export class DownloadService extends BaseService { continue; } - const { originalPath, originalFileName } = asset; + const { originalPath, editedPath, originalFileName } = asset; let filename = originalFileName; const count = paths[filename] || 0; @@ -104,9 +103,10 @@ export class DownloadService extends BaseService { filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } - let realpath = originalPath; + let realpath = dto.edited && editedPath ? editedPath : originalPath; + try { - realpath = await this.storageRepository.realpath(originalPath); + realpath = await this.storageRepository.realpath(realpath); } catch { this.logger.warn('Unable to resolve realpath', { originalPath }); } diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index e5ac9f82ba..0b216e8b8a 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -1,8 +1,9 @@ import { AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { DuplicateService } from 'src/services/duplicate.service'; import { SearchService } from 'src/services/search.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; +import { newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; @@ -38,19 +39,17 @@ describe(SearchService.name, () => { describe('getDuplicates', () => { it('should get duplicates', async () => { + const asset = AssetFactory.create(); mocks.duplicateRepository.getAll.mockResolvedValue([ { duplicateId: 'duplicate-id', - assets: [assetStub.image, assetStub.image], + assets: [asset, asset], }, ]); await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([ { duplicateId: 'duplicate-id', - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image.id }), - ], + assets: [expect.objectContaining({ id: asset.id }), expect.objectContaining({ id: asset.id })], }, ]); }); @@ -101,7 +100,8 @@ describe(SearchService.name, () => { }); it('should queue missing assets', async () => { - mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset])); await sut.handleQueueSearchDuplicates({}); @@ -109,13 +109,14 @@ describe(SearchService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectDuplicates, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue all assets', async () => { - mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSearchDuplicates.mockReturnValue(makeStream([asset])); await sut.handleQueueSearchDuplicates({ force: true }); @@ -123,7 +124,7 @@ describe(SearchService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectDuplicates, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); @@ -150,9 +151,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -167,9 +166,7 @@ describe(SearchService.name, () => { }, }, }); - const id = assetStub.livePhotoMotionAsset.id; - - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: newUuid() }); expect(result).toBe(JobStatus.Skipped); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); @@ -178,51 +175,49 @@ describe(SearchService.name, () => { it('should fail if asset is not found', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(void 0); - const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); + const asset = AssetFactory.create(); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Failed); - expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); + expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${asset.id} not found`); }); it('should skip if asset is part of stack', async () => { - const id = assetStub.primaryImage.id; - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: 'stack-id' }); + const asset = AssetFactory.from().stack().build(); + mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, stackId: asset.stackId }); - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Skipped); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is part of a stack, skipping`); }); it('should skip if asset is not visible', async () => { - const id = assetStub.livePhotoMotionAsset.id; - mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ - ...hasEmbedding, - visibility: AssetVisibility.Hidden, - }); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, ...asset }); - const result = await sut.handleSearchDuplicates({ id }); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Skipped); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is not visible, skipping`); }); it('should fail if asset is missing embedding', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, embedding: null }); - const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); + const asset = AssetFactory.create(); + const result = await sut.handleSearchDuplicates({ id: asset.id }); expect(result).toBe(JobStatus.Failed); - expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${asset.id} is missing embedding`); }); it('should search for duplicates and update asset with duplicateId', async () => { mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue(hasEmbedding); - mocks.duplicateRepository.search.mockResolvedValue([ - { assetId: assetStub.image.id, distance: 0.01, duplicateId: null }, - ]); + const asset = AssetFactory.create(); + mocks.duplicateRepository.search.mockResolvedValue([{ assetId: asset.id, distance: 0.01, duplicateId: null }]); mocks.duplicateRepository.merge.mockResolvedValue(); - const expectedAssetIds = [assetStub.image.id, hasEmbedding.id]; + const expectedAssetIds = [asset.id, hasEmbedding.id]; const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); diff --git a/server/src/services/index.ts b/server/src/services/index.ts index eeb8424048..ba54474b71 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -7,8 +7,8 @@ import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; -import { BackupService } from 'src/services/backup.service'; import { CliService } from 'src/services/cli.service'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; import { DuplicateService } from 'src/services/duplicate.service'; @@ -57,8 +57,8 @@ export const services = [ AuditService, AuthService, AuthAdminService, - BackupService, CliService, + DatabaseBackupService, DatabaseService, DownloadService, DuplicateService, diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index c23b4f05df..a464c9e174 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -1,7 +1,8 @@ -import { ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; +import { AssetType, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { JobService } from 'src/services/job.service'; import { JobItem } from 'src/types'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(JobService.name, () => { @@ -55,22 +56,22 @@ describe(JobService.name, () => { { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [assetStub.image], + stub: [AssetFactory.create({ id: 'asset-1' })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1' } }, jobs: [], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr], - stub: [assetStub.livePhotoStillAsset], + stub: [AssetFactory.create({ id: 'asset-1', livePhotoVideoId: newUuid() })], }, { item: { name: JobName.AssetGenerateThumbnails, data: { id: 'asset-1', source: 'upload' } }, jobs: [JobName.SmartSearch, JobName.AssetDetectFaces, JobName.Ocr, JobName.AssetEncodeVideo], - stub: [assetStub.video], + stub: [AssetFactory.create({ id: 'asset-1', type: AssetType.Video })], }, { item: { name: JobName.SmartSearch, data: { id: 'asset-1' } }, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index b57a203788..7c9581ff9a 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -96,6 +96,40 @@ export class JobService extends BaseService { break; } + case JobName.AssetEditThumbnailGeneration: { + const asset = await this.assetRepository.getById(item.data.id); + const edits = await this.assetEditRepository.getWithSyncInfo(item.data.id); + + if (asset) { + this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, { + asset: { + id: asset.id, + ownerId: asset.ownerId, + originalFileName: asset.originalFileName, + thumbhash: asset.thumbhash ? hexOrBufferToBase64(asset.thumbhash) : null, + checksum: hexOrBufferToBase64(asset.checksum), + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.localDateTime, + duration: asset.duration, + type: asset.type, + deletedAt: asset.deletedAt, + isFavorite: asset.isFavorite, + visibility: asset.visibility, + livePhotoVideoId: asset.livePhotoVideoId, + stackId: asset.stackId, + libraryId: asset.libraryId, + width: asset.width, + height: asset.height, + isEdited: asset.isEdited, + }, + edit: edits, + }); + } + + break; + } + case JobName.AssetGenerateThumbnails: { if (!item.data.notify && item.data.source !== 'upload') { break; @@ -141,6 +175,9 @@ export class JobService extends BaseService { livePhotoVideoId: asset.livePhotoVideoId, stackId: asset.stackId, libraryId: asset.libraryId, + width: asset.width, + height: asset.height, + isEdited: asset.isEdited, }, exif: { assetId: exif.assetId, diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index dbff1ca467..d0c2d0a785 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -6,11 +6,11 @@ import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; import { ILibraryBulkIdsJob, ILibraryFileJob } from 'src/types'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeMockWatcher } from 'test/repositories/storage.repository.mock'; -import { factory, newUuid } from 'test/small.factory'; +import { factory, newDate, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { vitest } from 'vitest'; @@ -306,13 +306,13 @@ describe(LibraryService.name, () => { it('should queue asset sync', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); mocks.storage.walk.mockImplementation(async function* generator() {}); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); + mocks.library.streamAssetIds.mockReturnValue(makeStream([asset])); mocks.asset.getLibraryAssetCount.mockResolvedValue(1); mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n }); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external])); const response = await sut.handleQueueSyncAssets({ id: library.id }); @@ -322,7 +322,7 @@ describe(LibraryService.name, () => { libraryId: library.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns, - assetIds: [assetStub.external.id], + assetIds: [asset.id], progressCounter: 1, totalAssets: 1, }, @@ -343,8 +343,9 @@ describe(LibraryService.name, () => { describe('handleSyncAssets', () => { it('should offline assets no longer on disk', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -352,20 +353,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should set assets deleted from disk as offline', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -373,20 +375,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: true, deletedAt: expect.anything(), }); }); it('should do nothing with offline assets deleted from disk', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/data/user2'], exclusionPatterns: [], @@ -394,7 +397,7 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockRejectedValue(new Error('Could not read file')); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -403,8 +406,9 @@ describe(LibraryService.name, () => { }); it('should un-trash an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: [], @@ -412,20 +416,21 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); - expect(mocks.asset.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + expect(mocks.asset.updateAll).toHaveBeenCalledWith([asset.id], { isOffline: false, deletedAt: null, }); }); it('should do nothing with offline asset if covered by exclusion pattern', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/original/'], exclusionPatterns: ['**/path.jpg'], @@ -433,8 +438,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -444,8 +449,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with offline asset if not in import path', async () => { + const asset = AssetFactory.create({ originalPath: '/original/path.jpg', isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/import/'], exclusionPatterns: [], @@ -453,8 +459,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -464,8 +470,9 @@ describe(LibraryService.name, () => { }); it('should do nothing with unchanged online assets', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -473,8 +480,8 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: asset.fileModifiedAt } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -482,8 +489,9 @@ describe(LibraryService.name, () => { }); it('should not touch fileCreatedAt when un-trashing an asset previously marked as offline', async () => { + const asset = AssetFactory.create({ isOffline: true, deletedAt: newDate() }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.trashedOffline.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -491,13 +499,13 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.trashedOffline]); - mocks.storage.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); + mocks.storage.stat.mockResolvedValue({ mtime: newDate() } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); expect(mocks.asset.updateAll).toHaveBeenCalledWith( - [assetStub.trashedOffline.id], + [asset.id], expect.not.objectContaining({ fileCreatedAt: expect.anything(), }), @@ -505,8 +513,9 @@ describe(LibraryService.name, () => { }); it('should update with online assets that have changed', async () => { + const asset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); const mockAssetJob: ILibraryBulkIdsJob = { - assetIds: [assetStub.external.id], + assetIds: [asset.id], libraryId: newUuid(), importPaths: ['/'], exclusionPatterns: [], @@ -514,13 +523,9 @@ describe(LibraryService.name, () => { progressCounter: 0, }; - if (assetStub.external.fileModifiedAt == null) { - throw new Error('fileModifiedAt is null'); - } + const mtime = new Date(asset.fileModifiedAt.getDate() + 1); - const mtime = new Date(assetStub.external.fileModifiedAt.getDate() + 1); - - mocks.assetJob.getForSyncAssets.mockResolvedValue([assetStub.external]); + mocks.assetJob.getForSyncAssets.mockResolvedValue([asset]); mocks.storage.stat.mockResolvedValue({ mtime } as Stats); await expect(sut.handleSyncAssets(mockAssetJob)).resolves.toBe(JobStatus.Success); @@ -529,7 +534,7 @@ describe(LibraryService.name, () => { { name: JobName.SidecarCheck, data: { - id: assetStub.external.id, + id: asset.id, source: 'upload', }, }, @@ -548,13 +553,14 @@ describe(LibraryService.name, () => { it('should import a new asset', async () => { const library = factory.library(); + const asset = AssetFactory.create(); const mockLibraryJob: ILibraryFileJob = { libraryId: library.id, paths: ['/data/user1/photo.jpg'], }; - mocks.asset.createAll.mockResolvedValue([assetStub.image]); + mocks.asset.createAll.mockResolvedValue([asset]); mocks.library.get.mockResolvedValue(library); await expect(sut.handleSyncFiles(mockLibraryJob)).resolves.toBe(JobStatus.Success); @@ -575,7 +581,7 @@ describe(LibraryService.name, () => { { name: JobName.SidecarCheck, data: { - id: assetStub.image.id, + id: asset.id, source: 'upload', }, }, @@ -602,7 +608,7 @@ describe(LibraryService.name, () => { it('should delete a library', async () => { const library = factory.library(); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); await sut.delete(library.id); @@ -614,7 +620,7 @@ describe(LibraryService.name, () => { it('should allow an external library to be deleted', async () => { const library = factory.library(); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); await sut.delete(library.id); @@ -630,7 +636,7 @@ describe(LibraryService.name, () => { it('should unwatch an external library when deleted', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); @@ -962,7 +968,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.storage.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); await sut.watchAll(); @@ -981,7 +987,7 @@ describe(LibraryService.name, () => { mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(AssetFactory.create()); mocks.storage.watch.mockImplementation( makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), ); @@ -999,12 +1005,13 @@ describe(LibraryService.name, () => { it('should handle a file unlink event', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create(); mocks.library.get.mockResolvedValue(library); mocks.library.getAll.mockResolvedValue([library]); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset); mocks.storage.watch.mockImplementation( - makeMockWatcher({ items: [{ event: 'unlink', value: assetStub.image.originalPath }] }), + makeMockWatcher({ items: [{ event: 'unlink', value: asset.originalPath }] }), ); await sut.watchAll(); @@ -1013,16 +1020,17 @@ describe(LibraryService.name, () => { name: JobName.LibraryRemoveAsset, data: { libraryId: library.id, - paths: [assetStub.image.originalPath], + paths: [asset.originalPath], }, }); }); it('should handle an error event', async () => { const library = factory.library({ importPaths: ['/foo', '/bar'] }); + const asset = AssetFactory.create({ libraryId: library.id, isExternal: true }); mocks.library.get.mockResolvedValue(library); - mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); + mocks.asset.getByLibraryIdAndOriginalPath.mockResolvedValue(asset); mocks.library.getAll.mockResolvedValue([library]); mocks.storage.watch.mockImplementation( makeMockWatcher({ @@ -1115,7 +1123,7 @@ describe(LibraryService.name, () => { const library = factory.library(); mocks.library.get.mockResolvedValue(library); - mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.image1])); + mocks.library.streamAssetIds.mockReturnValue(makeStream([AssetFactory.create()])); await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.Success); }); diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index cc497a6ea4..e598f1c71d 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/enum'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; import { MaintenanceService } from 'src/services/maintenance.service'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -36,28 +36,96 @@ describe(MaintenanceService.name, () => { }); it('should return true if enabled', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: '' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: '', + action: { action: MaintenanceAction.Start }, + }); await expect(sut.getMaintenanceMode()).resolves.toEqual({ isMaintenanceMode: true, secret: '', + action: { + action: 'start', + }, }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); }); + describe('integrityCheck', () => { + it('generate integrity report', async () => { + mocks.storage.readdir.mockResolvedValue(['.immich', 'file1', 'file2']); + mocks.storage.readFile.mockResolvedValue(undefined as never); + mocks.storage.overwriteFile.mockRejectedValue(undefined as never); + + await expect(sut.detectPriorInstall()).resolves.toMatchInlineSnapshot(` + { + "storage": [ + { + "files": 2, + "folder": "encoded-video", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "library", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "upload", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "profile", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "thumbs", + "readable": true, + "writable": false, + }, + { + "files": 2, + "folder": "backups", + "readable": true, + "writable": false, + }, + ], + } + `); + }); + }); + describe('startMaintenance', () => { it('should set maintenance mode and return a secret', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); - await expect(sut.startMaintenance('admin')).resolves.toMatchObject({ + await expect( + sut.startMaintenance( + { + action: MaintenanceAction.Start, + }, + 'admin', + ), + ).resolves.toMatchObject({ jwt: expect.any(String), }); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret: expect.stringMatching(/^\w{128}$/), + action: { + action: 'start', + }, }); expect(mocks.event.emit).toHaveBeenCalledWith('AppRestart', { @@ -78,7 +146,13 @@ describe(MaintenanceService.name, () => { }); it('should generate a login url with JWT', async () => { - mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: true, secret: 'secret' }); + mocks.systemMetadata.get.mockResolvedValue({ + isMaintenanceMode: true, + secret: 'secret', + action: { + action: MaintenanceAction.Start, + }, + }); await expect( sut.createLoginUrl({ diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index e6808300bc..8e711ef380 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,10 +1,21 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { SystemMetadataKey } from 'src/enum'; +import { + MaintenanceAuthDto, + MaintenanceDetectInstallResponseDto, + MaintenanceStatusResponseDto, + SetMaintenanceModeDto, +} from 'src/dtos/maintenance.dto'; +import { MaintenanceAction, SystemMetadataKey } from 'src/enum'; +import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { MaintenanceModeState } from 'src/types'; -import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance'; +import { + createMaintenanceLoginUrl, + detectPriorInstall, + generateMaintenanceSecret, + signMaintenanceJwt, +} from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -18,9 +29,25 @@ export class MaintenanceService extends BaseService { .then((state) => state ?? { isMaintenanceMode: false }); } - async startMaintenance(username: string): Promise<{ jwt: string }> { + getMaintenanceStatus(): MaintenanceStatusResponseDto { + return { + active: false, + action: MaintenanceAction.End, + }; + } + + detectPriorInstall(): Promise { + return detectPriorInstall(this.storageRepository); + } + + async startMaintenance(action: SetMaintenanceModeDto, username: string): Promise<{ jwt: string }> { const secret = generateMaintenanceSecret(); - await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret }); + await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret, + action, + }); + await this.eventRepository.emit('AppRestart', { isMaintenanceMode: true }); return { @@ -30,8 +57,25 @@ export class MaintenanceService extends BaseService { }; } + async startRestoreFlow(): Promise<{ jwt: string }> { + const adminUser = await this.userRepository.getAdmin(); + if (adminUser) { + throw new BadRequestException('The server already has an admin'); + } + + return this.startMaintenance( + { + action: MaintenanceAction.SelectDatabaseRestore, + }, + 'admin', + ); + } + @OnEvent({ name: 'AppRestart', server: true }) - onRestart(): void { + onRestart(event: ArgOf<'AppRestart'>, ack?: (ok: 'ok') => void): void { + this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`); + + ack?.('ok'); this.appRepository.exitApp(); } diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index 6dc56abf44..d58ae67140 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,7 +1,8 @@ import { MapService } from 'src/services/map.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { userStub } from 'test/fixtures/user.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -15,36 +16,41 @@ describe(MapService.name, () => { describe('getMapMarkers', () => { it('should get geo information of assets', async () => { - const asset = assetStub.withLocation; + const auth = AuthFactory.create(); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([]); mocks.map.getMapMarkers.mockResolvedValue([marker]); - const markers = await sut.getMapMarkers(authStub.user1, {}); + const markers = await sut.getMapMarkers(auth, {}); expect(markers).toHaveLength(1); expect(markers[0]).toEqual(marker); }); it('should include partner assets', async () => { - const partner = factory.partner(); - const auth = factory.auth({ user: { id: partner.sharedWithId } }); + const auth = AuthFactory.create(); + const partner = factory.partner({ sharedWithId: auth.user.id }); - const asset = assetStub.withLocation; + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([partner]); mocks.map.getMapMarkers.mockResolvedValue([marker]); @@ -61,21 +67,24 @@ describe(MapService.name, () => { }); it('should include assets from shared albums', async () => { - const asset = assetStub.withLocation; + const auth = AuthFactory.create(userStub.user1); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); const marker = { id: asset.id, - lat: asset.exifInfo!.latitude!, - lon: asset.exifInfo!.longitude!, - city: asset.exifInfo!.city, - state: asset.exifInfo!.state, - country: asset.exifInfo!.country, + lat: asset.exifInfo.latitude!, + lon: asset.exifInfo.longitude!, + city: asset.exifInfo.city, + state: asset.exifInfo.state, + country: asset.exifInfo.country, }; mocks.partner.getAll.mockResolvedValue([]); mocks.map.getMapMarkers.mockResolvedValue([marker]); - mocks.album.getOwned.mockResolvedValue([albumStub.empty]); - mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]); + mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]); + mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]); - const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true }); + const markers = await sut.getMapMarkers(auth, { withSharedAlbums: true }); expect(markers).toHaveLength(1); expect(markers[0]).toEqual(marker); diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 8617930534..e9c3775f96 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,10 +1,13 @@ import { OutputInfo } from 'sharp'; import { SystemConfig } from 'src/config'; import { Exif } from 'src/database'; +import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, AssetPathType, + AssetStatus, AssetType, + AssetVisibility, AudioCodec, Colorspace, ExifOrientation, @@ -18,13 +21,19 @@ import { } from 'src/enum'; import { MediaService } from 'src/services/media.service'; import { JobCounts, RawImageInfo } from 'src/types'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { faceStub } from 'test/fixtures/face.stub'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; import { probeStub } from 'test/fixtures/media.stub'; -import { personStub, personThumbnailStub } from 'test/fixtures/person.stub'; +import { personThumbnailStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { factory, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; +const fullsizeBuffer = Buffer.from('embedded image data'); +const rawBuffer = Buffer.from('raw image data'); +const extractedBuffer = Buffer.from('embedded image file'); + describe(MediaService.name, () => { let sut: MediaService; let mocks: ServiceMocks; @@ -37,19 +46,23 @@ describe(MediaService.name, () => { expect(sut).toBeDefined(); }); + // TODO these should all become medium tests of either the service or the repository. + // The entire logic of what to queue lives in the SQL query now describe('handleQueueGenerateThumbnails', () => { it('should queue all assets', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + const person = PersonFactory.create({ faceAssetId: newUuid() }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); - mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail])); + mocks.person.getAll.mockReturnValue(makeStream([person])); await sut.handleQueueGenerateThumbnails({ force: true }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: true, fullsizeEnabled: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); @@ -57,49 +70,56 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); it('should queue trashed assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ status: AssetStatus.Trashed, deletedAt: new Date() }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: true, fullsizeEnabled: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.trashed.id }, + data: { id: asset.id }, }, ]); }); it('should queue archived assets when force is true', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.archived])); + const asset = AssetFactory.create({ visibility: AssetVisibility.Archive }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: true }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: true, fullsizeEnabled: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.archived.id }, + data: { id: asset.id }, }, ]); }); it('should queue all people with missing thumbnail path', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.image])); - mocks.person.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail])); - mocks.person.getRandomFace.mockResolvedValueOnce(faceStub.face1); + const [person1, person2] = [ + PersonFactory.create({ thumbnailPath: undefined }), + PersonFactory.create({ thumbnailPath: undefined }), + ]; + + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([AssetFactory.create()])); + mocks.person.getAll.mockReturnValue(makeStream([person1, person2])); + mocks.person.getRandomFace.mockResolvedValueOnce(AssetFaceFactory.create()); await sut.handleQueueGenerateThumbnails({ force: false }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); expect(mocks.person.getRandomFace).toHaveBeenCalled(); expect(mocks.person.update).toHaveBeenCalledTimes(1); @@ -107,131 +127,227 @@ describe(MediaService.name, () => { { name: JobName.PersonGenerateThumbnail, data: { - id: personStub.newThumbnail.id, + id: person1.id, }, }, ]); }); it('should queue all assets with missing resize path', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noResizePath])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); - it('should queue all assets with missing webp path', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noWebpPath])); + it('should queue all assets with missing preview', async () => { + const asset = AssetFactory.create(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { - name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, - }, + { name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }, ]); - expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); it('should queue all assets with missing thumbhash', async () => { - mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.noThumbhash])); + const asset = AssetFactory.from({ thumbhash: null }) + .files([AssetFileType.Thumbnail, AssetFileType.Preview]) + .build(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueGenerateThumbnails({ force: false }); - expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); + }); + + it('should queue all assets with missing fullsize when feature is enabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); + const asset = { id: factory.uuid(), isEdited: false }; + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: false }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: true }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetGenerateThumbnails, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); + + it('should not queue assets with missing fullsize when feature is disabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); + const asset = { id: factory.uuid(), isEdited: false }; + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: false }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + + expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); + }); + + it('should queue assets with edits but missing edited thumbnails', async () => { + const asset = AssetFactory.from().edit().build(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: false }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.AssetEditThumbnailGeneration, + data: { id: asset.id }, + }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); + }); + + it('should not queue assets with missing edited fullsize when feature is disabled', async () => { + const asset = AssetFactory.from().edit().build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: false }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + + expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); + }); + + it('should queue assets with missing fullsize when force is true, regardless of setting', async () => { + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); + const asset = { id: factory.uuid(), isEdited: false }; + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: true }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: true, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.AssetGenerateThumbnails, + data: { id: asset.id }, + }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalled(); + }); + + it('should queue both regular and edited thumbnails for assets with edits when force is true', async () => { + const asset = AssetFactory.from().edit().build(); + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: true }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: true, fullsizeEnabled: false }); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.AssetGenerateThumbnails, + data: { id: asset.id }, + }, + { + name: JobName.AssetEditThumbnailGeneration, + data: { id: asset.id }, + }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalledWith(undefined); + }); }); describe('handleQueueMigration', () => { it('should remove empty directories and queue jobs', async () => { - mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + const person = PersonFactory.create(); + + mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([asset])); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts); - mocks.person.getAll.mockReturnValue(makeStream([personStub.withName])); + mocks.person.getAll.mockReturnValue(makeStream([person])); await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success); expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.AssetFileMigration, data: { id: assetStub.image.id } }, - ]); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.PersonFileMigration, data: { id: personStub.withName.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.PersonFileMigration, data: { id: person.id } }]); }); }); describe('handleAssetMigration', () => { it('should fail if asset does not exist', async () => { mocks.assetJob.getForMigrationJob.mockResolvedValue(void 0); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Failed); + await expect(sut.handleAssetMigration({ id: 'non-existent' })).resolves.toBe(JobStatus.Failed); expect(mocks.move.getByEntity).not.toHaveBeenCalled(); }); it('should move asset files', async () => { - mocks.assetJob.getForMigrationJob.mockResolvedValue(assetStub.image); + const asset = AssetFactory.from() + .files([AssetFileType.FullSize, AssetFileType.Preview, AssetFileType.Thumbnail]) + .build(); + mocks.assetJob.getForMigrationJob.mockResolvedValue(asset); mocks.move.create.mockResolvedValue({ - entityId: assetStub.image.id, + entityId: asset.id, id: 'move-id', newPath: '/new/path', oldPath: '/old/path', pathType: AssetPathType.Original, }); - await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleAssetMigration({ id: asset.id })).resolves.toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, - pathType: AssetPathType.FullSize, - oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), + entityId: asset.id, + pathType: AssetFileType.FullSize, + oldPath: asset.files[0].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_fullsize.jpeg`, }); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, - pathType: AssetPathType.Preview, - oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'), + entityId: asset.id, + pathType: AssetFileType.Preview, + oldPath: asset.files[1].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`, }); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: assetStub.image.id, - pathType: AssetPathType.Thumbnail, - oldPath: '/uploads/user-id/webp/path.ext', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'), + entityId: asset.id, + pathType: AssetFileType.Thumbnail, + oldPath: asset.files[2].path, + newPath: `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`, }); expect(mocks.move.create).toHaveBeenCalledTimes(3); }); }); describe('handleGenerateThumbnails', () => { - let rawBuffer: Buffer; - let fullsizeBuffer: Buffer; - let extractedBuffer: Buffer; let rawInfo: RawImageInfo; beforeEach(() => { - fullsizeBuffer = Buffer.from('embedded image data'); - rawBuffer = Buffer.from('raw image data'); - extractedBuffer = Buffer.from('embedded image file'); rawInfo = { width: 100, height: 100, channels: 3 }; + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); mocks.media.decodeImage.mockImplementation((input) => Promise.resolve( typeof input === 'string' @@ -239,65 +355,76 @@ describe(MediaService.name, () => { : { data: fullsizeBuffer, info: rawInfo as OutputInfo }, // buffer implies embedded image extracted ), ); + mocks.media.getImageMetadata.mockResolvedValue({ width: 100, height: 100, isTransparent: false }); }); it('should skip thumbnail generation if asset not found', async () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(void 0); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: 'non-existent' }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip thumbnail generation if asset type is unknown', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ ...assetStub.image, type: 'foo' as AssetType }); + const asset = AssetFactory.create({ type: 'foo' as AssetType }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await expect(sut.handleGenerateThumbnails({ id: assetStub.image.id })).resolves.toBe(JobStatus.Skipped); + await expect(sut.handleGenerateThumbnails({ id: asset.id })).resolves.toBe(JobStatus.Skipped); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip video thumbnail generation if no video stream', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toThrowError(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await expect(sut.handleGenerateThumbnails({ id: asset.id })).rejects.toThrowError(); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should skip invisible assets', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.livePhotoMotionAsset); + const asset = AssetFactory.create({ visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleGenerateThumbnails({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith(); }); it('should delete previous preview if different path', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); - expect(mocks.storage.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining([asset.files[0].path]), + }, + }); }); it('should generate P3 thumbnails for a wide gamut image', async () => { - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ - ...assetStub.image, - exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as Exif, - }); + const asset = AssetFactory.from() + .exif({ profileDescription: 'Adobe RGB', bitsPerSample: 14 }) + .files([AssetFileType.Preview, AssetFileType.Thumbnail]) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -311,8 +438,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -323,8 +452,10 @@ describe(MediaService.name, () => { format: ImageFormat.Webp, size: 250, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -334,27 +465,35 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, processInvalidImages: false, raw: rawInfo, + edits: [], }); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, ]); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, thumbhash: thumbhashBuffer }); }); it('should generate a thumbnail for a video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -374,22 +513,29 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, ]); }); it('should tonemap thumbnail for hdr video', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.transcode).toHaveBeenCalledWith( @@ -409,25 +555,32 @@ describe(MediaService.name, () => { ); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, { - assetId: 'asset-id', + assetId: asset.id, type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, + isTransparent: false, }, ]); }); it('should always generate video thumbnail in one pass', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '5000k' }, }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -447,9 +600,10 @@ describe(MediaService.name, () => { }); it('should not skip intra frames for MTS file', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamMTS); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -464,9 +618,10 @@ describe(MediaService.name, () => { }); it('should override reserved color metadata', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamReserved); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -483,10 +638,11 @@ describe(MediaService.name, () => { }); it('should use scaling divisible by 2 even when using quick sync', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); - await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -500,18 +656,19 @@ describe(MediaService.name, () => { }); it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { preview: { format } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = `/data/thumbs/user-id/as/se/asset-id-preview.${format}`; - const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id-thumbnail.webp`; + const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.${format}`; + const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.webp`; - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -525,8 +682,10 @@ describe(MediaService.name, () => { format, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, previewPath, ); @@ -537,26 +696,29 @@ describe(MediaService.name, () => { format: ImageFormat.Webp, size: 250, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, thumbnailPath, ); }); it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); - const previewPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-preview.jpeg`); - const thumbnailPath = expect.stringContaining(`/data/thumbs/user-id/as/se/asset-id-thumbnail.${format}`); + const previewPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_preview.jpeg`; + const thumbnailPath = `/data/thumbs/${asset.ownerId}/${asset.id.slice(0, 2)}/${asset.id.slice(2, 4)}/${asset.id}_thumbnail.${format}`; - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -570,8 +732,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, previewPath, ); @@ -582,29 +746,142 @@ describe(MediaService.name, () => { format, size: 250, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, thumbnailPath, ); }); + it('should generate progressive JPEG for preview when enabled', async () => { + const asset = AssetFactory.from().exif().build(); + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: true }, thumbnail: { progressive: false } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + format: ImageFormat.Jpeg, + progressive: true, + }), + expect.stringContaining('preview.jpeg'), + ); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + format: ImageFormat.Webp, + progressive: false, + }), + expect.stringContaining('thumbnail.webp'), + ); + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + expect.objectContaining({ + type: AssetFileType.Preview, + isProgressive: true, + isTransparent: false, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: false, + isTransparent: false, + }), + ]); + }); + + it('should generate progressive JPEG for thumbnail when enabled', async () => { + const asset = AssetFactory.from().exif().build(); + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + format: ImageFormat.Jpeg, + progressive: false, + }), + expect.stringContaining('preview.jpeg'), + ); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + format: ImageFormat.Jpeg, + progressive: true, + }), + expect.stringContaining('thumbnail.jpeg'), + ); + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + expect.objectContaining({ + type: AssetFileType.Preview, + isProgressive: false, + isTransparent: false, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: true, + isTransparent: false, + }), + ]); + }); + + it('should never set isProgressive for videos', async () => { + const asset = AssetFactory.create({ type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: true }, thumbnail: { progressive: true } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + expect.objectContaining({ + type: AssetFileType.Preview, + isProgressive: false, + isTransparent: false, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: false, + isTransparent: false, + }), + ]); + }); + it('should delete previous thumbnail if different path', async () => { + const asset = AssetFactory.from().exif().file({ type: AssetFileType.Preview }).build(); mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); - expect(mocks.storage.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining([asset.files[0].path]), + }, + }); }); it('should extract embedded image if enabled and available', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -614,15 +891,45 @@ describe(MediaService.name, () => { }); }); - it('should resize original image if embedded image is too small', async () => { + it('should not check transparency metadata for raw files without extracted images', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.getImageMetadata).not.toHaveBeenCalled(); + }); + + it('should not check transparency metadata for raw files with extracted images', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.getImageMetadata).toHaveBeenCalledOnce(); + expect(mocks.media.getImageMetadata).toHaveBeenCalledWith(extractedBuffer); + }); + + it('should resize original image if embedded image is too small', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, @@ -630,46 +937,53 @@ describe(MediaService.name, () => { }); it('should resize original image if embedded image not found', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, }); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); }); it('should resize original image if embedded image extraction is not enabled', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: false } }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.extract).not.toHaveBeenCalled(); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, size: 1440, }); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); }); it('should process invalid images if enabled', async () => { vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true'); + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, + asset.originalPath, expect.objectContaining({ processInvalidImages: true }), ); @@ -691,19 +1005,22 @@ describe(MediaService.name, () => { expect.objectContaining({ processInvalidImages: false }), ); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); vi.unstubAllEnvs(); }); it('should extract full-size JPEG preview from RAW', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -720,22 +1037,28 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); }); it('should convert full-size WEBP preview from JXL preview of RAW', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true, format: ImageFormat.Webp }, extractEmbedded: true }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jxl }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, { @@ -750,8 +1073,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Webp, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -762,23 +1087,29 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); }); it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => { + const asset = AssetFactory.from({ originalFileName: 'file.dng' }) + .exif({ fileSizeInByte: 5000, profileDescription: 'Adobe RGB', bitsPerSample: 14, orientation: undefined }) + .build(); + mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, }); @@ -790,8 +1121,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -801,9 +1134,11 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, size: 1440, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -812,14 +1147,21 @@ describe(MediaService.name, () => { it('should generate full-size preview from non-web-friendly images', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif); + const asset = AssetFactory.from({ originalFileName: 'image.hif' }) + .exif({ + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + }) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, }); @@ -831,23 +1173,26 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); }); it('should skip generating full-size preview for web-friendly images', async () => { + const asset = AssetFactory.from().exif().build(); mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, processInvalidImages: false, size: 1440, @@ -864,15 +1209,21 @@ describe(MediaService.name, () => { it('should always generate full-size preview from non-web-friendly panoramas', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: false } } }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - mocks.media.copyTagGroup.mockResolvedValue(true); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.panoramaTif); + const asset = AssetFactory.from({ originalFileName: 'panorama.tif' }) + .exif({ + fileSizeInByte: 5000, + projectionType: 'EQUIRECTANGULAR', + }) + .build(); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.panoramaTif.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.Srgb, orientation: undefined, processInvalidImages: false, @@ -886,18 +1237,13 @@ describe(MediaService.name, () => { colorspace: Colorspace.Srgb, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); - - expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2); - expect(mocks.media.copyTagGroup).toHaveBeenCalledWith( - 'XMP-GPano', - assetStub.panoramaTif.originalPath, - expect.any(String), - ); }); it('should respect encoding options when generating full-size preview', async () => { @@ -905,14 +1251,21 @@ describe(MediaService.name, () => { image: { fullsize: { enabled: true, format: ImageFormat.Webp, quality: 90 } }, }); mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); // HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari. - mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif); + const asset = AssetFactory.from({ originalFileName: 'image.hif' }) + .exif({ + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + }) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); - await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: asset.id }); expect(mocks.media.decodeImage).toHaveBeenCalledOnce(); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageHif.originalPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(asset.originalPath, { colorspace: Colorspace.P3, processInvalidImages: false, }); @@ -924,12 +1277,194 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Webp, quality: 90, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); }); + + it('should generate progressive JPEG for fullsize when enabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + image: { fullsize: { enabled: true, format: ImageFormat.Jpeg, progressive: true } }, + }); + mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.Jpeg }); + mocks.media.getImageMetadata.mockResolvedValue({ width: 3840, height: 2160, isTransparent: false }); + const asset = AssetFactory.from({ originalFileName: 'image.hif' }) + .exif({ + fileSizeInByte: 5000, + profileDescription: 'Adobe RGB', + bitsPerSample: 14, + }) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await sut.handleGenerateThumbnails({ id: asset.id }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + format: ImageFormat.Jpeg, + progressive: true, + }), + expect.stringContaining('fullsize.jpeg'), + ); + }); + }); + + describe('handleAssetEditThumbnailGeneration', () => { + let rawInfo: RawImageInfo; + + beforeEach(() => { + rawInfo = { width: 100, height: 100, channels: 3 }; + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + mocks.media.decodeImage.mockImplementation((input) => + Promise.resolve( + typeof input === 'string' + ? { data: rawBuffer, info: rawInfo as OutputInfo } // string implies original file + : { data: fullsizeBuffer, info: rawInfo as OutputInfo }, // buffer implies embedded image extracted + ), + ); + mocks.media.getImageMetadata.mockResolvedValue({ width: 100, height: 100, isTransparent: false }); + }); + + it('should skip videos', async () => { + const asset = AssetFactory.from({ type: AssetType.Video }).exif().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + await expect(sut.handleAssetEditThumbnailGeneration({ id: asset.id })).resolves.toBe(JobStatus.Success); + expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); + }); + + it('should upsert 3 edited files for edit jobs', async () => { + const asset = AssetFactory.from() + .exif() + .edit({ action: AssetEditAction.Crop }) + .files([ + { type: AssetFileType.FullSize, isEdited: true }, + { type: AssetFileType.Preview, isEdited: true }, + { type: AssetFileType.Thumbnail, isEdited: true }, + ]) + .build(); + + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ type: AssetFileType.FullSize, isEdited: true }), + expect.objectContaining({ type: AssetFileType.Preview, isEdited: true }), + expect.objectContaining({ type: AssetFileType.Thumbnail, isEdited: true }), + ]), + ); + }); + + it('should apply edits when generating thumbnails', async () => { + const asset = AssetFactory.from() + .exif() + .edit({ action: AssetEditAction.Crop, parameters: { height: 1152, width: 1512, x: 216, y: 1512 } }) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + edits: [ + expect.objectContaining({ + action: 'crop', + parameters: { height: 1152, width: 1512, x: 216, y: 1512 }, + }), + ], + }), + expect.any(String), + ); + }); + + it('should clean up edited files if an asset has no edits', async () => { + const asset = AssetFactory.from({ thumbhash: factory.buffer() }) + .exif() + .files([ + { type: AssetFileType.Preview, path: 'edited1.jpg', isEdited: true }, + { type: AssetFileType.Thumbnail, path: 'edited2.jpg', isEdited: true }, + { type: AssetFileType.FullSize, path: 'edited3.jpg', isEdited: true }, + ]) + .build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + + const status = await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); + + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining(['edited1.jpg', 'edited2.jpg', 'edited3.jpg']), + }, + }); + + expect(status).toBe(JobStatus.Success); + expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); + expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); + }); + + it('should generate all 3 edited files if an asset has edits', async () => { + const asset = AssetFactory.from().exif().edit().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); + + expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.anything(), + expect.stringContaining('preview_edited.jpeg'), + ); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.anything(), + expect.stringContaining('thumbnail_edited.webp'), + ); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.anything(), + expect.stringContaining('fullsize_edited.jpeg'), + ); + }); + + it('should generate the original thumbhash if no edits exist', async () => { + const asset = AssetFactory.from().exif().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + mocks.media.generateThumbhash.mockResolvedValue(factory.buffer()); + + await sut.handleAssetEditThumbnailGeneration({ id: asset.id, source: 'upload' }); + + expect(mocks.media.generateThumbhash).toHaveBeenCalled(); + }); + + it('should apply thumbhash if job source is edit and edits exist', async () => { + const asset = AssetFactory.from().exif().edit().build(); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(asset); + const thumbhashBuffer = factory.buffer(); + mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: asset.id }); + + expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ thumbhash: thumbhashBuffer })); + }); }); describe('handleGeneratePersonThumbnail', () => { @@ -947,8 +1482,9 @@ describe(MediaService.name, () => { }); it('should skip a person without a face asset id', async () => { - mocks.person.getById.mockResolvedValue(personStub.noThumbnail); - await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); + const person = PersonFactory.create({ faceAssetId: null }); + mocks.person.getById.mockResolvedValue(person); + await sut.handleGeneratePersonThumbnail({ id: person.id }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); @@ -958,17 +1494,17 @@ describe(MediaService.name, () => { }); it('should generate a thumbnail', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.originalPath, { colorspace: Colorspace.P3, @@ -981,35 +1517,41 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 238, - top: 163, - width: 274, - height: 274, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 274, + width: 274, + x: 238, + y: 163, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); }); it('should use preview path if video', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.videoThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); - expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.previewPath, { + expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), { colorspace: Colorspace.P3, orientation: undefined, processInvalidImages: false, @@ -1020,31 +1562,37 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 238, - top: 163, - width: 274, - height: 274, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 274, + width: 274, + x: 238, + y: 163, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); }); it('should generate a thumbnail without going negative', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailStart); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailStart.originalPath, { colorspace: Colorspace.P3, @@ -1057,12 +1605,18 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 0, - top: 85, - width: 510, - height: 510, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 510, + width: 510, + x: 0, + y: 85, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, @@ -1072,16 +1626,16 @@ describe(MediaService.name, () => { }); it('should generate a thumbnail without overflowing', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailEnd); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailEnd.originalPath, { colorspace: Colorspace.P3, @@ -1094,12 +1648,18 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 591, - top: 591, - width: 408, - height: 408, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 408, + width: 408, + x: 591, + y: 591, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, @@ -1109,16 +1669,16 @@ describe(MediaService.name, () => { }); it('should handle negative coordinates', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.negativeCoordinate); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.negativeCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1131,12 +1691,18 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 0, - top: 62, - width: 412, - height: 412, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 412, + width: 412, + x: 0, + y: 62, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, @@ -1146,16 +1712,16 @@ describe(MediaService.name, () => { }); it('should handle overflowing coordinate', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.overflowingCoordinate); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.overflowingCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1168,12 +1734,18 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - left: 4485, - top: 94, - width: 138, - height: 138, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 138, + width: 138, + x: 4485, + y: 94, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, @@ -1183,20 +1755,20 @@ describe(MediaService.name, () => { }); it('should use embedded preview if enabled and raw image', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); - mocks.person.update.mockResolvedValue(personStub.primaryPerson); + mocks.person.update.mockResolvedValue(person); mocks.media.generateThumbnail.mockResolvedValue(); const extracted = Buffer.from(''); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.decodeImage.mockResolvedValue({ data, info }); - mocks.media.getImageDimensions.mockResolvedValue(info); + mocks.media.getImageMetadata.mockResolvedValue({ width: 2160, height: 3840, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extracted, { @@ -1210,12 +1782,18 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, - crop: { - height: 844, - left: 388, - top: 730, - width: 844, - }, + progressive: false, + edits: [ + { + action: 'crop', + parameters: { + height: 844, + width: 844, + x: 388, + y: 730, + }, + }, + ], raw: info, processInvalidImages: false, size: 250, @@ -1225,21 +1803,23 @@ describe(MediaService.name, () => { }); it('should not use embedded preview if enabled and not raw image', async () => { + const person = PersonFactory.create(); + mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle); mocks.media.generateThumbnail.mockResolvedValue(); const data = Buffer.from(''); const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).toHaveBeenCalled(); }); it('should not use embedded preview if enabled and raw image if not exists', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); @@ -1247,9 +1827,7 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { @@ -1261,6 +1839,8 @@ describe(MediaService.name, () => { }); it('should not use embedded preview if enabled and raw image if low resolution', async () => { + const person = PersonFactory.create(); + mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } }); mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail); mocks.media.generateThumbnail.mockResolvedValue(); @@ -1269,11 +1849,9 @@ describe(MediaService.name, () => { const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); - mocks.media.getImageDimensions.mockResolvedValue(info); + mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe( - JobStatus.Success, - ); + await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { @@ -1287,7 +1865,8 @@ describe(MediaService.name, () => { describe('handleQueueVideoConversion', () => { it('should queue all video assets', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); mocks.person.getAll.mockReturnValue(makeStream()); await sut.handleQueueVideoConversion({ force: true }); @@ -1296,13 +1875,14 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); it('should queue all video assets without encoded videos', async () => { - mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video])); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([asset])); await sut.handleQueueVideoConversion({}); @@ -1310,7 +1890,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetEncodeVideo, - data: { id: assetStub.video.id }, + data: { id: asset.id }, }, ]); }); @@ -1318,13 +1898,14 @@ describe(MediaService.name, () => { describe('handleVideoConversion', () => { beforeEach(() => { - mocks.assetJob.getForVideoConversion.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ id: 'video-id', type: AssetType.Video, originalPath: '/original/path.ext' }); + mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); sut.videoInterfaces = { dri: ['renderD128'], mali: true }; }); it('should skip transcoding if asset not found', async () => { mocks.assetJob.getForVideoConversion.mockResolvedValue(void 0); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).not.toHaveBeenCalled(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1333,7 +1914,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1353,7 +1934,7 @@ describe(MediaService.name, () => { mocks.logger.isLevelEnabled.mockReturnValue(false); mocks.media.probe.mockResolvedValue(probeStub.multipleAudioStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); @@ -1371,13 +1952,13 @@ describe(MediaService.name, () => { it('should skip a video without any streams', async () => { mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should skip a video without any height', async () => { mocks.media.probe.mockResolvedValue(probeStub.noHeight); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1385,7 +1966,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1396,14 +1977,14 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video')); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.Failed); + await expect(sut.handleVideoConversion({ id: 'video-id' })).resolves.toBe(JobStatus.Failed); expect(mocks.media.transcode).toHaveBeenCalledTimes(1); }); it('should transcode when set to all', async () => { mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1418,7 +1999,7 @@ describe(MediaService.name, () => { it('should transcode when optimal and too big', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1433,7 +2014,7 @@ describe(MediaService.name, () => { it('should transcode when policy bitrate and bitrate higher than max bitrate', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: '30M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1448,7 +2029,7 @@ describe(MediaService.name, () => { it('should transcode when max bitrate is not a number', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream40Mbps); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Bitrate, maxBitrate: 'foo' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1465,7 +2046,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1480,7 +2061,7 @@ describe(MediaService.name, () => { it('should scale horizontally when video is horizontal', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1495,7 +2076,7 @@ describe(MediaService.name, () => { it('should scale vertically when video is vertical', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1512,7 +2093,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1529,7 +2110,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.All, targetResolution: 'original' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1546,7 +2127,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Hevc, acceptedAudioCodecs: [AudioCodec.Aac] }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1567,7 +2148,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1588,7 +2169,7 @@ describe(MediaService.name, () => { acceptedAudioCodecs: [AudioCodec.Aac], }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1603,7 +2184,7 @@ describe(MediaService.name, () => { it('should copy audio stream when audio matches target', async () => { mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1617,7 +2198,7 @@ describe(MediaService.name, () => { it('should remux when input is not an accepted container', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamAvi); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1633,33 +2214,33 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not remux when input is not an accepted container and transcoding is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should not transcode if target codec is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should delete existing transcode if current policy does not require transcoding', async () => { - const asset = assetStub.hasEncodedVideo; + const asset = AssetFactory.create({ type: AssetType.Video, encodedVideoPath: '/encoded/video/path.mp4' }); mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Disabled } }); mocks.assetJob.getForVideoConversion.mockResolvedValue(asset); @@ -1676,7 +2257,7 @@ describe(MediaService.name, () => { it('should set max bitrate if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1691,7 +2272,7 @@ describe(MediaService.name, () => { it('should default max bitrate to kbps if no unit is provided', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1706,7 +2287,7 @@ describe(MediaService.name, () => { it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1721,7 +2302,7 @@ describe(MediaService.name, () => { it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1742,7 +2323,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1763,7 +2344,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Vp9, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1778,7 +2359,7 @@ describe(MediaService.name, () => { it('should configure preset for vp9', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, preset: 'slow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1793,7 +2374,7 @@ describe(MediaService.name, () => { it('should not configure preset for vp9 if invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.Vp9 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1808,7 +2389,7 @@ describe(MediaService.name, () => { it('should configure threads if above 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Vp9, threads: 2 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1823,7 +2404,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for h264 if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1838,7 +2419,7 @@ describe(MediaService.name, () => { it('should omit thread flags for h264 if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1853,7 +2434,7 @@ describe(MediaService.name, () => { it('should disable thread pooling for hevc if thread limit is 1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1868,7 +2449,7 @@ describe(MediaService.name, () => { it('should omit thread flags for hevc if thread limit is at or below 0', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.Hevc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1883,7 +2464,7 @@ describe(MediaService.name, () => { it('should use av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1908,7 +2489,7 @@ describe(MediaService.name, () => { it('should map `veryslow` preset to 4 for av1', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, preset: 'veryslow' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1923,7 +2504,7 @@ describe(MediaService.name, () => { it('should set max bitrate for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, maxBitrate: '2M' } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1938,7 +2519,7 @@ describe(MediaService.name, () => { it('should set threads for av1 if specified', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4 } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1955,7 +2536,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.Av1, threads: 4, maxBitrate: '2M' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -1976,7 +2557,7 @@ describe(MediaService.name, () => { targetResolution: '1080p', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -1985,21 +2566,21 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, targetVideoCodec: VideoCodec.Vp9 }, }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should fail if hwaccel option is invalid', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); it('should set options for nvenc', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2036,7 +2617,7 @@ describe(MediaService.name, () => { twoPass: true, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2053,7 +2634,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2070,7 +2651,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2087,7 +2668,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2102,7 +2683,7 @@ describe(MediaService.name, () => { it('should ignore two pass for nvenc if max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2119,7 +2700,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2141,7 +2722,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2162,7 +2743,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Nvenc, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2179,7 +2760,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2219,7 +2800,7 @@ describe(MediaService.name, () => { preferredHwDevice: '/dev/dri/renderD128', }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2239,7 +2820,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2259,7 +2840,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, targetVideoCodec: VideoCodec.Vp9 }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2279,7 +2860,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2288,7 +2869,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2309,7 +2890,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2335,7 +2916,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2364,7 +2945,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2382,7 +2963,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Qsv, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2403,7 +2984,7 @@ describe(MediaService.name, () => { it('should set options for vaapi', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2435,7 +3016,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, maxBitrate: '10000k' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2459,7 +3040,7 @@ describe(MediaService.name, () => { it('should set cq options for vaapi when max bitrate is disabled', async () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2485,7 +3066,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preset: 'invalid' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2504,7 +3085,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2525,7 +3106,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, preferredHwDevice: '/dev/dri/renderD128' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2546,7 +3127,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2571,7 +3152,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2594,7 +3175,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', @@ -2614,7 +3195,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true, preferredHwDevice: 'renderD129' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2632,7 +3213,7 @@ describe(MediaService.name, () => { ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi, accelDecode: true }, }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -2655,7 +3236,7 @@ describe(MediaService.name, () => { }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(3); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -2672,7 +3253,7 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); mocks.media.transcode.mockRejectedValueOnce(new Error('error')); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledTimes(2); expect(mocks.media.transcode).toHaveBeenLastCalledWith( '/original/path.ext', @@ -2689,7 +3270,7 @@ describe(MediaService.name, () => { sut.videoInterfaces = { dri: [], mali: true }; mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Vaapi } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + await expect(sut.handleVideoConversion({ id: 'video-id' })).rejects.toThrowError(); expect(mocks.media.transcode).not.toHaveBeenCalled(); }); @@ -2698,7 +3279,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2738,7 +3319,7 @@ describe(MediaService.name, () => { targetVideoCodec: VideoCodec.Hevc, }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2755,7 +3336,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2772,7 +3353,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2794,7 +3375,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2813,7 +3394,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: false, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2835,7 +3416,7 @@ describe(MediaService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHardwareAcceleration.Rkmpp, accelDecode: true, crf: 30, maxBitrate: '0' }, }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2854,7 +3435,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is required and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2873,7 +3454,7 @@ describe(MediaService.name, () => { it('should tonemap when policy is optimal and video is hdr', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2892,7 +3473,7 @@ describe(MediaService.name, () => { it('should transcode when policy is required and video is not yuv420p', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2907,7 +3488,7 @@ describe(MediaService.name, () => { it('should convert to yuv420p when scaling without tone-mapping', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit); mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Required } }); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); expect(mocks.media.transcode).toHaveBeenCalledWith( '/original/path.ext', expect.any(String), @@ -2923,10 +3504,10 @@ describe(MediaService.name, () => { mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); mocks.logger.isLevelEnabled.mockReturnValue(true); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true }); - expect(mocks.media.transcode).toHaveBeenCalledWith(assetStub.video.originalPath, expect.any(String), { + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: true }); + expect(mocks.media.transcode).toHaveBeenCalledWith('/original/path.ext', expect.any(String), { inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, @@ -2940,19 +3521,23 @@ describe(MediaService.name, () => { it('should not count frames for progress when log level is not debug', async () => { mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p); mocks.logger.isLevelEnabled.mockReturnValue(false); - await sut.handleVideoConversion({ id: assetStub.video.id }); + await sut.handleVideoConversion({ id: 'video-id' }); - expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false }); + expect(mocks.media.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); }); it('should process unknown audio stream', async () => { + const asset = AssetFactory.create({ + type: AssetType.Video, + originalPath: '/original/path.ext', + }); mocks.media.probe.mockResolvedValue(probeStub.audioStreamUnknown); - mocks.asset.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); + mocks.asset.getByIds.mockResolvedValue([asset]); + await sut.handleVideoConversion({ id: asset.id }); expect(mocks.media.transcode).toHaveBeenCalledWith( - '/original/path.ext', - '/data/encoded-video/user-id/as/se/asset-id.mp4', + asset.originalPath, + expect.stringContaining('video-id.mp4'), expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:a copy']), @@ -2999,4 +3584,411 @@ describe(MediaService.name, () => { expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true); }); }); + + describe('syncFiles', () => { + it('should upsert new files when they do not exist', async () => { + const asset = { + id: 'asset-id', + files: [], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/new/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/new/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: 'asset-id', + path: '/new/thumbnail.jpg', + type: AssetFileType.Thumbnail, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); + expect(mocks.job.queue).not.toHaveBeenCalled(); + }); + + it('should replace existing files with new paths', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/new/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/new/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: 'asset-id', + path: '/new/thumbnail.jpg', + type: AssetFileType.Thumbnail, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: ['/old/preview.jpg', '/old/thumbnail.jpg'] }, + }); + }); + + it('should delete files when newPath is not provided', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, []); + + expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: ['/old/preview.jpg', '/old/thumbnail.jpg'] }, + }); + }); + + it('should not make changes when file paths already match', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/same/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/same/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/same/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/same/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + + expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); + expect(mocks.job.queue).not.toHaveBeenCalled(); + }); + + it('should handle mixed operations (upsert, replace, delete)', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/new/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, // replace + { + assetId: asset.id, + type: AssetFileType.FullSize, + path: '/new/fullsize.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, // new + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + assetId: 'asset-id', + path: '/new/fullsize.jpg', + type: AssetFileType.FullSize, + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: ['/old/preview.jpg', '/old/thumbnail.jpg'] }, + }); + }); + + it('should handle empty file list', async () => { + const asset = { + id: 'asset-id', + files: [], + }; + + await sut['syncFiles'](asset.files, []); + + expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); + expect(mocks.job.queue).not.toHaveBeenCalled(); + }); + + it('should delete non-existent file types when newPath is not provided', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, []); + + expect(mocks.asset.upsertFiles).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: ['/old/preview.jpg'] }, + }); + }); + + it('should update database when isProgressive changes', async () => { + const asset = { + id: 'asset-id', + files: [ + { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: true, + isTransparent: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + isTransparent: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/old/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: true, + isTransparent: false, + }, + ]); + expect(mocks.asset.deleteFiles).not.toHaveBeenCalled(); + expect(mocks.job.queue).not.toHaveBeenCalled(); + }); + }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index de543b33ba..08c432001f 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,12 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { SystemConfig } from 'src/config'; import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE, TILE_TARGET_SIZE } from 'src/constants'; -import { StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; -import { Exif } from 'src/database'; +import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; +import { AssetFile, Exif } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; +import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetFileType, - AssetPathType, AssetType, AssetVisibility, AudioCodec, @@ -18,18 +19,20 @@ import { QueueName, RawExtractedFormat, StorageFolder, + TilesFormat, TranscodeHardwareAcceleration, TranscodePolicy, TranscodeTarget, VideoCodec, VideoContainer, } from 'src/enum'; +import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { BoundingBox } from 'src/repositories/machine-learning.repository'; import { BaseService } from 'src/services/base.service'; import { AudioStreamInfo, - CropOptions, DecodeToBufferOptions, + GenerateThumbnailOptions, ImageDimensions, JobItem, JobOf, @@ -37,14 +40,20 @@ import { VideoInterfaces, VideoStreamInfo, } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getDimensions } from 'src/utils/asset.util'; +import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; import { clamp, isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc'; +import { getOutputDimensions } from 'src/utils/transform'; + interface UpsertFileOptions { assetId: string; type: AssetFileType; path: string; + isEdited: boolean; + isProgressive: boolean; + isTransparent: boolean; } interface TileInfo { @@ -56,6 +65,8 @@ interface TileInfo { }; } +type ThumbnailAsset = NonNullable>>; + @Injectable() export class MediaService extends BaseService { videoInterfaces: VideoInterfaces = { dri: [], mali: false }; @@ -68,6 +79,7 @@ export class MediaService extends BaseService { @OnJob({ name: JobName.AssetGenerateThumbnailsQueueAll, queue: QueueName.ThumbnailGeneration }) async handleQueueGenerateThumbnails({ force }: JobOf): Promise { + const config = await this.getConfig({ withCache: true }); let jobs: JobItem[] = []; const queueAll = async () => { @@ -75,13 +87,16 @@ export class MediaService extends BaseService { jobs = []; }; - for await (const asset of this.assetJobRepository.streamForThumbnailJob(!!force)) { - const { previewFile, thumbnailFile } = getAssetFiles(asset.files); - - if (!previewFile || !thumbnailFile || !asset.thumbhash || force) { + const fullsizeEnabled = config.image.fullsize.enabled; + for await (const asset of this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled })) { + if (force || !asset.isEdited) { jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } + if (asset.isEdited) { + jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); + } + if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -155,21 +170,62 @@ export class MediaService extends BaseService { return JobStatus.Failed; } - await this.storageCore.moveAssetImage(asset, AssetPathType.FullSize, image.fullsize.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.Preview, image.preview.format); - await this.storageCore.moveAssetImage(asset, AssetPathType.Thumbnail, image.thumbnail.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format); + await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format); // TODO: - // await this.storageCore.moveAssetImage(asset, AssetPathType.Tiles, image.???.format); + // await this.storageCore.moveAssetImage(asset, AssetFileType.Tiles, image.???.format); await this.storageCore.moveAssetVideo(asset); return JobStatus.Success; } + @OnJob({ name: JobName.AssetEditThumbnailGeneration, queue: QueueName.Editor }) + async handleAssetEditThumbnailGeneration({ id }: JobOf): Promise { + const asset = await this.assetJobRepository.getForGenerateThumbnailJob(id); + const config = await this.getConfig({ withCache: true }); + + if (!asset) { + this.logger.warn(`Thumbnail generation failed for asset ${id}: not found in database or missing metadata`); + return JobStatus.Failed; + } + + const generated = await this.generateEditedThumbnails(asset, config); + await this.syncFiles( + asset.files.filter((file) => file.isEdited), + generated?.files ?? [], + ); + + let thumbhash: Buffer | undefined = generated?.thumbhash; + if (!thumbhash) { + const extractedImage = await this.extractOriginalImage(asset, config.image); + const { info, data, colorspace } = extractedImage; + + thumbhash = await this.mediaRepository.generateThumbhash(data, { + colorspace, + processInvalidImages: false, + raw: info, + edits: [], + }); + } + + if (!asset.thumbhash || Buffer.compare(asset.thumbhash, thumbhash) !== 0) { + await this.assetRepository.update({ id: asset.id, thumbhash }); + } + + const fullsizeDimensions = generated?.fullsizeDimensions ?? getDimensions(asset.exifInfo!); + await this.assetRepository.update({ id: asset.id, ...fullsizeDimensions }); + + return JobStatus.Success; + } + @OnJob({ name: JobName.AssetGenerateThumbnails, queue: QueueName.ThumbnailGeneration }) async handleGenerateThumbnails({ id }: JobOf): Promise { const asset = await this.assetJobRepository.getForGenerateThumbnailJob(id); + const config = await this.getConfig({ withCache: true }); + if (!asset) { - this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`); + this.logger.warn(`Thumbnail generation failed for asset ${id}: not found in database or missing metadata`); return JobStatus.Failed; } @@ -178,83 +234,30 @@ export class MediaService extends BaseService { return JobStatus.Skipped; } - let generated: { - previewPath: string; - thumbnailPath: string; - fullsizePath?: string; - tileInfo?: TileInfo; - thumbhash: Buffer; - }; + let generated: Awaited>; if (asset.type === AssetType.Video || asset.originalFileName.toLowerCase().endsWith('.gif')) { this.logger.verbose(`Thumbnail generation for video ${id} ${asset.originalPath}`); - generated = await this.generateVideoThumbnails(asset); + generated = await this.generateVideoThumbnails(asset, config); } else if (asset.type === AssetType.Image) { this.logger.verbose(`Thumbnail generation for image ${id} ${asset.originalPath}`); - generated = await this.generateImageThumbnails(asset); + generated = await this.generateImageThumbnails(asset, config); } else { this.logger.warn(`Skipping thumbnail generation for asset ${id}: ${asset.type} is not an image or video`); return JobStatus.Skipped; } - const { previewFile, thumbnailFile, fullsizeFile, tilesPath } = getAssetFiles(asset.files); - const toUpsert: UpsertFileOptions[] = []; - if (previewFile?.path !== generated.previewPath) { - toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.Preview }); + const editedGenerated = await this.generateEditedThumbnails(asset, config); + if (editedGenerated) { + generated.files.push(...editedGenerated.files); } - if (thumbnailFile?.path !== generated.thumbnailPath) { - toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.Thumbnail }); - } + await this.syncFiles(asset.files, generated.files); + const thumbhash = editedGenerated?.thumbhash || generated.thumbhash; - if (generated.fullsizePath && fullsizeFile?.path !== generated.fullsizePath) { - toUpsert.push({ assetId: asset.id, path: generated.fullsizePath, type: AssetFileType.FullSize }); + if (!asset.thumbhash || Buffer.compare(asset.thumbhash, thumbhash) !== 0) { + await this.assetRepository.update({ id: asset.id, thumbhash }); } - if (generated.tileInfo?.path && tilesPath?.path !== generated.tileInfo.path) { - // TODO: save tileInfo.info (width, cols, rows) somewhere in the db. - toUpsert.push({ assetId: asset.id, path: generated.tileInfo.path, type: AssetFileType.Tiles }); - } - - if (toUpsert.length > 0) { - await this.assetRepository.upsertFiles(toUpsert); - } - - const pathsToDelete: string[] = []; - if (previewFile && previewFile.path !== generated.previewPath) { - this.logger.debug(`Deleting old preview for asset ${asset.id}`); - pathsToDelete.push(previewFile.path); - } - - if (thumbnailFile && thumbnailFile.path !== generated.thumbnailPath) { - this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); - pathsToDelete.push(thumbnailFile.path); - } - - if (fullsizeFile && fullsizeFile.path !== generated.fullsizePath) { - this.logger.debug(`Deleting old fullsize preview image for asset ${asset.id}`); - pathsToDelete.push(fullsizeFile.path); - if (!generated.fullsizePath) { - // did not generate a new fullsize image, delete the existing record - await this.assetRepository.deleteFiles([fullsizeFile]); - } - } - - if (tilesPath && tilesPath.path !== generated.tileInfo?.path) { - this.logger.debug(`Deleting old tiles for asset ${asset.id}`); - pathsToDelete.push(tilesPath.path.replace('.dz', '.dzi')); - await this.storageRepository.unlinkDir(tilesPath.path.replace('.dz', '_files'), { recursive: true }); - } - - if (pathsToDelete.length > 0) { - await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path))); - } - - if (!asset.thumbhash || Buffer.compare(asset.thumbhash, generated.thumbhash) !== 0) { - await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash }); - } - - await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date(), thumbnailAt: new Date() }); - return JobStatus.Success; } @@ -281,61 +284,114 @@ export class MediaService extends BaseService { return { info, data, colorspace }; } - private async generateImageThumbnails(asset: { - id: string; - ownerId: string; - originalFileName: string; - originalPath: string; - exifInfo: Exif; - }) { - const { image } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format); - const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format); - this.storageCore.ensureFolders(previewPath); - - // Handle embedded preview extraction for RAW files + private async extractOriginalImage(asset: ThumbnailAsset, image: SystemConfig['image'], useEdits = false) { const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName); const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null; const generateFullsize = - (image.fullsize.enabled || asset.exifInfo.projectionType == 'EQUIRECTANGULAR') && - !mimeTypes.isWebSupportedImage(asset.originalPath); + ((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') && + !mimeTypes.isWebSupportedImage(asset.originalPath)) || + useEdits; const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`)); - const { info, data, colorspace } = await this.decodeImage( - extracted ? extracted.buffer : asset.originalPath, + const thumbSource = extracted ? extracted.buffer : asset.originalPath; + const { data, info, colorspace } = await this.decodeImage( + thumbSource, // only specify orientation to extracted images which don't have EXIF orientation data // or it can double rotate the image extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null }, convertFullsize ? undefined : image.preview.size, ); + let isTransparent = false; + if (!extracted && mimeTypes.canBeTransparent(asset.originalPath)) { + ({ isTransparent } = await this.mediaRepository.getImageMetadata(asset.originalPath)); + } + + return { + extracted, + data, + info, + colorspace, + convertFullsize, + generateFullsize, + isTransparent, + }; + } + + private async generateImageThumbnails(asset: ThumbnailAsset, { image }: SystemConfig, useEdits: boolean = false) { + // Handle embedded preview extraction for RAW files + const extractedImage = await this.extractOriginalImage(asset, image, useEdits); + const { info, data, colorspace, generateFullsize, convertFullsize, extracted, isTransparent } = extractedImage; + + const previewFormat = image.preview.format; + this.warnOnTransparencyLoss(isTransparent, previewFormat, asset.id); + + const thumbnailFormat = image.thumbnail.format; + this.warnOnTransparencyLoss(isTransparent, thumbnailFormat, asset.id); + + const previewFile = this.getImageFile(asset, { + fileType: AssetFileType.Preview, + format: previewFormat, + isEdited: useEdits, + isProgressive: !!image.preview.progressive && previewFormat !== ImageFormat.Webp, + isTransparent, + }); + const thumbnailFile = this.getImageFile(asset, { + fileType: AssetFileType.Thumbnail, + format: thumbnailFormat, + isEdited: useEdits, + isProgressive: !!image.thumbnail.progressive && thumbnailFormat !== ImageFormat.Webp, + isTransparent, + }); + this.storageCore.ensureFolders(previewFile.path); + // generate final images - const thumbnailOptions = { colorspace, processInvalidImages: false, raw: info }; + const baseOptions = { colorspace, processInvalidImages: false, raw: info, edits: useEdits ? asset.edits : [] }; + const thumbnailOptions = { ...image.thumbnail, ...baseOptions, format: thumbnailFormat }; + const previewOptions = { ...image.preview, ...baseOptions, format: previewFormat }; const promises = [ - this.mediaRepository.generateThumbhash(data, thumbnailOptions), - this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailPath), - this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewPath), + this.mediaRepository.generateThumbhash(data, baseOptions), + this.mediaRepository.generateThumbnail(data, thumbnailOptions, thumbnailFile.path), + this.mediaRepository.generateThumbnail(data, previewOptions, previewFile.path), ]; - let fullsizePath: string | undefined; - + let fullsizeFile: UpsertFileOptions | undefined; if (convertFullsize) { + const fullsizeFormat = image.fullsize.format; + this.warnOnTransparencyLoss(isTransparent, fullsizeFormat, asset.id); // convert a new fullsize image from the same source as the thumbnail - fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, image.fullsize.format); - const fullsizeOptions = { format: image.fullsize.format, quality: image.fullsize.quality, ...thumbnailOptions }; - promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath)); + fullsizeFile = this.getImageFile(asset, { + fileType: AssetFileType.FullSize, + format: fullsizeFormat, + isEdited: useEdits, + isProgressive: !!image.fullsize.progressive && fullsizeFormat !== ImageFormat.Webp, + isTransparent, + }); + const fullsizeOptions = { + ...baseOptions, + format: fullsizeFormat, + quality: image.fullsize.quality, + progressive: image.fullsize.progressive, + }; + promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizeFile.path)); } else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.Jpeg) { - fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FullSize, extracted.format); - this.storageCore.ensureFolders(fullsizePath); + fullsizeFile = this.getImageFile(asset, { + fileType: AssetFileType.FullSize, + format: extracted.format, + isEdited: false, + isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp, + isTransparent, + }); + this.storageCore.ensureFolders(fullsizeFile.path); // Write the buffer to disk with essential EXIF data - await this.storageRepository.createOrOverwriteFile(fullsizePath, extracted.buffer); + await this.storageRepository.createOrOverwriteFile(fullsizeFile.path, extracted.buffer); await this.mediaRepository.writeExif( { orientation: asset.exifInfo.orientation, colorspace: asset.exifInfo.colorspace, }, - fullsizePath, + fullsizeFile.path, ); } @@ -351,14 +407,12 @@ export class MediaService extends BaseService { const tileSize = Math.ceil(originalSize / numTiles); const tileOptions = { - format: image.preview.format, + ...previewOptions, size: tileSize, - quality: image.preview.quality, - ...thumbnailOptions, }; tileInfo = { - path: StorageCore.getImagePath(asset, AssetPathType.Tiles, 'dz'), + path: StorageCore.getImagePath(asset, { fileType: AssetFileType.Tiles, format: TilesFormat.Dz, isEdited: useEdits }), info: { width: originalSize, cols: numTiles, @@ -374,17 +428,22 @@ export class MediaService extends BaseService { const outputs = await Promise.all(promises); - if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') { - const promises = [ - this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewPath), - fullsizePath - ? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizePath) - : Promise.resolve(), - ]; - await Promise.all(promises); + const decodedDimensions = { width: info.width, height: info.height }; + const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions; + const files = [previewFile, thumbnailFile]; + if (fullsizeFile) { + files.push(fullsizeFile); + } + if (tileInfo) { + console.warn('TODO: should push tile info to files'); + // files.push(tileInfo); } - return { previewPath, thumbnailPath, fullsizePath, tileInfo, thumbhash: outputs[0] as Buffer }; + return { + files, + thumbhash: outputs[0] as Buffer, + fullsizeDimensions, + }; } @OnJob({ name: JobName.PersonGenerateThumbnail, queue: QueueName.ThumbnailGeneration }) @@ -425,17 +484,23 @@ export class MediaService extends BaseService { const thumbnailPath = StorageCore.getPersonThumbnailPath({ id, ownerId }); this.storageCore.ensureFolders(thumbnailPath); - const thumbnailOptions = { + const thumbnailOptions: GenerateThumbnailOptions = { colorspace: image.colorspace, format: ImageFormat.Jpeg, raw: info, quality: image.thumbnail.quality, - crop: this.getCrop( - { old: { width: oldWidth, height: oldHeight }, new: { width: info.width, height: info.height } }, - { x1, y1, x2, y2 }, - ), + progressive: false, processInvalidImages: false, size: FACE_THUMBNAIL_SIZE, + edits: [ + { + action: AssetEditAction.Crop, + parameters: this.getCrop( + { old: { width: oldWidth, height: oldHeight }, new: { width: info.width, height: info.height } }, + { x1, y1, x2, y2 }, + ), + }, + ], }; await this.mediaRepository.generateThumbnail(decodedImage, thumbnailOptions, thumbnailPath); @@ -444,7 +509,10 @@ export class MediaService extends BaseService { return JobStatus.Success; } - private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { + private getCrop( + dims: { old: ImageDimensions; new: ImageDimensions }, + { x1, y1, x2, y2 }: BoundingBox, + ): CropParameters { // face bounding boxes can spill outside the image dimensions const clampedX1 = clamp(x1, 0, dims.old.width); const clampedY1 = clamp(y1, 0, dims.old.height); @@ -472,18 +540,32 @@ export class MediaService extends BaseService { ); return { - left: middleX - newHalfSize, - top: middleY - newHalfSize, + x: middleX - newHalfSize, + y: middleY - newHalfSize, width: newHalfSize * 2, height: newHalfSize * 2, }; } - private async generateVideoThumbnails(asset: ThumbnailPathEntity & { originalPath: string }) { - const { image, ffmpeg } = await this.getConfig({ withCache: true }); - const previewPath = StorageCore.getImagePath(asset, AssetPathType.Preview, image.preview.format); - const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.Thumbnail, image.thumbnail.format); - this.storageCore.ensureFolders(previewPath); + private async generateVideoThumbnails( + asset: ThumbnailPathEntity & { originalPath: string }, + { ffmpeg, image }: SystemConfig, + ) { + const previewFile = this.getImageFile(asset, { + fileType: AssetFileType.Preview, + format: image.preview.format, + isEdited: false, + isProgressive: false, + isTransparent: false, + }); + const thumbnailFile = this.getImageFile(asset, { + fileType: AssetFileType.Thumbnail, + format: image.thumbnail.format, + isEdited: false, + isProgressive: false, + isTransparent: false, + }); + this.storageCore.ensureFolders(previewFile.path); const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); const mainVideoStream = this.getMainStream(videoStreams); @@ -502,15 +584,19 @@ export class MediaService extends BaseService { format, ); - await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions); - await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions); + await this.mediaRepository.transcode(asset.originalPath, previewFile.path, previewOptions); + await this.mediaRepository.transcode(asset.originalPath, thumbnailFile.path, thumbnailOptions); - const thumbhash = await this.mediaRepository.generateThumbhash(previewPath, { + const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path, { colorspace: image.colorspace, processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', }); - return { previewPath, thumbnailPath, thumbhash }; + return { + files: [previewFile, thumbnailFile], + thumbhash, + fullsizeDimensions: { width: mainVideoStream.width, height: mainVideoStream.height }, + }; } @OnJob({ name: JobName.AssetEncodeVideoQueueAll, queue: QueueName.VideoConversion }) @@ -737,7 +823,7 @@ export class MediaService extends BaseService { } private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) { - const { width, height } = await this.mediaRepository.getImageDimensions(extractedPathOrBuffer); + const { width, height } = await this.mediaRepository.getImageMetadata(extractedPathOrBuffer); const extractedSize = Math.min(width, height); return extractedSize >= targetSize; } @@ -763,4 +849,106 @@ export class MediaService extends BaseService { return false; } } + + private async syncFiles( + oldFiles: (AssetFile & { isProgressive: boolean; isTransparent: boolean })[], + newFiles: UpsertFileOptions[], + ) { + const toUpsert: UpsertFileOptions[] = []; + const pathsToDelete: string[] = []; + const toDelete = new Set(oldFiles); + + for (const newFile of newFiles) { + const existingFile = oldFiles.find((file) => file.type === newFile.type && file.isEdited === newFile.isEdited); + if (existingFile) { + toDelete.delete(existingFile); + } + + // upsert new file path + if ( + existingFile?.path !== newFile.path || + existingFile.isProgressive !== newFile.isProgressive || + existingFile.isTransparent !== newFile.isTransparent + ) { + toUpsert.push(newFile); + + // delete old file from disk + if (existingFile && existingFile.path !== newFile.path) { + this.logger.debug( + `Deleting old ${newFile.type} image for asset ${newFile.assetId} in favor of a replacement`, + ); + pathsToDelete.push(existingFile.path); + } + } + } + + if (toUpsert.length > 0) { + await this.assetRepository.upsertFiles(toUpsert); + } + + if (toDelete.size > 0) { + const toDeleteArray = [...toDelete]; + for (const file of toDeleteArray) { + pathsToDelete.push(file.path); + } + await this.assetRepository.deleteFiles(toDeleteArray); + } + + if (pathsToDelete.length > 0) { + await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: pathsToDelete } }); + } + } + + private async generateEditedThumbnails(asset: ThumbnailAsset, config: SystemConfig) { + if (asset.type !== AssetType.Image || (asset.files.length === 0 && asset.edits.length === 0)) { + return; + } + + const generated = asset.edits.length > 0 ? await this.generateImageThumbnails(asset, config, true) : undefined; + + const crop = asset.edits.find((e) => e.action === AssetEditAction.Crop); + const cropBox = crop + ? { + x1: crop.parameters.x, + y1: crop.parameters.y, + x2: crop.parameters.x + crop.parameters.width, + y2: crop.parameters.y + crop.parameters.height, + } + : undefined; + + const originalDimensions = getDimensions(asset.exifInfo!); + const assetFaces = await this.personRepository.getFaces(asset.id, {}); + const ocrData = await this.ocrRepository.getByAssetId(asset.id, {}); + + const faceStatuses = checkFaceVisibility(assetFaces, originalDimensions, cropBox); + await this.personRepository.updateVisibility(faceStatuses.visible, faceStatuses.hidden); + + const ocrStatuses = checkOcrVisibility(ocrData, originalDimensions, cropBox); + await this.ocrRepository.updateOcrVisibilities(asset.id, ocrStatuses.visible, ocrStatuses.hidden); + + return generated; + } + + private warnOnTransparencyLoss(isTransparent: boolean, format: ImageFormat, assetId: string) { + if (isTransparent && format === ImageFormat.Jpeg) { + this.logger.warn( + `Asset ${assetId} has transparency but the configured format is ${format} which does not support it, consider using a format that does, such as ${ImageFormat.Webp}`, + ); + } + } + + private getImageFile( + asset: ThumbnailPathEntity, + options: ImagePathOptions & { isProgressive: boolean; isTransparent: boolean }, + ) { + const path = StorageCore.getImagePath(asset, options); + return { + assetId: asset.id, + type: options.fileType, + path, + isEdited: options.isEdited, + isProgressive: options.isProgressive, + isTransparent: options.isTransparent, + }; + } } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 8e91c232f7..2378d594e1 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -28,6 +28,7 @@ export class MemoryService extends BaseService { continue; } + this.logger.log(`Creating memories for ${target.toISO()}`); try { await Promise.all(users.map((owner) => this.createOnThisDayMemories(owner.id, target))); } catch (error) { @@ -99,6 +100,8 @@ export class MemoryService extends BaseService { data: dto.data, isSaved: dto.isSaved, memoryAt: dto.memoryAt, + showAt: dto.showAt, + hideAt: dto.hideAt, seenAt: dto.seenAt, }, allowedAssetIds, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 98c906d9c7..92ec13bea5 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { defaults } from 'src/config'; -import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetFileType, AssetType, @@ -16,26 +15,18 @@ import { } from 'src/enum'; import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { fileStub } from 'test/fixtures/file.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonFactory } from 'test/factories/person.factory'; import { probeStub } from 'test/fixtures/media.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { tagStub } from 'test/fixtures/tag.stub'; import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; -const removeNonSidecarFiles = (asset: any) => { - return { - ...asset, - files: asset.files.filter((file: any) => file.type === AssetFileType.Sidecar), - }; -}; - const forSidecarJob = ( asset: { id?: string; originalPath?: string; - files?: { id: string; type: AssetFileType; path: string }[]; + files?: { id: string; type: AssetFileType; path: string; isEdited: boolean }[]; } = {}, ) => { return { @@ -131,27 +122,29 @@ describe(MetadataService.name, () => { describe('handleQueueMetadataExtraction', () => { it('should queue metadata extraction for all assets without exif values', async () => { - mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset])); await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(false); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetExtractMetadata, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue metadata extraction for all assets', async () => { - mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([asset])); await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetExtractMetadata, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); @@ -172,9 +165,9 @@ describe(MetadataService.name, () => { it('should handle an asset that could not be found', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(void 0); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: 'non-existent' }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith('non-existent'); expect(mocks.asset.upsertExif).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -182,17 +175,18 @@ describe(MetadataService.name, () => { it('should handle a date in a sidecar file', async () => { const originalDate = new Date('2023-11-21T16:13:17.517Z'); const sidecarDate = new Date('2022-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar)); + const asset = AssetFactory.from().file({ type: AssetFileType.Sidecar }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.sidecar.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate }), { lockedPropertiesBehavior: 'skip', }); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, fileCreatedAt: sidecarDate, localDateTime: sidecarDate, @@ -203,7 +197,8 @@ describe(MetadataService.name, () => { it('should take the file modification date when missing exif and earlier than creation date', async () => { const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: fileModifiedAt, @@ -212,25 +207,28 @@ describe(MetadataService.name, () => { } as Stats); mockReadTags(); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ dateTimeOriginal: fileModifiedAt }), { lockedPropertiesBehavior: 'skip' }, ); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.image.id, + id: asset.id, duration: null, fileCreatedAt: fileModifiedAt, fileModifiedAt, localDateTime: fileModifiedAt, + width: null, + height: null, }); }); it('should take the file creation date when missing exif and earlier than modification date', async () => { const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: fileModifiedAt, @@ -239,27 +237,30 @@ describe(MetadataService.name, () => { } as Stats); mockReadTags(); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ dateTimeOriginal: fileCreatedAt }), { lockedPropertiesBehavior: 'skip' }, ); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.image.id, + id: asset.id, duration: null, fileCreatedAt, fileModifiedAt, localDateTime: fileCreatedAt, + width: null, + height: null, }); }); it('should determine dateTimeOriginal regardless of the server time zone', async () => { process.env.TZ = 'America/Los_Angeles'; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ dateTimeOriginal: new Date('2022-01-01T00:00:00.000Z'), @@ -275,100 +276,105 @@ describe(MetadataService.name, () => { }); it('should handle lists of numbers', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.image.fileModifiedAt, - mtimeMs: assetStub.image.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.image.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); - mockReadTags({ - ISO: [160], - }); + mockReadTags({ ISO: [160] }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ iso: 160 }), { lockedPropertiesBehavior: 'skip', }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.image.id, + id: asset.id, duration: null, - fileCreatedAt: assetStub.image.fileCreatedAt, - fileModifiedAt: assetStub.image.fileCreatedAt, - localDateTime: assetStub.image.fileCreatedAt, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.fileCreatedAt, + width: null, + height: null, }); }); it('should not delete latituide and longitude without reverse geocode', async () => { // regression test for issue 17511 - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation); + const asset = AssetFactory.from().exif().build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: false } }); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.withLocation.fileModifiedAt, - mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ - GPSLatitude: assetStub.withLocation.exifInfo!.latitude!, - GPSLongitude: assetStub.withLocation.exifInfo!.longitude!, + GPSLatitude: asset.exifInfo.latitude!, + GPSLongitude: asset.exifInfo.longitude!, }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ city: null, state: null, country: null }), { lockedPropertiesBehavior: 'skip' }, ); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.withLocation.id, + id: asset.id, duration: null, - fileCreatedAt: assetStub.withLocation.fileCreatedAt, - fileModifiedAt: assetStub.withLocation.fileModifiedAt, - localDateTime: new Date('2023-02-22T05:06:29.716Z'), + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.localDateTime, + width: null, + height: null, }); }); it('should apply reverse geocoding', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation)); + const asset = AssetFactory.from().exif({ latitude: 10, longitude: 20 }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } }); mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' }); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.withLocation.fileModifiedAt, - mtimeMs: assetStub.withLocation.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.withLocation.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); - mockReadTags({ - GPSLatitude: assetStub.withLocation.exifInfo!.latitude!, - GPSLongitude: assetStub.withLocation.exifInfo!.longitude!, - }); + mockReadTags({ GPSLatitude: 10, GPSLongitude: 20 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ city: 'City', state: 'State', country: 'Country' }), { lockedPropertiesBehavior: 'skip' }, ); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.withLocation.id, + id: asset.id, duration: null, - fileCreatedAt: assetStub.withLocation.fileCreatedAt, - fileModifiedAt: assetStub.withLocation.fileModifiedAt, - localDateTime: new Date('2023-02-22T05:06:29.716Z'), + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + localDateTime: asset.localDateTime, + width: null, + height: null, }); }); it('should discard latitude and longitude on null island', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ GPSLatitude: 0, GPSLongitude: 0, }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ latitude: null, longitude: null }), { lockedPropertiesBehavior: 'skip' }, @@ -376,187 +382,211 @@ describe(MetadataService.name, () => { }); it('should extract tags from TagsList', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] }); mockReadTags({ TagsList: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); }); it('should extract hierarchy from TagsList', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] }); mockReadTags({ TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); }); it('should extract tags from Keywords as a string', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] }); mockReadTags({ Keywords: 'Parent' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); }); it('should extract tags from Keywords as a list', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent'] }); mockReadTags({ Keywords: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); }); it('should extract tags from Keywords as a list with a number', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] }); mockReadTags({ Keywords: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined }); }); it('should extract hierarchal tags from Keywords', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child'] }); mockReadTags({ Keywords: 'Parent/Child' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); }); it('should ignore Keywords when TagsList is present', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Child'] }); mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); }); it('should extract hierarchy from HierarchicalSubject', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'TagA'] }); mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); - expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, { + userId: asset.ownerId, + value: 'TagA', + parent: undefined, + }); }); it('should extract tags from HierarchicalSubject as a list with a number', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent', '2024'] }); mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); - expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: 'Parent', parent: undefined }); + expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ userId: asset.ownerId, value: '2024', parent: undefined }); }); it('should extract ignore / characters in a HierarchicalSubject tag', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Mom|Dad'] }); mockReadTags({ HierarchicalSubject: ['Mom/Dad'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ - userId: 'user-id', + userId: asset.ownerId, value: 'Mom|Dad', parent: undefined, }); }); it('should ignore HierarchicalSubject when TagsList is present', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.getForMetadataExtractionTags.mockResolvedValue({ tags: ['Parent/Child', 'Parent2/Child2'] }); mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent', parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { - userId: 'user-id', + userId: asset.ownerId, value: 'Parent/Child', parentId: 'tag-parent', }); }); it('should remove existing tags', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({}); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith('asset-id', []); + expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith(asset.id, []); }); it('should not apply motion photos if asset is video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); @@ -566,23 +596,25 @@ describe(MetadataService.name, () => { }); it('should handle an invalid Directory Item', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ MotionPhoto: 1, ContainerDirectory: [{ Foo: 100 }], }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); }); it('should extract the correct video orientation', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); mockReadTags({}); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ orientation: ExifOrientation.Rotate270CW.toString() }), { lockedPropertiesBehavior: 'skip' }, @@ -590,16 +622,14 @@ describe(MetadataService.name, () => { }); it('should extract the MotionPhotoVideo tag from Samsung HEIC motion photos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -611,57 +641,52 @@ describe(MetadataService.name, () => { EmbeddedVideoType: 'MotionPhoto_Data', }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'MotionPhotoVideo', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'MotionPhotoVideo'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the EmbeddedVideo tag from Samsung JPEG motion photos', async () => { + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', EmbeddedVideoFile: new BinaryField(0, ''), @@ -669,56 +694,51 @@ describe(MetadataService.name, () => { MotionPhoto: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.metadata.extractBinaryTag.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - 'EmbeddedVideoFile', - ); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.metadata.extractBinaryTag).toHaveBeenCalledWith(asset.originalPath, 'EmbeddedVideoFile'); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should extract the motion photo video from the XMP directory entry ', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoWithOriginalFileName, - livePhotoVideoId: null, - libraryId: null, - }); + const asset = AssetFactory.create(); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.storage.stat.mockResolvedValue({ size: 123_456, - mtime: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - mtimeMs: assetStub.livePhotoWithOriginalFileName.fileModifiedAt.valueOf(), - birthtimeMs: assetStub.livePhotoWithOriginalFileName.fileCreatedAt.valueOf(), + mtime: asset.fileModifiedAt, + mtimeMs: asset.fileModifiedAt.valueOf(), + birthtimeMs: asset.fileCreatedAt.valueOf(), } as Stats); mockReadTags({ Directory: 'foo/bar/', @@ -727,47 +747,46 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); - mocks.crypto.randomUUID.mockReturnValue(fileStub.livePhotoMotion.uuid); + mocks.asset.create.mockResolvedValue(motionAsset); + mocks.crypto.randomUUID.mockReturnValue(motionAsset.id); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoWithOriginalFileName.id); - expect(mocks.storage.readFile).toHaveBeenCalledWith( - assetStub.livePhotoWithOriginalFileName.originalPath, - expect.any(Object), - ); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object)); expect(mocks.asset.create).toHaveBeenCalledWith({ checksum: expect.any(Buffer), deviceAssetId: 'NONE', deviceId: 'NONE', - fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, - id: fileStub.livePhotoMotion.uuid, + fileCreatedAt: asset.fileCreatedAt, + fileModifiedAt: asset.fileModifiedAt, + id: motionAsset.id, visibility: AssetVisibility.Hidden, - libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, - localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, - originalFileName: 'asset_1.mp4', - originalPath: expect.stringContaining('/data/encoded-video/user-id/li/ve/live-photo-motion-asset-MP.mp4'), - ownerId: assetStub.livePhotoWithOriginalFileName.ownerId, + libraryId: asset.libraryId, + localDateTime: asset.fileCreatedAt, + originalFileName: `IMG_${asset.id}.mp4`, + originalPath: expect.stringContaining(`${motionAsset.id}-MP.mp4`), + ownerId: asset.ownerId, type: AssetType.Video, }); - expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(mocks.storage.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(mocks.user.updateUsage).toHaveBeenCalledWith(asset.ownerId, 512); + expect(mocks.storage.createFile).toHaveBeenCalledWith(motionAsset.originalPath, video); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoWithOriginalFileName.id, - livePhotoVideoId: fileStub.livePhotoMotion.uuid, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(3); expect(mocks.job.queue).toHaveBeenCalledExactlyOnceWith({ name: JobName.AssetEncodeVideo, - data: { id: assetStub.livePhotoMotionAsset.id }, + data: { id: motionAsset.id }, }); }); it('should delete old motion photo video assets if they do not match what is extracted', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoWithOriginalFileName); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -775,21 +794,21 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockImplementation( - (asset) => Promise.resolve({ ...assetStub.livePhotoMotionAsset, ...asset }) as Promise, - ); + mocks.asset.create.mockResolvedValue(AssetFactory.create({ type: AssetType.Video })); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoWithOriginalFileName.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.job.queue).toHaveBeenNthCalledWith(1, { name: JobName.AssetDelete, - data: { id: assetStub.livePhotoWithOriginalFileName.livePhotoVideoId, deleteOnDisk: true }, + data: { id: asset.livePhotoVideoId, deleteOnDisk: true }, }); }); it('should not create a new motion photo video asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ livePhotoVideoId: motionAsset.id }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -797,12 +816,12 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); mocks.storage.checkFileExists.mockResolvedValue(true); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); // The still asset gets saved by handleMetadataExtraction, but not the video @@ -811,10 +830,9 @@ describe(MetadataService.name, () => { }); it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -822,31 +840,26 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.getByChecksum.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - visibility: AssetVisibility.Timeline, - }); + mocks.asset.getByChecksum.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledTimes(4); }); it('should not update storage usage if motion photo is external', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - livePhotoVideoId: null, - isExternal: true, - }); + const motionAsset = AssetFactory.create({ type: AssetType.Video, visibility: AssetVisibility.Hidden }); + const asset = AssetFactory.create({ isExternal: true }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Directory: 'foo/bar/', MotionPhoto: 1, @@ -854,16 +867,17 @@ describe(MetadataService.name, () => { MicroVideoOffset: 1, }); mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); - mocks.asset.create.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.asset.create.mockResolvedValue(motionAsset); const video = randomBytes(512); mocks.storage.readFile.mockResolvedValue(video); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.user.updateUsage).not.toHaveBeenCalled(); }); it('should save all metadata', async () => { const dateForTest = new Date('1970-01-01T00:00:00.000-11:30'); + const asset = AssetFactory.create(); const tags: ImmichTags = { BitsPerSample: 1, @@ -885,18 +899,19 @@ describe(MetadataService.name, () => { Orientation: 0, ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', - tz: 'UTC-11:30', + zone: 'UTC-11:30', + TagsList: ['parent/child'], Rating: 3, }; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(tags); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( { - assetId: assetStub.image.id, + assetId: asset.id, bitsPerSample: expect.any(Number), autoStackId: null, colorspace: tags.ColorSpace, @@ -920,17 +935,18 @@ describe(MetadataService.name, () => { orientation: tags.Orientation?.toString(), profileDescription: tags.ProfileDescription, projectionType: 'EQUIRECTANGULAR', - timeZone: tags.tz, + timeZone: tags.zone, rating: tags.Rating, country: null, state: null, city: null, + tags: ['parent/child'], }, { lockedPropertiesBehavior: 'skip' }, ); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, fileCreatedAt: dateForTest, localDateTime: DateTime.fromISO('1970-01-01T00:00:00.000Z').toJSDate(), @@ -943,6 +959,7 @@ describe(MetadataService.name, () => { // https://github.com/photostructure/exiftool-vendored.js/issues/203 // this only tests our assumptions of exiftool-vendored, demonstrating the issue + const asset = AssetFactory.create(); const someDate = '2024-09-01T00:00:00.000'; expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC'); expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0 @@ -950,13 +967,13 @@ describe(MetadataService.name, () => { const tags: ImmichTags = { DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'), - tz: undefined, + zone: undefined, }; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(tags); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ timeZone: 'UTC+0', @@ -966,7 +983,8 @@ describe(MetadataService.name, () => { }); it('should extract duration', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -975,20 +993,21 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: '00:00:06.210', }), ); }); it('should only extract duration for videos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -996,20 +1015,21 @@ describe(MetadataService.name, () => { duration: 6.21, }, }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, }), ); }); it('should omit duration of zero', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1018,20 +1038,21 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, + id: asset.id, duration: null, }), ); }); it('should a handle duration of 1 week', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, format: { @@ -1040,64 +1061,55 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.video.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.upsertExif).toHaveBeenCalled(); expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.video.id, + id: asset.id, duration: '168:00:00.000', }), ); }); it('should use Duration from exif', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.image, - originalPath: '/original/path.webp', - }); + const asset = AssetFactory.create({ originalFileName: 'file.webp' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, {}); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); }); it('should prefer Duration from exif over sidecar', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.image, - originalPath: '/original/path.webp', - files: [ - { - id: 'some-id', - type: AssetFileType.Sidecar, - path: '/path/to/something', - }, - ], - }); + const asset = AssetFactory.from({ originalFileName: 'file.webp' }).file({ type: AssetFileType.Sidecar }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, { Duration: 456 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' })); }); it('should ignore all Duration tags for definitely static images', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.imageDng); + const asset = AssetFactory.from({ originalFileName: 'file.dng' }).build(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, { Duration: 456 }); - await sut.handleMetadataExtraction({ id: assetStub.imageDng.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: null })); }); it('should ignore Duration from exif for videos', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.video); + const asset = AssetFactory.create({ type: AssetType.Video }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Duration: 123 }, {}); mocks.media.probe.mockResolvedValue({ ...probeStub.videoStreamH264, @@ -1107,17 +1119,18 @@ describe(MetadataService.name, () => { }, }); - await sut.handleMetadataExtraction({ id: assetStub.video.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1); expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' })); }); it('should trim whitespace from description', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Description: '\t \v \f \n \r' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: '', @@ -1126,7 +1139,7 @@ describe(MetadataService.name, () => { ); mockReadTags({ ImageDescription: ' my\n description' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: 'my\n description', @@ -1136,10 +1149,11 @@ describe(MetadataService.name, () => { }); it('should handle a numeric description', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Description: 1000 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ description: '1000', @@ -1149,63 +1163,68 @@ describe(MetadataService.name, () => { }); it('should skip importing metadata when the feature is disabled', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: false } } }); mockReadTags(makeFaceTags({ Name: 'Person 1' })); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.getDistinctNames).not.toHaveBeenCalled(); }); it('should skip importing metadata face for assets without tags.RegionInfo', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.getDistinctNames).not.toHaveBeenCalled(); }); it('should skip importing faces without name', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags()); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([]); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).not.toHaveBeenCalled(); expect(mocks.person.updateAll).not.toHaveBeenCalled(); }); it('should skip importing faces with empty name', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: '' })); mocks.person.getDistinctNames.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([]); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).not.toHaveBeenCalled(); expect(mocks.person.updateAll).not.toHaveBeenCalled(); }); - it('should apply metadata face tags creating new persons', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + it('should apply metadata face tags creating new people', async () => { + const asset = AssetFactory.create(); + const person = PersonFactory.create(); + + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name })); + mockReadTags(makeFaceTags({ Name: person.name })); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([personStub.withName.id]); - mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); - expect(mocks.person.createAll).toHaveBeenCalledWith([ - expect.objectContaining({ name: personStub.withName.name }), - ]); + mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.update.mockResolvedValue(person); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); + expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, + assetId: asset.id, personId: 'random-uuid', imageHeight: 100, imageWidth: 1000, @@ -1219,33 +1238,36 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' }, + { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.withName.id }, + data: { id: person.id }, }, ]); }); it('should assign metadata face tags to existing persons', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + const asset = AssetFactory.create(); + const person = PersonFactory.create(); + + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name })); - mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]); + mockReadTags(makeFaceTags({ Name: person.name })); + mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]); mocks.person.createAll.mockResolvedValue([]); - mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true }); + mocks.person.update.mockResolvedValue(person); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true }); expect(mocks.person.createAll).not.toHaveBeenCalled(); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, - personId: personStub.withName.id, + assetId: asset.id, + personId: person.id, imageHeight: 100, imageWidth: 1000, boundingBoxX1: 0, @@ -1314,26 +1336,26 @@ describe(MetadataService.name, () => { 'should transform RegionInfo geometry according to exif orientation $description', async ({ orientation, expected }) => { const { imgW, imgH, x1, x2, y1, y2 } = expected; + const asset = AssetFactory.create(); + const person = PersonFactory.create(); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.primaryImage); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); - mockReadTags(makeFaceTags({ Name: personStub.withName.name }, orientation)); + mockReadTags(makeFaceTags({ Name: person.name }, orientation)); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([personStub.withName.id]); - mocks.person.update.mockResolvedValue(personStub.withName); - await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.primaryImage.id); - expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { + mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.update.mockResolvedValue(person); + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); + expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true, }); - expect(mocks.person.createAll).toHaveBeenCalledWith([ - expect.objectContaining({ name: personStub.withName.name }), - ]); + expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]); expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ { id: 'random-uuid', - assetId: assetStub.primaryImage.id, + assetId: asset.id, personId: 'random-uuid', imageWidth: imgW, imageHeight: imgH, @@ -1347,12 +1369,12 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: 'admin-id', faceAssetId: 'random-uuid' }, + { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.withName.id }, + data: { id: person.id }, }, ]); }, @@ -1360,10 +1382,11 @@ describe(MetadataService.name, () => { }); it('should handle invalid modify date', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ModifyDate: '00:00:00.000' }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ modifyDate: expect.any(Date), @@ -1373,10 +1396,11 @@ describe(MetadataService.name, () => { }); it('should handle invalid rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: 6 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: null, @@ -1386,10 +1410,11 @@ describe(MetadataService.name, () => { }); it('should handle valid rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: 5 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: 5, @@ -1398,11 +1423,26 @@ describe(MetadataService.name, () => { ); }); + it('should handle 0 as unrated -> null', async () => { + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mockReadTags({ Rating: 0 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: null, + }), + { lockedPropertiesBehavior: 'skip' }, + ); + }); + it('should handle valid negative rating value', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ Rating: -1 }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ rating: -1, @@ -1412,11 +1452,12 @@ describe(MetadataService.name, () => { }); it('should handle livePhotoCID not set', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalledWith( expect.objectContaining({ visibility: AssetVisibility.Hidden }), @@ -1425,17 +1466,18 @@ describe(MetadataService.name, () => { }); it('should handle not finding a match', async () => { + const asset = AssetFactory.create({ type: AssetType.Video }); mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoMotionAsset); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ livePhotoCID: 'CID', - ownerId: assetStub.livePhotoMotionAsset.ownerId, - otherAssetId: assetStub.livePhotoMotionAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, libraryId: null, type: AssetType.Image, }); @@ -1446,65 +1488,67 @@ describe(MetadataService.name, () => { }); it('should link photo and video', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.livePhotoStillAsset); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.id); + expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + libraryId: null, livePhotoCID: 'CID', - ownerId: assetStub.livePhotoStillAsset.ownerId, - otherAssetId: assetStub.livePhotoStillAsset.id, + ownerId: asset.ownerId, + otherAssetId: asset.id, type: AssetType.Video, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + id: asset.id, + livePhotoVideoId: motionAsset.id, }); expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoMotionAsset.id, + id: motionAsset.id, visibility: AssetVisibility.Hidden, }); - expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); + expect(mocks.album.removeAssetsFromAll).toHaveBeenCalledWith([motionAsset.id]); }); it('should notify clients on live photo link', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video }); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { - userId: assetStub.livePhotoMotionAsset.ownerId, - assetId: assetStub.livePhotoMotionAsset.id, + userId: motionAsset.ownerId, + assetId: motionAsset.id, }); }); it('should search by libraryId', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - ...assetStub.livePhotoStillAsset, - libraryId: 'library-id', - }); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + const motionAsset = AssetFactory.create({ type: AssetType.Video, libraryId: 'library-id' }); + const asset = AssetFactory.create({ libraryId: 'library-id' }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mocks.asset.findLivePhotoMatch.mockResolvedValue(motionAsset); mockReadTags({ ContentIdentifier: 'CID' }); - await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', { - assetId: assetStub.livePhotoStillAsset.id, - userId: assetStub.livePhotoStillAsset.ownerId, + assetId: asset.id, + userId: asset.ownerId, }); expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - ownerId: 'user-id', - otherAssetId: 'live-photo-still-asset', + ownerId: asset.ownerId, + otherAssetId: asset.id, livePhotoCID: 'CID', libraryId: 'library-id', - type: 'VIDEO', + type: AssetType.Video, }); }); @@ -1525,10 +1569,11 @@ describe(MetadataService.name, () => { }, { exif: { AndroidMake: '1', AndroidModel: '2' }, expected: { make: '1', model: '2' } }, ])('should read camera make and model $exif -> $expected', async ({ exif, expected }) => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(exif); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith(expect.objectContaining(expected), { lockedPropertiesBehavior: 'skip', }); @@ -1549,10 +1594,11 @@ describe(MetadataService.name, () => { { exif: { LensID: ' Unknown 6-30mm' }, expected: null }, { exif: { LensID: '' }, expected: null }, ])('should read camera lens information $exif -> $expected', async ({ exif, expected }) => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); mockReadTags(exif); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.asset.upsertExif).toHaveBeenCalledWith( expect.objectContaining({ lensModel: expected, @@ -1560,35 +1606,69 @@ describe(MetadataService.name, () => { { lockedPropertiesBehavior: 'skip' }, ); }); + + it('should properly set width/height for normal images', async () => { + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 1000, + height: 2000, + }), + ); + }); + + it('should properly swap asset width/height for rotated images', async () => { + const asset = AssetFactory.create(); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 2000, + height: 1000, + }), + ); + }); + + it('should not overwrite existing width/height if they already exist', async () => { + const asset = AssetFactory.create({ width: 1920, height: 1080 }); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset); + mockReadTags({ ImageWidth: 1280, ImageHeight: 720 }); + + await sut.handleMetadataExtraction({ id: asset.id }); + expect(mocks.asset.update).not.toHaveBeenCalledWith( + expect.objectContaining({ + width: 1280, + height: 720, + }), + ); + }); }); describe('handleQueueSidecar', () => { it('should queue assets with sidecar files', async () => { - mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset])); await sut.handleQueueSidecar({ force: true }); - expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { - name: JobName.SidecarCheck, - data: { id: assetStub.sidecar.id }, - }, - ]); + expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(true); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]); }); it('should queue assets without sidecar files', async () => { - mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForSidecar.mockReturnValue(makeStream([asset])); await sut.handleQueueSidecar({ force: false }); expect(mocks.assetJob.streamForSidecar).toHaveBeenCalledWith(false); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { - name: JobName.SidecarCheck, - data: { id: assetStub.image.id }, - }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SidecarCheck, data: { id: asset.id } }]); }); }); @@ -1596,7 +1676,7 @@ describe(MetadataService.name, () => { it('should do nothing if asset could not be found', async () => { mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(void 0); - await expect(sut.handleSidecarCheck({ id: assetStub.image.id })).resolves.toBeUndefined(); + await expect(sut.handleSidecarCheck({ id: 'non-existent' })).resolves.toBeUndefined(); expect(mocks.asset.update).not.toHaveBeenCalled(); }); @@ -1638,7 +1718,7 @@ describe(MetadataService.name, () => { it('should unset sidecar path if file no longer exist', async () => { const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', - files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }], }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValue(false); @@ -1651,7 +1731,7 @@ describe(MetadataService.name, () => { it('should do nothing if the sidecar file still exists', async () => { const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', - files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar, isEdited: false }], }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); @@ -1684,13 +1764,14 @@ describe(MetadataService.name, () => { const asset = factory.jobAssets.sidecarWrite(); const description = 'this is a description'; const gps = 12; - const date = '2023-11-22T04:56:12.196Z'; + const date = '2023-11-21T22:56:12.196-06:00'; mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue([ 'description', 'latitude', 'longitude', 'dateTimeOriginal', + 'timeZone', ]); mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); await expect( @@ -1705,6 +1786,35 @@ describe(MetadataService.name, () => { GPSLatitude: gps, GPSLongitude: gps, }); + expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, [ + 'description', + 'latitude', + 'longitude', + 'dateTimeOriginal', + 'timeZone', + ]); + }); + + it('should write rating', async () => { + const asset = factory.jobAssets.sidecarWrite(); + asset.exifInfo.rating = 4; + + mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']); + mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); + await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success); + expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 4 }); + expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']); + }); + + it('should write null rating as 0', async () => { + const asset = factory.jobAssets.sidecarWrite(); + asset.exifInfo.rating = null; + + mocks.assetJob.getLockedPropertiesForMetadataExtraction.mockResolvedValue(['rating']); + mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(asset); + await expect(sut.handleSidecarWrite({ id: asset.id })).resolves.toBe(JobStatus.Success); + expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Rating: 0 }); + expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, ['rating']); }); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 8da7c88ecc..f22d4682fa 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -32,9 +32,14 @@ import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; +import { mergeTimeZone } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; import { isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; +import { Tasks } from 'src/utils/tasks'; + +const POSTGRES_INT_MAX = 2_147_483_647; +const POSTGRES_INT_MIN = -2_147_483_648; /** look for a date from these tags (in order) */ const EXIF_DATE_TAGS: Array = [ @@ -88,7 +93,10 @@ const validate = (value: T): NonNullable | null => { return null; } - if (typeof value === 'number' && (Number.isNaN(value) || !Number.isFinite(value))) { + if ( + typeof value === 'number' && + (Number.isNaN(value) || !Number.isFinite(value) || value < POSTGRES_INT_MIN || value > POSTGRES_INT_MAX) + ) { return null; } @@ -161,7 +169,7 @@ export class MetadataService extends BaseService { this.logger.log(`Initialized local reverse geocoder`); } catch (error: Error | any) { this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack); - throw new Error(`Metadata service init failed`); + throw new Error('Metadata service init failed', { cause: error }); } } @@ -196,6 +204,15 @@ export class MetadataService extends BaseService { await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId }); } + private isOrientationSidewards(orientation: ExifOrientation | number): boolean { + return [ + ExifOrientation.MirrorHorizontalRotate270CW, + ExifOrientation.Rotate90CW, + ExifOrientation.MirrorHorizontalRotate90CW, + ExifOrientation.Rotate270CW, + ].includes(orientation); + } + @OnJob({ name: JobName.AssetExtractMetadataQueueAll, queue: QueueName.MetadataExtraction }) async handleQueueMetadataExtraction(job: JobOf): Promise { const { force } = job; @@ -245,6 +262,8 @@ export class MetadataService extends BaseService { } } + const tags = this.getTagList(exifTags); + const exifData: Insertable = { assetId: asset.id, @@ -267,11 +286,11 @@ export class MetadataService extends BaseService { orientation: validate(exifTags.Orientation)?.toString() ?? null, projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, bitsPerSample: this.getBitsPerSample(exifTags), - colorspace: exifTags.ColorSpace ?? null, + colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace), // camera - make: exifTags.Make ?? exifTags?.Device?.Manufacturer ?? exifTags.AndroidMake ?? null, - model: exifTags.Model ?? exifTags?.Device?.ModelName ?? exifTags.AndroidModel ?? null, + make: exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? null, + model: exifTags.Model ?? exifTags.Device?.ModelName ?? exifTags.AndroidModel ?? null, fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), iso: validate(exifTags.ISO) as number, exposureTime: exifTags.ExposureTime ?? null, @@ -282,34 +301,51 @@ export class MetadataService extends BaseService { // comments description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), profileDescription: exifTags.ProfileDescription || null, - rating: validateRange(exifTags.Rating, -1, 5), + rating: exifTags.Rating === 0 ? null : validateRange(exifTags.Rating, -1, 5), // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, autoStackId: this.getAutoStackId(exifTags), + + tags: tags.length > 0 ? tags : null, }; - const promises: Promise[] = [ - this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }), - this.assetRepository.update({ - id: asset.id, - duration: this.getDuration(exifTags), - localDateTime: dates.localDateTime, - fileCreatedAt: dates.dateTimeOriginal ?? undefined, - fileModifiedAt: stats.mtime, - }), - this.applyTagList(asset, exifTags), - ]; + const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation); + const assetWidth = isSidewards ? validate(height) : validate(width); + const assetHeight = isSidewards ? validate(width) : validate(height); + + const tasks = new Tasks(); + + tasks.push( + () => + this.assetRepository.update({ + id: asset.id, + duration: this.getDuration(exifTags), + localDateTime: dates.localDateTime, + fileCreatedAt: dates.dateTimeOriginal ?? undefined, + fileModifiedAt: stats.mtime, + + // only update the dimensions if they don't already exist + // we don't want to overwrite width/height that are modified by edits + width: asset.width == null ? assetWidth : undefined, + height: asset.height == null ? assetHeight : undefined, + }), + async () => { + await this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }); + await this.applyTagList(asset); + }, + ); if (this.isMotionPhoto(asset, exifTags)) { - promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats)); + tasks.push(() => this.applyMotionPhotos(asset, exifTags, dates, stats)); } if (isFaceImportEnabled(metadata) && this.hasTaggedFaces(exifTags)) { - promises.push(this.applyTaggedFaces(asset, exifTags)); + tasks.push(() => this.applyTaggedFaces(asset, exifTags)); } - await Promise.all(promises); + await tasks.all(); + if (exifData.livePhotoCID) { await this.linkLivePhotos(asset, exifData); } @@ -366,9 +402,13 @@ export class MetadataService extends BaseService { const isChanged = sidecarPath !== sidecarFile?.path; - this.logger.debug( - `Sidecar check found old=${sidecarFile?.path}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`, - ); + if (sidecarFile?.path || sidecarPath) { + this.logger.debug( + `Sidecar check found old=${sidecarFile?.path}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`, + ); + } else { + this.logger.verbose(`No sidecars found for asset ${asset.id}: ${asset.originalPath}`); + } if (!isChanged) { return JobStatus.Skipped; @@ -383,35 +423,37 @@ export class MetadataService extends BaseService { @OnEvent({ name: 'AssetTag' }) async handleTagAsset({ assetId }: ArgOf<'AssetTag'>) { - await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId } }); } @OnEvent({ name: 'AssetUntag' }) async handleUntagAsset({ assetId }: ArgOf<'AssetUntag'>) { - await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId, tags: true } }); + await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id: assetId } }); } @OnJob({ name: JobName.SidecarWrite, queue: QueueName.Sidecar }) async handleSidecarWrite(job: JobOf): Promise { - const { id, tags } = job; + const { id } = job; const asset = await this.assetJobRepository.getForSidecarWriteJob(id); if (!asset) { return JobStatus.Failed; } const lockedProperties = await this.assetJobRepository.getLockedPropertiesForMetadataExtraction(id); - const tagsList = (asset.tags || []).map((tag) => tag.value); const { sidecarFile } = getAssetFiles(asset.files); const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; - const { description, dateTimeOriginal, latitude, longitude, rating } = _.pick( + const { description, dateTimeOriginal, latitude, longitude, rating, tags, timeZone } = _.pick( { description: asset.exifInfo.description, - dateTimeOriginal: asset.exifInfo.dateTimeOriginal, + // the kysely type is wrong here; fixed in 0.28.3 + dateTimeOriginal: asset.exifInfo.dateTimeOriginal as string | null, latitude: asset.exifInfo.latitude, longitude: asset.exifInfo.longitude, - rating: asset.exifInfo.rating, + rating: asset.exifInfo.rating ?? 0, + tags: asset.exifInfo.tags, + timeZone: asset.exifInfo.timeZone, }, lockedProperties, ); @@ -420,11 +462,11 @@ export class MetadataService extends BaseService { { Description: description, ImageDescription: description, - DateTimeOriginal: dateTimeOriginal ? String(dateTimeOriginal) : undefined, + DateTimeOriginal: mergeTimeZone(dateTimeOriginal, timeZone)?.toISO(), GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, - TagsList: tags ? tagsList : undefined, + TagsList: tags?.length ? tags : undefined, }, _.isUndefined, ); @@ -439,6 +481,8 @@ export class MetadataService extends BaseService { await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: sidecarPath }); } + await this.assetRepository.unlockProperties(asset.id, lockedProperties); + return JobStatus.Success; } @@ -495,6 +539,15 @@ export class MetadataService extends BaseService { for (const tag of EXIF_DATE_TAGS) { delete mediaTags[tag]; } + + // exiftool-vendored derives tz information from the date. + // if the sidecar file has date information, we also assume the tz information come from there. + // + // this is especially important in the case of UTC+0 where exiftool-vendored does not return tz/zone fields + // and as such the tags aren't overwritten when returning all tags. + for (const tag of ['zone', 'tz', 'tzSource'] as const) { + delete mediaTags[tag]; + } } } @@ -536,11 +589,14 @@ export class MetadataService extends BaseService { return tags; } - private async applyTagList(asset: { id: string; ownerId: string }, exifTags: ImmichTags) { - const tags = this.getTagList(exifTags); - const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); + private async applyTagList({ id, ownerId }: { id: string; ownerId: string }) { + const asset = await this.assetRepository.getForMetadataExtractionTags(id); + const results = await upsertTags(this.tagRepository, { + userId: ownerId, + tags: asset?.tags ?? [], + }); await this.tagRepository.replaceAssetTags( - asset.id, + id, results.map((tag) => tag.id), ); } @@ -712,12 +768,7 @@ export class MetadataService extends BaseService { return regionInfo; } - const isSidewards = [ - ExifOrientation.MirrorHorizontalRotate270CW, - ExifOrientation.Rotate90CW, - ExifOrientation.MirrorHorizontalRotate90CW, - ExifOrientation.Rotate270CW, - ].includes(orientation); + const isSidewards = this.isOrientationSidewards(orientation); // swap image dimensions in AppliedToDimensions if orientation is sidewards const adjustedAppliedToDimensions = isSidewards @@ -858,13 +909,17 @@ export class MetadataService extends BaseService { const result = firstDateTime(exifTags); const tag = result?.tag; const dateTime = result?.dateTime; - this.logger.verbose( - `Date and time is ${dateTime} using exifTag ${tag} for asset ${asset.id}: ${asset.originalPath}`, - ); + if (dateTime) { + this.logger.verbose( + `Date and time is ${dateTime} using exifTag ${tag} for asset ${asset.id}: ${asset.originalPath}`, + ); + } else { + this.logger.verbose(`No exif date time information found for asset ${asset.id}: ${asset.originalPath}`); + } // timezone - let timeZone = exifTags.tz ?? null; - if (timeZone == null && dateTime?.rawValue?.endsWith('+00:00')) { + let timeZone = exifTags.zone ?? null; + if (timeZone == null && (dateTime?.rawValue?.endsWith('Z') || dateTime?.rawValue?.endsWith('+00:00'))) { // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly // https://github.com/photostructure/exiftool-vendored.js/issues/203 timeZone = 'UTC+0'; @@ -872,7 +927,7 @@ export class MetadataService extends BaseService { if (timeZone) { this.logger.verbose( - `Found timezone ${timeZone} via ${exifTags.tzSource} for asset ${asset.id}: ${asset.originalPath}`, + `Found timezone ${timeZone} via ${exifTags.zoneSource} for asset ${asset.id}: ${asset.originalPath}`, ); } else { this.logger.debug(`No timezone information found for asset ${asset.id}: ${asset.originalPath}`); @@ -963,9 +1018,17 @@ export class MetadataService extends BaseService { private async getVideoTags(originalPath: string) { const { videoStreams, format } = await this.mediaRepository.probe(originalPath); - const tags: Pick = {}; + const tags: Pick = {}; if (videoStreams[0]) { + // Set video dimensions + if (videoStreams[0].width) { + tags.ImageWidth = videoStreams[0].width; + } + if (videoStreams[0].height) { + tags.ImageHeight = videoStreams[0].height; + } + switch (videoStreams[0].rotation) { case -90: { tags.Orientation = ExifOrientation.Rotate90CW; diff --git a/server/src/services/notification-admin.service.ts b/server/src/services/notification-admin.service.ts index bf0d2bba41..2fc4584dca 100644 --- a/server/src/services/notification-admin.service.ts +++ b/server/src/services/notification-admin.service.ts @@ -59,7 +59,7 @@ export class NotificationAdminService extends BaseService { async getTemplate(name: EmailTemplate, customTemplate: string) { const { server, templates } = await this.getConfig({ withCache: false }); - let templateResponse = ''; + let templateResponse: string; switch (name) { case EmailTemplate.WELCOME: { diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index daa3f221ae..ee4b4ec05f 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,14 +1,16 @@ import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; -import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { notificationStub } from 'test/fixtures/notification.stub'; import { userStub } from 'test/fixtures/user.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; const configs = { @@ -267,14 +269,14 @@ describe(NotificationService.name, () => { }); it('should skip if recipient could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should skip if the recipient has email notifications disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -290,7 +292,7 @@ describe(NotificationService.name, () => { }); it('should skip if the recipient has email notifications for album invite disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -306,7 +308,7 @@ describe(NotificationService.name, () => { }); it('should send invite email', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -328,7 +330,8 @@ describe(NotificationService.name, () => { }); it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -345,7 +348,7 @@ describe(NotificationService.name, () => { await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -358,7 +361,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail as jpeg', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail }); + const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -371,13 +376,11 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ - { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg' }, - ]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -390,7 +393,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail and arbitrary extension', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const asset = AssetFactory.from().file({ type: AssetFileType.Thumbnail }).build(); + const album = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).asset(asset).build(); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -403,18 +408,18 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetStub.image.files[2]]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([asset.files[0]]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SendMail, data: expect.objectContaining({ subject: expect.stringContaining('You have been added to a shared album'), - imageAttachments: [{ filename: 'album-thumbnail.ext', path: expect.anything(), cid: expect.anything() }], + imageAttachments: [{ filename: 'album-thumbnail.jpg', path: expect.anything(), cid: expect.anything() }], }), }); }); @@ -427,85 +432,74 @@ describe(NotificationService.name, () => { }); it('should skip if owner could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + mocks.album.getById.mockResolvedValue(AlbumFactory.create({ ownerId: 'non-existent' })); await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); it('should skip recipient that could not be looked up', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValueOnce(userStub.user1); + const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValueOnce(album.owner); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: 'non-existent' }); + expect(mocks.user.get).toHaveBeenCalledWith('non-existent', { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: false, albumUpdate: true } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: false, albumUpdate: true } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications for the album update event', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: true, albumUpdate: false } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: true, albumUpdate: false } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should send email', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalled(); }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index ee87fcf775..9f11d19af7 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -134,7 +134,7 @@ export class NotificationService extends BaseService { } } catch (error: Error | any) { this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack); - throw new Error(`Invalid SMTP configuration: ${error}`); + throw new Error('Invalid SMTP configuration', { cause: error }); } } diff --git a/server/src/services/ocr.service.spec.ts b/server/src/services/ocr.service.spec.ts index 404f423cac..d5b146e942 100644 --- a/server/src/services/ocr.service.spec.ts +++ b/server/src/services/ocr.service.spec.ts @@ -1,6 +1,6 @@ -import { AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; +import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { OcrService } from 'src/services/ocr.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -14,7 +14,7 @@ describe(OcrService.name, () => { mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Timeline, - previewFile: assetStub.image.files[1].path, + previewFile: '/uploads/user-id/thumbs/path.jpg', }); }); @@ -41,20 +41,22 @@ describe(OcrService.name, () => { }); it('should queue the assets without ocr', async () => { - mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset])); await sut.handleQueueOcr({ force: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]); expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(false); }); it('should queue all the assets', async () => { - mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForOcrJob.mockReturnValue(makeStream([asset])); await sut.handleQueueOcr({ force: true }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: assetStub.image.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.Ocr, data: { id: asset.id } }]); expect(mocks.assetJob.streamForOcrJob).toHaveBeenCalledWith(true); }); }); @@ -70,15 +72,17 @@ describe(OcrService.name, () => { }); it('should skip assets without a resize path', async () => { + const asset = AssetFactory.create(); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Timeline, previewFile: null }); - expect(await sut.handleOcr({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Failed); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { + const asset = AssetFactory.create(); mocks.machineLearning.ocr.mockResolvedValue({ box: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160], boxScore: [0.9, 0.8], @@ -86,7 +90,7 @@ describe(OcrService.name, () => { textScore: [0.95, 0.85], }); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.ocr).toHaveBeenCalledWith( '/uploads/user-id/thumbs/path.jpg', @@ -98,10 +102,10 @@ describe(OcrService.name, () => { }), ); expect(mocks.ocr.upsert).toHaveBeenCalledWith( - assetStub.image.id, + asset.id, [ { - assetId: assetStub.image.id, + assetId: asset.id, boxScore: 0.9, text: 'One Two Three', textScore: 0.95, @@ -115,7 +119,7 @@ describe(OcrService.name, () => { y4: 80, }, { - assetId: assetStub.image.id, + assetId: asset.id, boxScore: 0.8, text: 'Four Five', textScore: 0.85, @@ -134,6 +138,7 @@ describe(OcrService.name, () => { }); it('should apply config settings', async () => { + const asset = AssetFactory.create(); mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { enabled: true, @@ -148,7 +153,7 @@ describe(OcrService.name, () => { }); mockOcrResult(); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.ocr).toHaveBeenCalledWith( '/uploads/user-id/thumbs/path.jpg', @@ -159,16 +164,17 @@ describe(OcrService.name, () => { maxResolution: 1500, }), ); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, [], ''); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, [], ''); }); it('should skip invisible assets', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.assetJob.getForOcr.mockResolvedValue({ visibility: AssetVisibility.Hidden, - previewFile: assetStub.image.files[1].path, + previewFile: asset.files[0].path, }); - expect(await sut.handleOcr({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleOcr({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); @@ -177,7 +183,7 @@ describe(OcrService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForOcr.mockResolvedValue(void 0); - expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Failed); + expect(await sut.handleOcr({ id: 'non-existent' })).toEqual(JobStatus.Failed); expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); @@ -185,79 +191,84 @@ describe(OcrService.name, () => { describe('search tokenization', () => { it('should generate bigrams for Chinese text', async () => { + const asset = AssetFactory.create(); mockOcrResult('抟器學įŋ’'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '抟器 器學 å­¸įŋ’'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '抟器 器學 å­¸įŋ’'); }); it('should generate bigrams for Japanese text', async () => { + const asset = AssetFactory.create(); mockOcrResult('テ゚ト'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'テ゚ ゚ト'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'テ゚ ゚ト'); }); it('should generate bigrams for Korean text', async () => { + const asset = AssetFactory.create(); mockOcrResult('한ęĩ­ė–´'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '한ęĩ­ ęĩ­ė–´'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '한ęĩ­ ęĩ­ė–´'); }); it('should pass through Latin text unchanged', async () => { + const asset = AssetFactory.create(); mockOcrResult('Hello World'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World'); }); it('should handle mixed CJK and Latin text', async () => { + const asset = AssetFactory.create(); mockOcrResult('抟器學įŋ’Model'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '抟器 器學 å­¸įŋ’ Model'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '抟器 器學 å­¸įŋ’ Model'); }); it('should handle year followed by CJK', async () => { + const asset = AssetFactory.create(); mockOcrResult('2024åš´ãƒŦポãƒŧト'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith( - assetStub.image.id, - expect.any(Array), - '2024 åš´ãƒŦ ãƒŦポ ポãƒŧ ãƒŧト', - ); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '2024 åš´ãƒŦ ãƒŦポ ポãƒŧ ãƒŧト'); }); it('should join multiple OCR boxes', async () => { + const asset = AssetFactory.create(); mockOcrResult('抟器', 'Learning'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '抟器 Learning'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), '抟器 Learning'); }); it('should normalize whitespace', async () => { + const asset = AssetFactory.create(); mockOcrResult(' Hello World '); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'Hello World'); }); it('should keep single CJK characters', async () => { + const asset = AssetFactory.create(); mockOcrResult('A', '中', 'B'); - await sut.handleOcr({ id: assetStub.image.id }); + await sut.handleOcr({ id: asset.id }); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'A 中 B'); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(asset.id, expect.any(Array), 'A 中 B'); }); }); }); diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 41c44ea476..c22fd65a1a 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,61 +1,21 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto'; -import { CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; -import { DetectedFaces } from 'src/repositories/machine-learning.repository'; +import { mapFaces, mapPerson } from 'src/dtos/person.dto'; +import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; import { FaceSearchResult } from 'src/repositories/search.repository'; import { PersonService } from 'src/services/person.service'; import { ImmichFileResponse } from 'src/utils/file'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { PersonFactory } from 'test/factories/person.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { faceStub } from 'test/fixtures/face.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; -import { factory } from 'test/small.factory'; +import { getAsDetectedFace, getForFacialRecognitionJob } from 'test/mappers'; +import { newDate, newUuid } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; -const responseDto: PersonResponseDto = { - id: 'person-1', - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - isHidden: false, - updatedAt: expect.any(Date), - isFavorite: false, - color: expect.any(String), -}; - -const statistics = { assets: 3 }; - -const faceId = 'face-id'; -const face = { - id: faceId, - assetId: 'asset-id', - boundingBoxX1: 100, - boundingBoxY1: 100, - boundingBoxX2: 200, - boundingBoxY2: 200, - imageHeight: 500, - imageWidth: 400, -}; -const faceSearch = { faceId, embedding: '[1, 2, 3, 4]' }; -const detectFaceMock: DetectedFaces = { - faces: [ - { - boundingBox: { - x1: face.boundingBoxX1, - y1: face.boundingBoxY1, - x2: face.boundingBoxX2, - y2: face.boundingBoxY2, - }, - embedding: faceSearch.embedding, - score: 0.2, - }, - ], - imageHeight: face.imageHeight, - imageWidth: face.imageWidth, -}; - describe(PersonService.name, () => { let sut: PersonService; let mocks: ServiceMocks; @@ -70,60 +30,54 @@ describe(PersonService.name, () => { describe('getAll', () => { it('should get all hidden and visible people with thumbnails', async () => { + const auth = AuthFactory.create(); + const [person, hiddenPerson] = [PersonFactory.create(), PersonFactory.create({ isHidden: true })]; + mocks.person.getAllForUser.mockResolvedValue({ - items: [personStub.withName, personStub.hidden], + items: [person, hiddenPerson], hasNextPage: false, }); mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 }); - await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({ + await expect(sut.getAll(auth, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({ hasNextPage: false, total: 2, hidden: 1, people: [ - responseDto, - { - id: 'person-1', - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', + expect.objectContaining({ id: person.id, isHidden: false }), + expect.objectContaining({ + id: hiddenPerson.id, isHidden: true, - isFavorite: false, - updatedAt: expect.any(Date), - color: expect.any(String), - }, + }), ], }); - expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { + expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { minimumFaceCount: 3, withHidden: true, }); }); it('should get all visible people and favorites should be first in the array', async () => { + const auth = AuthFactory.create(); + const [isFavorite, person] = [PersonFactory.create({ isFavorite: true }), PersonFactory.create()]; + mocks.person.getAllForUser.mockResolvedValue({ - items: [personStub.isFavorite, personStub.withName], + items: [isFavorite, person], hasNextPage: false, }); mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 }); - await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({ + await expect(sut.getAll(auth, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({ hasNextPage: false, total: 2, hidden: 1, people: [ - { - id: 'person-4', - name: personStub.isFavorite.name, - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - isHidden: false, + expect.objectContaining({ + id: isFavorite.id, isFavorite: true, - updatedAt: expect.any(Date), - color: personStub.isFavorite.color, - }, - responseDto, + }), + expect.objectContaining({ id: person.id, isFavorite: false }), ], }); - expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, { + expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { minimumFaceCount: 3, withHidden: false, }); @@ -132,71 +86,89 @@ describe(PersonService.name, () => { describe('getById', () => { it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getById(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw a bad request when person is not found', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown'])); + await expect(sut.getById(auth, 'unknown')).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown'])); }); it('should get a person by id', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); - expect(mocks.person.getById).toHaveBeenCalledWith('person-1'); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getById(auth, person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); + expect(mocks.person.getById).toHaveBeenCalledWith(person.id); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('getThumbnail', () => { it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when personId is invalid', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); + const auth = AuthFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown'])); + await expect(sut.getThumbnail(auth, 'unknown')).rejects.toBeInstanceOf(NotFoundException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown'])); }); it('should throw an error when person has no thumbnail', async () => { - mocks.person.getById.mockResolvedValue(personStub.noThumbnail); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ thumbnailPath: '' }); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(NotFoundException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should serve the thumbnail', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getThumbnail(authStub.admin, 'person-1')).resolves.toEqual( + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getThumbnail(auth, person.id)).resolves.toEqual( new ImmichFileResponse({ - path: '/path/to/thumbnail.jpg', + path: person.thumbnailPath, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithoutCache, }), ); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('update', () => { it('should require person.write permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( - BadRequestException, - ); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.update(auth, person.id, { name: 'Person 1' })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when personId is invalid', async () => { @@ -209,88 +181,108 @@ describe(PersonService.name, () => { }); it("should update a person's name", async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ name: 'Person 1' }); - await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { name: 'Person 1' })).resolves.toEqual( + expect.objectContaining({ id: person.id, name: 'Person 1' }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, name: 'Person 1' }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it("should update a person's date of birth", async () => { - mocks.person.update.mockResolvedValue(personStub.withBirthDate); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ birthDate: new Date('1976-06-30') }); - await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ - id: 'person-1', - name: 'Person 1', + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + + await expect(sut.update(auth, person.id, { birthDate: new Date('1976-06-30') })).resolves.toEqual({ + id: person.id, + name: person.name, birthDate: '1976-06-30', - thumbnailPath: '/path/to/thumbnail.jpg', + thumbnailPath: person.thumbnailPath, isHidden: false, isFavorite: false, updatedAt: expect.any(Date), - color: expect.any(String), }); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: new Date('1976-06-30') }); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should update a person visibility', async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ isHidden: true }); - await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { isHidden: true })).resolves.toEqual( + expect.objectContaining({ isHidden: true }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isHidden: true }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should update a person favorite status', async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create({ isFavorite: true }); - await expect(sut.update(authStub.admin, 'person-1', { isFavorite: true })).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isFavorite: true }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + await expect(sut.update(auth, person.id, { isFavorite: true })).resolves.toEqual( + expect.objectContaining({ isFavorite: true }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isFavorite: true }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it("should update a person's thumbnailPath", async () => { - mocks.person.update.mockResolvedValue(personStub.withName); - mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const face = AssetFaceFactory.create(); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); - await expect( - sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), - ).resolves.toEqual(responseDto); + mocks.person.update.mockResolvedValue(person); + mocks.person.getForFeatureFaceUpdate.mockResolvedValue(face); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([face.assetId])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id }); - expect(mocks.person.getFacesByIds).toHaveBeenCalledWith([ - { - assetId: faceStub.face1.assetId, - personId: 'person-1', - }, - ]); + await expect(sut.update(auth, person.id, { featureFaceAssetId: face.assetId })).resolves.toEqual( + expect.objectContaining({ id: person.id }), + ); + + expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: face.id }); + expect(mocks.person.getForFeatureFaceUpdate).toHaveBeenCalledWith({ + assetId: face.assetId, + personId: person.id, + }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonGenerateThumbnail, - data: { id: 'person-1' }, + data: { id: person.id }, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when the face feature assetId is invalid', async () => { - mocks.person.getById.mockResolvedValue(personStub.withName); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); - await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow( - BadRequestException, - ); + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + + await expect(sut.update(auth, person.id, { featureFaceAssetId: '-1' })).rejects.toThrow(BadRequestException); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); @@ -311,34 +303,39 @@ describe(PersonService.name, () => { mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.reassignFaces(authStub.admin, personStub.noName.id, { + sut.reassignFaces(AuthFactory.create(), 'person-id', { data: [{ personId: 'asset-face-1', assetId: '' }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queueAll).not.toHaveBeenCalledWith(); }); + it('should reassign a face', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id])); - mocks.person.getById.mockResolvedValue(personStub.noName); - mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); - mocks.person.getFacesByIds.mockResolvedValue([faceStub.face1]); + const face = AssetFaceFactory.create(); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); + mocks.person.getFacesByIds.mockResolvedValue([face]); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getRandomFace.mockResolvedValue(faceStub.primaryFace1); + mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create()); mocks.person.refreshFaces.mockResolvedValue(); mocks.person.reassignFace.mockResolvedValue(5); - mocks.person.update.mockResolvedValue(personStub.noName); + mocks.person.update.mockResolvedValue(person); await expect( - sut.reassignFaces(authStub.admin, personStub.noName.id, { - data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }], + sut.reassignFaces(auth, person.id, { + data: [{ personId: person.id, assetId: face.assetId }], }), ).resolves.toBeDefined(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); @@ -346,22 +343,26 @@ describe(PersonService.name, () => { describe('handlePersonMigration', () => { it('should not move person files', async () => { - await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.Failed); + await expect(sut.handlePersonMigration(PersonFactory.create())).resolves.toBe(JobStatus.Failed); }); }); describe('getFacesById', () => { it('should get the bounding boxes for an asset', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId])); - mocks.person.getFaces.mockResolvedValue([faceStub.primaryFace1]); - await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([ - mapFaces(faceStub.primaryFace1, authStub.admin), - ]); + const auth = AuthFactory.create(); + const face = AssetFaceFactory.create(); + const asset = AssetFactory.from({ id: face.assetId }).exif().build(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.person.getFaces.mockResolvedValue([face]); + mocks.asset.getForFaces.mockResolvedValue({ edits: [], ...asset.exifInfo }); + await expect(sut.getFacesById(auth, { id: face.assetId })).resolves.toStrictEqual([mapFaces(face, auth)]); }); + it('should reject if the user has not access to the asset', async () => { + const face = AssetFaceFactory.create(); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set()); - mocks.person.getFaces.mockResolvedValue([faceStub.primaryFace1]); - await expect(sut.getFacesById(authStub.admin, { id: faceStub.primaryFace1.assetId })).rejects.toBeInstanceOf( + mocks.person.getFaces.mockResolvedValue([face]); + await expect(sut.getFacesById(AuthFactory.create(), { id: face.assetId })).rejects.toBeInstanceOf( BadRequestException, ); }); @@ -369,12 +370,14 @@ describe(PersonService.name, () => { describe('createNewFeaturePhoto', () => { it('should change person feature photo', async () => { - mocks.person.getRandomFace.mockResolvedValue(faceStub.primaryFace1); - await sut.createNewFeaturePhoto([personStub.newThumbnail.id]); + const person = PersonFactory.create(); + + mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create()); + await sut.createNewFeaturePhoto([person.id]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: personStub.newThumbnail.id }, + data: { id: person.id }, }, ]); }); @@ -382,24 +385,22 @@ describe(PersonService.name, () => { describe('reassignFacesById', () => { it('should create a new person', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); - mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); - mocks.person.getFaceById.mockResolvedValue(faceStub.face1); + const face = AssetFaceFactory.create(); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); + mocks.person.getFaceById.mockResolvedValue(face); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(personStub.noName); - await expect( - sut.reassignFacesById(authStub.admin, personStub.noName.id, { - id: faceStub.face1.id, - }), - ).resolves.toEqual({ - birthDate: personStub.noName.birthDate, - isHidden: personStub.noName.isHidden, - isFavorite: personStub.noName.isFavorite, - id: personStub.noName.id, - name: personStub.noName.name, - thumbnailPath: personStub.noName.thumbnailPath, + mocks.person.getById.mockResolvedValue(person); + await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({ + birthDate: person.birthDate, + isHidden: person.isHidden, + isFavorite: person.isFavorite, + id: person.id, + name: person.name, + thumbnailPath: person.thumbnailPath, updatedAt: expect.any(Date), - color: personStub.noName.color, }); expect(mocks.job.queue).not.toHaveBeenCalledWith(); @@ -407,13 +408,16 @@ describe(PersonService.name, () => { }); it('should fail if user has not the correct permissions on the asset', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id])); - mocks.person.getFaceById.mockResolvedValue(faceStub.face1); + const face = AssetFaceFactory.create(); + const person = PersonFactory.create(); + + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.person.getFaceById.mockResolvedValue(face); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(personStub.noName); + mocks.person.getById.mockResolvedValue(person); await expect( - sut.reassignFacesById(authStub.admin, personStub.noName.id, { - id: faceStub.face1.id, + sut.reassignFacesById(AuthFactory.create(), person.id, { + id: face.id, }), ).rejects.toBeInstanceOf(BadRequestException); @@ -424,22 +428,25 @@ describe(PersonService.name, () => { describe('createPerson', () => { it('should create a new person', async () => { - mocks.person.create.mockResolvedValue(personStub.primaryPerson); + const auth = AuthFactory.create(); - await expect(sut.create(authStub.admin, {})).resolves.toBeDefined(); + mocks.person.create.mockResolvedValue(PersonFactory.create()); + await expect(sut.create(auth, {})).resolves.toBeDefined(); - expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id }); + expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: auth.user.id }); }); }); describe('handlePersonCleanup', () => { it('should delete people without faces', async () => { - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.noName]); + const person = PersonFactory.create(); + + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); await sut.handlePersonCleanup(); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.noName.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.noName.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); }); }); @@ -454,7 +461,8 @@ describe(PersonService.name, () => { }); it('should queue missing assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); await sut.handleQueueDetectFaces({ force: false }); @@ -463,32 +471,36 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should queue all assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]); + const asset = AssetFactory.create(); + const person = PersonFactory.create(); + + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); await sut.handleQueueDetectFaces({ force: true }); expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning }); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); }); it('should refresh all assets', async () => { - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); await sut.handleQueueDetectFaces({ force: undefined }); @@ -500,17 +512,21 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonCleanup }); }); it('should delete existing people and faces if forced', async () => { - mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson])); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); - mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([assetStub.image])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + const asset = AssetFactory.create(); + const face = AssetFaceFactory.from().person().build(); + const person = PersonFactory.create(); + + mocks.person.getAll.mockReturnValue(makeStream([face.person!, person])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); + mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); mocks.person.deleteFaces.mockResolvedValue(); await sut.handleQueueDetectFaces({ force: true }); @@ -519,11 +535,11 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.AssetDetectFaces, - data: { id: assetStub.image.id }, + data: { id: asset.id }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); }); }); @@ -562,6 +578,7 @@ describe(PersonService.name, () => { }); it('should queue missing assets', async () => { + const face = AssetFaceFactory.create(); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, @@ -570,7 +587,7 @@ describe(PersonService.name, () => { failed: 0, delayed: 0, }); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.person.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({}); @@ -582,7 +599,7 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognition, - data: { id: faceStub.face1.id, deferred: false }, + data: { id: face.id, deferred: false }, }, ]); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { @@ -592,6 +609,7 @@ describe(PersonService.name, () => { }); it('should queue all assets', async () => { + const face = AssetFaceFactory.create(); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, @@ -601,7 +619,7 @@ describe(PersonService.name, () => { delayed: 0, }); mocks.person.getAll.mockReturnValue(makeStream()); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.person.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({ force: true }); @@ -610,7 +628,7 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognition, - data: { id: faceStub.face1.id, deferred: false }, + data: { id: face.id, deferred: false }, }, ]); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { @@ -620,8 +638,9 @@ describe(PersonService.name, () => { }); it('should run nightly if new face has been added since last run', async () => { + const face = AssetFaceFactory.create(); mocks.person.getLatestFaceDate.mockResolvedValue(new Date().toISOString()); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, @@ -631,7 +650,7 @@ describe(PersonService.name, () => { delayed: 0, }); mocks.person.getAll.mockReturnValue(makeStream()); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.person.getAllWithoutFaces.mockResolvedValue([]); mocks.person.unassignFaces.mockResolvedValue(); @@ -646,7 +665,7 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognition, - data: { id: faceStub.face1.id, deferred: false }, + data: { id: face.id, deferred: false }, }, ]); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState, { @@ -660,7 +679,7 @@ describe(PersonService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ lastRun: lastRun.toISOString() }); mocks.person.getLatestFaceDate.mockResolvedValue(new Date(lastRun.getTime() - 1).toISOString()); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); + mocks.person.getAllFaces.mockReturnValue(makeStream([AssetFaceFactory.create()])); mocks.person.getAllWithoutFaces.mockResolvedValue([]); await sut.handleQueueRecognizeFaces({ force: true, nightly: true }); @@ -674,6 +693,9 @@ describe(PersonService.name, () => { }); it('should delete existing people if forced', async () => { + const face = AssetFaceFactory.from().person().build(); + const person = PersonFactory.create(); + mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, @@ -682,9 +704,9 @@ describe(PersonService.name, () => { failed: 0, delayed: 0, }); - mocks.person.getAll.mockReturnValue(makeStream([faceStub.face1.person, personStub.randomPerson])); - mocks.person.getAllFaces.mockReturnValue(makeStream([faceStub.face1])); - mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]); + mocks.person.getAll.mockReturnValue(makeStream([face.person!, person])); + mocks.person.getAllFaces.mockReturnValue(makeStream([face])); + mocks.person.getAllWithoutFaces.mockResolvedValue([person]); mocks.person.unassignFaces.mockResolvedValue(); await sut.handleQueueRecognizeFaces({ force: true }); @@ -694,20 +716,16 @@ describe(PersonService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognition, - data: { id: faceStub.face1.id, deferred: false }, + data: { id: face.id, deferred: false }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]); - expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath); + expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); }); }); describe('handleDetectFaces', () => { - beforeEach(() => { - mocks.crypto.randomUUID.mockReturnValue(faceId); - }); - it('should skip if machine learning is disabled', async () => { mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); @@ -717,26 +735,28 @@ describe(PersonService.name, () => { }); it('should skip when no resize path', async () => { - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); - await sut.handleDetectFaces({ id: assetStub.noResizePath.id }); + const asset = AssetFactory.create(); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.machineLearning.detectFaces).not.toHaveBeenCalled(); }); it('should handle no results', async () => { const start = Date.now(); + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue({ imageHeight: 500, imageWidth: 400, faces: [] }); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); - await sut.handleDetectFaces({ id: assetStub.image.id }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + await sut.handleDetectFaces({ id: asset.id }); expect(mocks.machineLearning.detectFaces).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({ - assetId: assetStub.image.id, + assetId: asset.id, facesRecognizedAt: expect.any(Date), }); const facesRecognizedAt = mocks.asset.upsertJobStatus.mock.calls[0][0].facesRecognizedAt as Date; @@ -744,93 +764,105 @@ describe(PersonService.name, () => { }); it('should create a face with no person and queue recognition job', async () => { - mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.search.searchFaces.mockResolvedValue([{ ...faceStub.face1, distance: 0.7 }]); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); + const face = AssetFaceFactory.create({ assetId: asset.id }); + mocks.crypto.randomUUID.mockReturnValue(face.id); + mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face)); + mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith( + [expect.objectContaining({ id: face.id, assetId: asset.id })], + [], + [{ faceId: face.id, embedding: '[1, 2, 3, 4]' }], + ); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, - { name: JobName.FacialRecognition, data: { id: faceId } }, + { name: JobName.FacialRecognition, data: { id: face.id } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); }); it('should delete an existing face not among the new detected faces', async () => { + const asset = AssetFactory.from().face().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.detectFaces.mockResolvedValue({ faces: [], imageHeight: 500, imageWidth: 400 }); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.primaryFace1], - files: [assetStub.image.files[1]], - }); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([], [faceStub.primaryFace1.id], []); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith([], [asset.faces[0].id], []); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); }); it('should add new face and delete an existing face not among the new detected faces', async () => { - mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.primaryFace1], - files: [assetStub.image.files[1]], + const assetId = newUuid(); + const face = AssetFaceFactory.create({ + assetId, + boundingBoxX1: 200, + boundingBoxX2: 300, + boundingBoxY1: 200, + boundingBoxY2: 300, }); + const asset = AssetFactory.from({ id: assetId }).face().file({ type: AssetFileType.Preview }).build(); + mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face)); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + mocks.crypto.randomUUID.mockReturnValue(face.id); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [faceStub.primaryFace1.id], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith( + [expect.objectContaining({ id: face.id, assetId: asset.id })], + [asset.faces[0].id], + [{ faceId: face.id, embedding: '[1, 2, 3, 4]' }], + ); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, - { name: JobName.FacialRecognition, data: { id: faceId } }, + { name: JobName.FacialRecognition, data: { id: face.id } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); }); it('should add embedding to matching metadata face', async () => { - mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.fromExif1], - files: [assetStub.image.files[1]], - }); + const face = AssetFaceFactory.create({ sourceType: SourceType.Exif }); + const asset = AssetFactory.from().face(face).file({ type: AssetFileType.Preview }).build(); + mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face)); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); mocks.person.refreshFaces.mockResolvedValue(); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith( - [], - [], - [{ faceId: faceStub.fromExif1.id, embedding: faceSearch.embedding }], - ); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith([], [], [{ faceId: face.id, embedding: '[1, 2, 3, 4]' }]); expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); }); it('should not add embedding to non-matching metadata face', async () => { - mocks.machineLearning.detectFaces.mockResolvedValue(detectFaceMock); - mocks.assetJob.getForDetectFacesJob.mockResolvedValue({ - ...assetStub.image, - faces: [faceStub.fromExif2], - files: [assetStub.image.files[1]], - }); + const assetId = newUuid(); + const face = AssetFaceFactory.create({ assetId, sourceType: SourceType.Exif }); + const asset = AssetFactory.from({ id: assetId }).file({ type: AssetFileType.Preview }).build(); + mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face)); + mocks.assetJob.getForDetectFacesJob.mockResolvedValue(asset); + mocks.crypto.randomUUID.mockReturnValue(face.id); - await sut.handleDetectFaces({ id: assetStub.image.id }); + await sut.handleDetectFaces({ id: asset.id }); - expect(mocks.person.refreshFaces).toHaveBeenCalledWith([face], [], [faceSearch]); + expect(mocks.person.refreshFaces).toHaveBeenCalledWith( + [expect.objectContaining({ id: face.id, assetId: asset.id })], + [], + [{ faceId: face.id, embedding: '[1, 2, 3, 4]' }], + ); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.FacialRecognitionQueueAll, data: { force: false } }, - { name: JobName.FacialRecognition, data: { id: faceId } }, + { name: JobName.FacialRecognition, data: { id: face.id } }, ]); expect(mocks.person.reassignFace).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); @@ -839,153 +871,172 @@ describe(PersonService.name, () => { describe('handleRecognizeFaces', () => { it('should fail if face does not exist', async () => { - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Failed); + expect(await sut.handleRecognizeFaces({ id: 'unknown-face' })).toBe(JobStatus.Failed); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); }); it('should fail if face does not have asset', async () => { - const face = { ...faceStub.face1, asset: null }; - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(face); + const face = AssetFaceFactory.create(); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(face, null)); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Failed); + expect(await sut.handleRecognizeFaces({ id: face.id })).toBe(JobStatus.Failed); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); }); it('should skip if face already has an assigned person', async () => { - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.face1); + const asset = AssetFactory.create(); + const face = AssetFaceFactory.from({ assetId: asset.id }).person().build(); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(face, asset)); - expect(await sut.handleRecognizeFaces({ id: faceStub.face1.id })).toBe(JobStatus.Skipped); + expect(await sut.handleRecognizeFaces({ id: face.id })).toBe(JobStatus.Skipped); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.create).not.toHaveBeenCalled(); }); it('should match existing person', async () => { - if (!faceStub.primaryFace1.person) { - throw new Error('faceStub.primaryFace1.person is null'); - } + const asset = AssetFactory.create(); + + const [noPerson1, noPerson2, primaryFace, face] = [ + AssetFaceFactory.create({ assetId: asset.id }), + AssetFaceFactory.create(), + AssetFaceFactory.from().person().build(), + AssetFaceFactory.from().person().build(), + ]; const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.primaryFace1, distance: 0.2 }, - { ...faceStub.noPerson2, distance: 0.3 }, - { ...faceStub.face1, distance: 0.4 }, + { ...noPerson1, distance: 0 }, + { ...primaryFace, distance: 0.2 }, + { ...noPerson2, distance: 0.3 }, + { ...face, distance: 0.4 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.create.mockResolvedValue(primaryFace.person!); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: noPerson1.id }); expect(mocks.person.create).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.arrayContaining([faceStub.noPerson1.id]), - newPersonId: faceStub.primaryFace1.person.id, + faceIds: expect.arrayContaining([noPerson1.id]), + newPersonId: primaryFace.person!.id, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.not.arrayContaining([faceStub.face1.id]), - newPersonId: faceStub.primaryFace1.person.id, + faceIds: expect.not.arrayContaining([face.id]), + newPersonId: primaryFace.person!.id, }); }); it('should match existing person if their birth date is unknown', async () => { - if (!faceStub.primaryFace1.person) { - throw new Error('faceStub.primaryFace1.person is null'); - } + const asset = AssetFactory.create(); + const [noPerson, face, faceWithBirthDate] = [ + AssetFaceFactory.create({ assetId: asset.id }), + AssetFaceFactory.from().person().build(), + AssetFaceFactory.from().person({ birthDate: newDate() }).build(), + ]; const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.primaryFace1, distance: 0.2 }, - { ...faceStub.withBirthDate, distance: 0.3 }, + { ...noPerson, distance: 0 }, + { ...face, distance: 0.2 }, + { ...faceWithBirthDate, distance: 0.3 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson, asset)); + mocks.person.create.mockResolvedValue(face.person!); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: noPerson.id }); expect(mocks.person.create).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.arrayContaining([faceStub.noPerson1.id]), - newPersonId: faceStub.primaryFace1.person.id, + faceIds: expect.arrayContaining([noPerson.id]), + newPersonId: face.person!.id, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.not.arrayContaining([faceStub.face1.id]), - newPersonId: faceStub.primaryFace1.person.id, + faceIds: expect.not.arrayContaining([face.id]), + newPersonId: face.person!.id, }); }); it('should match existing person if their birth date is before file creation', async () => { - if (!faceStub.primaryFace1.person) { - throw new Error('faceStub.primaryFace1.person is null'); - } + const asset = AssetFactory.create(); + const [noPerson, face, faceWithBirthDate] = [ + AssetFaceFactory.create({ assetId: asset.id }), + AssetFaceFactory.from().person().build(), + AssetFaceFactory.from().person({ birthDate: newDate() }).build(), + ]; const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.withBirthDate, distance: 0.2 }, - { ...faceStub.primaryFace1, distance: 0.3 }, + { ...noPerson, distance: 0 }, + { ...faceWithBirthDate, distance: 0.2 }, + { ...face, distance: 0.3 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(faceStub.primaryFace1.person); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson, asset)); + mocks.person.create.mockResolvedValue(face.person!); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: noPerson.id }); expect(mocks.person.create).not.toHaveBeenCalled(); expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.arrayContaining([faceStub.noPerson1.id]), - newPersonId: faceStub.withBirthDate.person?.id, + faceIds: expect.arrayContaining([noPerson.id]), + newPersonId: faceWithBirthDate.person!.id, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: expect.not.arrayContaining([faceStub.face1.id]), - newPersonId: faceStub.withBirthDate.person?.id, + faceIds: expect.not.arrayContaining([face.id]), + newPersonId: faceWithBirthDate.person!.id, }); }); it('should create a new person if the face is a core point with no person', async () => { + const asset = AssetFactory.create(); + const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; + const person = PersonFactory.create(); + const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.noPerson2, distance: 0.3 }, + { ...noPerson1, distance: 0 }, + { ...noPerson2, distance: 0.3 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(personStub.withName); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.create.mockResolvedValue(person); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: noPerson1.id }); expect(mocks.person.create).toHaveBeenCalledWith({ - ownerId: faceStub.noPerson1.asset.ownerId, - faceAssetId: faceStub.noPerson1.id, + ownerId: asset.ownerId, + faceAssetId: noPerson1.id, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - faceIds: [faceStub.noPerson1.id], - newPersonId: personStub.withName.id, + faceIds: [noPerson1.id], + newPersonId: person.id, }); }); it('should not queue face with no matches', async () => { - const faces = [{ ...faceStub.noPerson1, distance: 0 }] as FaceSearchResult[]; + const asset = AssetFactory.create(); + const face = AssetFaceFactory.create({ assetId: asset.id }); + const faces = [{ ...face, distance: 0 }] as FaceSearchResult[]; mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(personStub.withName); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(face, asset)); + mocks.person.create.mockResolvedValue(PersonFactory.create()); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: face.id }); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.search.searchFaces).toHaveBeenCalledTimes(1); @@ -994,21 +1045,24 @@ describe(PersonService.name, () => { }); it('should defer non-core faces to end of queue', async () => { + const asset = AssetFactory.create(); + const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; + const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.noPerson2, distance: 0.4 }, + { ...noPerson1, distance: 0 }, + { ...noPerson2, distance: 0.4 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); mocks.search.searchFaces.mockResolvedValue(faces); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(personStub.withName); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.create.mockResolvedValue(PersonFactory.create()); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id }); + await sut.handleRecognizeFaces({ id: noPerson1.id }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognition, - data: { id: faceStub.noPerson1.id, deferred: true }, + data: { id: noPerson1.id, deferred: true }, }); expect(mocks.search.searchFaces).toHaveBeenCalledTimes(1); expect(mocks.person.create).not.toHaveBeenCalled(); @@ -1016,17 +1070,20 @@ describe(PersonService.name, () => { }); it('should not assign person to deferred non-core face with no matching person', async () => { + const asset = AssetFactory.create(); + const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; + const faces = [ - { ...faceStub.noPerson1, distance: 0 }, - { ...faceStub.noPerson2, distance: 0.4 }, + { ...noPerson1, distance: 0 }, + { ...noPerson2, distance: 0.4 }, ] as FaceSearchResult[]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); mocks.search.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); - mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(faceStub.noPerson1); - mocks.person.create.mockResolvedValue(personStub.withName); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.create.mockResolvedValue(PersonFactory.create()); - await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true }); + await sut.handleRecognizeFaces({ id: noPerson1.id, deferred: true }); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.search.searchFaces).toHaveBeenCalledTimes(2); @@ -1037,59 +1094,71 @@ describe(PersonService.name, () => { describe('mergePerson', () => { it('should require person.write and person.merge permission', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).rejects.toBeInstanceOf( BadRequestException, ); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should merge two people without smart merge', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: true }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: true }, ]); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.primaryPerson.id, - oldPersonId: personStub.mergePerson.id, + newPersonId: person.id, + oldPersonId: mergePerson.id, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should merge two people with smart merge', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.randomPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name }); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [ + PersonFactory.create({ name: undefined }), + PersonFactory.create({ name: 'Merge person' }), + ]; - await expect(sut.mergePerson(authStub.admin, 'person-3', { ids: ['person-1'] })).resolves.toEqual([ - { id: 'person-1', success: true }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.person.update.mockResolvedValue({ ...person, name: mergePerson.name }); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: true }, ]); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: personStub.randomPerson.id, - oldPersonId: personStub.primaryPerson.id, + newPersonId: person.id, + oldPersonId: mergePerson.id, }); expect(mocks.person.update).toHaveBeenCalledWith({ - id: personStub.randomPerson.id, - name: personStub.primaryPerson.name, + id: person.id, + name: mergePerson.name, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should throw an error when the primary person is not found', async () => { @@ -1104,73 +1173,89 @@ describe(PersonService.name, () => { }); it('should handle invalid merge ids', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['unknown'])); + + await expect(sut.mergePerson(auth, person.id, { ids: ['unknown'] })).resolves.toEqual([ + { id: 'unknown', success: false, error: BulkIdErrorReason.NOT_FOUND }, ]); expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should handle an error reassigning faces', async () => { - mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson); - mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson); - mocks.person.reassignFaces.mockRejectedValue(new Error('update failed')); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); + const auth = AuthFactory.create(); + const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ - { id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN }, + mocks.person.getById.mockResolvedValueOnce(person); + mocks.person.getById.mockResolvedValueOnce(mergePerson); + mocks.person.reassignFaces.mockRejectedValue(new Error('update failed')); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); + + await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ + { id: mergePerson.id, success: false, error: BulkIdErrorReason.UNKNOWN }, ]); expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('getStatistics', () => { it('should get correct number of person', async () => { - mocks.person.getById.mockResolvedValue(personStub.primaryPerson); - mocks.person.getStatistics.mockResolvedValue(statistics); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + mocks.person.getStatistics.mockResolvedValue({ assets: 3 }); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + await expect(sut.getStatistics(auth, person.id)).resolves.toEqual({ assets: 3 }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); it('should require person.read permission', async () => { - mocks.person.getById.mockResolvedValue(personStub.primaryPerson); - await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); + const auth = AuthFactory.create(); + const person = PersonFactory.create(); + + mocks.person.getById.mockResolvedValue(person); + await expect(sut.getStatistics(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); }); }); describe('mapFace', () => { it('should map a face', () => { - const authDto = factory.auth({ user: { id: faceStub.face1.person.ownerId } }); - expect(mapFaces(faceStub.face1, authDto)).toEqual({ - boundingBoxX1: 0, - boundingBoxX2: 1, - boundingBoxY1: 0, - boundingBoxY2: 1, - id: faceStub.face1.id, - imageHeight: 1024, - imageWidth: 1024, + const user = UserFactory.create(); + const auth = AuthFactory.create({ id: user.id }); + const person = PersonFactory.create({ ownerId: user.id }); + const face = AssetFaceFactory.from().person(person).build(); + + expect(mapFaces(face, auth)).toEqual({ + boundingBoxX1: 100, + boundingBoxX2: 200, + boundingBoxY1: 100, + boundingBoxY2: 200, + id: face.id, + imageHeight: 500, + imageWidth: 400, sourceType: SourceType.MachineLearning, - person: mapPerson(personStub.withName), + person: mapPerson(person), }); }); it('should not map person if person is null', () => { - expect(mapFaces({ ...faceStub.face1, person: null }, authStub.user1).person).toBeNull(); + expect(mapFaces(AssetFaceFactory.create(), AuthFactory.create()).person).toBeNull(); }); it('should not map person if person does not match auth user id', () => { - expect(mapFaces(faceStub.face1, authStub.user1).person).toBeNull(); + expect(mapFaces(AssetFaceFactory.from().person().build(), AuthFactory.create()).person).toBeNull(); }); }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 6fa9b3fdd2..8a902590e3 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -40,9 +40,11 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; +import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFacialRecognitionEnabled } from 'src/utils/misc'; +import { Point, transformPoints } from 'src/utils/transform'; @Injectable() export class PersonService extends BaseService { @@ -126,7 +128,10 @@ export class PersonService extends BaseService { async getFacesById(auth: AuthDto, dto: FaceDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] }); const faces = await this.personRepository.getFaces(dto.id); - return faces.map((asset) => mapFaces(asset, auth)); + const asset = await this.assetRepository.getForFaces(dto.id); + const assetDimensions = getDimensions(asset); + + return faces.map((face) => mapFaces(face, auth, asset.edits, assetDimensions)); } async createNewFeaturePhoto(changeFeaturePhoto: string[]) { @@ -192,13 +197,9 @@ export class PersonService extends BaseService { let faceId: string | undefined = undefined; if (assetId) { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] }); - const [face] = await this.personRepository.getFacesByIds([{ personId: id, assetId }]); + const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId }); if (!face) { - throw new BadRequestException('Invalid assetId for feature face'); - } - - if (face.asset.isOffline) { - throw new BadRequestException('An offline asset cannot be used for feature face'); + throw new BadRequestException('Invalid assetId for feature face or asset is offline'); } faceId = face.id; @@ -594,7 +595,7 @@ export class PersonService extends BaseService { update.birthDate = mergePerson.birthDate; } - if (Object.keys(update).length > 0) { + if (Object.keys(update).length > 1) { primaryPerson = await this.personRepository.update(update); } @@ -630,15 +631,62 @@ export class PersonService extends BaseService { this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }), ]); + const asset = await this.assetRepository.getById(dto.assetId, { edits: true, exifInfo: true }); + if (!asset) { + throw new NotFoundException('Asset not found'); + } + + const edits = asset.edits || []; + + let topLeft: Point = { x: dto.x, y: dto.y }; + let bottomRight: Point = { x: dto.x + dto.width, y: dto.y + dto.height }; + + // the coordinates received from the client are based on the edited preview image + // we need to convert them to the coordinate space of the original unedited image + if (edits.length > 0) { + if (!asset.width || !asset.height || !asset.exifInfo?.exifImageWidth || !asset.exifInfo?.exifImageHeight) { + throw new BadRequestException('Asset does not have valid dimensions'); + } + + // convert from preview to full dimensions + const scaleFactor = asset.width / dto.imageWidth; + topLeft = { x: topLeft.x * scaleFactor, y: topLeft.y * scaleFactor }; + bottomRight = { x: bottomRight.x * scaleFactor, y: bottomRight.y * scaleFactor }; + + const { + points: [invertedTopLeft, invertedBottomRight], + } = transformPoints( + [topLeft, bottomRight], + edits, + { width: asset.width, height: asset.height }, + { inverse: true }, + ); + + // make sure topLeft is top-left and bottomRight is bottom-right + topLeft = { + x: Math.min(invertedTopLeft.x, invertedBottomRight.x), + y: Math.min(invertedTopLeft.y, invertedBottomRight.y), + }; + bottomRight = { + x: Math.max(invertedTopLeft.x, invertedBottomRight.x), + y: Math.max(invertedTopLeft.y, invertedBottomRight.y), + }; + + // now coordinates are in original image space + const originalDimensions = getDimensions(asset.exifInfo); + dto.imageWidth = originalDimensions.width; + dto.imageHeight = originalDimensions.height; + } + await this.personRepository.createAssetFace({ personId: dto.personId, assetId: dto.assetId, imageHeight: dto.imageHeight, imageWidth: dto.imageWidth, - boundingBoxX1: dto.x, - boundingBoxX2: dto.x + dto.width, - boundingBoxY1: dto.y, - boundingBoxY2: dto.y + dto.height, + boundingBoxX1: Math.round(topLeft.x), + boundingBoxX2: Math.round(bottomRight.x), + boundingBoxY1: Math.round(topLeft.y), + boundingBoxY2: Math.round(bottomRight.y), sourceType: SourceType.Manual, }); } diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index 9336f0003a..d78b8940d3 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -6,8 +6,9 @@ import { join } from 'node:path'; import { Asset, WorkflowAction, WorkflowFilter } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; -import { mapPlugin, PluginResponseDto } from 'src/dtos/plugin.dto'; +import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum'; +import { pluginTriggers } from 'src/plugins'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { PluginHostFunctions } from 'src/services/plugin-host.functions'; @@ -50,6 +51,10 @@ export class PluginService extends BaseService { await this.loadPlugins(); } + getTriggers(): PluginTriggerResponseDto[] { + return pluginTriggers; + } + // // CRUD operations for plugins // @@ -80,8 +85,8 @@ export class PluginService extends BaseService { this.logger.log(`Successfully processed core plugin: ${coreManifest.name} (version ${coreManifest.version})`); // Load external plugins - if (plugins.enabled && plugins.installFolder) { - await this.loadExternalPlugins(plugins.installFolder); + if (plugins.external.allow && plugins.external.installFolder) { + await this.loadExternalPlugins(plugins.external.installFolder); } } diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index f5cf20413e..2c76fee877 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -23,7 +23,7 @@ describe(QueueService.name, () => { it('should update concurrency', () => { sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig }); - expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(17); + expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(18); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5); @@ -77,6 +77,7 @@ describe(QueueService.name, () => { [QueueName.BackupDatabase]: expected, [QueueName.Ocr]: expected, [QueueName.Workflow]: expected, + [QueueName.Editor]: expected, }); }); }); diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 0dec02f18f..62575d0f07 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -2,9 +2,9 @@ import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { SearchSuggestionType } from 'src/dtos/search.dto'; import { SearchService } from 'src/services/search.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { personStub } from 'test/fixtures/person.stub'; import { newTestService, ServiceMocks } from 'test/utils'; import { beforeEach, vitest } from 'vitest'; @@ -25,17 +25,18 @@ describe(SearchService.name, () => { describe('searchPerson', () => { it('should pass options to search', async () => { - const { name } = personStub.withName; + const auth = AuthFactory.create(); + const name = 'foo'; mocks.person.getByName.mockResolvedValue([]); - await sut.searchPerson(authStub.user1, { name, withHidden: false }); + await sut.searchPerson(auth, { name, withHidden: false }); - expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false }); + expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: false }); - await sut.searchPerson(authStub.user1, { name, withHidden: true }); + await sut.searchPerson(auth, { name, withHidden: true }); - expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true }); + expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: true }); }); }); @@ -64,16 +65,18 @@ describe(SearchService.name, () => { describe('getExploreData', () => { it('should get assets by city and tag', async () => { + const auth = AuthFactory.create(); + const asset = AssetFactory.from() + .exif({ latitude: 42, longitude: 69, city: 'city', state: 'state', country: 'country' }) + .build(); mocks.asset.getAssetIdByCity.mockResolvedValue({ fieldName: 'exifInfo.city', - items: [{ value: 'test-city', data: assetStub.withLocation.id }], + items: [{ value: 'city', data: asset.id }], }); - mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([assetStub.withLocation]); - const expectedResponse = [ - { fieldName: 'exifInfo.city', items: [{ value: 'test-city', data: mapAsset(assetStub.withLocation) }] }, - ]; + mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue([asset as never]); + const expectedResponse = [{ fieldName: 'exifInfo.city', items: [{ value: 'city', data: mapAsset(asset) }] }]; - const result = await sut.getExploreData(authStub.user1); + const result = await sut.getExploreData(auth); expect(result).toEqual(expectedResponse); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index af4d706061..30bc1f1f0d 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -115,8 +115,9 @@ export class ServerService extends BaseService { } async getSystemConfig(): Promise { + const { setup } = this.configRepository.getEnv(); const config = await this.getConfig({ withCache: false }); - const isInitialized = await this.userRepository.hasAdmin(); + const isInitialized = !setup.allow || (await this.userRepository.hasAdmin()); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 2f477c0d6a..8b5bd13928 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -33,14 +33,14 @@ export class SessionService extends BaseService { } const token = this.cryptoRepository.randomBytesAsText(32); - const tokenHashed = this.cryptoRepository.hashSha256(token); + const hashed = this.cryptoRepository.hashSha256(token); const session = await this.sessionRepository.create({ parentId: auth.session.id, userId: auth.user.id, expiresAt: dto.duration ? DateTime.now().plus({ seconds: dto.duration }).toJSDate() : null, deviceType: dto.deviceType, deviceOS: dto.deviceOS, - token: tokenHashed, + token: hashed, }); return { ...mapSession(session), token }; diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 062214b975..07f31db4da 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -1,10 +1,10 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; -import _ from 'lodash'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { SharedLinkType } from 'src/enum'; import { SharedLinkService } from 'src/services/shared-link.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { SharedLinkFactory } from 'test/factories/shared-link.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { factory } from 'test/small.factory'; @@ -35,14 +35,14 @@ describe(SharedLinkService.name, () => { describe('getMine', () => { it('should only work for a public user', async () => { - await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException); + await expect(sut.getMine(authStub.admin, [])).rejects.toBeInstanceOf(ForbiddenException); expect(mocks.sharedLink.get).not.toHaveBeenCalled(); }); it('should return the shared link for the public user', async () => { const authDto = authStub.adminSharedLink; mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid); - await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid); + await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); @@ -55,20 +55,23 @@ describe(SharedLinkService.name, () => { }, }); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); - await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); + const response = await sut.getMine(authDto, []); + expect(response.assets[0]).toMatchObject({ hasMetadata: false }); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); - it('should throw an error for an invalid password protected shared link', async () => { + it('should throw an error for a request without a shared link auth token', async () => { const authDto = authStub.adminSharedLink; mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.passwordRequired); - await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect(sut.getMine(authDto, [])).rejects.toBeInstanceOf(UnauthorizedException); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); - it('should allow a correct password on a password protected shared link', async () => { + it('should accept a valid shared link auth token', async () => { mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' }); - await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined(); + const secret = Buffer.from('auth-token-123'); + mocks.crypto.hashSha256.mockReturnValue(secret); + await expect(sut.getMine(authStub.adminSharedLink, [secret.toString('base64')])).resolves.toBeDefined(); expect(mocks.sharedLink.get).toHaveBeenCalledWith( authStub.adminSharedLink.user.id, authStub.adminSharedLink.sharedLink?.id, @@ -119,19 +122,17 @@ describe(SharedLinkService.name, () => { }); it('should create an album shared link', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + const album = AlbumFactory.from().asset().build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid); - await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: albumStub.oneAsset.id }); + await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: album.id }); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([albumStub.oneAsset.id]), - ); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([album.id])); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ type: SharedLinkType.Album, userId: authStub.admin.user.id, - albumId: albumStub.oneAsset.id, + albumId: album.id, allowDownload: true, allowUpload: true, description: null, @@ -143,12 +144,13 @@ describe(SharedLinkService.name, () => { }); it('should create an individual shared link', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.Individual, - assetIds: [assetStub.image.id], + assetIds: [asset.id], showMetadata: true, allowDownload: true, allowUpload: true, @@ -156,7 +158,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ @@ -166,7 +168,7 @@ describe(SharedLinkService.name, () => { allowDownload: true, slug: null, allowUpload: true, - assetIds: [assetStub.image.id], + assetIds: [asset.id], description: null, expiresAt: null, showExif: true, @@ -175,12 +177,13 @@ describe(SharedLinkService.name, () => { }); it('should create a shared link with allowDownload set to false when showMetadata is false', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + const asset = AssetFactory.create(); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { type: SharedLinkType.Individual, - assetIds: [assetStub.image.id], + assetIds: [asset.id], showMetadata: false, allowDownload: true, allowUpload: true, @@ -188,7 +191,7 @@ describe(SharedLinkService.name, () => { expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( authStub.admin.user.id, - new Set([assetStub.image.id]), + new Set([asset.id]), false, ); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ @@ -197,7 +200,7 @@ describe(SharedLinkService.name, () => { albumId: null, allowDownload: false, allowUpload: true, - assetIds: [assetStub.image.id], + assetIds: [asset.id], description: null, expiresAt: null, showExif: false, @@ -264,25 +267,28 @@ describe(SharedLinkService.name, () => { }); it('should add assets to a shared link', async () => { - mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); + const asset = AssetFactory.create(); + const sharedLink = SharedLinkFactory.from().asset(asset).build(); + const newAsset = AssetFactory.create(); + mocks.sharedLink.get.mockResolvedValue(sharedLink); + mocks.sharedLink.create.mockResolvedValue(sharedLink); + mocks.sharedLink.update.mockResolvedValue(sharedLink); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([newAsset.id])); await expect( - sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }), + sut.addAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2', newAsset.id] }), ).resolves.toEqual([ - { assetId: assetStub.image.id, success: false, error: AssetIdErrorReason.DUPLICATE }, + { assetId: asset.id, success: false, error: AssetIdErrorReason.DUPLICATE }, { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NO_PERMISSION }, - { assetId: 'asset-3', success: true }, + { assetId: newAsset.id, success: true }, ]); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledTimes(1); expect(mocks.sharedLink.update).toHaveBeenCalled(); expect(mocks.sharedLink.update).toHaveBeenCalledWith({ - ...sharedLinkStub.individual, + ...sharedLink, slug: null, - assetIds: ['asset-3'], + assetIds: [newAsset.id], }); }); }); @@ -297,20 +303,22 @@ describe(SharedLinkService.name, () => { }); it('should remove assets from a shared link', async () => { - mocks.sharedLink.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); - mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLink.update.mockResolvedValue(sharedLinkStub.individual); - mocks.sharedLinkAsset.remove.mockResolvedValue([assetStub.image.id]); + const asset = AssetFactory.create(); + const sharedLink = SharedLinkFactory.from().asset(asset).build(); + mocks.sharedLink.get.mockResolvedValue(sharedLink); + mocks.sharedLink.create.mockResolvedValue(sharedLink); + mocks.sharedLink.update.mockResolvedValue(sharedLink); + mocks.sharedLinkAsset.remove.mockResolvedValue([asset.id]); await expect( - sut.removeAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2'] }), + sut.removeAssets(authStub.admin, sharedLink.id, { assetIds: [asset.id, 'asset-2'] }), ).resolves.toEqual([ - { assetId: assetStub.image.id, success: true }, + { assetId: asset.id, success: true }, { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, ]); - expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith('link-1', [assetStub.image.id, 'asset-2']); - expect(mocks.sharedLink.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [] }); + expect(mocks.sharedLinkAsset.remove).toHaveBeenCalledWith(sharedLink.id, [asset.id, 'asset-2']); + expect(mocks.sharedLink.update).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); }); }); @@ -334,7 +342,7 @@ describe(SharedLinkService.name, () => { await expect(sut.getMetadataTags(authStub.adminSharedLink)).resolves.toEqual({ description: '1 shared photos & videos', - imageUrl: `https://my.immich.app/api/assets/asset-id/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, + imageUrl: `https://my.immich.app/api/assets/${sharedLinkStub.individual.assets[0].id}/thumbnail?key=LCtkaJX4R1O_9D-2lq0STzsPryoL1UdAbyb6Sna1xxmQCSuqU2J1ZUsqt6GR-yGm1s0`, title: 'Public Share', }); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 3c1a6083e9..b942c32326 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,15 +1,13 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; import { PostgresError } from 'postgres'; -import { SharedLink } from 'src/database'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { mapSharedLink, - mapSharedLinkWithoutMetadata, SharedLinkCreateDto, SharedLinkEditDto, - SharedLinkPasswordDto, + SharedLinkLoginDto, SharedLinkResponseDto, SharedLinkSearchDto, } from 'src/dtos/shared-link.dto'; @@ -19,29 +17,52 @@ import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { - async getAll(auth: AuthDto, { albumId }: SharedLinkSearchDto): Promise { + async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise { return this.sharedLinkRepository - .getAll({ userId: auth.user.id, albumId }) - .then((links) => links.map((link) => mapSharedLink(link))); + .getAll({ userId: auth.user.id, id, albumId }) + .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false }))); } - async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { + async login(auth: AuthDto, dto: SharedLinkLoginDto) { if (!auth.sharedLink) { throw new ForbiddenException(); } const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); - const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif }); - if (sharedLink.password) { - response.token = this.validateAndRefreshToken(sharedLink, dto); + const { id, password } = sharedLink; + + if (!password) { + throw new BadRequestException('Shared link is not password protected'); } - return response; + if (password !== dto.password) { + throw new UnauthorizedException('Invalid password'); + } + + return { + sharedLink: mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }), + token: this.asToken({ id, password }), + }; + } + + async getMine(auth: AuthDto, authTokens: string[]) { + if (!auth.sharedLink) { + throw new ForbiddenException(); + } + + const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); + const { id, password } = sharedLink; + + if (password && !authTokens.includes(this.asToken({ id, password }))) { + throw new UnauthorizedException('Password required'); + } + + return mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }); } async get(auth: AuthDto, id: string): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise { @@ -81,7 +102,7 @@ export class SharedLinkService extends BaseService { slug: dto.slug || null, }); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } catch (error) { this.handleError(error); } @@ -108,7 +129,7 @@ export class SharedLinkService extends BaseService { showExif: dto.showMetadata, slug: dto.slug || null, }); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } catch (error) { this.handleError(error); } @@ -214,20 +235,7 @@ export class SharedLinkService extends BaseService { }; } - private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) { - return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); - } - - private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string { - const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); - const sharedLinkTokens = dto.token?.split(',') || []; - if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) { - throw new UnauthorizedException('Invalid password'); - } - - if (!sharedLinkTokens.includes(token)) { - sharedLinkTokens.push(token); - } - return sharedLinkTokens.join(','); + private asToken(sharedLink: { id: string; password: string }) { + return this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`).toString('base64'); } } diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index b3af5cd15f..6bd0a3c9b2 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,8 +1,8 @@ import { SystemConfig } from 'src/config'; -import { ImmichWorker, JobName, JobStatus } from 'src/enum'; +import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { SmartInfoService } from 'src/services/smart-info.service'; import { getCLIPModelInfo } from 'src/utils/misc'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; @@ -13,7 +13,7 @@ describe(SmartInfoService.name, () => { beforeEach(() => { ({ sut, mocks } = newTestService(SmartInfoService)); - mocks.asset.getByIds.mockResolvedValue([assetStub.image]); + mocks.asset.getByIds.mockResolvedValue([AssetFactory.create()]); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); }); @@ -155,25 +155,23 @@ describe(SmartInfoService.name, () => { }); it('should queue the assets without clip embeddings', async () => { - mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset])); await sut.handleQueueEncodeClip({ force: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(false); expect(mocks.database.setDimensionSize).not.toHaveBeenCalled(); }); it('should queue all the assets', async () => { - mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([assetStub.image])); + const asset = AssetFactory.create(); + mocks.assetJob.streamForEncodeClip.mockReturnValue(makeStream([asset])); await sut.handleQueueEncodeClip({ force: true }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.SmartSearch, data: { id: assetStub.image.id } }, - ]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.SmartSearch, data: { id: asset.id } }]); expect(mocks.assetJob.streamForEncodeClip).toHaveBeenCalledWith(true); expect(mocks.database.setDimensionSize).toHaveBeenCalledExactlyOnceWith(512); }); @@ -190,34 +188,36 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.noResizePath, files: [] }); + const asset = AssetFactory.create(); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.Failed); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Failed); expect(mocks.search.upsert).not.toHaveBeenCalled(); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); }); it('should save the returned objects', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); - expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]'); + expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]'); }); it('should skip invisible assets', async () => { - mocks.assetJob.getForClipEncoding.mockResolvedValue({ - ...assetStub.livePhotoMotionAsset, - files: [assetStub.image.files[1]], - }); + const asset = AssetFactory.from({ visibility: AssetVisibility.Hidden }) + .file({ type: AssetFileType.Preview }) + .build(); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.Skipped); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Skipped); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); @@ -226,25 +226,26 @@ describe(SmartInfoService.name, () => { it('should fail if asset could not be found', async () => { mocks.assetJob.getForClipEncoding.mockResolvedValue(void 0); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Failed); + expect(await sut.handleEncodeClip({ id: 'non-existent' })).toEqual(JobStatus.Failed); expect(mocks.machineLearning.encodeImage).not.toHaveBeenCalled(); expect(mocks.search.upsert).not.toHaveBeenCalled(); }); it('should wait for database', async () => { + const asset = AssetFactory.from().file({ type: AssetFileType.Preview }).build(); mocks.machineLearning.encodeImage.mockResolvedValue('[0.01, 0.02, 0.03]'); mocks.database.isBusy.mockReturnValue(true); - mocks.assetJob.getForClipEncoding.mockResolvedValue({ ...assetStub.image, files: [assetStub.image.files[1]] }); + mocks.assetJob.getForClipEncoding.mockResolvedValue(asset); - expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.Success); + expect(await sut.handleEncodeClip({ id: asset.id })).toEqual(JobStatus.Success); expect(mocks.database.wait).toHaveBeenCalledWith(512); expect(mocks.machineLearning.encodeImage).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', + asset.files[0].path, expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); - expect(mocks.search.upsert).toHaveBeenCalledWith(assetStub.image.id, '[0.01, 0.02, 0.03]'); + expect(mocks.search.upsert).toHaveBeenCalledWith(asset.id, '[0.01, 0.02, 0.03]'); }); }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index eff16fea45..d484fe8b6a 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -117,7 +117,7 @@ export class SmartInfoService extends BaseService { const newConfig = await this.getConfig({ withCache: true }); if (machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName) { - // Skip the job if the the model has changed since the embedding was generated. + // Skip the job if the model has changed since the embedding was generated. return JobStatus.Skipped; } diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index 5517cf17f8..93f84e28e1 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -1,7 +1,10 @@ import { BadRequestException } from '@nestjs/common'; import { StackService } from 'src/services/stack.service'; -import { assetStub, stackStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { StackFactory } from 'test/factories/stack.factory'; import { authStub } from 'test/fixtures/auth.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(StackService.name, () => { @@ -18,43 +21,49 @@ describe(StackService.name, () => { describe('search', () => { it('should search stacks', async () => { - mocks.stack.search.mockResolvedValue([stackStub('stack-id', [assetStub.image])]); + const auth = AuthFactory.create(); + const asset = AssetFactory.create(); + const stack = StackFactory.from().primaryAsset(asset).build(); + mocks.stack.search.mockResolvedValue([stack]); - await sut.search(authStub.admin, { primaryAssetId: assetStub.image.id }); + await sut.search(auth, { primaryAssetId: asset.id }); expect(mocks.stack.search).toHaveBeenCalledWith({ - ownerId: authStub.admin.user.id, - primaryAssetId: assetStub.image.id, + ownerId: auth.user.id, + primaryAssetId: asset.id, }); }); }); describe('create', () => { it('should require asset.update permissions', async () => { - await expect( - sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }), - ).rejects.toBeInstanceOf(BadRequestException); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + + await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.create).not.toHaveBeenCalled(); }); it('should create a stack', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id, assetStub.image1.id])); - mocks.stack.create.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); - await expect( - sut.create(authStub.admin, { assetIds: [assetStub.image.id, assetStub.image1.id] }), - ).resolves.toEqual({ - id: 'stack-id', - primaryAssetId: assetStub.image.id, - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image1.id }), - ], + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([primaryAsset.id, asset.id])); + mocks.stack.create.mockResolvedValue(stack); + + await expect(sut.create(auth, { assetIds: [primaryAsset.id, asset.id] })).resolves.toEqual({ + id: stack.id, + primaryAssetId: primaryAsset.id, + assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })], }); expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', { - stackId: 'stack-id', - userId: authStub.admin.user.id, + stackId: stack.id, + userId: auth.user.id, }); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalled(); }); @@ -78,25 +87,26 @@ describe(StackService.name, () => { }); it('should get stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); - await expect(sut.get(authStub.admin, 'stack-id')).resolves.toEqual({ - id: 'stack-id', - primaryAssetId: assetStub.image.id, - assets: [ - expect.objectContaining({ id: assetStub.image.id }), - expect.objectContaining({ id: assetStub.image1.id }), - ], + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + + await expect(sut.get(auth, stack.id)).resolves.toEqual({ + id: stack.id, + primaryAssetId: primaryAsset.id, + assets: [expect.objectContaining({ id: primaryAsset.id }), expect.objectContaining({ id: asset.id })], }); expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); }); }); describe('update', () => { it('should require stack.update permissions', async () => { - await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.getById).not.toHaveBeenCalled(); expect(mocks.stack.update).not.toHaveBeenCalled(); @@ -106,7 +116,7 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - await expect(sut.update(authStub.admin, 'stack-id', {})).rejects.toBeInstanceOf(Error); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.update).not.toHaveBeenCalled(); @@ -114,55 +124,64 @@ describe(StackService.name, () => { }); it('should fail if the provided primary asset id is not in the stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const stack = StackFactory.from().primaryAsset().asset().build(); - await expect(sut.update(authStub.admin, 'stack-id', { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf( + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + + await expect(sut.update(auth, stack.id, { primaryAssetId: 'unknown-asset' })).rejects.toBeInstanceOf( BadRequestException, ); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); expect(mocks.stack.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it('should update stack', async () => { - mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getById.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); - mocks.stack.update.mockResolvedValue(stackStub('stack-id', [assetStub.image, assetStub.image1])); + const auth = AuthFactory.create(); + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; + const stack = StackFactory.from().primaryAsset(primaryAsset).asset(asset).build(); - await sut.update(authStub.admin, 'stack-id', { primaryAssetId: assetStub.image1.id }); + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set([stack.id])); + mocks.stack.getById.mockResolvedValue(stack); + mocks.stack.update.mockResolvedValue(stack); - expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); - expect(mocks.stack.update).toHaveBeenCalledWith('stack-id', { - id: 'stack-id', - primaryAssetId: assetStub.image1.id, + await sut.update(auth, stack.id, { primaryAssetId: asset.id }); + + expect(mocks.stack.getById).toHaveBeenCalledWith(stack.id); + expect(mocks.stack.update).toHaveBeenCalledWith(stack.id, { + id: stack.id, + primaryAssetId: asset.id, }); expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', { - stackId: 'stack-id', - userId: authStub.admin.user.id, + stackId: stack.id, + userId: auth.user.id, }); }); }); describe('delete', () => { it('should require stack.delete permissions', async () => { - await expect(sut.delete(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.delete(AuthFactory.create(), 'stack-id')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.delete).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it('should delete stack', async () => { + const auth = AuthFactory.create(); + mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); mocks.stack.delete.mockResolvedValue(); - await sut.delete(authStub.admin, 'stack-id'); + await sut.delete(auth, 'stack-id'); expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id'); expect(mocks.event.emit).toHaveBeenCalledWith('StackDelete', { stackId: 'stack-id', - userId: authStub.admin.user.id, + userId: auth.user.id, }); }); }); @@ -204,33 +223,35 @@ describe(StackService.name, () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); mocks.stack.getForAssetRemoval.mockResolvedValue({ id: null, primaryAssetId: null }); - await expect( - sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.imageFrom2015.id }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: newUuid() })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it('should fail if the assetId is the primaryAssetId', async () => { + const asset = AssetFactory.create(); mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id }); + mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: asset.id }); - await expect( - sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image.id }), - ).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.event.emit).not.toHaveBeenCalled(); }); it("should update the asset to nullify it's stack-id", async () => { + const [primaryAsset, asset] = [AssetFactory.create(), AssetFactory.create()]; mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); - mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: assetStub.image.id }); + mocks.stack.getForAssetRemoval.mockResolvedValue({ id: 'stack-id', primaryAssetId: primaryAsset.id }); - await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: assetStub.image1.id }); + await sut.removeAsset(authStub.admin, { id: 'stack-id', assetId: asset.id }); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.image1.id, stackId: null }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, stackId: null }); expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', { stackId: 'stack-id', userId: authStub.admin.user.id, diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index d0d7ea3a3c..57343bb622 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,15 +1,16 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; -import { AssetPathType, JobStatus } from 'src/enum'; +import { AssetPathType, AssetType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { userStub } from 'test/fixtures/user.stub'; -import { factory } from 'test/small.factory'; +import { getForStorageTemplate } from 'test/mappers'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; -const motionAsset = assetStub.storageAsset({}); -const stillAsset = assetStub.storageAsset({ livePhotoVideoId: motionAsset.id }); +const motionAsset = AssetFactory.from({ type: AssetType.Video }).exif().build(); +const stillAsset = AssetFactory.from({ livePhotoVideoId: motionAsset.id }).exif().build(); describe(StorageTemplateService.name, () => { let sut: StorageTemplateService; @@ -84,6 +85,7 @@ describe(StorageTemplateService.name, () => { '{{y}}/{{y}}-{{MM}}/{{assetId}}', '{{y}}/{{y}}-{{WW}}/{{assetId}}', '{{album}}/{{filename}}', + '{{make}}/{{model}}/{{lensModel}}/{{filename}}', ], secondOptions: ['s', 'ss', 'SSS'], weekOptions: ['W', 'WW'], @@ -109,12 +111,27 @@ describe(StorageTemplateService.name, () => { }); it('should migrate single moving picture', async () => { + const motionAsset = AssetFactory.from({ + type: AssetType.Video, + + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const stillAsset = AssetFactory.from({ + livePhotoVideoId: motionAsset.id, + + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + mocks.user.get.mockResolvedValue(userStub.user1); - const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${motionAsset.originalFileName}`; + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName.slice(0, -4)}.mp4`; const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/2022-06-19/${stillAsset.originalFileName}`; - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(stillAsset); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(motionAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset)); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); mocks.move.create.mockResolvedValueOnce({ id: '123', @@ -139,17 +156,69 @@ describe(StorageTemplateService.name, () => { expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath }); }); + it('should migrate live photo motion video alongside the still image using album in path', async () => { + const motionAsset = AssetFactory.from({ + type: AssetType.Video, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const stillAsset = AssetFactory.from({ + livePhotoVideoId: motionAsset.id, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + + const album = AlbumFactory.from().asset().build(); + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; + sut.onConfigInit({ newConfig: config }); + + mocks.user.get.mockResolvedValue(userStub.user1); + + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName.slice(0, -4)}.mp4`; + const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName}`; + + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(stillAsset)); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); + mocks.album.getByAssetId.mockResolvedValue([album]); + + mocks.move.create.mockResolvedValueOnce({ + id: '123', + entityId: stillAsset.id, + pathType: AssetPathType.Original, + oldPath: stillAsset.originalPath, + newPath: newStillPicturePath, + }); + + mocks.move.create.mockResolvedValueOnce({ + id: '124', + entityId: motionAsset.id, + pathType: AssetPathType.Original, + oldPath: motionAsset.originalPath, + newPath: newMotionPicturePath, + }); + + await expect(sut.handleMigrationSingle({ id: stillAsset.id })).resolves.toBe(JobStatus.Success); + + expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2); + expect(mocks.album.getByAssetId).toHaveBeenCalledWith(stillAsset.ownerId, stillAsset.id); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath }); + }); + it('should use handlebar if condition for album', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; - const album = albumStub.oneAsset; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); mocks.album.getByAssetId.mockResolvedValueOnce([album]); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -165,14 +234,14 @@ describe(StorageTemplateService.name, () => { }); it('should use handlebar else condition for album', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}'; sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -188,9 +257,9 @@ describe(StorageTemplateService.name, () => { }); it('should handle album startDate', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; - const album = albumStub.oneAsset; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; @@ -198,7 +267,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); mocks.album.getByAssetId.mockResolvedValueOnce([album]); mocks.album.getMetadataForIds.mockResolvedValueOnce([ { @@ -224,8 +293,8 @@ describe(StorageTemplateService.name, () => { }); it('should handle else condition from album startDate', async () => { - const asset = assetStub.storageAsset(); - const user = userStub.user1; + const user = UserFactory.create(); + const asset = AssetFactory.from().owner(user).exif().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; @@ -233,7 +302,7 @@ describe(StorageTemplateService.name, () => { sut.onConfigInit({ newConfig: config }); mocks.user.get.mockResolvedValue(user); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(asset)); expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.Success); @@ -247,11 +316,18 @@ describe(StorageTemplateService.name, () => { }); it('should migrate previously failed move from original path when it still exists', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); - const asset = assetStub.storageAsset(); - const previousFailedNewPath = `/data/library/${userStub.user1.id}/2023/Feb/${asset.originalFileName}`; - const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${asset.originalFileName}`; + mocks.user.get.mockResolvedValue(user); + + const previousFailedNewPath = `/data/library/${user.id}/2023/Feb/${asset.originalFileName}`; + const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.checkFileExists.mockImplementation((path) => Promise.resolve(path === asset.originalPath)); mocks.move.getByEntity.mockResolvedValue({ @@ -261,7 +337,7 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, @@ -287,9 +363,16 @@ describe(StorageTemplateService.name, () => { }); it('should migrate previously failed move from previous new path when old path no longer exists, should validate file size still matches before moving', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif({ fileSizeInByte: 5000 }) + .build(); + + mocks.user.get.mockResolvedValue(user); - const asset = assetStub.storageAsset({ fileSizeInByte: 5000 }); const previousFailedNewPath = `/data/library/${asset.ownerId}/2022/June/${asset.originalFileName}`; const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; @@ -303,7 +386,7 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(asset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: asset.id, @@ -324,45 +407,53 @@ describe(StorageTemplateService.name, () => { }); it('should fail move if copying and hash of asset and the new file do not match', async () => { - mocks.user.get.mockResolvedValue(userStub.user1); - const newPath = `/data/library/${userStub.user1.id}/2022/2022-06-19/${testAsset.originalFileName}`; + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.user.get.mockResolvedValue(user); + const newPath = `/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`; mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.storage.stat.mockResolvedValue({ size: 5000 } as Stats); mocks.crypto.hashFile.mockResolvedValue(Buffer.from('different-hash', 'utf8')); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(asset)); mocks.move.create.mockResolvedValue({ id: '123', - entityId: testAsset.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: testAsset.originalPath, + oldPath: asset.originalPath, newPath, }); - await expect(sut.handleMigrationSingle({ id: testAsset.id })).resolves.toBe(JobStatus.Success); + await expect(sut.handleMigrationSingle({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(testAsset.id); + expect(mocks.assetJob.getForStorageTemplateJob).toHaveBeenCalledWith(asset.id); expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(1); expect(mocks.storage.stat).toHaveBeenCalledWith(newPath); expect(mocks.move.create).toHaveBeenCalledWith({ - entityId: testAsset.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: testAsset.originalPath, + oldPath: asset.originalPath, newPath, }); - expect(mocks.storage.rename).toHaveBeenCalledWith(testAsset.originalPath, newPath); - expect(mocks.storage.copyFile).toHaveBeenCalledWith(testAsset.originalPath, newPath); + expect(mocks.storage.rename).toHaveBeenCalledWith(asset.originalPath, newPath); + expect(mocks.storage.copyFile).toHaveBeenCalledWith(asset.originalPath, newPath); expect(mocks.storage.unlink).toHaveBeenCalledWith(newPath); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.asset.update).not.toHaveBeenCalled(); }); - const testAsset = assetStub.storageAsset(); + const testAsset = AssetFactory.from().exif({ fileSizeInByte: 12_345 }).build(); it.each` - failedPathChecksum | failedPathSize | reason - ${testAsset.checksum} | ${500} | ${'file size'} - ${Buffer.from('bad checksum', 'utf8')} | ${testAsset.fileSizeInByte} | ${'checksum'} + failedPathChecksum | failedPathSize | reason + ${testAsset.checksum} | ${500} | ${'file size'} + ${Buffer.from('bad checksum', 'utf8')} | ${testAsset.exifInfo.fileSizeInByte} | ${'checksum'} `( 'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails', async ({ failedPathChecksum, failedPathSize }) => { @@ -380,7 +471,7 @@ describe(StorageTemplateService.name, () => { oldPath: testAsset.originalPath, newPath: previousFailedNewPath, }); - mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(testAsset); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValue(getForStorageTemplate(testAsset)); mocks.move.update.mockResolvedValue({ id: '123', entityId: testAsset.id, @@ -413,12 +504,17 @@ describe(StorageTemplateService.name, () => { }); it('should handle an asset with a duplicate destination', async () => { - const asset = assetStub.storageAsset(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; const newPath2 = newPath.replace('.jpg', '+1.jpg'); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', @@ -440,9 +536,13 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset already matches the template', async () => { - const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg' }); + const asset = AssetFactory.from({ + originalPath: '/data/library/user-id/2023/2023-02-23/asset-id.jpg', + }) + .exif() + .build(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); await sut.handleMigration(); @@ -455,9 +555,13 @@ describe(StorageTemplateService.name, () => { }); it('should skip when an asset is probably a duplicate', async () => { - const asset = assetStub.storageAsset({ originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg' }); + const asset = AssetFactory.from({ + originalPath: '/data/library/user-id/2023/2023-02-23/asset-id+1.jpg', + }) + .exif() + .build(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); await sut.handleMigration(); @@ -470,16 +574,21 @@ describe(StorageTemplateService.name, () => { }); it('should move an asset', async () => { - const asset = assetStub.storageAsset(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ id: '123', - entityId: assetStub.image.id, + entityId: asset.id, pathType: AssetPathType.Original, - oldPath: assetStub.image.originalPath, + oldPath: asset.originalPath, newPath, }); @@ -491,9 +600,15 @@ describe(StorageTemplateService.name, () => { }); it('should use the user storage label', async () => { - const user = factory.userAdmin({ storageLabel: 'label-1' }); - const asset = assetStub.storageAsset({ ownerId: user.id }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create({ storageLabel: 'label-1' }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -507,7 +622,7 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', + asset.originalPath, expect.stringContaining(`/data/library/${user.storageLabel}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).toHaveBeenCalledWith({ @@ -519,10 +634,16 @@ describe(StorageTemplateService.name, () => { }); it('should copy the file if rename fails due to EXDEV (rename across filesystems)', async () => { - const asset = assetStub.storageAsset({ originalPath: '/path/to/original.jpg', fileSizeInByte: 5000 }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + originalPath: '/path/to/original.jpg', + }) + .exif({ fileSizeInByte: 5000 }) + .build(); + const oldPath = asset.originalPath; - const newPath = `/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`; - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const newPath = `/data/library/${asset.ownerId}/2022/2022-06-19/${asset.originalFileName}`; + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); mocks.user.getList.mockResolvedValue([userStub.user1]); mocks.move.create.mockResolvedValue({ @@ -560,10 +681,17 @@ describe(StorageTemplateService.name, () => { }); it('should not update the database if the move fails due to incorrect newPath filesize', async () => { - const asset = assetStub.storageAsset(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue({ code: 'EXDEV' }); - mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', entityId: asset.id, @@ -579,22 +707,29 @@ describe(StorageTemplateService.name, () => { expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.copyFile).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.storage.stat).toHaveBeenCalledWith( - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); it('should not update the database if the move fails', async () => { - const asset = assetStub.storageAsset(); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.storage.rename.mockRejectedValue(new Error('Read only system')); mocks.storage.copyFile.mockRejectedValue(new Error('Read only system')); mocks.move.create.mockResolvedValue({ @@ -604,28 +739,125 @@ describe(StorageTemplateService.name, () => { oldPath: asset.originalPath, newPath: '', }); - mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.user.getList.mockResolvedValue([user]); await sut.handleMigration(); expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalledWith( - '/original/path.jpg', - expect.stringContaining(`/data/library/user-id/2022/2022-06-19/${asset.originalFileName}`), + asset.originalPath, + expect.stringContaining(`/data/library/${user.id}/2022/2022-06-19/${asset.originalFileName}`), ); expect(mocks.asset.update).not.toHaveBeenCalled(); }); + + it('should migrate live photo motion video alongside the still image', async () => { + const motionAsset = AssetFactory.from({ + type: AssetType.Video, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const stillAsset = AssetFactory.from({ + livePhotoVideoId: motionAsset.id, + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), + }) + .exif() + .build(); + const album = AlbumFactory.from().asset().build(); + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; + sut.onConfigInit({ newConfig: config }); + + const newMotionPicturePath = `/data/library/${motionAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName.slice(0, -4)}.mp4`; + const newStillPicturePath = `/data/library/${stillAsset.ownerId}/2022/${album.albumName}/${stillAsset.originalFileName}`; + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)])); + mocks.user.getList.mockResolvedValue([userStub.user1]); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); + mocks.album.getByAssetId.mockResolvedValue([album]); + + mocks.move.create.mockResolvedValueOnce({ + id: '123', + entityId: stillAsset.id, + pathType: AssetPathType.Original, + oldPath: stillAsset.originalPath, + newPath: newStillPicturePath, + }); + + mocks.move.create.mockResolvedValueOnce({ + id: '124', + entityId: motionAsset.id, + pathType: AssetPathType.Original, + oldPath: motionAsset.originalPath, + newPath: newMotionPicturePath, + }); + + await sut.handleMigration(); + + expect(mocks.assetJob.streamForStorageTemplateJob).toHaveBeenCalled(); + expect(mocks.storage.checkFileExists).toHaveBeenCalledTimes(2); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: stillAsset.id, originalPath: newStillPicturePath }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: motionAsset.id, originalPath: newMotionPicturePath }); + }); + + it('should use still photo album info when migrating live photo motion video', async () => { + const user = userStub.user1; + const album = AlbumFactory.from().asset().build(); + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other{{/if}}/{{filename}}'; + + sut.onConfigInit({ newConfig: config }); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(stillAsset)])); + mocks.user.getList.mockResolvedValue([user]); + mocks.assetJob.getForStorageTemplateJob.mockResolvedValueOnce(getForStorageTemplate(motionAsset)); + mocks.album.getByAssetId.mockResolvedValue([album]); + + mocks.move.create.mockResolvedValueOnce({ + id: '123', + entityId: stillAsset.id, + pathType: AssetPathType.Original, + oldPath: stillAsset.originalPath, + newPath: `/data/library/${user.id}/2022/${album.albumName}/${stillAsset.originalFileName}`, + }); + + mocks.move.create.mockResolvedValueOnce({ + id: '124', + entityId: motionAsset.id, + pathType: AssetPathType.Original, + oldPath: motionAsset.originalPath, + newPath: `/data/library/${user.id}/2022/${album.albumName}/${motionAsset.originalFileName}`, + }); + + await sut.handleMigration(); + + expect(mocks.album.getByAssetId).toHaveBeenCalledWith(stillAsset.ownerId, stillAsset.id); + expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(2); + expect(mocks.asset.update).toHaveBeenCalledWith({ + id: stillAsset.id, + originalPath: expect.stringContaining(`/${album.albumName}/`), + }); + expect(mocks.asset.update).toHaveBeenCalledWith({ + id: motionAsset.id, + originalPath: expect.stringContaining(`/${album.albumName}/`), + }); + }); }); describe('file rename correctness', () => { it('should not create double extensions when filename has lower extension', async () => { - const user = factory.userAdmin({ storageLabel: 'label-1' }); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create({ storageLabel: 'label-1' }); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.heic`, originalFileName: 'IMG_7065.HEIC', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -645,13 +877,17 @@ describe(StorageTemplateService.name, () => { }); it('should not create double extensions when filename has uppercase extension', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.HEIC`, originalFileName: 'IMG_7065.HEIC', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif({ fileSizeInByte: 12_345 }) + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -671,13 +907,17 @@ describe(StorageTemplateService.name, () => { }); it('should normalize the filename to lowercase (JPEG > jpg)', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: `/data/library/${user.id}/2022/2022-06-19/IMG_7065.JPEG`, originalFileName: 'IMG_7065.JPEG', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', @@ -697,13 +937,17 @@ describe(StorageTemplateService.name, () => { }); it('should normalize the filename to lowercase (JPG > jpg)', async () => { - const user = factory.userAdmin(); - const asset = assetStub.storageAsset({ - ownerId: user.id, + const user = UserFactory.create(); + const asset = AssetFactory.from({ + fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: '/data/library/user-id/2022/2022-06-19/IMG_7065.JPG', originalFileName: 'IMG_7065.JPG', - }); - mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([asset])); + }) + .owner(user) + .exif() + .build(); + + mocks.assetJob.streamForStorageTemplateJob.mockReturnValue(makeStream([getForStorageTemplate(asset)])); mocks.user.getList.mockResolvedValue([user]); mocks.move.create.mockResolvedValue({ id: '123', diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 864207bf05..3d1bc8f835 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -53,6 +53,7 @@ const storagePresets = [ '{{y}}/{{y}}-{{MM}}/{{assetId}}', '{{y}}/{{y}}-{{WW}}/{{assetId}}', '{{album}}/{{filename}}', + '{{make}}/{{model}}/{{lensModel}}/{{filename}}', ]; export interface MoveAssetMetadata { @@ -67,6 +68,9 @@ interface RenderMetadata { albumName: string | null; albumStartDate: Date | null; albumEndDate: Date | null; + make: string | null; + model: string | null; + lensModel: string | null; } @Injectable() @@ -115,10 +119,13 @@ export class StorageTemplateService extends BaseService { albumName: 'album', albumStartDate: new Date(), albumEndDate: new Date(), + make: 'FUJIFILM', + model: 'X-T50', + lensModel: 'XF27mm F2.8 R WR', }); } catch (error) { this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); - throw new Error(`Invalid storage template: ${error}`); + throw new Error('Invalid storage template', { cause: error }); } } @@ -151,12 +158,14 @@ export class StorageTemplateService extends BaseService { // move motion part of live photo if (asset.livePhotoVideoId) { - const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId); + const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId, { + includeHidden: true, + }); if (!livePhotoVideo) { return JobStatus.Failed; } const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath); - await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }); + await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }, asset); } return JobStatus.Success; } @@ -181,6 +190,17 @@ export class StorageTemplateService extends BaseService { const storageLabel = user?.storageLabel || null; const filename = asset.originalFileName || asset.id; await this.moveAsset(asset, { storageLabel, filename }); + + // move motion part of live photo + if (asset.livePhotoVideoId) { + const livePhotoVideo = await this.assetJobRepository.getForStorageTemplateJob(asset.livePhotoVideoId, { + includeHidden: true, + }); + if (livePhotoVideo) { + const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath); + await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }, asset); + } + } } this.logger.debug('Cleaning up empty directories...'); @@ -198,7 +218,7 @@ export class StorageTemplateService extends BaseService { await this.moveRepository.cleanMoveHistorySingle(assetId); } - async moveAsset(asset: StorageAsset, metadata: MoveAssetMetadata) { + async moveAsset(asset: StorageAsset, metadata: MoveAssetMetadata, stillPhoto?: StorageAsset) { if (asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) { // External assets are not affected by storage template // TODO: shouldn't this only apply to external assets? @@ -208,7 +228,7 @@ export class StorageTemplateService extends BaseService { return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => { const { id, originalPath, checksum, fileSizeInByte } = asset; const oldPath = originalPath; - const newPath = await this.getTemplatePath(asset, metadata); + const newPath = await this.getTemplatePath(asset, metadata, stillPhoto); if (!fileSizeInByte) { this.logger.error(`Asset ${id} missing exif info, skipping storage template migration`); @@ -224,11 +244,11 @@ export class StorageTemplateService extends BaseService { assetInfo: { sizeInBytes: fileSizeInByte, checksum }, }); - const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar)?.path; + const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar, { isEdited: false })?.path; if (sidecarPath) { await this.storageCore.moveFile({ entityId: id, - pathType: AssetPathType.Sidecar, + pathType: AssetFileType.Sidecar, oldPath: sidecarPath, newPath: `${newPath}.xmp`, }); @@ -239,7 +259,11 @@ export class StorageTemplateService extends BaseService { }); } - private async getTemplatePath(asset: StorageAsset, metadata: MoveAssetMetadata): Promise { + private async getTemplatePath( + asset: StorageAsset, + metadata: MoveAssetMetadata, + stillPhoto?: StorageAsset, + ): Promise { const { storageLabel, filename } = metadata; try { @@ -280,8 +304,12 @@ export class StorageTemplateService extends BaseService { let albumName = null; let albumStartDate = null; let albumEndDate = null; + const assetForMetadata = stillPhoto || asset; + if (this.template.needsAlbum) { - const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id); + // For motion videos, use the still photo's album information since motion videos + // don't have album metadata attached directly + const albums = await this.albumRepository.getByAssetId(assetForMetadata.ownerId, assetForMetadata.id); const album = albums?.[0]; if (album) { albumName = album.albumName || null; @@ -294,13 +322,18 @@ export class StorageTemplateService extends BaseService { } } + // For motion videos that are part of live photos, use the still photo's date + // to ensure both parts end up in the same folder const storagePath = this.render(this.template.compiled, { - asset, + asset: assetForMetadata, filename: sanitized, extension, albumName, albumStartDate, albumEndDate, + make: assetForMetadata.make, + model: assetForMetadata.model, + lensModel: assetForMetadata.lensModel, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); let destination = `${fullPath}.${extension}`; @@ -365,7 +398,7 @@ export class StorageTemplateService extends BaseService { } private render(template: HandlebarsTemplateDelegate, options: RenderMetadata) { - const { filename, extension, asset, albumName, albumStartDate, albumEndDate } = options; + const { filename, extension, asset, albumName, albumStartDate, albumEndDate, make, model, lensModel } = options; const substitutions: Record = { filename, ext: extension, @@ -375,6 +408,9 @@ export class StorageTemplateService extends BaseService { assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', + make: make ?? '', + model: model ?? '', + lensModel: lensModel ?? '', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 71cf0d0ce8..b443d31c7f 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { join } from 'node:path'; +import { ErrorMessages } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { @@ -114,9 +115,7 @@ export class StorageService extends BaseService { this.logger.log(`Media location changed (from=${previous}, to=${current})`); if (!path.startsWith(previous)) { - throw new Error( - 'Detected an inconsistent media location. For more information, see https://docs.immich.app/errors#inconsistent-media-location', - ); + throw new Error(ErrorMessages.InconsistentMediaLocation); } this.logger.warn( diff --git a/server/src/services/sync.service.spec.ts b/server/src/services/sync.service.spec.ts index 5b50340a9f..395ff86099 100644 --- a/server/src/services/sync.service.spec.ts +++ b/server/src/services/sync.service.spec.ts @@ -1,6 +1,6 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { SyncService } from 'src/services/sync.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -22,10 +22,14 @@ describe(SyncService.name, () => { describe('getAllAssetsForUserFullSync', () => { it('should return a list of all assets owned by the user', async () => { - mocks.asset.getAllForUserFullSync.mockResolvedValue([assetStub.external, assetStub.hasEncodedVideo]); + const [asset1, asset2] = [ + AssetFactory.from({ libraryId: 'library-id', isExternal: true }).owner(authStub.user1.user).build(), + AssetFactory.from().owner(authStub.user1.user).build(), + ]; + mocks.asset.getAllForUserFullSync.mockResolvedValue([asset1, asset2]); await expect(sut.getFullSync(authStub.user1, { limit: 2, updatedUntil: untilDate })).resolves.toEqual([ - mapAsset(assetStub.external, mapAssetOpts), - mapAsset(assetStub.hasEncodedVideo, mapAssetOpts), + mapAsset(asset1, mapAssetOpts), + mapAsset(asset2, mapAssetOpts), ]); expect(mocks.asset.getAllForUserFullSync).toHaveBeenCalledWith({ ownerId: authStub.user1.user.id, @@ -60,10 +64,9 @@ describe(SyncService.name, () => { }); it('should return a response requiring a full sync when there are too many changes', async () => { + const asset = AssetFactory.create(); mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue( - Array.from({ length: 10_000 }).fill(assetStub.image), - ); + mocks.asset.getChangedDeltaSync.mockResolvedValue(Array.from({ length: 10_000 }).fill(asset)); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: true, upserted: [], deleted: [] }); @@ -72,15 +75,17 @@ describe(SyncService.name, () => { }); it('should return a response with changes and deletions', async () => { + const asset = AssetFactory.create({ ownerId: authStub.user1.user.id }); + const deletedAsset = AssetFactory.create({ libraryId: 'library-id', isExternal: true }); mocks.partner.getAll.mockResolvedValue([]); - mocks.asset.getChangedDeltaSync.mockResolvedValue([assetStub.image1]); - mocks.audit.getAfter.mockResolvedValue([assetStub.external.id]); + mocks.asset.getChangedDeltaSync.mockResolvedValue([asset]); + mocks.audit.getAfter.mockResolvedValue([deletedAsset.id]); await expect( sut.getDeltaSync(authStub.user1, { updatedAfter: new Date(), userIds: [authStub.user1.user.id] }), ).resolves.toEqual({ needsFullSync: false, - upserted: [mapAsset(assetStub.image1, mapAssetOpts)], - deleted: [assetStub.external.id], + upserted: [mapAsset(asset, mapAssetOpts)], + deleted: [deletedAsset.id], }); expect(mocks.asset.getChangedDeltaSync).toHaveBeenCalledTimes(1); expect(mocks.audit.getAfter).toHaveBeenCalledTimes(1); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index f354a71791..9bdeca14d7 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -12,6 +12,7 @@ import { AssetFullSyncDto, SyncAckDeleteDto, SyncAckSetDto, + syncAssetFaceV2ToV1, SyncAssetV1, SyncItem, SyncStreamDto, @@ -85,8 +86,10 @@ export const SYNC_TYPES_ORDER = [ SyncRequestType.MemoryToAssetsV1, SyncRequestType.PeopleV1, SyncRequestType.AssetFacesV1, + SyncRequestType.AssetFacesV2, SyncRequestType.UserMetadataV1, SyncRequestType.AssetMetadataV1, + SyncRequestType.AssetEditsV1, ]; const throwSessionRequired = () => { @@ -173,6 +176,7 @@ export class SyncService extends BaseService { [SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap), [SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap), [SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap), + [SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap), [SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id), [SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth), [SyncRequestType.PartnerAssetExifsV1]: () => @@ -189,6 +193,7 @@ export class SyncService extends BaseService { [SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id), [SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap), [SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap), + [SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap), [SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap), }; @@ -212,6 +217,7 @@ export class SyncService extends BaseService { await this.syncRepository.asset.cleanupAuditTable(pruneThreshold); await this.syncRepository.assetFace.cleanupAuditTable(pruneThreshold); await this.syncRepository.assetMetadata.cleanupAuditTable(pruneThreshold); + await this.syncRepository.assetEdit.cleanupAuditTable(pruneThreshold); await this.syncRepository.memory.cleanupAuditTable(pruneThreshold); await this.syncRepository.memoryToAsset.cleanupAuditTable(pruneThreshold); await this.syncRepository.partner.cleanupAuditTable(pruneThreshold); @@ -349,6 +355,21 @@ export class SyncService extends BaseService { } } + private async syncAssetEditsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { + const deleteType = SyncEntityType.AssetEditDeleteV1; + const deletes = this.syncRepository.assetEdit.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + const upsertType = SyncEntityType.AssetEditV1; + const upserts = this.syncRepository.assetEdit.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + + for await (const { updateId, ...data } of upserts) { + send(response, { type: upsertType, ids: [updateId], data }); + } + } + private async syncPartnerAssetExifsV1( options: SyncQueryOptions, response: Writable, @@ -789,6 +810,21 @@ export class SyncService extends BaseService { const upsertType = SyncEntityType.AssetFaceV1; const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] }); + for await (const { updateId, ...data } of upserts) { + const v1 = syncAssetFaceV2ToV1(data); + send(response, { type: upsertType, ids: [updateId], data: v1 }); + } + } + + private async syncAssetFacesV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) { + const deleteType = SyncEntityType.AssetFaceDeleteV1; + const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] }); + for await (const { id, ...data } of deletes) { + send(response, { type: deleteType, ids: [id], data }); + } + + const upsertType = SyncEntityType.AssetFaceV2; + const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { send(response, { type: upsertType, ids: [updateId], data }); } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index fbdd655bbc..1c93c9d7d3 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -41,6 +41,7 @@ const updatedConfig = Object.freeze({ [QueueName.Notification]: { concurrency: 5 }, [QueueName.Ocr]: { concurrency: 1 }, [QueueName.Workflow]: { concurrency: 5 }, + [QueueName.Editor]: { concurrency: 2 }, }, backup: { database: { @@ -166,13 +167,15 @@ const updatedConfig = Object.freeze({ size: 250, format: ImageFormat.Webp, quality: 80, + progressive: false, }, preview: { size: 1440, format: ImageFormat.Jpeg, quality: 80, + progressive: false, }, - fullsize: { enabled: false, format: ImageFormat.Jpeg, quality: 80 }, + fullsize: { enabled: false, format: ImageFormat.Jpeg, quality: 80, progressive: false }, colorspace: Colorspace.P3, extractEmbedded: false, }, diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 6bb92abd8c..6fc472bb87 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -191,6 +191,7 @@ describe(TagService.name, () => { it('should upsert records', async () => { mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2'])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); + mocks.asset.getForUpdateTags.mockResolvedValue({ tags: [{ value: 'tag-1' }, { value: 'tag-2' }] }); mocks.tag.upsertAssetIds.mockResolvedValue([ { tagId: 'tag-1', assetId: 'asset-1' }, { tagId: 'tag-1', assetId: 'asset-2' }, @@ -204,6 +205,18 @@ describe(TagService.name, () => { ).resolves.toEqual({ count: 6, }); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-1', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-2', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-3', lockedProperties: ['tags'], tags: ['tag-1', 'tag-2'] }, + { lockedPropertiesBehavior: 'append' }, + ); expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([ { tagId: 'tag-1', assetId: 'asset-1' }, { tagId: 'tag-1', assetId: 'asset-2' }, @@ -229,6 +242,7 @@ describe(TagService.name, () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); mocks.tag.addAssetIds.mockResolvedValue(); + mocks.asset.getForUpdateTags.mockResolvedValue({ tags: [{ value: 'tag-1' }] }); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( @@ -240,6 +254,14 @@ describe(TagService.name, () => { { id: 'asset-2', success: true }, ]); + expect(mocks.asset.upsertExif).not.toHaveBeenCalledWith( + { assetId: 'asset-1', lockedProperties: ['tags'], tags: ['tag-1'] }, + { lockedPropertiesBehavior: 'append' }, + ); + expect(mocks.asset.upsertExif).toHaveBeenCalledWith( + { assetId: 'asset-2', lockedProperties: ['tags'], tags: ['tag-1'] }, + { lockedPropertiesBehavior: 'append' }, + ); expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); expect(mocks.tag.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']); }); @@ -249,6 +271,7 @@ describe(TagService.name, () => { it('should throw an error for an invalid id', async () => { mocks.tag.getAssetIds.mockResolvedValue(new Set()); mocks.tag.removeAssetIds.mockResolvedValue(); + mocks.asset.getForUpdateTags.mockResolvedValue({ tags: [] }); await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ { id: 'asset-1', success: false, error: 'not_found' }, @@ -259,6 +282,7 @@ describe(TagService.name, () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); mocks.tag.removeAssetIds.mockResolvedValue(); + mocks.asset.getForUpdateTags.mockResolvedValue({ tags: [] }); await expect( sut.removeAssets(authStub.admin, 'tag-1', { diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 3ee5d29b75..d34cd84ecd 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -16,6 +16,7 @@ import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { updateLockedColumns } from 'src/utils/database'; import { upsertTags } from 'src/utils/tag'; @Injectable() @@ -90,6 +91,7 @@ export class TagService extends BaseService { const results = await this.tagRepository.upsertAssetIds(items); for (const assetId of new Set(results.map((item) => item.assetId))) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetTag', { assetId }); } @@ -107,6 +109,7 @@ export class TagService extends BaseService { for (const { id: assetId, success } of results) { if (success) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetTag', { assetId }); } } @@ -125,6 +128,7 @@ export class TagService extends BaseService { for (const { id: assetId, success } of results) { if (success) { + await this.updateTags(assetId); await this.eventRepository.emit('AssetUntag', { assetId }); } } @@ -145,4 +149,11 @@ export class TagService extends BaseService { } return tag; } + + private async updateTags(assetId: string) { + const { tags } = await this.assetRepository.getForUpdateTags(assetId); + await this.assetRepository.upsertExif(updateLockedColumns({ assetId, tags: tags.map(({ value }) => value) }), { + lockedPropertiesBehavior: 'append', + }); + } } diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index 3301e61318..4f447f6c3d 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -23,6 +23,24 @@ describe(TimelineService.name, () => { userIds: [authStub.admin.user.id], }); }); + + it('should pass bbox options to repository when all bbox fields are provided', async () => { + mocks.asset.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]); + + await sut.getTimeBuckets(authStub.admin, { + bbox: { + west: -70, + south: -30, + east: 120, + north: 55, + }, + }); + + expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({ + userIds: [authStub.admin.user.id], + bbox: { west: -70, south: -30, east: 120, north: 55 }, + }); + }); }); describe('getTimeBucket', () => { diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 84c7b578dd..7872f720a9 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -130,7 +130,7 @@ describe(VersionService.name, () => { }); }); - describe('onWebsocketConnectionEvent', () => { + describe('onWebsocketConnection', () => { it('should send on_server_version client event', async () => { await sut.onWebsocketConnection({ userId: '42' }); expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); @@ -143,5 +143,12 @@ describe(VersionService.name, () => { expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); }); + + it('should not send a release notification when the version check is disabled', async () => { + mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } }); + await sut.onWebsocketConnection({ userId: '42' }); + expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer)); + expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object)); + }); }); }); diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 2d3924bc49..fd51fa9adf 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -105,6 +105,12 @@ export class VersionService extends BaseService { @OnEvent({ name: 'WebsocketConnect' }) async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) { this.websocketRepository.clientSend('on_server_version', userId, serverVersion); + + const { newVersionCheck } = await this.getConfig({ withCache: true }); + if (!newVersionCheck.enabled) { + return; + } + const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState); if (metadata) { this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata)); diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts index 86bfcef734..7b26fb5eb3 100644 --- a/server/src/services/view.service.spec.ts +++ b/server/src/services/view.service.spec.ts @@ -1,6 +1,6 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; import { ViewService } from 'src/services/view.service'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -32,8 +32,8 @@ describe(ViewService.name, () => { it('should return assets by original path', async () => { const path = '/asset'; - const asset1 = { ...assetStub.image, originalPath: '/asset/path1' }; - const asset2 = { ...assetStub.image, originalPath: '/asset/path2' }; + const asset1 = AssetFactory.create({ originalPath: '/asset/path1' }); + const asset2 = AssetFactory.create({ originalPath: '/asset/path2' }); const mockAssets = [asset1, asset2]; diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 301931421f..1a65182b1f 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -16,10 +16,10 @@ import { BaseService } from 'src/services/base.service'; @Injectable() export class WorkflowService extends BaseService { async create(auth: AuthDto, dto: WorkflowCreateDto): Promise { - const trigger = this.getTriggerOrFail(dto.triggerType); + const context = this.getContextForTrigger(dto.triggerType); - const filterInserts = await this.validateAndMapFilters(dto.filters, trigger.context); - const actionInserts = await this.validateAndMapActions(dto.actions, trigger.context); + const filterInserts = await this.validateAndMapFilters(dto.filters, context); + const actionInserts = await this.validateAndMapActions(dto.actions, context); const workflow = await this.workflowRepository.createWorkflow( { @@ -56,11 +56,11 @@ export class WorkflowService extends BaseService { } const workflow = await this.findOrFail(id); - const trigger = this.getTriggerOrFail(workflow.triggerType); + const context = this.getContextForTrigger(dto.triggerType ?? workflow.triggerType); const { filters, actions, ...workflowUpdate } = dto; - const filterInserts = filters && (await this.validateAndMapFilters(filters, trigger.context)); - const actionInserts = actions && (await this.validateAndMapActions(actions, trigger.context)); + const filterInserts = filters && (await this.validateAndMapFilters(filters, context)); + const actionInserts = actions && (await this.validateAndMapActions(actions, context)); const updatedWorkflow = await this.workflowRepository.updateWorkflow( id, @@ -124,12 +124,12 @@ export class WorkflowService extends BaseService { })); } - private getTriggerOrFail(triggerType: PluginTriggerType) { - const trigger = pluginTriggers.find((t) => t.type === triggerType); + private getContextForTrigger(type: PluginTriggerType) { + const trigger = pluginTriggers.find((t) => t.type === type); if (!trigger) { - throw new BadRequestException(`Invalid trigger type: ${triggerType}`); + throw new BadRequestException(`Invalid trigger type: ${type}`); } - return trigger; + return trigger.contextType; } private async findOrFail(id: string) { diff --git a/server/src/sql-tools/comparers/column.comparer.spec.ts b/server/src/sql-tools/comparers/column.comparer.spec.ts deleted file mode 100644 index 0fd4ed74b5..0000000000 --- a/server/src/sql-tools/comparers/column.comparer.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; -import { DatabaseColumn, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testColumn: DatabaseColumn = { - name: 'test', - tableName: 'table1', - primary: false, - nullable: false, - isArray: false, - type: 'character varying', - synchronize: true, -}; - -describe('compareColumns', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareColumns.onExtra(testColumn)).toEqual([ - { - tableName: 'table1', - columnName: 'test', - type: 'ColumnDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareColumns.onMissing(testColumn)).toEqual([ - { - type: 'ColumnAdd', - column: testColumn, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareColumns.onCompare(testColumn, testColumn)).toEqual([]); - }); - - it('should detect a change in type', () => { - const source: DatabaseColumn = { ...testColumn }; - const target: DatabaseColumn = { ...testColumn, type: 'text' }; - const reason = 'column type is different (character varying vs text)'; - expect(compareColumns.onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnDrop', - reason, - }, - { - type: 'ColumnAdd', - column: source, - reason, - }, - ]); - }); - - it('should detect a change in default', () => { - const source: DatabaseColumn = { ...testColumn, nullable: true }; - const target: DatabaseColumn = { ...testColumn, nullable: true, default: "''" }; - const reason = `default is different (null vs '')`; - expect(compareColumns.onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnAlter', - changes: { - default: 'NULL', - }, - reason, - }, - ]); - }); - - it('should detect a comment change', () => { - const source: DatabaseColumn = { ...testColumn, comment: 'new comment' }; - const target: DatabaseColumn = { ...testColumn, comment: 'old comment' }; - const reason = 'comment is different (new comment vs old comment)'; - expect(compareColumns.onCompare(source, target)).toEqual([ - { - columnName: 'test', - tableName: 'table1', - type: 'ColumnAlter', - changes: { - comment: 'new comment', - }, - reason, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/column.comparer.ts b/server/src/sql-tools/comparers/column.comparer.ts deleted file mode 100644 index d3033430ef..0000000000 --- a/server/src/sql-tools/comparers/column.comparer.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types'; - -export const compareColumns = { - getRenameKey: (column) => { - return asRenameKey([ - column.tableName, - column.type, - column.nullable, - column.default, - column.storage, - column.primary, - column.isArray, - column.length, - column.identity, - column.enumName, - column.numericPrecision, - column.numericScale, - ]); - }, - onRename: (source, target) => [ - { - type: 'ColumnRename', - tableName: source.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'ColumnAdd', - column: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ColumnDrop', - tableName: target.tableName, - columnName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - const sourceType = getColumnType(source); - const targetType = getColumnType(target); - - const isTypeChanged = sourceType !== targetType; - - if (isTypeChanged) { - // TODO: convert between types via UPDATE when possible - return dropAndRecreateColumn(source, target, `column type is different (${sourceType} vs ${targetType})`); - } - - const items: SchemaDiff[] = []; - if (source.nullable !== target.nullable) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - nullable: source.nullable, - }, - reason: `nullable is different (${source.nullable} vs ${target.nullable})`, - }); - } - - if (!isDefaultEqual(source, target)) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - default: String(source.default ?? 'NULL'), - }, - reason: `default is different (${source.default ?? 'null'} vs ${target.default})`, - }); - } - - if (source.comment !== target.comment) { - items.push({ - type: 'ColumnAlter', - tableName: source.tableName, - columnName: source.name, - changes: { - comment: String(source.comment), - }, - reason: `comment is different (${source.comment} vs ${target.comment})`, - }); - } - - return items; - }, -} satisfies Comparer; - -const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => { - return [ - { - type: 'ColumnDrop', - tableName: target.tableName, - columnName: target.name, - reason, - }, - { type: 'ColumnAdd', column: source, reason }, - ]; -}; diff --git a/server/src/sql-tools/comparers/constraint.comparer.spec.ts b/server/src/sql-tools/comparers/constraint.comparer.spec.ts deleted file mode 100644 index b5da19e8df..0000000000 --- a/server/src/sql-tools/comparers/constraint.comparer.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; -import { ConstraintType, DatabaseConstraint, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testConstraint: DatabaseConstraint = { - type: ConstraintType.PRIMARY_KEY, - name: 'test', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, -}; - -describe('compareConstraints', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareConstraints.onExtra(testConstraint)).toEqual([ - { - type: 'ConstraintDrop', - constraintName: 'test', - tableName: 'table1', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareConstraints.onMissing(testConstraint)).toEqual([ - { - type: 'ConstraintAdd', - constraint: testConstraint, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareConstraints.onCompare(testConstraint, testConstraint)).toEqual([]); - }); - - it('should detect a change in type', () => { - const source: DatabaseConstraint = { ...testConstraint }; - const target: DatabaseConstraint = { ...testConstraint, columnNames: ['column1', 'column2'] }; - const reason = 'Primary key columns are different: (column1 vs column1,column2)'; - expect(compareConstraints.onCompare(source, target)).toEqual([ - { - constraintName: 'test', - tableName: 'table1', - type: 'ConstraintDrop', - reason, - }, - { - type: 'ConstraintAdd', - constraint: source, - reason, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/constraint.comparer.ts b/server/src/sql-tools/comparers/constraint.comparer.ts deleted file mode 100644 index dda184039f..0000000000 --- a/server/src/sql-tools/comparers/constraint.comparer.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; -import { - CompareFunction, - Comparer, - ConstraintType, - DatabaseCheckConstraint, - DatabaseConstraint, - DatabaseForeignKeyConstraint, - DatabasePrimaryKeyConstraint, - DatabaseUniqueConstraint, - Reason, - SchemaDiff, -} from 'src/sql-tools/types'; - -export const compareConstraints: Comparer = { - getRenameKey: (constraint) => { - switch (constraint.type) { - case ConstraintType.PRIMARY_KEY: - case ConstraintType.UNIQUE: { - return asRenameKey([constraint.type, constraint.tableName, ...constraint.columnNames.toSorted()]); - } - - case ConstraintType.FOREIGN_KEY: { - return asRenameKey([ - constraint.type, - constraint.tableName, - ...constraint.columnNames.toSorted(), - constraint.referenceTableName, - ...constraint.referenceColumnNames.toSorted(), - ]); - } - - case ConstraintType.CHECK: { - const expression = constraint.expression.replaceAll('(', '').replaceAll(')', ''); - return asRenameKey([constraint.type, constraint.tableName, expression]); - } - } - }, - onRename: (source, target) => [ - { - type: 'ConstraintRename', - tableName: target.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'ConstraintAdd', - constraint: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ConstraintDrop', - tableName: target.tableName, - constraintName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - switch (source.type) { - case ConstraintType.PRIMARY_KEY: { - return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint); - } - - case ConstraintType.FOREIGN_KEY: { - return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint); - } - - case ConstraintType.UNIQUE: { - return compareUniqueConstraint(source, target as DatabaseUniqueConstraint); - } - - case ConstraintType.CHECK: { - return compareCheckConstraint(source, target as DatabaseCheckConstraint); - } - - default: { - return []; - } - } - }, -}; - -const comparePrimaryKeyConstraint: CompareFunction = (source, target) => { - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - return dropAndRecreateConstraint( - source, - target, - `Primary key columns are different: (${source.columnNames} vs ${target.columnNames})`, - ); - } - - return []; -}; - -const compareForeignKeyConstraint: CompareFunction = (source, target) => { - let reason = ''; - - const sourceDeleteAction = source.onDelete ?? 'NO ACTION'; - const targetDeleteAction = target.onDelete ?? 'NO ACTION'; - - const sourceUpdateAction = source.onUpdate ?? 'NO ACTION'; - const targetUpdateAction = target.onUpdate ?? 'NO ACTION'; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } else if (!haveEqualColumns(source.referenceColumnNames, target.referenceColumnNames)) { - reason = `reference columns are different (${source.referenceColumnNames} vs ${target.referenceColumnNames})`; - } else if (source.referenceTableName !== target.referenceTableName) { - reason = `reference table is different (${source.referenceTableName} vs ${target.referenceTableName})`; - } else if (sourceDeleteAction !== targetDeleteAction) { - reason = `ON DELETE action is different (${sourceDeleteAction} vs ${targetDeleteAction})`; - } else if (sourceUpdateAction !== targetUpdateAction) { - reason = `ON UPDATE action is different (${sourceUpdateAction} vs ${targetUpdateAction})`; - } - - if (reason) { - return dropAndRecreateConstraint(source, target, reason); - } - - return []; -}; - -const compareUniqueConstraint: CompareFunction = (source, target) => { - let reason = ''; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } - - if (reason) { - return dropAndRecreateConstraint(source, target, reason); - } - - return []; -}; - -const compareCheckConstraint: CompareFunction = (source, target) => { - if (source.expression !== target.expression) { - // comparing expressions is hard because postgres reconstructs it with different formatting - // for now if the constraint exists with the same name, we will just skip it - } - - return []; -}; - -const dropAndRecreateConstraint = ( - source: DatabaseConstraint, - target: DatabaseConstraint, - reason: string, -): SchemaDiff[] => { - return [ - { - type: 'ConstraintDrop', - tableName: target.tableName, - constraintName: target.name, - reason, - }, - { type: 'ConstraintAdd', constraint: source, reason }, - ]; -}; diff --git a/server/src/sql-tools/comparers/enum.comparer.spec.ts b/server/src/sql-tools/comparers/enum.comparer.spec.ts deleted file mode 100644 index 82fc205662..0000000000 --- a/server/src/sql-tools/comparers/enum.comparer.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; -import { DatabaseEnum, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testEnum: DatabaseEnum = { name: 'test', values: ['foo', 'bar'], synchronize: true }; - -describe('compareEnums', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareEnums.onExtra(testEnum)).toEqual([ - { - enumName: 'test', - type: 'EnumDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareEnums.onMissing(testEnum)).toEqual([ - { - type: 'EnumCreate', - enum: testEnum, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareEnums.onCompare(testEnum, testEnum)).toEqual([]); - }); - - it('should drop and recreate when values list is different', () => { - const source = { name: 'test', values: ['foo', 'bar'], synchronize: true }; - const target = { name: 'test', values: ['foo', 'bar', 'world'], synchronize: true }; - expect(compareEnums.onCompare(source, target)).toEqual([ - { - enumName: 'test', - type: 'EnumDrop', - reason: 'enum values has changed (foo,bar vs foo,bar,world)', - }, - { - type: 'EnumCreate', - enum: source, - reason: 'enum values has changed (foo,bar vs foo,bar,world)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/enum.comparer.ts b/server/src/sql-tools/comparers/enum.comparer.ts deleted file mode 100644 index d81f9ed3c0..0000000000 --- a/server/src/sql-tools/comparers/enum.comparer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types'; - -export const compareEnums: Comparer = { - onMissing: (source) => [ - { - type: 'EnumCreate', - enum: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'EnumDrop', - enumName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.values.toString() !== target.values.toString()) { - // TODO add or remove values if the lists are different or the order has changed - const reason = `enum values has changed (${source.values} vs ${target.values})`; - return [ - { - type: 'EnumDrop', - enumName: source.name, - reason, - }, - { - type: 'EnumCreate', - enum: source, - reason, - }, - ]; - } - - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/extension.comparer.spec.ts b/server/src/sql-tools/comparers/extension.comparer.spec.ts deleted file mode 100644 index 38e553719d..0000000000 --- a/server/src/sql-tools/comparers/extension.comparer.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; -import { Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testExtension = { name: 'test', synchronize: true }; - -describe('compareExtensions', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareExtensions.onExtra(testExtension)).toEqual([ - { - extensionName: 'test', - type: 'ExtensionDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareExtensions.onMissing(testExtension)).toEqual([ - { - type: 'ExtensionCreate', - extension: testExtension, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareExtensions.onCompare(testExtension, testExtension)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/extension.comparer.ts b/server/src/sql-tools/comparers/extension.comparer.ts deleted file mode 100644 index 441b00e3e3..0000000000 --- a/server/src/sql-tools/comparers/extension.comparer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types'; - -export const compareExtensions: Comparer = { - onMissing: (source) => [ - { - type: 'ExtensionCreate', - extension: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ExtensionDrop', - extensionName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: () => { - // if the name matches they are the same - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/function.comparer.spec.ts b/server/src/sql-tools/comparers/function.comparer.spec.ts deleted file mode 100644 index 964768cf98..0000000000 --- a/server/src/sql-tools/comparers/function.comparer.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; -import { DatabaseFunction, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testFunction: DatabaseFunction = { - name: 'test', - expression: 'CREATE FUNCTION something something something', - synchronize: true, -}; - -describe('compareFunctions', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareFunctions.onExtra(testFunction)).toEqual([ - { - functionName: 'test', - type: 'FunctionDrop', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareFunctions.onMissing(testFunction)).toEqual([ - { - type: 'FunctionCreate', - function: testFunction, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should ignore functions with the same hash', () => { - expect(compareFunctions.onCompare(testFunction, testFunction)).toEqual([]); - }); - - it('should report differences if functions have different hashes', () => { - const source: DatabaseFunction = { ...testFunction, expression: 'SELECT 1' }; - const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' }; - expect(compareFunctions.onCompare(source, target)).toEqual([ - { - type: 'FunctionCreate', - reason: 'function expression has changed (SELECT 1 vs SELECT 2)', - function: source, - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/function.comparer.ts b/server/src/sql-tools/comparers/function.comparer.ts deleted file mode 100644 index 000cf07058..0000000000 --- a/server/src/sql-tools/comparers/function.comparer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Comparer, DatabaseFunction, Reason } from 'src/sql-tools/types'; - -export const compareFunctions: Comparer = { - onMissing: (source) => [ - { - type: 'FunctionCreate', - function: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'FunctionDrop', - functionName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.expression !== target.expression) { - const reason = `function expression has changed (${source.expression} vs ${target.expression})`; - return [ - { - type: 'FunctionCreate', - function: source, - reason, - }, - ]; - } - - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/index.comparer.spec.ts b/server/src/sql-tools/comparers/index.comparer.spec.ts deleted file mode 100644 index b00be386e0..0000000000 --- a/server/src/sql-tools/comparers/index.comparer.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; -import { DatabaseIndex, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testIndex: DatabaseIndex = { - name: 'test', - tableName: 'table1', - columnNames: ['column1', 'column2'], - unique: false, - synchronize: true, -}; - -describe('compareIndexes', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareIndexes.onExtra(testIndex)).toEqual([ - { - type: 'IndexDrop', - indexName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareIndexes.onMissing(testIndex)).toEqual([ - { - type: 'IndexCreate', - index: testIndex, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareIndexes.onCompare(testIndex, testIndex)).toEqual([]); - }); - - it('should drop and recreate when column list is different', () => { - const source = { - name: 'test', - tableName: 'table1', - columnNames: ['column1'], - unique: true, - synchronize: true, - }; - const target = { - name: 'test', - tableName: 'table1', - columnNames: ['column1', 'column2'], - unique: true, - synchronize: true, - }; - expect(compareIndexes.onCompare(source, target)).toEqual([ - { - indexName: 'test', - type: 'IndexDrop', - reason: 'columns are different (column1 vs column1,column2)', - }, - { - type: 'IndexCreate', - index: source, - reason: 'columns are different (column1 vs column1,column2)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/index.comparer.ts b/server/src/sql-tools/comparers/index.comparer.ts deleted file mode 100644 index a3db9a61e0..0000000000 --- a/server/src/sql-tools/comparers/index.comparer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types'; - -export const compareIndexes: Comparer = { - getRenameKey: (index) => { - if (index.override) { - return index.override.value.sql.replace(index.name, 'INDEX_NAME'); - } - - return asRenameKey([index.tableName, ...(index.columnNames || []), index.unique]); - }, - onRename: (source, target) => [ - { - type: 'IndexRename', - tableName: source.tableName, - oldName: target.name, - newName: source.name, - reason: Reason.Rename, - }, - ], - onMissing: (source) => [ - { - type: 'IndexCreate', - index: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'IndexDrop', - indexName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - const sourceUsing = source.using ?? 'btree'; - const targetUsing = target.using ?? 'btree'; - - let reason = ''; - - if (!haveEqualColumns(source.columnNames, target.columnNames)) { - reason = `columns are different (${source.columnNames} vs ${target.columnNames})`; - } else if (source.unique !== target.unique) { - reason = `uniqueness is different (${source.unique} vs ${target.unique})`; - } else if (sourceUsing !== targetUsing) { - reason = `using method is different (${source.using} vs ${target.using})`; - } else if (source.where !== target.where) { - reason = `where clause is different (${source.where} vs ${target.where})`; - } else if (source.expression !== target.expression) { - reason = `expression is different (${source.expression} vs ${target.expression})`; - } - - if (reason) { - return [ - { type: 'IndexDrop', indexName: target.name, reason }, - { type: 'IndexCreate', index: source, reason }, - ]; - } - - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/override.comparer.spec.ts b/server/src/sql-tools/comparers/override.comparer.spec.ts deleted file mode 100644 index 22093381ff..0000000000 --- a/server/src/sql-tools/comparers/override.comparer.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { compareOverrides } from 'src/sql-tools/comparers/override.comparer'; -import { DatabaseOverride, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testOverride: DatabaseOverride = { - name: 'test', - value: { type: 'function', name: 'test_func', sql: 'func implementation' }, - synchronize: true, -}; - -describe('compareOverrides', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareOverrides.onExtra(testOverride)).toEqual([ - { - type: 'OverrideDrop', - overrideName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareOverrides.onMissing(testOverride)).toEqual([ - { - type: 'OverrideCreate', - override: testOverride, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareOverrides.onCompare(testOverride, testOverride)).toEqual([]); - }); - - it('should drop and recreate when the value changes', () => { - const source: DatabaseOverride = { - name: 'test', - value: { - type: 'function', - name: 'test_func', - sql: 'func implementation', - }, - synchronize: true, - }; - const target: DatabaseOverride = { - name: 'test', - value: { - type: 'function', - name: 'test_func', - sql: 'func implementation2', - }, - synchronize: true, - }; - expect(compareOverrides.onCompare(source, target)).toEqual([ - { - override: source, - type: 'OverrideUpdate', - reason: expect.stringContaining('value is different'), - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/override.comparer.ts b/server/src/sql-tools/comparers/override.comparer.ts deleted file mode 100644 index 369f7cd59f..0000000000 --- a/server/src/sql-tools/comparers/override.comparer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Comparer, DatabaseOverride, Reason } from 'src/sql-tools/types'; - -export const compareOverrides: Comparer = { - onMissing: (source) => [ - { - type: 'OverrideCreate', - override: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'OverrideDrop', - overrideName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - if (source.value.name !== target.value.name || source.value.sql !== target.value.sql) { - const sourceValue = JSON.stringify(source.value); - const targetValue = JSON.stringify(target.value); - return [ - { type: 'OverrideUpdate', override: source, reason: `value is different (${sourceValue} vs ${targetValue})` }, - ]; - } - - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/parameter.comparer.spec.ts b/server/src/sql-tools/comparers/parameter.comparer.spec.ts deleted file mode 100644 index cd1520faff..0000000000 --- a/server/src/sql-tools/comparers/parameter.comparer.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; -import { DatabaseParameter, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testParameter: DatabaseParameter = { - name: 'test', - databaseName: 'immich', - value: 'on', - scope: 'database', - synchronize: true, -}; - -describe('compareParameters', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareParameters.onExtra(testParameter)).toEqual([ - { - type: 'ParameterReset', - databaseName: 'immich', - parameterName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareParameters.onMissing(testParameter)).toEqual([ - { - type: 'ParameterSet', - parameter: testParameter, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareParameters.onCompare(testParameter, testParameter)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/parameter.comparer.ts b/server/src/sql-tools/comparers/parameter.comparer.ts deleted file mode 100644 index d1a33ad090..0000000000 --- a/server/src/sql-tools/comparers/parameter.comparer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Comparer, DatabaseParameter, Reason } from 'src/sql-tools/types'; - -export const compareParameters: Comparer = { - onMissing: (source) => [ - { - type: 'ParameterSet', - parameter: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'ParameterReset', - databaseName: target.databaseName, - parameterName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: () => { - // TODO - return []; - }, -}; diff --git a/server/src/sql-tools/comparers/table.comparer.spec.ts b/server/src/sql-tools/comparers/table.comparer.spec.ts deleted file mode 100644 index 575e25ab44..0000000000 --- a/server/src/sql-tools/comparers/table.comparer.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { compareTables } from 'src/sql-tools/comparers/table.comparer'; -import { DatabaseTable, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testTable: DatabaseTable = { - name: 'test', - columns: [], - constraints: [], - indexes: [], - triggers: [], - synchronize: true, -}; - -describe('compareParameters', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareTables.onExtra(testTable)).toEqual([ - { - type: 'TableDrop', - tableName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareTables.onMissing(testTable)).toEqual([ - { - type: 'TableCreate', - table: testTable, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareTables.onCompare(testTable, testTable)).toEqual([]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/table.comparer.ts b/server/src/sql-tools/comparers/table.comparer.ts deleted file mode 100644 index 0b36b7fce4..0000000000 --- a/server/src/sql-tools/comparers/table.comparer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { compareColumns } from 'src/sql-tools/comparers/column.comparer'; -import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer'; -import { compareIndexes } from 'src/sql-tools/comparers/index.comparer'; -import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; -import { compare } from 'src/sql-tools/helpers'; -import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types'; - -export const compareTables: Comparer = { - onMissing: (source) => [ - { - type: 'TableCreate', - table: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'TableDrop', - tableName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => compareTable(source, target), -}; - -const compareTable = (source: DatabaseTable, target: DatabaseTable): SchemaDiff[] => { - return [ - ...compare(source.columns, target.columns, {}, compareColumns), - ...compare(source.indexes, target.indexes, {}, compareIndexes), - ...compare(source.constraints, target.constraints, {}, compareConstraints), - ...compare(source.triggers, target.triggers, {}, compareTriggers), - ]; -}; diff --git a/server/src/sql-tools/comparers/trigger.comparer.spec.ts b/server/src/sql-tools/comparers/trigger.comparer.spec.ts deleted file mode 100644 index 731fae8da2..0000000000 --- a/server/src/sql-tools/comparers/trigger.comparer.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer'; -import { DatabaseTrigger, Reason } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const testTrigger: DatabaseTrigger = { - name: 'test', - tableName: 'table1', - timing: 'before', - actions: ['delete'], - scope: 'row', - functionName: 'my_trigger_function', - synchronize: true, -}; - -describe('compareTriggers', () => { - describe('onExtra', () => { - it('should work', () => { - expect(compareTriggers.onExtra(testTrigger)).toEqual([ - { - type: 'TriggerDrop', - tableName: 'table1', - triggerName: 'test', - reason: Reason.MissingInSource, - }, - ]); - }); - }); - - describe('onMissing', () => { - it('should work', () => { - expect(compareTriggers.onMissing(testTrigger)).toEqual([ - { - type: 'TriggerCreate', - trigger: testTrigger, - reason: Reason.MissingInTarget, - }, - ]); - }); - }); - - describe('onCompare', () => { - it('should work', () => { - expect(compareTriggers.onCompare(testTrigger, testTrigger)).toEqual([]); - }); - - it('should detect a change in function name', () => { - const source: DatabaseTrigger = { ...testTrigger, functionName: 'my_new_name' }; - const target: DatabaseTrigger = { ...testTrigger, functionName: 'my_old_name' }; - const reason = `function is different (my_new_name vs my_old_name)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in actions', () => { - const source: DatabaseTrigger = { ...testTrigger, actions: ['delete'] }; - const target: DatabaseTrigger = { ...testTrigger, actions: ['delete', 'insert'] }; - const reason = `action is different (delete vs delete,insert)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in timing', () => { - const source: DatabaseTrigger = { ...testTrigger, timing: 'before' }; - const target: DatabaseTrigger = { ...testTrigger, timing: 'after' }; - const reason = `timing method is different (before vs after)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in scope', () => { - const source: DatabaseTrigger = { ...testTrigger, scope: 'row' }; - const target: DatabaseTrigger = { ...testTrigger, scope: 'statement' }; - const reason = `scope is different (row vs statement)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in new table reference', () => { - const source: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: 'new_table' }; - const target: DatabaseTrigger = { ...testTrigger, referencingNewTableAs: undefined }; - const reason = `new table reference is different (new_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - - it('should detect a change in old table reference', () => { - const source: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: 'old_table' }; - const target: DatabaseTrigger = { ...testTrigger, referencingOldTableAs: undefined }; - const reason = `old table reference is different (old_table vs undefined)`; - expect(compareTriggers.onCompare(source, target)).toEqual([{ type: 'TriggerCreate', trigger: source, reason }]); - }); - }); -}); diff --git a/server/src/sql-tools/comparers/trigger.comparer.ts b/server/src/sql-tools/comparers/trigger.comparer.ts deleted file mode 100644 index da1de6e48b..0000000000 --- a/server/src/sql-tools/comparers/trigger.comparer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Comparer, DatabaseTrigger, Reason } from 'src/sql-tools/types'; - -export const compareTriggers: Comparer = { - onMissing: (source) => [ - { - type: 'TriggerCreate', - trigger: source, - reason: Reason.MissingInTarget, - }, - ], - onExtra: (target) => [ - { - type: 'TriggerDrop', - tableName: target.tableName, - triggerName: target.name, - reason: Reason.MissingInSource, - }, - ], - onCompare: (source, target) => { - let reason = ''; - if (source.functionName !== target.functionName) { - reason = `function is different (${source.functionName} vs ${target.functionName})`; - } else if (source.actions.join(' OR ') !== target.actions.join(' OR ')) { - reason = `action is different (${source.actions} vs ${target.actions})`; - } else if (source.timing !== target.timing) { - reason = `timing method is different (${source.timing} vs ${target.timing})`; - } else if (source.scope !== target.scope) { - reason = `scope is different (${source.scope} vs ${target.scope})`; - } else if (source.referencingNewTableAs !== target.referencingNewTableAs) { - reason = `new table reference is different (${source.referencingNewTableAs} vs ${target.referencingNewTableAs})`; - } else if (source.referencingOldTableAs !== target.referencingOldTableAs) { - reason = `old table reference is different (${source.referencingOldTableAs} vs ${target.referencingOldTableAs})`; - } - - if (reason) { - return [{ type: 'TriggerCreate', trigger: source, reason }]; - } - - return []; - }, -}; diff --git a/server/src/sql-tools/contexts/base-context.ts b/server/src/sql-tools/contexts/base-context.ts deleted file mode 100644 index 0fa7230a00..0000000000 --- a/server/src/sql-tools/contexts/base-context.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; -import { HashNamingStrategy } from 'src/sql-tools/naming/hash.naming'; -import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; -import { - BaseContextOptions, - DatabaseEnum, - DatabaseExtension, - DatabaseFunction, - DatabaseOverride, - DatabaseParameter, - DatabaseSchema, - DatabaseTable, -} from 'src/sql-tools/types'; - -const asOverrideKey = (type: string, name: string) => `${type}:${name}`; - -const isNamingInterface = (strategy: any): strategy is NamingInterface => { - return typeof strategy === 'object' && typeof strategy.getName === 'function'; -}; - -const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => { - if (isNamingInterface(strategy)) { - return strategy; - } - - switch (strategy) { - case 'hash': { - return new HashNamingStrategy(); - } - - default: { - return new DefaultNamingStrategy(); - } - } -}; - -export class BaseContext { - databaseName: string; - schemaName: string; - overrideTableName: string; - - tables: DatabaseTable[] = []; - functions: DatabaseFunction[] = []; - enums: DatabaseEnum[] = []; - extensions: DatabaseExtension[] = []; - parameters: DatabaseParameter[] = []; - overrides: DatabaseOverride[] = []; - warnings: string[] = []; - - private namingStrategy: NamingInterface; - - constructor(options: BaseContextOptions) { - this.databaseName = options.databaseName ?? 'postgres'; - this.schemaName = options.schemaName ?? 'public'; - this.overrideTableName = options.overrideTableName ?? 'migration_overrides'; - this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash'); - } - - getNameFor(item: NamingItem) { - return this.namingStrategy.getName(item); - } - - getTableByName(name: string) { - return this.tables.find((table) => table.name === name); - } - - warn(context: string, message: string) { - this.warnings.push(`[${context}] ${message}`); - } - - build(): DatabaseSchema { - const overrideMap = new Map(); - for (const override of this.overrides) { - const { type, name } = override.value; - overrideMap.set(asOverrideKey(type, name), override); - } - - for (const func of this.functions) { - func.override = overrideMap.get(asOverrideKey('function', func.name)); - } - - for (const { indexes, triggers } of this.tables) { - for (const index of indexes) { - index.override = overrideMap.get(asOverrideKey('index', index.name)); - } - - for (const trigger of triggers) { - trigger.override = overrideMap.get(asOverrideKey('trigger', trigger.name)); - } - } - - return { - databaseName: this.databaseName, - schemaName: this.schemaName, - tables: this.tables, - functions: this.functions, - enums: this.enums, - extensions: this.extensions, - parameters: this.parameters, - overrides: this.overrides, - warnings: this.warnings, - }; - } -} diff --git a/server/src/sql-tools/contexts/processor-context.ts b/server/src/sql-tools/contexts/processor-context.ts deleted file mode 100644 index 3ab196b0af..0000000000 --- a/server/src/sql-tools/contexts/processor-context.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; -import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types'; - -type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map }; - -export class ProcessorContext extends BaseContext { - constructor(public options: SchemaFromCodeOptions) { - options.createForeignKeyIndexes = options.createForeignKeyIndexes ?? true; - options.overrides = options.overrides ?? false; - super(options); - } - - classToTable: WeakMap = new WeakMap(); - tableToMetadata: WeakMap = new WeakMap(); - - getTableByObject(object: Function) { - return this.classToTable.get(object); - } - - getTableMetadata(table: DatabaseTable) { - const metadata = this.tableToMetadata.get(table); - if (!metadata) { - throw new Error(`Table metadata not found for table: ${table.name}`); - } - return metadata; - } - - addTable(table: DatabaseTable, options: TableOptions, object: Function) { - this.tables.push(table); - this.classToTable.set(object, table); - this.tableToMetadata.set(table, { options, object, methodToColumn: new Map() }); - } - - getColumnByObjectAndPropertyName( - object: object, - propertyName: string | symbol, - ): { table?: DatabaseTable; column?: DatabaseColumn } { - const table = this.getTableByObject(object.constructor); - if (!table) { - return {}; - } - - const tableMetadata = this.tableToMetadata.get(table); - if (!tableMetadata) { - return {}; - } - - const column = tableMetadata.methodToColumn.get(propertyName); - - return { table, column }; - } - - addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) { - table.columns.push(column); - const tableMetadata = this.getTableMetadata(table); - tableMetadata.methodToColumn.set(propertyName, column); - } - - warnMissingTable(context: string, object: object, propertyName?: symbol | string) { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - this.warn(context, `Unable to find table (${label})`); - } - - warnMissingColumn(context: string, object: object, propertyName?: symbol | string) { - const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : ''); - this.warn(context, `Unable to find column (${label})`); - } -} diff --git a/server/src/sql-tools/contexts/reader-context.ts b/server/src/sql-tools/contexts/reader-context.ts deleted file mode 100644 index 94f5c82fc1..0000000000 --- a/server/src/sql-tools/contexts/reader-context.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { SchemaFromDatabaseOptions } from 'src/sql-tools/types'; - -export class ReaderContext extends BaseContext { - constructor(public options: SchemaFromDatabaseOptions) { - super(options); - } -} diff --git a/server/src/sql-tools/decorators/after-delete.decorator.ts b/server/src/sql-tools/decorators/after-delete.decorator.ts deleted file mode 100644 index 181bfab6c8..0000000000 --- a/server/src/sql-tools/decorators/after-delete.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const AfterDeleteTrigger = (options: Omit) => - TriggerFunction({ - timing: 'after', - actions: ['delete'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/after-insert.decorator.ts b/server/src/sql-tools/decorators/after-insert.decorator.ts deleted file mode 100644 index c302a5cebe..0000000000 --- a/server/src/sql-tools/decorators/after-insert.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const AfterInsertTrigger = (options: Omit) => - TriggerFunction({ - timing: 'after', - actions: ['insert'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/before-update.decorator.ts b/server/src/sql-tools/decorators/before-update.decorator.ts deleted file mode 100644 index 2119e29c9b..0000000000 --- a/server/src/sql-tools/decorators/before-update.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator'; - -export const BeforeUpdateTrigger = (options: Omit) => - TriggerFunction({ - timing: 'before', - actions: ['update'], - ...options, - }); diff --git a/server/src/sql-tools/decorators/check.decorator.ts b/server/src/sql-tools/decorators/check.decorator.ts deleted file mode 100644 index 56fe1ecc3f..0000000000 --- a/server/src/sql-tools/decorators/check.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type CheckOptions = { - name?: string; - expression: string; - synchronize?: boolean; -}; -export const Check = (options: CheckOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/column.decorator.ts b/server/src/sql-tools/decorators/column.decorator.ts deleted file mode 100644 index e5a0eb52f8..0000000000 --- a/server/src/sql-tools/decorators/column.decorator.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; -import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types'; - -export type ColumnValue = null | boolean | string | number | Array | object | Date | (() => string); - -export type ColumnBaseOptions = { - name?: string; - primary?: boolean; - type?: ColumnType; - nullable?: boolean; - length?: number; - default?: ColumnValue; - comment?: string; - synchronize?: boolean; - storage?: ColumnStorage; - identity?: boolean; - index?: boolean; - indexName?: string; - unique?: boolean; - uniqueConstraintName?: string; -}; - -export type ColumnOptions = ColumnBaseOptions & { - enum?: DatabaseEnum; - array?: boolean; -}; - -export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => - void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/configuration-parameter.decorator.ts b/server/src/sql-tools/decorators/configuration-parameter.decorator.ts deleted file mode 100644 index 953027d25c..0000000000 --- a/server/src/sql-tools/decorators/configuration-parameter.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { register } from 'src/sql-tools/register'; -import { ParameterScope } from 'src/sql-tools/types'; - -export type ConfigurationParameterOptions = { - name: string; - value: ColumnValue; - scope: ParameterScope; - synchronize?: boolean; -}; -export const ConfigurationParameter = (options: ConfigurationParameterOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'configurationParameter', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/create-date-column.decorator.ts b/server/src/sql-tools/decorators/create-date-column.decorator.ts deleted file mode 100644 index 1a3362a614..0000000000 --- a/server/src/sql-tools/decorators/create-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - default: () => 'now()', - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/database.decorator.ts b/server/src/sql-tools/decorators/database.decorator.ts deleted file mode 100644 index 17b2460df6..0000000000 --- a/server/src/sql-tools/decorators/database.decorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type DatabaseOptions = { - name?: string; - synchronize?: boolean; -}; -export const Database = (options: DatabaseOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'database', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/delete-date-column.decorator.ts b/server/src/sql-tools/decorators/delete-date-column.decorator.ts deleted file mode 100644 index ca5427c27f..0000000000 --- a/server/src/sql-tools/decorators/delete-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - nullable: true, - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/extension.decorator.ts b/server/src/sql-tools/decorators/extension.decorator.ts deleted file mode 100644 index d431cbfd02..0000000000 --- a/server/src/sql-tools/decorators/extension.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type ExtensionOptions = { - name: string; - synchronize?: boolean; -}; -export const Extension = (options: string | ExtensionOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'extension', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/extensions.decorator.ts b/server/src/sql-tools/decorators/extensions.decorator.ts deleted file mode 100644 index 724446c5fa..0000000000 --- a/server/src/sql-tools/decorators/extensions.decorator.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type ExtensionsOptions = { - name: string; - synchronize?: boolean; -}; -export const Extensions = (options: Array): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => { - for (const option of options) { - register({ type: 'extension', item: { object, options: asOptions(option) } }); - } - }; -}; diff --git a/server/src/sql-tools/decorators/foreign-key-column.decorator.ts b/server/src/sql-tools/decorators/foreign-key-column.decorator.ts deleted file mode 100644 index c9c83f010d..0000000000 --- a/server/src/sql-tools/decorators/foreign-key-column.decorator.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { ForeignKeyAction } from 'src/sql-tools//decorators/foreign-key-constraint.decorator'; -import { ColumnBaseOptions } from 'src/sql-tools/decorators/column.decorator'; -import { register } from 'src/sql-tools/register'; - -export type ForeignKeyColumnOptions = ColumnBaseOptions & { - onUpdate?: ForeignKeyAction; - onDelete?: ForeignKeyAction; - constraintName?: string; -}; - -export const ForeignKeyColumn = (target: () => Function, options: ForeignKeyColumnOptions): PropertyDecorator => { - return (object: object, propertyName: string | symbol) => { - register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } }); - }; -}; diff --git a/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts b/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts deleted file mode 100644 index e5d2f513dc..0000000000 --- a/server/src/sql-tools/decorators/foreign-key-constraint.decorator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION'; - -export type ForeignKeyConstraintOptions = { - name?: string; - index?: boolean; - indexName?: string; - columns: string[]; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - referenceTable: () => Function; - referenceColumns?: string[]; - onUpdate?: ForeignKeyAction; - onDelete?: ForeignKeyAction; - synchronize?: boolean; -}; - -export const ForeignKeyConstraint = (options: ForeignKeyConstraintOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (target: Function) => { - register({ type: 'foreignKeyConstraint', item: { object: target, options } }); - }; -}; diff --git a/server/src/sql-tools/decorators/generated-column.decorator.ts b/server/src/sql-tools/decorators/generated-column.decorator.ts deleted file mode 100644 index 4338b4146c..0000000000 --- a/server/src/sql-tools/decorators/generated-column.decorator.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { ColumnType } from 'src/sql-tools/types'; - -export type GeneratedColumnStrategy = 'uuid' | 'identity'; - -export type GenerateColumnOptions = Omit & { - strategy?: GeneratedColumnStrategy; -}; - -export const GeneratedColumn = ({ strategy = 'uuid', ...options }: GenerateColumnOptions): PropertyDecorator => { - let columnType: ColumnType | undefined; - let columnDefault: ColumnValue | undefined; - - switch (strategy) { - case 'uuid': { - columnType = 'uuid'; - columnDefault = () => 'uuid_generate_v4()'; - break; - } - - case 'identity': { - columnType = 'integer'; - options.identity = true; - break; - } - - default: { - throw new Error(`Unsupported strategy for @GeneratedColumn ${strategy}`); - } - } - - return Column({ - type: columnType, - default: columnDefault, - ...options, - }); -}; diff --git a/server/src/sql-tools/decorators/index.decorator.ts b/server/src/sql-tools/decorators/index.decorator.ts deleted file mode 100644 index 1b6d38e390..0000000000 --- a/server/src/sql-tools/decorators/index.decorator.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type IndexOptions = { - name?: string; - unique?: boolean; - expression?: string; - using?: string; - with?: string; - where?: string; - columns?: string[]; - synchronize?: boolean; -}; -export const Index = (options: string | IndexOptions = {}): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/primary-column.decorator.ts b/server/src/sql-tools/decorators/primary-column.decorator.ts deleted file mode 100644 index e605b4be5d..0000000000 --- a/server/src/sql-tools/decorators/primary-column.decorator.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const PrimaryColumn = (options: Omit = {}) => Column({ ...options, primary: true }); diff --git a/server/src/sql-tools/decorators/primary-generated-column.decorator.ts b/server/src/sql-tools/decorators/primary-generated-column.decorator.ts deleted file mode 100644 index 25e125ebf6..0000000000 --- a/server/src/sql-tools/decorators/primary-generated-column.decorator.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/decorators/generated-column.decorator'; - -export const PrimaryGeneratedColumn = (options: Omit = {}) => - GeneratedColumn({ ...options, primary: true }); diff --git a/server/src/sql-tools/decorators/table.decorator.ts b/server/src/sql-tools/decorators/table.decorator.ts deleted file mode 100644 index 7ea5882147..0000000000 --- a/server/src/sql-tools/decorators/table.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { asOptions } from 'src/sql-tools/helpers'; -import { register } from 'src/sql-tools/register'; - -export type TableOptions = { - name?: string; - primaryConstraintName?: string; - synchronize?: boolean; -}; - -/** Table comments here */ -export const Table = (options: string | TableOptions = {}): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } }); -}; diff --git a/server/src/sql-tools/decorators/trigger-function.decorator.ts b/server/src/sql-tools/decorators/trigger-function.decorator.ts deleted file mode 100644 index 17016f7946..0000000000 --- a/server/src/sql-tools/decorators/trigger-function.decorator.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Trigger, TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { DatabaseFunction } from 'src/sql-tools/types'; - -export type TriggerFunctionOptions = Omit & { function: DatabaseFunction }; -export const TriggerFunction = (options: TriggerFunctionOptions) => - Trigger({ - name: options.function.name, - ...options, - functionName: options.function.name, - }); diff --git a/server/src/sql-tools/decorators/trigger.decorator.ts b/server/src/sql-tools/decorators/trigger.decorator.ts deleted file mode 100644 index ce9a5c17f7..0000000000 --- a/server/src/sql-tools/decorators/trigger.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export type TriggerOptions = { - name?: string; - timing: TriggerTiming; - actions: TriggerAction[]; - scope: TriggerScope; - functionName: string; - referencingNewTableAs?: string; - referencingOldTableAs?: string; - when?: string; - synchronize?: boolean; -}; - -export const Trigger = (options: TriggerOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'trigger', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/unique.decorator.ts b/server/src/sql-tools/decorators/unique.decorator.ts deleted file mode 100644 index 1f61fccb6f..0000000000 --- a/server/src/sql-tools/decorators/unique.decorator.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { register } from 'src/sql-tools/register'; - -export type UniqueOptions = { - name?: string; - columns: string[]; - synchronize?: boolean; -}; -export const Unique = (options: UniqueOptions): ClassDecorator => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } }); -}; diff --git a/server/src/sql-tools/decorators/update-date-column.decorator.ts b/server/src/sql-tools/decorators/update-date-column.decorator.ts deleted file mode 100644 index 68dd50c617..0000000000 --- a/server/src/sql-tools/decorators/update-date-column.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; - -export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => { - return Column({ - type: 'timestamp with time zone', - default: () => 'now()', - ...options, - }); -}; diff --git a/server/src/sql-tools/helpers.ts b/server/src/sql-tools/helpers.ts deleted file mode 100644 index e0daf8262f..0000000000 --- a/server/src/sql-tools/helpers.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { createHash } from 'node:crypto'; -import { ColumnValue } from 'src/sql-tools/decorators/column.decorator'; -import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types'; - -export const asOptions = (options: string | T): T => { - if (typeof options === 'string') { - return { name: options } as T; - } - - return options; -}; - -export const sha1 = (value: string) => createHash('sha1').update(value).digest('hex'); - -export const fromColumnValue = (columnValue?: ColumnValue) => { - if (columnValue === undefined) { - return; - } - - if (typeof columnValue === 'function') { - return columnValue() as string; - } - - const value = columnValue; - - if (value === null) { - return value; - } - - if (typeof value === 'number') { - return String(value); - } - - if (typeof value === 'boolean') { - return value ? 'true' : 'false'; - } - - if (value instanceof Date) { - return `'${value.toISOString()}'`; - } - - if (Array.isArray(value)) { - return "'{}'"; - } - - return `'${String(value)}'`; -}; - -export const setIsEqual = (source: Set, target: Set) => - source.size === target.size && [...source].every((x) => target.has(x)); - -export const haveEqualColumns = (sourceColumns?: string[], targetColumns?: string[]) => { - return setIsEqual(new Set(sourceColumns), new Set(targetColumns)); -}; - -export const haveEqualOverrides = (source: T, target: T) => { - if (!source.override || !target.override) { - return false; - } - - const sourceValue = source.override.value; - const targetValue = target.override.value; - - return sourceValue.name === targetValue.name && sourceValue.sql === targetValue.sql; -}; - -export const compare = ( - sources: T[], - targets: T[], - options: IgnoreOptions | undefined, - comparer: Comparer, -) => { - options = options || {}; - const sourceMap = Object.fromEntries(sources.map((table) => [table.name, table])); - const targetMap = Object.fromEntries(targets.map((table) => [table.name, table])); - const items: SchemaDiff[] = []; - - const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]); - const missingKeys = new Set(); - const extraKeys = new Set(); - - // common keys - for (const key of keys) { - const source = sourceMap[key]; - const target = targetMap[key]; - - if (isIgnored(source, target, options ?? true)) { - continue; - } - - if (isSynchronizeDisabled(source, target)) { - continue; - } - - if (source && !target) { - missingKeys.add(key); - continue; - } - - if (!source && target) { - extraKeys.add(key); - continue; - } - - if ( - haveEqualOverrides( - source as unknown as { override?: DatabaseOverride }, - target as unknown as { override?: DatabaseOverride }, - ) - ) { - continue; - } - - items.push(...comparer.onCompare(source, target)); - } - - // renames - if (comparer.getRenameKey && comparer.onRename) { - const renameMap: Record = {}; - for (const sourceKey of missingKeys) { - const source = sourceMap[sourceKey]; - const renameKey = comparer.getRenameKey(source); - renameMap[renameKey] = sourceKey; - } - - for (const targetKey of extraKeys) { - const target = targetMap[targetKey]; - const renameKey = comparer.getRenameKey(target); - const sourceKey = renameMap[renameKey]; - if (!sourceKey) { - continue; - } - - const source = sourceMap[sourceKey]; - - items.push(...comparer.onRename(source, target)); - - missingKeys.delete(sourceKey); - extraKeys.delete(targetKey); - } - } - - // missing - for (const key of missingKeys) { - items.push(...comparer.onMissing(sourceMap[key])); - } - - // extra - for (const key of extraKeys) { - items.push(...comparer.onExtra(targetMap[key])); - } - - return items; -}; - -const isIgnored = ( - source: { synchronize?: boolean } | undefined, - target: { synchronize?: boolean } | undefined, - options: IgnoreOptions, -) => { - if (typeof options === 'boolean') { - return !options; - } - return (options.ignoreExtra && !source) || (options.ignoreMissing && !target); -}; - -const isSynchronizeDisabled = (source?: { synchronize?: boolean }, target?: { synchronize?: boolean }) => { - return source?.synchronize === false || target?.synchronize === false; -}; - -export const isDefaultEqual = (source: DatabaseColumn, target: DatabaseColumn) => { - if (source.default === target.default) { - return true; - } - - if (source.default === undefined || target.default === undefined) { - return false; - } - - if ( - withTypeCast(source.default, getColumnType(source)) === target.default || - withTypeCast(target.default, getColumnType(target)) === source.default - ) { - return true; - } - - return false; -}; - -export const getColumnType = (column: DatabaseColumn) => { - let type = column.enumName || column.type; - if (column.isArray) { - type += `[${column.length ?? ''}]`; - } else if (column.length !== undefined) { - type += `(${column.length})`; - } - - return type; -}; - -const withTypeCast = (value: string, type: string) => { - if (!value.startsWith(`'`)) { - value = `'${value}'`; - } - return `${value}::${type}`; -}; - -export const getColumnModifiers = (column: DatabaseColumn) => { - const modifiers: string[] = []; - - if (!column.nullable) { - modifiers.push('NOT NULL'); - } - - if (column.default) { - modifiers.push(`DEFAULT ${column.default}`); - } - if (column.identity) { - modifiers.push(`GENERATED ALWAYS AS IDENTITY`); - } - - return modifiers.length === 0 ? '' : ' ' + modifiers.join(' '); -}; - -export const asColumnComment = (tableName: string, columnName: string, comment: string): string => { - return `COMMENT ON COLUMN "${tableName}"."${columnName}" IS '${comment}';`; -}; - -export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', '); - -export const asJsonString = (value: unknown): string => { - return `'${escape(JSON.stringify(value))}'::jsonb`; -}; - -const escape = (value: string) => { - return value - .replaceAll("'", "''") - .replaceAll(/[\\]/g, '\\\\') - .replaceAll(/[\b]/g, String.raw`\b`) - .replaceAll(/[\f]/g, String.raw`\f`) - .replaceAll(/[\n]/g, String.raw`\n`) - .replaceAll(/[\r]/g, String.raw`\r`) - .replaceAll(/[\t]/g, String.raw`\t`); -}; - -export const asRenameKey = (values: Array) => - values.map((value) => value ?? '').join('|'); diff --git a/server/src/sql-tools/index.ts b/server/src/sql-tools/index.ts deleted file mode 100644 index 0d3e53df51..0000000000 --- a/server/src/sql-tools/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'src/sql-tools/public_api'; diff --git a/server/src/sql-tools/naming/default.naming.ts b/server/src/sql-tools/naming/default.naming.ts deleted file mode 100644 index 807580169d..0000000000 --- a/server/src/sql-tools/naming/default.naming.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { sha1 } from 'src/sql-tools/helpers'; -import { NamingItem } from 'src/sql-tools/naming/naming.interface'; - -const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); - -export class DefaultNamingStrategy { - getName(item: NamingItem): string { - switch (item.type) { - case 'database': { - return asSnakeCase(item.name); - } - - case 'table': { - return asSnakeCase(item.name); - } - - case 'column': { - return item.name; - } - - case 'primaryKey': { - return `${item.tableName}_pkey`; - } - - case 'foreignKey': { - return `${item.tableName}_${item.columnNames.join('_')}_fkey`; - } - - case 'check': { - return `${item.tableName}_${sha1(item.expression).slice(0, 8)}_chk`; - } - - case 'unique': { - return `${item.tableName}_${item.columnNames.join('_')}_uq`; - } - - case 'index': { - if (item.columnNames) { - return `${item.tableName}_${item.columnNames.join('_')}_idx`; - } - - return `${item.tableName}_${sha1(item.expression || item.where || '').slice(0, 8)}_idx`; - } - - case 'trigger': { - return `${item.tableName}_${item.functionName}`; - } - } - } -} diff --git a/server/src/sql-tools/naming/hash.naming.ts b/server/src/sql-tools/naming/hash.naming.ts deleted file mode 100644 index 575d0f1239..0000000000 --- a/server/src/sql-tools/naming/hash.naming.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { sha1 } from 'src/sql-tools/helpers'; -import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming'; -import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface'; - -const fallback = new DefaultNamingStrategy(); - -const asKey = (prefix: string, tableName: string, values: string[]) => - (prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30); - -export class HashNamingStrategy implements NamingInterface { - getName(item: NamingItem): string { - switch (item.type) { - case 'primaryKey': { - return asKey('PK_', item.tableName, item.columnNames); - } - - case 'foreignKey': { - return asKey('FK_', item.tableName, item.columnNames); - } - - case 'check': { - return asKey('CHK_', item.tableName, [item.expression]); - } - - case 'unique': { - return asKey('UQ_', item.tableName, item.columnNames); - } - - case 'index': { - const items: string[] = []; - for (const columnName of item.columnNames ?? []) { - items.push(columnName); - } - - if (item.where) { - items.push(item.where); - } - - return asKey('IDX_', item.tableName, items); - } - - case 'trigger': { - return asKey('TR_', item.tableName, [...item.actions, item.scope, item.timing, item.functionName]); - } - - default: { - return fallback.getName(item); - } - } - } -} diff --git a/server/src/sql-tools/naming/naming.interface.ts b/server/src/sql-tools/naming/naming.interface.ts deleted file mode 100644 index f331a22c46..0000000000 --- a/server/src/sql-tools/naming/naming.interface.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export type NamingItem = - | { - type: 'database'; - name: string; - } - | { - type: 'table'; - name: string; - } - | { - type: 'column'; - name: string; - } - | { - type: 'primaryKey'; - tableName: string; - columnNames: string[]; - } - | { - type: 'foreignKey'; - tableName: string; - columnNames: string[]; - referenceTableName: string; - referenceColumnNames: string[]; - } - | { - type: 'check'; - tableName: string; - expression: string; - } - | { - type: 'unique'; - tableName: string; - columnNames: string[]; - } - | { - type: 'index'; - tableName: string; - columnNames?: string[]; - expression?: string; - where?: string; - } - | { - type: 'trigger'; - tableName: string; - functionName: string; - actions: TriggerAction[]; - scope: TriggerScope; - timing: TriggerTiming; - columnNames?: string[]; - expression?: string; - where?: string; - }; - -export interface NamingInterface { - getName(item: NamingItem): string; -} diff --git a/server/src/sql-tools/processors/check-constraint.processor.ts b/server/src/sql-tools/processors/check-constraint.processor.ts deleted file mode 100644 index 5eba1015bf..0000000000 --- a/server/src/sql-tools/processors/check-constraint.processor.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processCheckConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'checkConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Check', object); - continue; - } - - const tableName = table.name; - - table.constraints.push({ - type: ConstraintType.CHECK, - name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }), - tableName, - expression: options.expression, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/column.processor.ts b/server/src/sql-tools/processors/column.processor.ts deleted file mode 100644 index 9b499b380b..0000000000 --- a/server/src/sql-tools/processors/column.processor.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { fromColumnValue } from 'src/sql-tools/helpers'; -import { Processor } from 'src/sql-tools/types'; - -export const processColumns: Processor = (ctx, items) => { - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const table = ctx.getTableByObject(object.constructor); - if (!table) { - ctx.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName); - continue; - } - - const columnName = options.name ?? ctx.getNameFor({ type: 'column', name: String(propertyName) }); - const existingColumn = table.columns.find((column) => column.name === columnName); - if (existingColumn) { - // TODO log warnings if column name is not unique - continue; - } - - let defaultValue = fromColumnValue(options.default); - let nullable = options.nullable ?? false; - - // map `{ default: null }` to `{ nullable: true }` - if (defaultValue === null) { - nullable = true; - defaultValue = undefined; - } - - const isEnum = !!(options as ColumnOptions).enum; - - ctx.addColumn( - table, - { - name: columnName, - tableName: table.name, - primary: options.primary ?? false, - default: defaultValue, - nullable, - isArray: (options as ColumnOptions).array ?? false, - length: options.length, - type: isEnum ? 'enum' : options.type || 'character varying', - enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined, - comment: options.comment, - storage: options.storage, - identity: options.identity, - synchronize: options.synchronize ?? true, - }, - options, - propertyName, - ); - } -}; diff --git a/server/src/sql-tools/processors/configuration-parameter.processor.ts b/server/src/sql-tools/processors/configuration-parameter.processor.ts deleted file mode 100644 index dbb5cd4636..0000000000 --- a/server/src/sql-tools/processors/configuration-parameter.processor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { fromColumnValue } from 'src/sql-tools/helpers'; -import { Processor } from 'src/sql-tools/types'; - -export const processConfigurationParameters: Processor = (ctx, items) => { - for (const { - item: { options }, - } of items.filter((item) => item.type === 'configurationParameter')) { - ctx.parameters.push({ - databaseName: ctx.databaseName, - name: options.name, - value: fromColumnValue(options.value), - scope: options.scope, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/database.processor.ts b/server/src/sql-tools/processors/database.processor.ts deleted file mode 100644 index 9f2e847fd6..0000000000 --- a/server/src/sql-tools/processors/database.processor.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processDatabases: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'database')) { - ctx.databaseName = options.name || ctx.getNameFor({ type: 'database', name: object.name }); - } -}; diff --git a/server/src/sql-tools/processors/enum.processor.ts b/server/src/sql-tools/processors/enum.processor.ts deleted file mode 100644 index 1ef65231c9..0000000000 --- a/server/src/sql-tools/processors/enum.processor.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processEnums: Processor = (ctx, items) => { - for (const { item } of items.filter((item) => item.type === 'enum')) { - // TODO log warnings if enum name is not unique - ctx.enums.push(item); - } -}; diff --git a/server/src/sql-tools/processors/extension.processor.ts b/server/src/sql-tools/processors/extension.processor.ts deleted file mode 100644 index 068c66883c..0000000000 --- a/server/src/sql-tools/processors/extension.processor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processExtensions: Processor = (ctx, items) => { - if (ctx.options.extensions === false) { - return; - } - - for (const { - item: { options }, - } of items.filter((item) => item.type === 'extension')) { - ctx.extensions.push({ - name: options.name, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/foreign-key-column.processor.ts b/server/src/sql-tools/processors/foreign-key-column.processor.ts deleted file mode 100644 index 6d147a78eb..0000000000 --- a/server/src/sql-tools/processors/foreign-key-column.processor.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processForeignKeyColumns: Processor = (ctx, items) => { - for (const { - item: { object, propertyName, options, target }, - } of items.filter((item) => item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@ForeignKeyColumn', object); - continue; - } - - if (!column) { - // should be impossible since they are pre-created in `column.processor.ts` - ctx.warnMissingColumn('@ForeignKeyColumn', object, propertyName); - continue; - } - - const referenceTable = ctx.getTableByObject(target()); - if (!referenceTable) { - ctx.warnMissingTable('@ForeignKeyColumn', object, propertyName); - continue; - } - - const columnNames = [column.name]; - const referenceColumns = referenceTable.columns.filter((column) => column.primary); - - // infer FK column type from reference table - if (referenceColumns.length === 1) { - column.type = referenceColumns[0].type; - } - - const referenceTableName = referenceTable.name; - const referenceColumnNames = referenceColumns.map((column) => column.name); - const name = - options.constraintName || - ctx.getNameFor({ - type: 'foreignKey', - tableName: table.name, - columnNames, - referenceTableName, - referenceColumnNames, - }); - - table.constraints.push({ - name, - tableName: table.name, - columnNames, - type: ConstraintType.FOREIGN_KEY, - referenceTableName, - referenceColumnNames, - onUpdate: options.onUpdate as ActionType, - onDelete: options.onDelete as ActionType, - synchronize: options.synchronize ?? true, - }); - - if (options.unique || options.uniqueConstraintName) { - table.constraints.push({ - name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }), - tableName: table.name, - columnNames, - type: ConstraintType.UNIQUE, - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts b/server/src/sql-tools/processors/foreign-key-constraint.processor.ts deleted file mode 100644 index 39d7508d11..0000000000 --- a/server/src/sql-tools/processors/foreign-key-constraint.processor.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processForeignKeyConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'foreignKeyConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@ForeignKeyConstraint', { name: 'referenceTable' }); - continue; - } - - const referenceTable = ctx.getTableByObject(options.referenceTable()); - if (!referenceTable) { - const referenceTableName = options.referenceTable()?.name; - ctx.warn( - '@ForeignKeyConstraint.referenceTable', - `Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''), - ); - continue; - } - - let missingColumn = false; - - for (const columnName of options.columns) { - if (!table.columns.some(({ name }) => name === columnName)) { - const metadata = ctx.getTableMetadata(table); - ctx.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`); - missingColumn = true; - } - } - - for (const columnName of options.referenceColumns || []) { - if (!referenceTable.columns.some(({ name }) => name === columnName)) { - const metadata = ctx.getTableMetadata(referenceTable); - ctx.warn( - '@ForeignKeyConstraint.referenceColumns', - `Unable to find column (${metadata.object.name}.${columnName})`, - ); - missingColumn = true; - } - } - - if (missingColumn) { - continue; - } - - const referenceTableName = referenceTable.name; - const referenceColumnNames = - options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name); - - const name = - options.name || - ctx.getNameFor({ - type: 'foreignKey', - tableName: table.name, - columnNames: options.columns, - referenceTableName, - referenceColumnNames, - }); - - table.constraints.push({ - type: ConstraintType.FOREIGN_KEY, - name, - tableName: table.name, - columnNames: options.columns, - referenceTableName, - referenceColumnNames, - onUpdate: options.onUpdate as ActionType, - onDelete: options.onDelete as ActionType, - synchronize: options.synchronize ?? true, - }); - - if (options.index === false) { - continue; - } - - if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) { - const indexName = - options.indexName || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: options.columns, - }); - table.indexes.push({ - name: indexName, - tableName: table.name, - columnNames: options.columns, - unique: false, - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/function.processor.ts b/server/src/sql-tools/processors/function.processor.ts deleted file mode 100644 index 9b351b77f7..0000000000 --- a/server/src/sql-tools/processors/function.processor.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processFunctions: Processor = (ctx, items) => { - if (ctx.options.functions === false) { - return; - } - - for (const { item } of items.filter((item) => item.type === 'function')) { - // TODO log warnings if function name is not unique - ctx.functions.push(item); - } -}; diff --git a/server/src/sql-tools/processors/index.processor.ts b/server/src/sql-tools/processors/index.processor.ts deleted file mode 100644 index 766e83fe8b..0000000000 --- a/server/src/sql-tools/processors/index.processor.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processIndexes: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'index')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Check', object); - continue; - } - - const indexName = - options.name || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: options.columns, - where: options.where, - }); - - table.indexes.push({ - name: indexName, - tableName: table.name, - unique: options.unique ?? false, - expression: options.expression, - using: options.using, - with: options.with, - where: options.where, - columnNames: options.columns, - synchronize: options.synchronize ?? true, - }); - } - - // column indexes - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@Column', object); - continue; - } - - if (!column) { - // should be impossible since they are created in `column.processor.ts` - ctx.warnMissingColumn('@Column', object, propertyName); - continue; - } - - if (options.index === false) { - continue; - } - - const isIndexRequested = - options.indexName || options.index || (type === 'foreignKeyColumn' && ctx.options.createForeignKeyIndexes); - if (!isIndexRequested) { - continue; - } - - const indexName = - options.indexName || - ctx.getNameFor({ - type: 'index', - tableName: table.name, - columnNames: [column.name], - }); - - const isIndexPresent = table.indexes.some((index) => index.name === indexName); - if (isIndexPresent) { - continue; - } - - const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1; - if (isOnlyPrimaryColumn) { - // will have an index created by the primary key constraint - continue; - } - - table.indexes.push({ - name: indexName, - tableName: table.name, - unique: false, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/index.ts b/server/src/sql-tools/processors/index.ts deleted file mode 100644 index feb0a82f05..0000000000 --- a/server/src/sql-tools/processors/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { processCheckConstraints } from 'src/sql-tools/processors/check-constraint.processor'; -import { processColumns } from 'src/sql-tools/processors/column.processor'; -import { processConfigurationParameters } from 'src/sql-tools/processors/configuration-parameter.processor'; -import { processDatabases } from 'src/sql-tools/processors/database.processor'; -import { processEnums } from 'src/sql-tools/processors/enum.processor'; -import { processExtensions } from 'src/sql-tools/processors/extension.processor'; -import { processForeignKeyColumns } from 'src/sql-tools/processors/foreign-key-column.processor'; -import { processForeignKeyConstraints } from 'src/sql-tools/processors/foreign-key-constraint.processor'; -import { processFunctions } from 'src/sql-tools/processors/function.processor'; -import { processIndexes } from 'src/sql-tools/processors/index.processor'; -import { processOverrides } from 'src/sql-tools/processors/override.processor'; -import { processPrimaryKeyConstraints } from 'src/sql-tools/processors/primary-key-contraint.processor'; -import { processTables } from 'src/sql-tools/processors/table.processor'; -import { processTriggers } from 'src/sql-tools/processors/trigger.processor'; -import { processUniqueConstraints } from 'src/sql-tools/processors/unique-constraint.processor'; -import { Processor } from 'src/sql-tools/types'; - -export const processors: Processor[] = [ - processDatabases, - processConfigurationParameters, - processEnums, - processExtensions, - processFunctions, - processTables, - processColumns, - processForeignKeyColumns, - processForeignKeyConstraints, - processUniqueConstraints, - processCheckConstraints, - processPrimaryKeyConstraints, - processIndexes, - processTriggers, - processOverrides, -]; diff --git a/server/src/sql-tools/processors/override.processor.ts b/server/src/sql-tools/processors/override.processor.ts deleted file mode 100644 index 67b92fbd40..0000000000 --- a/server/src/sql-tools/processors/override.processor.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { asFunctionCreate } from 'src/sql-tools/transformers/function.transformer'; -import { asIndexCreate } from 'src/sql-tools/transformers/index.transformer'; -import { asTriggerCreate } from 'src/sql-tools/transformers/trigger.transformer'; -import { Processor } from 'src/sql-tools/types'; - -export const processOverrides: Processor = (ctx) => { - if (ctx.options.overrides === false) { - return; - } - - for (const func of ctx.functions) { - if (!func.synchronize) { - continue; - } - - ctx.overrides.push({ - name: `function_${func.name}`, - value: { type: 'function', name: func.name, sql: asFunctionCreate(func) }, - synchronize: true, - }); - } - - for (const { triggers, indexes } of ctx.tables) { - for (const trigger of triggers) { - if (!trigger.synchronize) { - continue; - } - - ctx.overrides.push({ - name: `trigger_${trigger.name}`, - value: { type: 'trigger', name: trigger.name, sql: asTriggerCreate(trigger) }, - synchronize: true, - }); - } - - for (const index of indexes) { - if (!index.synchronize) { - continue; - } - - if (index.expression || index.using || index.with || index.where) { - ctx.overrides.push({ - name: `index_${index.name}`, - value: { type: 'index', name: index.name, sql: asIndexCreate(index) }, - synchronize: true, - }); - } - } - } -}; diff --git a/server/src/sql-tools/processors/primary-key-contraint.processor.ts b/server/src/sql-tools/processors/primary-key-contraint.processor.ts deleted file mode 100644 index 0971bfc337..0000000000 --- a/server/src/sql-tools/processors/primary-key-contraint.processor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processPrimaryKeyConstraints: Processor = (ctx) => { - for (const table of ctx.tables) { - const columnNames: string[] = []; - - for (const column of table.columns) { - if (column.primary) { - columnNames.push(column.name); - } - } - - if (columnNames.length > 0) { - const tableMetadata = ctx.getTableMetadata(table); - table.constraints.push({ - type: ConstraintType.PRIMARY_KEY, - name: - tableMetadata.options.primaryConstraintName || - ctx.getNameFor({ - type: 'primaryKey', - tableName: table.name, - columnNames, - }), - tableName: table.name, - columnNames, - synchronize: tableMetadata.options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/processors/table.processor.ts b/server/src/sql-tools/processors/table.processor.ts deleted file mode 100644 index 993c9ec45d..0000000000 --- a/server/src/sql-tools/processors/table.processor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processTables: Processor = (ctx, items) => { - for (const { - item: { options, object }, - } of items.filter((item) => item.type === 'table')) { - const test = ctx.getTableByObject(object); - if (test) { - throw new Error( - `Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`, - ); - } - - ctx.addTable( - { - name: options.name || ctx.getNameFor({ type: 'table', name: object.name }), - columns: [], - constraints: [], - indexes: [], - triggers: [], - synchronize: options.synchronize ?? true, - }, - options, - object, - ); - } -}; diff --git a/server/src/sql-tools/processors/trigger.processor.ts b/server/src/sql-tools/processors/trigger.processor.ts deleted file mode 100644 index b50b42cc49..0000000000 --- a/server/src/sql-tools/processors/trigger.processor.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Processor } from 'src/sql-tools/types'; - -export const processTriggers: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'trigger')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Trigger', object); - continue; - } - - const triggerName = - options.name || - ctx.getNameFor({ - type: 'trigger', - tableName: table.name, - actions: options.actions, - scope: options.scope, - timing: options.timing, - functionName: options.functionName, - }); - - table.triggers.push({ - name: triggerName, - tableName: table.name, - timing: options.timing, - actions: options.actions, - when: options.when, - scope: options.scope, - referencingNewTableAs: options.referencingNewTableAs, - referencingOldTableAs: options.referencingOldTableAs, - functionName: options.functionName, - synchronize: options.synchronize ?? true, - }); - } -}; diff --git a/server/src/sql-tools/processors/unique-constraint.processor.ts b/server/src/sql-tools/processors/unique-constraint.processor.ts deleted file mode 100644 index 0cbfc26a70..0000000000 --- a/server/src/sql-tools/processors/unique-constraint.processor.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ConstraintType, Processor } from 'src/sql-tools/types'; - -export const processUniqueConstraints: Processor = (ctx, items) => { - for (const { - item: { object, options }, - } of items.filter((item) => item.type === 'uniqueConstraint')) { - const table = ctx.getTableByObject(object); - if (!table) { - ctx.warnMissingTable('@Unique', object); - continue; - } - - const tableName = table.name; - const columnNames = options.columns; - - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }), - tableName, - columnNames, - synchronize: options.synchronize ?? true, - }); - } - - // column level constraints - for (const { - type, - item: { object, propertyName, options }, - } of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) { - const { table, column } = ctx.getColumnByObjectAndPropertyName(object, propertyName); - if (!table) { - ctx.warnMissingTable('@Column', object); - continue; - } - - if (!column) { - // should be impossible since they are created in `column.processor.ts` - ctx.warnMissingColumn('@Column', object, propertyName); - continue; - } - - if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) { - const uniqueConstraintName = - options.uniqueConstraintName || - ctx.getNameFor({ - type: 'unique', - tableName: table.name, - columnNames: [column.name], - }); - - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: uniqueConstraintName, - tableName: table.name, - columnNames: [column.name], - synchronize: options.synchronize ?? true, - }); - } - } -}; diff --git a/server/src/sql-tools/public_api.ts b/server/src/sql-tools/public_api.ts deleted file mode 100644 index 9e7983383e..0000000000 --- a/server/src/sql-tools/public_api.ts +++ /dev/null @@ -1,31 +0,0 @@ -export * from 'src/sql-tools/decorators/after-delete.decorator'; -export * from 'src/sql-tools/decorators/after-insert.decorator'; -export * from 'src/sql-tools/decorators/before-update.decorator'; -export * from 'src/sql-tools/decorators/check.decorator'; -export * from 'src/sql-tools/decorators/column.decorator'; -export * from 'src/sql-tools/decorators/configuration-parameter.decorator'; -export * from 'src/sql-tools/decorators/create-date-column.decorator'; -export * from 'src/sql-tools/decorators/database.decorator'; -export * from 'src/sql-tools/decorators/delete-date-column.decorator'; -export * from 'src/sql-tools/decorators/extension.decorator'; -export * from 'src/sql-tools/decorators/extensions.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-column.decorator'; -export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; -export * from 'src/sql-tools/decorators/generated-column.decorator'; -export * from 'src/sql-tools/decorators/index.decorator'; -export * from 'src/sql-tools/decorators/primary-column.decorator'; -export * from 'src/sql-tools/decorators/primary-generated-column.decorator'; -export * from 'src/sql-tools/decorators/table.decorator'; -export * from 'src/sql-tools/decorators/trigger-function.decorator'; -export * from 'src/sql-tools/decorators/trigger.decorator'; -export * from 'src/sql-tools/decorators/unique.decorator'; -export * from 'src/sql-tools/decorators/update-date-column.decorator'; -export * from 'src/sql-tools/naming/default.naming'; -export * from 'src/sql-tools/naming/hash.naming'; -export * from 'src/sql-tools/naming/naming.interface'; -export * from 'src/sql-tools/register-enum'; -export * from 'src/sql-tools/register-function'; -export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff'; -export { schemaFromCode } from 'src/sql-tools/schema-from-code'; -export { schemaFromDatabase } from 'src/sql-tools/schema-from-database'; -export * from 'src/sql-tools/types'; diff --git a/server/src/sql-tools/readers/column.reader.ts b/server/src/sql-tools/readers/column.reader.ts deleted file mode 100644 index 249bd77f2c..0000000000 --- a/server/src/sql-tools/readers/column.reader.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { sql } from 'kysely'; -import { jsonArrayFrom } from 'kysely/helpers/postgres'; -import { ColumnType, DatabaseColumn, Reader } from 'src/sql-tools/types'; - -export const readColumns: Reader = async (ctx, db) => { - const columns = await db - .selectFrom('information_schema.columns as c') - .leftJoin('information_schema.element_types as o', (join) => - join - .onRef('c.table_catalog', '=', 'o.object_catalog') - .onRef('c.table_schema', '=', 'o.object_schema') - .onRef('c.table_name', '=', 'o.object_name') - .on('o.object_type', '=', sql.lit('TABLE')) - .onRef('c.dtd_identifier', '=', 'o.collection_type_identifier'), - ) - .leftJoin('pg_type as t', (join) => - join.onRef('t.typname', '=', 'c.udt_name').on('c.data_type', '=', sql.lit('USER-DEFINED')), - ) - .leftJoin('pg_enum as e', (join) => join.onRef('e.enumtypid', '=', 't.oid')) - .select([ - 'c.table_name', - 'c.column_name', - - // is ARRAY, USER-DEFINED, or data type - 'c.data_type', - 'c.column_default', - 'c.is_nullable', - 'c.character_maximum_length', - - // number types - 'c.numeric_precision', - 'c.numeric_scale', - - // date types - 'c.datetime_precision', - - // user defined type - 'c.udt_catalog', - 'c.udt_schema', - 'c.udt_name', - - // data type for ARRAYs - 'o.data_type as array_type', - ]) - .where('table_schema', '=', ctx.schemaName) - .execute(); - - const enumRaw = await db - .selectFrom('pg_type') - .innerJoin('pg_namespace', (join) => - join.onRef('pg_namespace.oid', '=', 'pg_type.typnamespace').on('pg_namespace.nspname', '=', ctx.schemaName), - ) - .where('typtype', '=', sql.lit('e')) - .select((eb) => [ - 'pg_type.typname as name', - jsonArrayFrom( - eb.selectFrom('pg_enum as e').select(['e.enumlabel as value']).whereRef('e.enumtypid', '=', 'pg_type.oid'), - ).as('values'), - ]) - .execute(); - - const enums = enumRaw.map((item) => ({ name: item.name, values: item.values.map(({ value }) => value) })); - for (const { name, values } of enums) { - ctx.enums.push({ name, values, synchronize: true }); - } - - const enumMap = Object.fromEntries(enums.map((e) => [e.name, e.values])); - // add columns to tables - for (const column of columns) { - const table = ctx.getTableByName(column.table_name); - if (!table) { - continue; - } - - const columnName = column.column_name; - - const item: DatabaseColumn = { - type: column.data_type as ColumnType, - // TODO infer this from PK constraints - primary: false, - name: columnName, - tableName: column.table_name, - nullable: column.is_nullable === 'YES', - isArray: column.array_type !== null, - numericPrecision: column.numeric_precision ?? undefined, - numericScale: column.numeric_scale ?? undefined, - length: column.character_maximum_length ?? undefined, - default: column.column_default ?? undefined, - synchronize: true, - }; - - const columnLabel = `${table.name}.${columnName}`; - - switch (column.data_type) { - // array types - case 'ARRAY': { - if (!column.array_type) { - ctx.warnings.push(`Unable to find type for ${columnLabel} (ARRAY)`); - continue; - } - item.type = column.array_type as ColumnType; - break; - } - - // enum types - case 'USER-DEFINED': { - if (!enumMap[column.udt_name]) { - ctx.warnings.push(`Unable to find type for ${columnLabel} (ENUM)`); - continue; - } - - item.type = 'enum'; - item.enumName = column.udt_name; - break; - } - } - - table.columns.push(item); - } -}; diff --git a/server/src/sql-tools/readers/comment.reader.ts b/server/src/sql-tools/readers/comment.reader.ts deleted file mode 100644 index 05cc91e7a9..0000000000 --- a/server/src/sql-tools/readers/comment.reader.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Reader } from 'src/sql-tools/types'; - -export const readComments: Reader = async (ctx, db) => { - const comments = await db - .selectFrom('pg_description as d') - .innerJoin('pg_class as c', 'd.objoid', 'c.oid') - .leftJoin('pg_attribute as a', (join) => - join.onRef('a.attrelid', '=', 'c.oid').onRef('a.attnum', '=', 'd.objsubid'), - ) - .select([ - 'c.relname as object_name', - 'c.relkind as object_type', - 'd.description as value', - 'a.attname as column_name', - ]) - .where('d.description', 'is not', null) - .orderBy('object_type') - .orderBy('object_name') - .execute(); - - for (const comment of comments) { - if (comment.object_type === 'r') { - const table = ctx.getTableByName(comment.object_name); - if (!table) { - continue; - } - - if (comment.column_name) { - const column = table.columns.find(({ name }) => name === comment.column_name); - if (column) { - column.comment = comment.value; - } - } - } - } -}; diff --git a/server/src/sql-tools/readers/constraint.reader.ts b/server/src/sql-tools/readers/constraint.reader.ts deleted file mode 100644 index 662c6f414a..0000000000 --- a/server/src/sql-tools/readers/constraint.reader.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { sql } from 'kysely'; -import { ActionType, ConstraintType, Reader } from 'src/sql-tools/types'; - -export const readConstraints: Reader = async (ctx, db) => { - const constraints = await db - .selectFrom('pg_constraint') - .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_constraint.connamespace') // namespace - .innerJoin('pg_class as source_table', (join) => - join.onRef('source_table.oid', '=', 'pg_constraint.conrelid').on('source_table.relkind', 'in', [ - // ordinary table - sql.lit('r'), - // partitioned table - sql.lit('p'), - // foreign table - sql.lit('f'), - ]), - ) // table - .leftJoin('pg_class as reference_table', 'reference_table.oid', 'pg_constraint.confrelid') // reference table - .select((eb) => [ - 'pg_constraint.contype as constraint_type', - 'pg_constraint.conname as constraint_name', - 'source_table.relname as table_name', - 'reference_table.relname as reference_table_name', - 'pg_constraint.confupdtype as update_action', - 'pg_constraint.confdeltype as delete_action', - // 'pg_constraint.oid as constraint_id', - eb - .selectFrom('pg_attribute') - // matching table for PK, FK, and UQ - .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.conrelid') - .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."conkey")`) - .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) - .as('column_names'), - eb - .selectFrom('pg_attribute') - // matching foreign table for FK - .whereRef('pg_attribute.attrelid', '=', 'pg_constraint.confrelid') - .whereRef('pg_attribute.attnum', '=', sql`any("pg_constraint"."confkey")`) - .select((eb) => eb.fn('json_agg', ['pg_attribute.attname']).as('column_name')) - .as('reference_column_names'), - eb.fn('pg_get_constraintdef', ['pg_constraint.oid']).as('expression'), - ]) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .execute(); - - for (const constraint of constraints) { - const table = ctx.getTableByName(constraint.table_name); - if (!table) { - continue; - } - - const constraintName = constraint.constraint_name; - - switch (constraint.constraint_type) { - // primary key constraint - case 'p': { - if (!constraint.column_names) { - ctx.warnings.push(`Skipping CONSTRAINT "${constraintName}", no columns found`); - continue; - } - table.constraints.push({ - type: ConstraintType.PRIMARY_KEY, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names, - synchronize: true, - }); - break; - } - - // foreign key constraint - case 'f': { - if (!constraint.column_names || !constraint.reference_table_name || !constraint.reference_column_names) { - ctx.warnings.push( - `Skipping CONSTRAINT "${constraintName}", missing either columns, referenced table, or referenced columns,`, - ); - continue; - } - - table.constraints.push({ - type: ConstraintType.FOREIGN_KEY, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names, - referenceTableName: constraint.reference_table_name, - referenceColumnNames: constraint.reference_column_names, - onUpdate: asDatabaseAction(constraint.update_action), - onDelete: asDatabaseAction(constraint.delete_action), - synchronize: true, - }); - break; - } - - // unique constraint - case 'u': { - table.constraints.push({ - type: ConstraintType.UNIQUE, - name: constraintName, - tableName: constraint.table_name, - columnNames: constraint.column_names as string[], - synchronize: true, - }); - break; - } - - // check constraint - case 'c': { - table.constraints.push({ - type: ConstraintType.CHECK, - name: constraint.constraint_name, - tableName: constraint.table_name, - expression: constraint.expression.replace('CHECK ', ''), - synchronize: true, - }); - break; - } - } - } -}; - -const asDatabaseAction = (action: string) => { - switch (action) { - case 'a': { - return ActionType.NO_ACTION; - } - case 'c': { - return ActionType.CASCADE; - } - case 'r': { - return ActionType.RESTRICT; - } - case 'n': { - return ActionType.SET_NULL; - } - case 'd': { - return ActionType.SET_DEFAULT; - } - - default: { - return ActionType.NO_ACTION; - } - } -}; diff --git a/server/src/sql-tools/readers/extension.reader.ts b/server/src/sql-tools/readers/extension.reader.ts deleted file mode 100644 index aa33f4d21e..0000000000 --- a/server/src/sql-tools/readers/extension.reader.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Reader } from 'src/sql-tools/types'; - -export const readExtensions: Reader = async (ctx, db) => { - const extensions = await db - .selectFrom('pg_catalog.pg_extension') - // .innerJoin('pg_namespace', 'pg_namespace.oid', 'pg_catalog.pg_extension.extnamespace') - // .where('pg_namespace.nspname', '=', schemaName) - .select(['extname as name', 'extversion as version']) - .execute(); - - for (const { name } of extensions) { - ctx.extensions.push({ name, synchronize: true }); - } -}; diff --git a/server/src/sql-tools/readers/function.reader.ts b/server/src/sql-tools/readers/function.reader.ts deleted file mode 100644 index 4696747f52..0000000000 --- a/server/src/sql-tools/readers/function.reader.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readFunctions: Reader = async (ctx, db) => { - const routines = await db - .selectFrom('pg_proc as p') - .innerJoin('pg_namespace', 'pg_namespace.oid', 'p.pronamespace') - .leftJoin('pg_depend as d', (join) => join.onRef('d.objid', '=', 'p.oid').on('d.deptype', '=', sql.lit('e'))) - .where('d.objid', 'is', sql.lit(null)) - .where('p.prokind', '=', sql.lit('f')) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .select((eb) => [ - 'p.proname as name', - eb.fn('pg_get_function_identity_arguments', ['p.oid']).as('arguments'), - eb.fn('pg_get_functiondef', ['p.oid']).as('expression'), - ]) - .execute(); - - for (const { name, expression } of routines) { - ctx.functions.push({ - name, - // TODO read expression from the overrides table - expression, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/index.reader.ts b/server/src/sql-tools/readers/index.reader.ts deleted file mode 100644 index 26b17a0d19..0000000000 --- a/server/src/sql-tools/readers/index.reader.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readIndexes: Reader = async (ctx, db) => { - const indexes = await db - .selectFrom('pg_index as ix') - // matching index, which has column information - .innerJoin('pg_class as i', 'ix.indexrelid', 'i.oid') - .innerJoin('pg_am as a', 'i.relam', 'a.oid') - // matching table - .innerJoin('pg_class as t', 'ix.indrelid', 't.oid') - // namespace - .innerJoin('pg_namespace', 'pg_namespace.oid', 'i.relnamespace') - // PK and UQ constraints automatically have indexes, so we can ignore those - .leftJoin('pg_constraint', (join) => - join - .onRef('pg_constraint.conindid', '=', 'i.oid') - .on('pg_constraint.contype', 'in', [sql.lit('p'), sql.lit('u')]), - ) - .where('pg_constraint.oid', 'is', null) - .select((eb) => [ - 'i.relname as index_name', - 't.relname as table_name', - 'ix.indisunique as unique', - 'a.amname as using', - eb.fn('pg_get_expr', ['ix.indexprs', 'ix.indrelid']).as('expression'), - eb.fn('pg_get_expr', ['ix.indpred', 'ix.indrelid']).as('where'), - eb - .selectFrom('pg_attribute as a') - .where('t.relkind', '=', sql.lit('r')) - .whereRef('a.attrelid', '=', 't.oid') - // list of columns numbers in the index - .whereRef('a.attnum', '=', sql`any("ix"."indkey")`) - .select((eb) => eb.fn('json_agg', ['a.attname']).as('column_name')) - .as('column_names'), - ]) - .where('pg_namespace.nspname', '=', ctx.schemaName) - .where('ix.indisprimary', '=', sql.lit(false)) - .execute(); - - for (const index of indexes) { - const table = ctx.getTableByName(index.table_name); - if (!table) { - continue; - } - - table.indexes.push({ - name: index.index_name, - tableName: index.table_name, - columnNames: index.column_names ?? undefined, - expression: index.expression ?? undefined, - using: index.using, - where: index.where ?? undefined, - unique: index.unique, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/index.ts b/server/src/sql-tools/readers/index.ts deleted file mode 100644 index 354f99c7ca..0000000000 --- a/server/src/sql-tools/readers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { readColumns } from 'src/sql-tools/readers/column.reader'; -import { readComments } from 'src/sql-tools/readers/comment.reader'; -import { readConstraints } from 'src/sql-tools/readers/constraint.reader'; -import { readExtensions } from 'src/sql-tools/readers/extension.reader'; -import { readFunctions } from 'src/sql-tools/readers/function.reader'; -import { readIndexes } from 'src/sql-tools/readers/index.reader'; -import { readName } from 'src/sql-tools/readers/name.reader'; -import { readOverrides } from 'src/sql-tools/readers/override.reader'; -import { readParameters } from 'src/sql-tools/readers/parameter.reader'; -import { readTables } from 'src/sql-tools/readers/table.reader'; -import { readTriggers } from 'src/sql-tools/readers/trigger.reader'; -import { Reader } from 'src/sql-tools/types'; - -export const readers: Reader[] = [ - readName, - readParameters, - readExtensions, - readFunctions, - readTables, - readColumns, - readIndexes, - readConstraints, - readTriggers, - readComments, - readOverrides, -]; diff --git a/server/src/sql-tools/readers/name.reader.ts b/server/src/sql-tools/readers/name.reader.ts deleted file mode 100644 index de4f1af3a6..0000000000 --- a/server/src/sql-tools/readers/name.reader.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { QueryResult, sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readName: Reader = async (ctx, db) => { - const result = (await sql`SELECT current_database() as name`.execute(db)) as QueryResult<{ name: string }>; - - ctx.databaseName = result.rows[0].name; -}; diff --git a/server/src/sql-tools/readers/override.reader.ts b/server/src/sql-tools/readers/override.reader.ts deleted file mode 100644 index 34f0004f95..0000000000 --- a/server/src/sql-tools/readers/override.reader.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { sql } from 'kysely'; -import { OverrideType, Reader } from 'src/sql-tools/types'; - -export const readOverrides: Reader = async (ctx, db) => { - try { - const result = await sql - .raw<{ - name: string; - value: { type: OverrideType; name: string; sql: string }; - }>(`SELECT name, value FROM "${ctx.overrideTableName}"`) - .execute(db); - - for (const { name, value } of result.rows) { - ctx.overrides.push({ name, value, synchronize: true }); - } - } catch (error) { - ctx.warn('Overrides', `Error reading override table: ${error}`); - } -}; diff --git a/server/src/sql-tools/readers/parameter.reader.ts b/server/src/sql-tools/readers/parameter.reader.ts deleted file mode 100644 index c5f36591a3..0000000000 --- a/server/src/sql-tools/readers/parameter.reader.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { sql } from 'kysely'; -import { ParameterScope, Reader } from 'src/sql-tools/types'; - -export const readParameters: Reader = async (ctx, db) => { - const parameters = await db - .selectFrom('pg_settings') - .where('source', 'in', [sql.lit('database'), sql.lit('user')]) - .select(['name', 'setting as value', 'source as scope']) - .execute(); - - for (const parameter of parameters) { - ctx.parameters.push({ - name: parameter.name, - value: parameter.value, - databaseName: ctx.databaseName, - scope: parameter.scope as ParameterScope, - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/table.reader.ts b/server/src/sql-tools/readers/table.reader.ts deleted file mode 100644 index 4570179bbf..0000000000 --- a/server/src/sql-tools/readers/table.reader.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { sql } from 'kysely'; -import { Reader } from 'src/sql-tools/types'; - -export const readTables: Reader = async (ctx, db) => { - const tables = await db - .selectFrom('information_schema.tables') - .where('table_schema', '=', ctx.schemaName) - .where('table_type', '=', sql.lit('BASE TABLE')) - .selectAll() - .execute(); - - for (const table of tables) { - ctx.tables.push({ - name: table.table_name, - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }); - } -}; diff --git a/server/src/sql-tools/readers/trigger.reader.ts b/server/src/sql-tools/readers/trigger.reader.ts deleted file mode 100644 index 92fb1d12bf..0000000000 --- a/server/src/sql-tools/readers/trigger.reader.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Reader, TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types'; - -export const readTriggers: Reader = async (ctx, db) => { - const triggers = await db - .selectFrom('pg_trigger as t') - .innerJoin('pg_proc as p', 't.tgfoid', 'p.oid') - .innerJoin('pg_namespace as n', 'p.pronamespace', 'n.oid') - .innerJoin('pg_class as c', 't.tgrelid', 'c.oid') - .select((eb) => [ - 't.tgname as name', - 't.tgenabled as enabled', - 't.tgtype as type', - 't.tgconstraint as _constraint', - 't.tgdeferrable as is_deferrable', - 't.tginitdeferred as is_initially_deferred', - 't.tgargs as arguments', - 't.tgoldtable as referencing_old_table_as', - 't.tgnewtable as referencing_new_table_as', - eb.fn('pg_get_expr', ['t.tgqual', 't.tgrelid']).as('when_expression'), - 'p.proname as function_name', - 'c.relname as table_name', - ]) - .where('t.tgisinternal', '=', false) // Exclude internal system triggers - .where('n.nspname', '=', ctx.schemaName) - .execute(); - - // add triggers to tables - for (const trigger of triggers) { - const table = ctx.getTableByName(trigger.table_name); - if (!table) { - continue; - } - - table.triggers.push({ - name: trigger.name, - tableName: trigger.table_name, - functionName: trigger.function_name, - referencingNewTableAs: trigger.referencing_new_table_as ?? undefined, - referencingOldTableAs: trigger.referencing_old_table_as ?? undefined, - when: trigger.when_expression, - synchronize: true, - ...parseTriggerType(trigger.type), - }); - } -}; - -export const hasMask = (input: number, mask: number) => (input & mask) === mask; - -export const parseTriggerType = (type: number) => { - // eslint-disable-next-line unicorn/prefer-math-trunc - const scope: TriggerScope = hasMask(type, 1 << 0) ? 'row' : 'statement'; - - let timing: TriggerTiming = 'after'; - const timingMasks: Array<{ mask: number; value: TriggerTiming }> = [ - { mask: 1 << 1, value: 'before' }, - { mask: 1 << 6, value: 'instead of' }, - ]; - - for (const { mask, value } of timingMasks) { - if (hasMask(type, mask)) { - timing = value; - break; - } - } - - const actions: TriggerAction[] = []; - const actionMasks: Array<{ mask: number; value: TriggerAction }> = [ - { mask: 1 << 2, value: 'insert' }, - { mask: 1 << 3, value: 'delete' }, - { mask: 1 << 4, value: 'update' }, - { mask: 1 << 5, value: 'truncate' }, - ]; - - for (const { mask, value } of actionMasks) { - if (hasMask(type, mask)) { - actions.push(value); - break; - } - } - - if (actions.length === 0) { - throw new Error(`Unable to parse trigger type ${type}`); - } - - return { actions, timing, scope }; -}; diff --git a/server/src/sql-tools/register-enum.ts b/server/src/sql-tools/register-enum.ts deleted file mode 100644 index 5e9b41adcb..0000000000 --- a/server/src/sql-tools/register-enum.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { DatabaseEnum } from 'src/sql-tools/types'; - -export type EnumOptions = { - name: string; - values: string[]; - synchronize?: boolean; -}; - -export const registerEnum = (options: EnumOptions) => { - const item: DatabaseEnum = { - name: options.name, - values: options.values, - synchronize: options.synchronize ?? true, - }; - - register({ type: 'enum', item }); - - return item; -}; diff --git a/server/src/sql-tools/register-function.ts b/server/src/sql-tools/register-function.ts deleted file mode 100644 index 9f1c84c4fa..0000000000 --- a/server/src/sql-tools/register-function.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { register } from 'src/sql-tools/register'; -import { ColumnType, DatabaseFunction } from 'src/sql-tools/types'; - -export type FunctionOptions = { - name: string; - arguments?: string[]; - returnType: ColumnType | string; - language?: 'SQL' | 'PLPGSQL'; - behavior?: 'immutable' | 'stable' | 'volatile'; - parallel?: 'safe' | 'unsafe' | 'restricted'; - strict?: boolean; - synchronize?: boolean; -} & ({ body: string } | { return: string }); - -export const registerFunction = (options: FunctionOptions) => { - const name = options.name; - const expression = asFunctionExpression(options); - - const item: DatabaseFunction = { - name, - expression, - synchronize: options.synchronize ?? true, - }; - - register({ type: 'function', item }); - - return item; -}; - -const asFunctionExpression = (options: FunctionOptions) => { - const name = options.name; - const sql: string[] = [ - `CREATE OR REPLACE FUNCTION ${name}(${(options.arguments || []).join(', ')})`, - `RETURNS ${options.returnType}`, - ]; - - const flags = [ - options.parallel ? `PARALLEL ${options.parallel.toUpperCase()}` : undefined, - options.strict ? 'STRICT' : undefined, - options.behavior ? options.behavior.toUpperCase() : undefined, - `LANGUAGE ${options.language ?? 'SQL'}`, - ].filter((x) => x !== undefined); - - if (flags.length > 0) { - sql.push(flags.join(' ')); - } - - if ('return' in options) { - sql.push(` RETURN ${options.return}`); - } - - if ('body' in options) { - const body = options.body; - sql.push(...(body.includes('\n') ? [`AS $$`, ' ' + body.trim(), `$$;`] : [`AS $$${body}$$;`])); - } - - return sql.join('\n ').trim(); -}; diff --git a/server/src/sql-tools/register-item.ts b/server/src/sql-tools/register-item.ts deleted file mode 100644 index fede281a1b..0000000000 --- a/server/src/sql-tools/register-item.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { CheckOptions } from 'src/sql-tools/decorators/check.decorator'; -import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator'; -import { ConfigurationParameterOptions } from 'src/sql-tools/decorators/configuration-parameter.decorator'; -import { DatabaseOptions } from 'src/sql-tools/decorators/database.decorator'; -import { ExtensionOptions } from 'src/sql-tools/decorators/extension.decorator'; -import { ForeignKeyColumnOptions } from 'src/sql-tools/decorators/foreign-key-column.decorator'; -import { ForeignKeyConstraintOptions } from 'src/sql-tools/decorators/foreign-key-constraint.decorator'; -import { IndexOptions } from 'src/sql-tools/decorators/index.decorator'; -import { TableOptions } from 'src/sql-tools/decorators/table.decorator'; -import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator'; -import { UniqueOptions } from 'src/sql-tools/decorators/unique.decorator'; -import { DatabaseEnum, DatabaseFunction } from 'src/sql-tools/types'; - -export type ClassBased = { object: Function } & T; -export type PropertyBased = { object: object; propertyName: string | symbol } & T; -export type RegisterItem = - | { type: 'database'; item: ClassBased<{ options: DatabaseOptions }> } - | { type: 'table'; item: ClassBased<{ options: TableOptions }> } - | { type: 'index'; item: ClassBased<{ options: IndexOptions }> } - | { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> } - | { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> } - | { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> } - | { type: 'function'; item: DatabaseFunction } - | { type: 'enum'; item: DatabaseEnum } - | { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> } - | { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> } - | { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> } - | { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => Function }> } - | { type: 'foreignKeyConstraint'; item: ClassBased<{ options: ForeignKeyConstraintOptions }> }; -export type RegisterItemType = Extract['item']; diff --git a/server/src/sql-tools/register.ts b/server/src/sql-tools/register.ts deleted file mode 100644 index 4df04c935a..0000000000 --- a/server/src/sql-tools/register.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RegisterItem } from 'src/sql-tools/register-item'; - -const items: RegisterItem[] = []; - -export const register = (item: RegisterItem) => void items.push(item); - -export const getRegisteredItems = () => items; - -export const resetRegisteredItems = () => { - items.length = 0; -}; diff --git a/server/src/sql-tools/schema-diff.spec.ts b/server/src/sql-tools/schema-diff.spec.ts deleted file mode 100644 index f45fb98bd3..0000000000 --- a/server/src/sql-tools/schema-diff.spec.ts +++ /dev/null @@ -1,689 +0,0 @@ -import { schemaDiff } from 'src/sql-tools/schema-diff'; -import { - ActionType, - ColumnType, - ConstraintType, - DatabaseColumn, - DatabaseConstraint, - DatabaseIndex, - DatabaseSchema, - DatabaseTable, -} from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const fromColumn = (column: Partial>): DatabaseSchema => { - const tableName = 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - ...column, - tableName, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => { - const tableName = constraint?.tableName || 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - tableName, - }, - ], - indexes: [], - triggers: [], - constraints: constraint ? [constraint] : [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const fromIndex = (index?: DatabaseIndex): DatabaseSchema => { - const tableName = index?.tableName || 'table1'; - - return { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: tableName, - columns: [ - { - name: 'column1', - primary: false, - synchronize: true, - isArray: false, - type: 'character varying', - nullable: false, - tableName, - }, - ], - indexes: index ? [index] : [], - constraints: [], - triggers: [], - synchronize: true, - }, - ], - warnings: [], - }; -}; - -const newSchema = (schema: { - name?: string; - tables: Array<{ - name: string; - columns?: Array<{ - name: string; - type?: ColumnType; - nullable?: boolean; - isArray?: boolean; - }>; - indexes?: DatabaseIndex[]; - constraints?: DatabaseConstraint[]; - }>; -}): DatabaseSchema => { - const tables: DatabaseTable[] = []; - - for (const table of schema.tables || []) { - const tableName = table.name; - const columns: DatabaseColumn[] = []; - - for (const column of table.columns || []) { - const columnName = column.name; - - columns.push({ - tableName, - name: columnName, - primary: false, - type: column.type || 'character varying', - isArray: column.isArray ?? false, - nullable: column.nullable ?? false, - synchronize: true, - }); - } - - tables.push({ - name: tableName, - columns, - indexes: table.indexes ?? [], - constraints: table.constraints ?? [], - triggers: [], - synchronize: true, - }); - } - - return { - databaseName: 'immich', - schemaName: schema?.name || 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables, - warnings: [], - }; -}; - -describe(schemaDiff.name, () => { - it('should work', () => { - const diff = schemaDiff(newSchema({ tables: [] }), newSchema({ tables: [] })); - expect(diff.items).toEqual([]); - }); - - describe('table', () => { - describe('TableCreate', () => { - it('should find a missing table', () => { - const column: DatabaseColumn = { - type: 'character varying', - tableName: 'table1', - primary: false, - name: 'column1', - isArray: false, - nullable: false, - synchronize: true, - }; - const diff = schemaDiff( - newSchema({ tables: [{ name: 'table1', columns: [column] }] }), - newSchema({ tables: [] }), - ); - - expect(diff.items).toHaveLength(1); - expect(diff.items[0]).toEqual({ - type: 'TableCreate', - table: { - name: 'table1', - columns: [column], - constraints: [], - indexes: [], - triggers: [], - synchronize: true, - }, - reason: 'missing in target', - }); - }); - }); - - describe('TableDrop', () => { - it('should find an extra table', () => { - const diff = schemaDiff( - newSchema({ tables: [] }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - { tables: { ignoreExtra: false } }, - ); - - expect(diff.items).toHaveLength(1); - expect(diff.items[0]).toEqual({ - type: 'TableDrop', - tableName: 'table1', - reason: 'missing in source', - }); - }); - }); - - it('should skip identical tables', () => { - const diff = schemaDiff( - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - ); - - expect(diff.items).toEqual([]); - }); - }); - - describe('column', () => { - describe('ColumnAdd', () => { - it('should find a new column', () => { - const diff = schemaDiff( - newSchema({ - tables: [ - { - name: 'table1', - columns: [{ name: 'column1' }, { name: 'column2' }], - }, - ], - }), - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAdd', - column: { - tableName: 'table1', - isArray: false, - primary: false, - name: 'column2', - nullable: false, - type: 'character varying', - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('ColumnDrop', () => { - it('should find an extra column', () => { - const diff = schemaDiff( - newSchema({ - tables: [{ name: 'table1', columns: [{ name: 'column1' }] }], - }), - newSchema({ - tables: [ - { - name: 'table1', - columns: [{ name: 'column1' }, { name: 'column2' }], - }, - ], - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnDrop', - tableName: 'table1', - columnName: 'column2', - reason: 'missing in source', - }, - ]); - }); - }); - - describe('nullable', () => { - it('should make a column nullable', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', nullable: true }), - fromColumn({ name: 'column1', nullable: false }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - nullable: true, - }, - reason: 'nullable is different (true vs false)', - }, - ]); - }); - - it('should make a column non-nullable', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', nullable: false }), - fromColumn({ name: 'column1', nullable: true }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - nullable: false, - }, - reason: 'nullable is different (false vs true)', - }, - ]); - }); - }); - - describe('default', () => { - it('should set a default value to a function', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', default: 'uuid_generate_v4()' }), - fromColumn({ name: 'column1' }), - ); - - expect(diff.items).toEqual([ - { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - default: 'uuid_generate_v4()', - }, - reason: 'default is different (uuid_generate_v4() vs undefined)', - }, - ]); - }); - - it('should ignore explicit casts for strings', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'character varying', default: `''` }), - fromColumn({ name: 'column1', type: 'character varying', default: `''::character varying` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should ignore explicit casts for numbers', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'bigint', default: `0` }), - fromColumn({ name: 'column1', type: 'bigint', default: `'0'::bigint` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should ignore explicit casts for enums', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `test` }), - fromColumn({ name: 'column1', type: 'enum', enumName: 'enum1', default: `'test'::enum1` }), - ); - - expect(diff.items).toEqual([]); - }); - - it('should support arrays, ignoring types', () => { - const diff = schemaDiff( - fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }), - fromColumn({ - name: 'column1', - type: 'character varying', - isArray: true, - default: "'{}'::character varying[]", - }), - ); - - expect(diff.items).toEqual([]); - }); - }); - }); - - describe('constraint', () => { - describe('ConstraintAdd', () => { - it('should detect a new constraint', () => { - const diff = schemaDiff( - fromConstraint({ - name: 'PK_test', - type: ConstraintType.PRIMARY_KEY, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - fromConstraint(), - ); - - expect(diff.items).toEqual([ - { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - columnNames: ['id'], - tableName: 'table1', - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('ConstraintDrop', () => { - it('should detect an extra constraint', () => { - const diff = schemaDiff( - fromConstraint(), - fromConstraint({ - name: 'PK_test', - type: ConstraintType.PRIMARY_KEY, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - ); - - expect(diff.items).toEqual([ - { - type: 'ConstraintDrop', - tableName: 'table1', - constraintName: 'PK_test', - reason: 'missing in source', - }, - ]); - }); - }); - - describe('primary key', () => { - it('should skip identical primary key constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - - describe('foreign key', () => { - it('should skip identical foreign key constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint(constraint), fromConstraint(constraint)); - - expect(diff.items).toEqual([]); - }); - - it('should drop and recreate when the column changes', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff( - fromConstraint(constraint), - fromConstraint({ ...constraint, columnNames: ['parentId2'] }), - ); - - expect(diff.items).toEqual([ - { - constraintName: 'FK_test', - reason: 'columns are different (parentId vs parentId2)', - tableName: 'table1', - type: 'ConstraintDrop', - }, - { - constraint: { - columnNames: ['parentId'], - name: 'FK_test', - referenceColumnNames: ['id'], - referenceTableName: 'table2', - synchronize: true, - tableName: 'table1', - type: 'foreign-key', - }, - reason: 'columns are different (parentId vs parentId2)', - type: 'ConstraintAdd', - }, - ]); - }); - - it('should drop and recreate when the ON DELETE action changes', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceTableName: 'table2', - referenceColumnNames: ['id'], - onDelete: ActionType.CASCADE, - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint(constraint), fromConstraint({ ...constraint, onDelete: undefined })); - - expect(diff.items).toEqual([ - { - constraintName: 'FK_test', - reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', - tableName: 'table1', - type: 'ConstraintDrop', - }, - { - constraint: { - columnNames: ['parentId'], - name: 'FK_test', - referenceColumnNames: ['id'], - referenceTableName: 'table2', - onDelete: ActionType.CASCADE, - synchronize: true, - tableName: 'table1', - type: 'foreign-key', - }, - reason: 'ON DELETE action is different (CASCADE vs NO ACTION)', - type: 'ConstraintAdd', - }, - ]); - }); - }); - - describe('unique', () => { - it('should skip identical unique constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - - describe('check', () => { - it('should skip identical check constraints', () => { - const constraint: DatabaseConstraint = { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: 'column1 > 0', - synchronize: true, - }; - - const diff = schemaDiff(fromConstraint({ ...constraint }), fromConstraint({ ...constraint })); - - expect(diff.items).toEqual([]); - }); - }); - }); - - describe('index', () => { - describe('IndexCreate', () => { - it('should detect a new index', () => { - const diff = schemaDiff( - fromIndex({ - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: false, - synchronize: true, - }), - fromIndex(), - ); - - expect(diff.items).toEqual([ - { - type: 'IndexCreate', - index: { - name: 'IDX_test', - columnNames: ['id'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - reason: 'missing in target', - }, - ]); - }); - }); - - describe('IndexDrop', () => { - it('should detect an extra index', () => { - const diff = schemaDiff( - fromIndex(), - fromIndex({ - name: 'IDX_test', - unique: true, - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }), - ); - - expect(diff.items).toEqual([ - { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'missing in source', - }, - ]); - }); - }); - - it('should recreate the index if unique changes', () => { - const index: DatabaseIndex = { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: true, - synchronize: true, - }; - const diff = schemaDiff(fromIndex(index), fromIndex({ ...index, unique: false })); - - expect(diff.items).toEqual([ - { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'uniqueness is different (true vs false)', - }, - { - type: 'IndexCreate', - index, - reason: 'uniqueness is different (true vs false)', - }, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/schema-diff.ts b/server/src/sql-tools/schema-diff.ts deleted file mode 100644 index bca58c3228..0000000000 --- a/server/src/sql-tools/schema-diff.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { compareEnums } from 'src/sql-tools/comparers/enum.comparer'; -import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer'; -import { compareFunctions } from 'src/sql-tools/comparers/function.comparer'; -import { compareOverrides } from 'src/sql-tools/comparers/override.comparer'; -import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer'; -import { compareTables } from 'src/sql-tools/comparers/table.comparer'; -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { compare } from 'src/sql-tools/helpers'; -import { transformers } from 'src/sql-tools/transformers'; -import { - ConstraintType, - DatabaseSchema, - SchemaDiff, - SchemaDiffOptions, - SchemaDiffToSqlOptions, -} from 'src/sql-tools/types'; - -/** - * Compute the difference between two database schemas - */ -export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => { - const items = [ - ...compare(source.parameters, target.parameters, options.parameters, compareParameters), - ...compare(source.extensions, target.extensions, options.extensions, compareExtensions), - ...compare(source.functions, target.functions, options.functions, compareFunctions), - ...compare(source.enums, target.enums, options.enums, compareEnums), - ...compare(source.tables, target.tables, options.tables, compareTables), - ...compare(source.overrides, target.overrides, options.overrides, compareOverrides), - ]; - - type SchemaName = SchemaDiff['type']; - const itemMap: Record = { - ColumnRename: [], - ConstraintRename: [], - IndexRename: [], - - ExtensionDrop: [], - ExtensionCreate: [], - - ParameterSet: [], - ParameterReset: [], - - FunctionDrop: [], - FunctionCreate: [], - - EnumDrop: [], - EnumCreate: [], - - TriggerDrop: [], - ConstraintDrop: [], - TableDrop: [], - ColumnDrop: [], - ColumnAdd: [], - ColumnAlter: [], - TableCreate: [], - ConstraintAdd: [], - TriggerCreate: [], - - IndexCreate: [], - IndexDrop: [], - - OverrideCreate: [], - OverrideUpdate: [], - OverrideDrop: [], - }; - - for (const item of items) { - itemMap[item.type].push(item); - } - - const constraintAdds = itemMap.ConstraintAdd.filter((item) => item.type === 'ConstraintAdd'); - - const orderedItems = [ - ...itemMap.ExtensionCreate, - ...itemMap.FunctionCreate, - ...itemMap.ParameterSet, - ...itemMap.ParameterReset, - ...itemMap.EnumCreate, - ...itemMap.TriggerDrop, - ...itemMap.IndexDrop, - ...itemMap.ConstraintDrop, - ...itemMap.TableCreate, - ...itemMap.ColumnAlter, - ...itemMap.ColumnAdd, - ...itemMap.ColumnRename, - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE), - ...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK), - ...itemMap.ConstraintRename, - ...itemMap.IndexCreate, - ...itemMap.IndexRename, - ...itemMap.TriggerCreate, - ...itemMap.ColumnDrop, - ...itemMap.TableDrop, - ...itemMap.EnumDrop, - ...itemMap.FunctionDrop, - ...itemMap.OverrideCreate, - ...itemMap.OverrideUpdate, - ...itemMap.OverrideDrop, - ]; - - return { - items: orderedItems, - asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options), - }; -}; - -/** - * Convert schema diffs into SQL statements - */ -export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => { - return items.flatMap((item) => asSql(item, options)); -}; - -const asSql = (item: SchemaDiff, options: SchemaDiffToSqlOptions): string[] => { - const ctx = new BaseContext(options); - for (const transform of transformers) { - const result = transform(ctx, item); - if (!result) { - continue; - } - - return asArray(result).map((result) => result + withComments(options.comments, item)); - } - - throw new Error(`Unhandled schema diff type: ${item.type}`); -}; - -const withComments = (comments: boolean | undefined, item: SchemaDiff): string => { - if (!comments) { - return ''; - } - - return ` -- ${item.reason}`; -}; - -const asArray = (items: T | T[]): T[] => { - if (Array.isArray(items)) { - return items; - } - - return [items]; -}; diff --git a/server/src/sql-tools/schema-from-code.spec.ts b/server/src/sql-tools/schema-from-code.spec.ts deleted file mode 100644 index b0c88d1f57..0000000000 --- a/server/src/sql-tools/schema-from-code.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { schemaFromCode } from 'src/sql-tools/schema-from-code'; -import { SchemaFromCodeOptions } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const importModule = async (filePath: string) => { - const module = await import(filePath); - const options: SchemaFromCodeOptions = module.options; - - return { module, options }; -}; - -describe(schemaFromCode.name, () => { - it('should work', () => { - expect(schemaFromCode({ reset: true })).toEqual({ - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [], - warnings: [], - }); - }); - - describe('test files', () => { - const errorStubs = readdirSync('test/sql-tools/errors', { withFileTypes: true }); - for (const file of errorStubs) { - const filePath = join(file.parentPath, file.name); - it(filePath, async () => { - const { module, options } = await importModule(filePath); - - expect(module.message).toBeDefined(); - expect(() => schemaFromCode({ ...options, reset: true })).toThrowError(module.message); - }); - } - - const stubs = readdirSync('test/sql-tools', { withFileTypes: true }); - for (const file of stubs) { - if (file.isDirectory()) { - continue; - } - - const filePath = join(file.parentPath, file.name); - it(filePath, async () => { - const { module, options } = await importModule(filePath); - - expect(module.description).toBeDefined(); - expect(module.schema).toBeDefined(); - expect(schemaFromCode({ ...options, reset: true }), module.description).toEqual(module.schema); - }); - } - }); -}); diff --git a/server/src/sql-tools/schema-from-code.ts b/server/src/sql-tools/schema-from-code.ts deleted file mode 100644 index 2e19f414e4..0000000000 --- a/server/src/sql-tools/schema-from-code.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; -import { processors } from 'src/sql-tools/processors'; -import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/register'; -import { ConstraintType, SchemaFromCodeOptions } from 'src/sql-tools/types'; - -/** - * Load schema from code (decorators, etc) - */ -export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => { - try { - const ctx = new ProcessorContext(options); - const items = getRegisteredItems(); - - for (const processor of processors) { - processor(ctx, items); - } - - if (ctx.options.overrides) { - ctx.tables.push({ - name: ctx.overrideTableName, - columns: [ - { - name: 'name', - tableName: ctx.overrideTableName, - primary: true, - type: 'character varying', - nullable: false, - isArray: false, - synchronize: true, - }, - { - name: 'value', - tableName: ctx.overrideTableName, - primary: false, - type: 'jsonb', - nullable: false, - isArray: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: `${ctx.overrideTableName}_pkey`, - tableName: ctx.overrideTableName, - columnNames: ['name'], - synchronize: true, - }, - ], - synchronize: true, - }); - } - - return ctx.build(); - } finally { - if (options.reset) { - resetRegisteredItems(); - } - } -}; diff --git a/server/src/sql-tools/schema-from-database.ts b/server/src/sql-tools/schema-from-database.ts deleted file mode 100644 index b7b76a68b1..0000000000 --- a/server/src/sql-tools/schema-from-database.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Kysely } from 'kysely'; -import { PostgresJSDialect } from 'kysely-postgres-js'; -import { Sql } from 'postgres'; -import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; -import { readers } from 'src/sql-tools/readers'; -import { DatabaseSchema, PostgresDB, SchemaFromDatabaseOptions } from 'src/sql-tools/types'; - -/** - * Load schema from a database url - */ -export const schemaFromDatabase = async ( - postgres: Sql, - options: SchemaFromDatabaseOptions = {}, -): Promise => { - const db = new Kysely({ dialect: new PostgresJSDialect({ postgres }) }); - const ctx = new ReaderContext(options); - - try { - for (const reader of readers) { - await reader(ctx, db); - } - - return ctx.build(); - } finally { - await db.destroy(); - } -}; diff --git a/server/src/sql-tools/transformers/column.transformer.spec.ts b/server/src/sql-tools/transformers/column.transformer.spec.ts deleted file mode 100644 index 6828e2a72d..0000000000 --- a/server/src/sql-tools/transformers/column.transformer.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformColumns.name, () => { - describe('ColumnAdd', () => { - it('should work', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - nullable: false, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" character varying NOT NULL;'); - }); - - it('should add a nullable column', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" character varying;'); - }); - - it('should add a column with an enum type', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'character varying', - enumName: 'table1_column1_enum', - nullable: true, - isArray: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" table1_column1_enum;'); - }); - - it('should add a column that is an array type', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAdd', - column: { - name: 'column1', - tableName: 'table1', - primary: false, - type: 'boolean', - nullable: true, - isArray: true, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD "column1" boolean[];'); - }); - }); - - describe('ColumnAlter', () => { - it('should make a column nullable', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { nullable: true }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" DROP NOT NULL;`]); - }); - - it('should make a column non-nullable', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { nullable: false }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET NOT NULL;`]); - }); - - it('should update the default value', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { default: 'uuid_generate_v4()' }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT uuid_generate_v4();`]); - }); - - it('should update the default value to NULL', () => { - expect( - transformColumns(ctx, { - type: 'ColumnAlter', - tableName: 'table1', - columnName: 'column1', - changes: { - default: 'NULL', - }, - reason: 'unknown', - }), - ).toEqual([`ALTER TABLE "table1" ALTER COLUMN "column1" SET DEFAULT NULL;`]); - }); - }); - - describe('ColumnDrop', () => { - it('should work', () => { - expect( - transformColumns(ctx, { - type: 'ColumnDrop', - tableName: 'table1', - columnName: 'column1', - reason: 'unknown', - }), - ).toEqual(`ALTER TABLE "table1" DROP COLUMN "column1";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/column.transformer.ts b/server/src/sql-tools/transformers/column.transformer.ts deleted file mode 100644 index ffa565e533..0000000000 --- a/server/src/sql-tools/transformers/column.transformer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { ColumnChanges, DatabaseColumn } from 'src/sql-tools/types'; - -export const transformColumns: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ColumnAdd': { - return asColumnAdd(item.column); - } - - case 'ColumnAlter': { - return asColumnAlter(item.tableName, item.columnName, item.changes); - } - - case 'ColumnRename': { - return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`; - } - - case 'ColumnDrop': { - return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`; - } - - default: { - return false; - } - } -}; - -const asColumnAdd = (column: DatabaseColumn): string => { - return ( - `ALTER TABLE "${column.tableName}" ADD "${column.name}" ${getColumnType(column)}` + getColumnModifiers(column) + ';' - ); -}; - -export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => { - const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`; - const items: string[] = []; - if (changes.nullable !== undefined) { - items.push(changes.nullable ? `${base} DROP NOT NULL;` : `${base} SET NOT NULL;`); - } - - if (changes.default !== undefined) { - items.push(`${base} SET DEFAULT ${changes.default};`); - } - - if (changes.storage !== undefined) { - items.push(`${base} SET STORAGE ${changes.storage.toUpperCase()};`); - } - - if (changes.comment !== undefined) { - items.push(asColumnComment(tableName, columnName, changes.comment)); - } - - return items; -}; diff --git a/server/src/sql-tools/transformers/constraint.transformer.spec.ts b/server/src/sql-tools/transformers/constraint.transformer.spec.ts deleted file mode 100644 index 6e512afdca..0000000000 --- a/server/src/sql-tools/transformers/constraint.transformer.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; -import { ConstraintType } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformConstraints.name, () => { - describe('ConstraintAdd', () => { - describe('primary keys', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "PK_test" PRIMARY KEY ("id");'); - }); - }); - - describe('foreign keys', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_test', - tableName: 'table1', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table2', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - 'ALTER TABLE "table1" ADD CONSTRAINT "FK_test" FOREIGN KEY ("parentId") REFERENCES "table2" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;', - ); - }); - }); - - describe('unique', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "UQ_test" UNIQUE ("id");'); - }); - }); - - describe('check', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintAdd', - constraint: { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('ALTER TABLE "table1" ADD CONSTRAINT "CHK_test" CHECK ("id" IS NOT NULL);'); - }); - }); - }); - - describe('ConstraintDrop', () => { - it('should work', () => { - expect( - transformConstraints(ctx, { - type: 'ConstraintDrop', - tableName: 'table1', - constraintName: 'PK_test', - reason: 'unknown', - }), - ).toEqual(`ALTER TABLE "table1" DROP CONSTRAINT "PK_test";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/constraint.transformer.ts b/server/src/sql-tools/transformers/constraint.transformer.ts deleted file mode 100644 index 94421e56fa..0000000000 --- a/server/src/sql-tools/transformers/constraint.transformer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/types'; - -export const transformConstraints: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ConstraintAdd': { - return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`; - } - - case 'ConstraintRename': { - return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`; - } - - case 'ConstraintDrop': { - return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`; - } - default: { - return false; - } - } -}; - -const withAction = (constraint: { onDelete?: ActionType; onUpdate?: ActionType }) => - ` ON UPDATE ${constraint.onUpdate ?? ActionType.NO_ACTION} ON DELETE ${constraint.onDelete ?? ActionType.NO_ACTION}`; - -export const asConstraintBody = (constraint: DatabaseConstraint): string => { - const base = `CONSTRAINT "${constraint.name}"`; - - switch (constraint.type) { - case ConstraintType.PRIMARY_KEY: { - const columnNames = asColumnList(constraint.columnNames); - return `${base} PRIMARY KEY (${columnNames})`; - } - - case ConstraintType.FOREIGN_KEY: { - const columnNames = asColumnList(constraint.columnNames); - const referenceColumnNames = asColumnList(constraint.referenceColumnNames); - return ( - `${base} FOREIGN KEY (${columnNames}) REFERENCES "${constraint.referenceTableName}" (${referenceColumnNames})` + - withAction(constraint) - ); - } - - case ConstraintType.UNIQUE: { - const columnNames = asColumnList(constraint.columnNames); - return `${base} UNIQUE (${columnNames})`; - } - - case ConstraintType.CHECK: { - return `${base} CHECK (${constraint.expression})`; - } - - default: { - throw new Error(`Unknown constraint type: ${(constraint as any).type}`); - } - } -}; diff --git a/server/src/sql-tools/transformers/enum.transformer.ts b/server/src/sql-tools/transformers/enum.transformer.ts deleted file mode 100644 index cd7bddc2d2..0000000000 --- a/server/src/sql-tools/transformers/enum.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseEnum } from 'src/sql-tools/types'; - -export const transformEnums: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'EnumCreate': { - return asEnumCreate(item.enum); - } - - case 'EnumDrop': { - return asEnumDrop(item.enumName); - } - - default: { - return false; - } - } -}; - -const asEnumCreate = ({ name, values }: DatabaseEnum): string => { - return `CREATE TYPE "${name}" AS ENUM (${values.map((value) => `'${value}'`)});`; -}; - -const asEnumDrop = (enumName: string): string => { - return `DROP TYPE "${enumName}";`; -}; diff --git a/server/src/sql-tools/transformers/extension.transformer.spec.ts b/server/src/sql-tools/transformers/extension.transformer.spec.ts deleted file mode 100644 index 2ab0402875..0000000000 --- a/server/src/sql-tools/transformers/extension.transformer.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformExtensions.name, () => { - describe('ExtensionDrop', () => { - it('should work', () => { - expect( - transformExtensions(ctx, { - type: 'ExtensionDrop', - extensionName: 'cube', - reason: 'unknown', - }), - ).toEqual(`DROP EXTENSION "cube";`); - }); - }); - - describe('ExtensionCreate', () => { - it('should work', () => { - expect( - transformExtensions(ctx, { - type: 'ExtensionCreate', - extension: { - name: 'cube', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual(`CREATE EXTENSION IF NOT EXISTS "cube";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/extension.transformer.ts b/server/src/sql-tools/transformers/extension.transformer.ts deleted file mode 100644 index 26e76c1157..0000000000 --- a/server/src/sql-tools/transformers/extension.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseExtension } from 'src/sql-tools/types'; - -export const transformExtensions: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ExtensionCreate': { - return asExtensionCreate(item.extension); - } - - case 'ExtensionDrop': { - return asExtensionDrop(item.extensionName); - } - - default: { - return false; - } - } -}; - -const asExtensionCreate = (extension: DatabaseExtension): string => { - return `CREATE EXTENSION IF NOT EXISTS "${extension.name}";`; -}; - -const asExtensionDrop = (extensionName: string): string => { - return `DROP EXTENSION "${extensionName}";`; -}; diff --git a/server/src/sql-tools/transformers/function.transformer.spec.ts b/server/src/sql-tools/transformers/function.transformer.spec.ts deleted file mode 100644 index 5b0ba71c7d..0000000000 --- a/server/src/sql-tools/transformers/function.transformer.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformFunctions.name, () => { - describe('FunctionDrop', () => { - it('should work', () => { - expect( - transformFunctions(ctx, { - type: 'FunctionDrop', - functionName: 'test_func', - reason: 'unknown', - }), - ).toEqual(`DROP FUNCTION test_func;`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/function.transformer.ts b/server/src/sql-tools/transformers/function.transformer.ts deleted file mode 100644 index 42a56cbe13..0000000000 --- a/server/src/sql-tools/transformers/function.transformer.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseFunction } from 'src/sql-tools/types'; - -export const transformFunctions: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'FunctionCreate': { - return asFunctionCreate(item.function); - } - - case 'FunctionDrop': { - return asFunctionDrop(item.functionName); - } - - default: { - return false; - } - } -}; - -export const asFunctionCreate = (func: DatabaseFunction): string => { - return func.expression; -}; - -const asFunctionDrop = (functionName: string): string => { - return `DROP FUNCTION ${functionName};`; -}; diff --git a/server/src/sql-tools/transformers/index.transformer.spec.ts b/server/src/sql-tools/transformers/index.transformer.spec.ts deleted file mode 100644 index c9656463bf..0000000000 --- a/server/src/sql-tools/transformers/index.transformer.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformIndexes.name, () => { - describe('IndexCreate', () => { - it('should work', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['column1'], - unique: false, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("column1");'); - }); - - it('should create an unique index', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['column1'], - unique: true, - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE UNIQUE INDEX "IDX_test" ON "table1" ("column1");'); - }); - - it('should create an index with a custom expression', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - unique: false, - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id" IS NOT NULL);'); - }); - - it('should create an index with a where clause', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - columnNames: ['id'], - unique: false, - where: '("id" IS NOT NULL)', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" ("id") WHERE ("id" IS NOT NULL);'); - }); - - it('should create an index with a custom expression', () => { - expect( - transformIndexes(ctx, { - type: 'IndexCreate', - index: { - name: 'IDX_test', - tableName: 'table1', - unique: false, - using: 'gin', - expression: '"id" IS NOT NULL', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual('CREATE INDEX "IDX_test" ON "table1" USING gin ("id" IS NOT NULL);'); - }); - }); - - describe('IndexDrop', () => { - it('should work', () => { - expect( - transformIndexes(ctx, { - type: 'IndexDrop', - indexName: 'IDX_test', - reason: 'unknown', - }), - ).toEqual(`DROP INDEX "IDX_test";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/index.transformer.ts b/server/src/sql-tools/transformers/index.transformer.ts deleted file mode 100644 index acd65140ee..0000000000 --- a/server/src/sql-tools/transformers/index.transformer.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { asColumnList } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseIndex } from 'src/sql-tools/types'; - -export const transformIndexes: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'IndexCreate': { - return asIndexCreate(item.index); - } - - case 'IndexRename': { - return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`; - } - - case 'IndexDrop': { - return `DROP INDEX "${item.indexName}";`; - } - - default: { - return false; - } - } -}; - -export const asIndexCreate = (index: DatabaseIndex): string => { - let sql = `CREATE`; - - if (index.unique) { - sql += ' UNIQUE'; - } - - sql += ` INDEX "${index.name}" ON "${index.tableName}"`; - - if (index.columnNames) { - const columnNames = asColumnList(index.columnNames); - sql += ` (${columnNames})`; - } - - if (index.using && index.using !== 'btree') { - sql += ` USING ${index.using}`; - } - - if (index.expression) { - sql += ` (${index.expression})`; - } - - if (index.with) { - sql += ` WITH (${index.with})`; - } - - if (index.where) { - sql += ` WHERE ${index.where}`; - } - - return sql + ';'; -}; diff --git a/server/src/sql-tools/transformers/index.ts b/server/src/sql-tools/transformers/index.ts deleted file mode 100644 index 395d69f2e2..0000000000 --- a/server/src/sql-tools/transformers/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { transformColumns } from 'src/sql-tools/transformers/column.transformer'; -import { transformConstraints } from 'src/sql-tools/transformers/constraint.transformer'; -import { transformEnums } from 'src/sql-tools/transformers/enum.transformer'; -import { transformExtensions } from 'src/sql-tools/transformers/extension.transformer'; -import { transformFunctions } from 'src/sql-tools/transformers/function.transformer'; -import { transformIndexes } from 'src/sql-tools/transformers/index.transformer'; -import { transformOverrides } from 'src/sql-tools/transformers/override.transformer'; -import { transformParameters } from 'src/sql-tools/transformers/parameter.transformer'; -import { transformTables } from 'src/sql-tools/transformers/table.transformer'; -import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; - -export const transformers: SqlTransformer[] = [ - transformColumns, - transformConstraints, - transformEnums, - transformExtensions, - transformFunctions, - transformIndexes, - transformParameters, - transformTables, - transformTriggers, - transformOverrides, -]; diff --git a/server/src/sql-tools/transformers/override.transformer.ts b/server/src/sql-tools/transformers/override.transformer.ts deleted file mode 100644 index 1e2e981128..0000000000 --- a/server/src/sql-tools/transformers/override.transformer.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { asJsonString } from 'src/sql-tools/helpers'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseOverride } from 'src/sql-tools/types'; - -export const transformOverrides: SqlTransformer = (ctx, item) => { - const tableName = ctx.overrideTableName; - - switch (item.type) { - case 'OverrideCreate': { - return asOverrideCreate(tableName, item.override); - } - - case 'OverrideUpdate': { - return asOverrideUpdate(tableName, item.override); - } - - case 'OverrideDrop': { - return asOverrideDrop(tableName, item.overrideName); - } - - default: { - return false; - } - } -}; - -export const asOverrideCreate = (tableName: string, override: DatabaseOverride): string => { - return `INSERT INTO "${tableName}" ("name", "value") VALUES ('${override.name}', ${asJsonString(override.value)});`; -}; - -export const asOverrideUpdate = (tableName: string, override: DatabaseOverride): string => { - return `UPDATE "${tableName}" SET "value" = ${asJsonString(override.value)} WHERE "name" = '${override.name}';`; -}; - -export const asOverrideDrop = (tableName: string, overrideName: string): string => { - return `DELETE FROM "${tableName}" WHERE "name" = '${overrideName}';`; -}; diff --git a/server/src/sql-tools/transformers/parameter.transformer.ts b/server/src/sql-tools/transformers/parameter.transformer.ts deleted file mode 100644 index d23472f991..0000000000 --- a/server/src/sql-tools/transformers/parameter.transformer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseParameter } from 'src/sql-tools/types'; - -export const transformParameters: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'ParameterSet': { - return asParameterSet(item.parameter); - } - - case 'ParameterReset': { - return asParameterReset(item.databaseName, item.parameterName); - } - - default: { - return false; - } - } -}; - -const asParameterSet = (parameter: DatabaseParameter): string => { - let sql = ''; - if (parameter.scope === 'database') { - sql += `ALTER DATABASE "${parameter.databaseName}" `; - } - - sql += `SET ${parameter.name} TO ${parameter.value}`; - - return sql; -}; - -const asParameterReset = (databaseName: string, parameterName: string): string => { - return `ALTER DATABASE "${databaseName}" RESET "${parameterName}"`; -}; diff --git a/server/src/sql-tools/transformers/table.transformer.spec.ts b/server/src/sql-tools/transformers/table.transformer.spec.ts deleted file mode 100644 index 0d89fcd278..0000000000 --- a/server/src/sql-tools/transformers/table.transformer.spec.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformTables } from 'src/sql-tools/transformers/table.transformer'; -import { ConstraintType, DatabaseTable } from 'src/sql-tools/types'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -const table1: DatabaseTable = { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - primary: true, - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - { - name: 'column2', - primary: false, - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'index1', - tableName: 'table1', - columnNames: ['column2'], - unique: false, - synchronize: true, - }, - ], - constraints: [ - { - name: 'constraint1', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.PRIMARY_KEY, - synchronize: true, - }, - { - name: 'constraint2', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.FOREIGN_KEY, - referenceTableName: 'table2', - referenceColumnNames: ['parentId'], - synchronize: true, - }, - { - name: 'constraint3', - tableName: 'table1', - columnNames: ['column1'], - type: ConstraintType.UNIQUE, - synchronize: true, - }, - ], - triggers: [], - synchronize: true, -}; - -describe(transformTables.name, () => { - describe('TableDrop', () => { - it('should work', () => { - expect( - transformTables(ctx, { - type: 'TableDrop', - tableName: 'table1', - reason: 'unknown', - }), - ).toEqual(`DROP TABLE "table1";`); - }); - }); - - describe('TableCreate', () => { - it('should work', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: table1, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying, - "column2" character varying, - CONSTRAINT "constraint1" PRIMARY KEY ("column1"), - CONSTRAINT "constraint2" FOREIGN KEY ("column1") REFERENCES "table2" ("parentId") ON UPDATE NO ACTION ON DELETE NO ACTION, - CONSTRAINT "constraint3" UNIQUE ("column1") -);`, - `CREATE INDEX "index1" ON "table1" ("column2");`, - ]); - }); - - it('should handle a non-nullable column', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - isArray: false, - nullable: false, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying NOT NULL -);`, - ]); - }); - - it('should handle a default value', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - name: 'column1', - primary: false, - type: 'character varying', - isArray: false, - nullable: true, - default: 'uuid_generate_v4()', - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying DEFAULT uuid_generate_v4() -);`, - ]); - }); - - it('should handle a string with a fixed length', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - length: 2, - isArray: false, - nullable: true, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying(2) -);`, - ]); - }); - - it('should handle an array type', () => { - expect( - transformTables(ctx, { - type: 'TableCreate', - table: { - name: 'table1', - columns: [ - { - tableName: 'table1', - primary: false, - name: 'column1', - type: 'character varying', - isArray: true, - nullable: true, - synchronize: true, - }, - ], - indexes: [], - constraints: [], - triggers: [], - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual([ - `CREATE TABLE "table1" ( - "column1" character varying[] -);`, - ]); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/table.transformer.ts b/server/src/sql-tools/transformers/table.transformer.ts deleted file mode 100644 index a81bfc25aa..0000000000 --- a/server/src/sql-tools/transformers/table.transformer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers'; -import { asColumnAlter } from 'src/sql-tools/transformers/column.transformer'; -import { asConstraintBody } from 'src/sql-tools/transformers/constraint.transformer'; -import { asIndexCreate } from 'src/sql-tools/transformers/index.transformer'; -import { asTriggerCreate } from 'src/sql-tools/transformers/trigger.transformer'; -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseTable } from 'src/sql-tools/types'; - -export const transformTables: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'TableCreate': { - return asTableCreate(item.table); - } - - case 'TableDrop': { - return asTableDrop(item.tableName); - } - - default: { - return false; - } - } -}; - -const asTableCreate = (table: DatabaseTable) => { - const tableName = table.name; - - const items: string[] = []; - for (const column of table.columns) { - items.push(`"${column.name}" ${getColumnType(column)}${getColumnModifiers(column)}`); - } - - for (const constraint of table.constraints) { - items.push(asConstraintBody(constraint)); - } - - const sql = [`CREATE TABLE "${tableName}" (\n ${items.join(',\n ')}\n);`]; - - for (const column of table.columns) { - if (column.comment) { - sql.push(asColumnComment(tableName, column.name, column.comment)); - } - - if (column.storage) { - sql.push(...asColumnAlter(tableName, column.name, { storage: column.storage })); - } - } - - for (const index of table.indexes) { - sql.push(asIndexCreate(index)); - } - - for (const trigger of table.triggers) { - sql.push(asTriggerCreate(trigger)); - } - - return sql; -}; - -const asTableDrop = (tableName: string) => { - return `DROP TABLE "${tableName}";`; -}; diff --git a/server/src/sql-tools/transformers/trigger.transformer.spec.ts b/server/src/sql-tools/transformers/trigger.transformer.spec.ts deleted file mode 100644 index f6ba889c29..0000000000 --- a/server/src/sql-tools/transformers/trigger.transformer.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { transformTriggers } from 'src/sql-tools/transformers/trigger.transformer'; -import { describe, expect, it } from 'vitest'; - -const ctx = new BaseContext({}); - -describe(transformTriggers.name, () => { - describe('TriggerCreate', () => { - it('should work', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update'], - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE ON "table1" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - - it('should work with multiple actions', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update', 'delete'], - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE OR DELETE ON "table1" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - - it('should work with old/new reference table aliases', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerCreate', - trigger: { - name: 'trigger1', - tableName: 'table1', - timing: 'before', - actions: ['update'], - referencingNewTableAs: 'new', - referencingOldTableAs: 'old', - scope: 'row', - functionName: 'function1', - synchronize: true, - }, - reason: 'unknown', - }), - ).toEqual( - `CREATE OR REPLACE TRIGGER "trigger1" - BEFORE UPDATE ON "table1" - REFERENCING OLD TABLE AS "old" NEW TABLE AS "new" - FOR EACH ROW - EXECUTE FUNCTION function1();`, - ); - }); - }); - - describe('TriggerDrop', () => { - it('should work', () => { - expect( - transformTriggers(ctx, { - type: 'TriggerDrop', - tableName: 'table1', - triggerName: 'trigger1', - reason: 'unknown', - }), - ).toEqual(`DROP TRIGGER "trigger1" ON "table1";`); - }); - }); -}); diff --git a/server/src/sql-tools/transformers/trigger.transformer.ts b/server/src/sql-tools/transformers/trigger.transformer.ts deleted file mode 100644 index fca557abfc..0000000000 --- a/server/src/sql-tools/transformers/trigger.transformer.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SqlTransformer } from 'src/sql-tools/transformers/types'; -import { DatabaseTrigger } from 'src/sql-tools/types'; - -export const transformTriggers: SqlTransformer = (ctx, item) => { - switch (item.type) { - case 'TriggerCreate': { - return asTriggerCreate(item.trigger); - } - - case 'TriggerDrop': { - return asTriggerDrop(item.tableName, item.triggerName); - } - - default: { - return false; - } - } -}; - -export const asTriggerCreate = (trigger: DatabaseTrigger): string => { - const sql: string[] = [ - `CREATE OR REPLACE TRIGGER "${trigger.name}"`, - `${trigger.timing.toUpperCase()} ${trigger.actions.map((action) => action.toUpperCase()).join(' OR ')} ON "${trigger.tableName}"`, - ]; - - if (trigger.referencingOldTableAs || trigger.referencingNewTableAs) { - let statement = `REFERENCING`; - if (trigger.referencingOldTableAs) { - statement += ` OLD TABLE AS "${trigger.referencingOldTableAs}"`; - } - if (trigger.referencingNewTableAs) { - statement += ` NEW TABLE AS "${trigger.referencingNewTableAs}"`; - } - sql.push(statement); - } - - if (trigger.scope) { - sql.push(`FOR EACH ${trigger.scope.toUpperCase()}`); - } - - if (trigger.when) { - sql.push(`WHEN (${trigger.when})`); - } - - sql.push(`EXECUTE FUNCTION ${trigger.functionName}();`); - - return sql.join('\n '); -}; - -export const asTriggerDrop = (tableName: string, triggerName: string): string => { - return `DROP TRIGGER "${triggerName}" ON "${tableName}";`; -}; diff --git a/server/src/sql-tools/transformers/types.ts b/server/src/sql-tools/transformers/types.ts deleted file mode 100644 index 96cbe4d918..0000000000 --- a/server/src/sql-tools/transformers/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { BaseContext } from 'src/sql-tools/contexts/base-context'; -import { SchemaDiff } from 'src/sql-tools/types'; - -export type SqlTransformer = (ctx: BaseContext, item: SchemaDiff) => string | string[] | false; diff --git a/server/src/sql-tools/types.ts b/server/src/sql-tools/types.ts deleted file mode 100644 index 899ba1b963..0000000000 --- a/server/src/sql-tools/types.ts +++ /dev/null @@ -1,534 +0,0 @@ -import { Kysely, ColumnType as KyselyColumnType } from 'kysely'; -import { ProcessorContext } from 'src/sql-tools/contexts/processor-context'; -import { ReaderContext } from 'src/sql-tools/contexts/reader-context'; -import { NamingInterface } from 'src/sql-tools/naming/naming.interface'; -import { RegisterItem } from 'src/sql-tools/register-item'; - -export type BaseContextOptions = { - databaseName?: string; - schemaName?: string; - overrideTableName?: string; - namingStrategy?: 'default' | 'hash' | NamingInterface; -}; - -export type SchemaFromCodeOptions = BaseContextOptions & { - /** automatically create indexes on foreign key columns */ - createForeignKeyIndexes?: boolean; - reset?: boolean; - - functions?: boolean; - extensions?: boolean; - parameters?: boolean; - overrides?: boolean; -}; - -export type SchemaFromDatabaseOptions = BaseContextOptions; - -export type SchemaDiffToSqlOptions = BaseContextOptions & { - comments?: boolean; -}; - -export type SchemaDiffOptions = BaseContextOptions & { - tables?: IgnoreOptions; - functions?: IgnoreOptions; - enums?: IgnoreOptions; - extensions?: IgnoreOptions; - parameters?: IgnoreOptions; - overrides?: IgnoreOptions; -}; - -export type IgnoreOptions = - | boolean - | { - ignoreExtra?: boolean; - ignoreMissing?: boolean; - }; - -export type Processor = (ctx: ProcessorContext, items: RegisterItem[]) => void; -export type Reader = (ctx: ReaderContext, db: DatabaseClient) => Promise; - -export type PostgresDB = { - pg_am: { - oid: number; - amname: string; - amhandler: string; - amtype: string; - }; - - pg_attribute: { - attrelid: number; - attname: string; - attnum: number; - atttypeid: number; - attstattarget: number; - attstatarget: number; - aanum: number; - }; - - pg_class: { - oid: number; - relname: string; - relkind: string; - relnamespace: string; - reltype: string; - relowner: string; - relam: string; - relfilenode: string; - reltablespace: string; - relpages: number; - reltuples: number; - relallvisible: number; - reltoastrelid: string; - relhasindex: PostgresYesOrNo; - relisshared: PostgresYesOrNo; - relpersistence: string; - }; - - pg_constraint: { - oid: number; - conname: string; - conrelid: string; - contype: string; - connamespace: string; - conkey: number[]; - confkey: number[]; - confrelid: string; - confupdtype: string; - confdeltype: string; - confmatchtype: number; - condeferrable: PostgresYesOrNo; - condeferred: PostgresYesOrNo; - convalidated: PostgresYesOrNo; - conindid: number; - }; - - pg_description: { - objoid: string; - classoid: string; - objsubid: number; - description: string; - }; - - pg_trigger: { - oid: string; - tgisinternal: boolean; - tginitdeferred: boolean; - tgdeferrable: boolean; - tgrelid: string; - tgfoid: string; - tgname: string; - tgenabled: string; - tgtype: number; - tgconstraint: string; - tgdeferred: boolean; - tgargs: Buffer; - tgoldtable: string; - tgnewtable: string; - tgqual: string; - }; - - 'pg_catalog.pg_extension': { - oid: string; - extname: string; - extowner: string; - extnamespace: string; - extrelocatable: boolean; - extversion: string; - extconfig: string[]; - extcondition: string[]; - }; - - pg_enum: { - oid: string; - enumtypid: string; - enumsortorder: number; - enumlabel: string; - }; - - pg_index: { - indexrelid: string; - indrelid: string; - indisready: boolean; - indexprs: string | null; - indpred: string | null; - indkey: number[]; - indisprimary: boolean; - indisunique: boolean; - }; - - pg_indexes: { - schemaname: string; - tablename: string; - indexname: string; - tablespace: string | null; - indexrelid: string; - indexdef: string; - }; - - pg_namespace: { - oid: number; - nspname: string; - nspowner: number; - nspacl: string[]; - }; - - pg_type: { - oid: string; - typname: string; - typnamespace: string; - typowner: string; - typtype: string; - typcategory: string; - typarray: string; - }; - - pg_depend: { - objid: string; - deptype: string; - }; - - pg_proc: { - oid: string; - proname: string; - pronamespace: string; - prokind: string; - }; - - pg_settings: { - name: string; - setting: string; - unit: string | null; - category: string; - short_desc: string | null; - extra_desc: string | null; - context: string; - vartype: string; - source: string; - min_val: string | null; - max_val: string | null; - enumvals: string[] | null; - boot_val: string | null; - reset_val: string | null; - sourcefile: string | null; - sourceline: number | null; - pending_restart: PostgresYesOrNo; - }; - - 'information_schema.tables': { - table_catalog: string; - table_schema: string; - table_name: string; - table_type: 'VIEW' | 'BASE TABLE' | string; - is_insertable_info: PostgresYesOrNo; - is_typed: PostgresYesOrNo; - commit_action: string | null; - }; - - 'information_schema.columns': { - table_catalog: string; - table_schema: string; - table_name: string; - column_name: string; - ordinal_position: number; - column_default: string | null; - is_nullable: PostgresYesOrNo; - data_type: string; - dtd_identifier: string; - character_maximum_length: number | null; - character_octet_length: number | null; - numeric_precision: number | null; - numeric_precision_radix: number | null; - numeric_scale: number | null; - datetime_precision: number | null; - interval_type: string | null; - interval_precision: number | null; - udt_catalog: string; - udt_schema: string; - udt_name: string; - maximum_cardinality: number | null; - is_updatable: PostgresYesOrNo; - }; - - 'information_schema.element_types': { - object_catalog: string; - object_schema: string; - object_name: string; - object_type: string; - collection_type_identifier: string; - data_type: string; - }; - - 'information_schema.routines': { - specific_catalog: string; - specific_schema: string; - specific_name: string; - routine_catalog: string; - routine_schema: string; - routine_name: string; - routine_type: string; - data_type: string; - type_udt_catalog: string; - type_udt_schema: string; - type_udt_name: string; - dtd_identifier: string; - routine_body: string; - routine_definition: string; - external_name: string; - external_language: string; - is_deterministic: PostgresYesOrNo; - security_type: string; - }; -}; - -type PostgresYesOrNo = 'YES' | 'NO'; - -export type DatabaseClient = Kysely; - -export enum ConstraintType { - PRIMARY_KEY = 'primary-key', - FOREIGN_KEY = 'foreign-key', - UNIQUE = 'unique', - CHECK = 'check', -} - -export enum ActionType { - NO_ACTION = 'NO ACTION', - RESTRICT = 'RESTRICT', - CASCADE = 'CASCADE', - SET_NULL = 'SET NULL', - SET_DEFAULT = 'SET DEFAULT', -} - -export type ColumnStorage = 'default' | 'external' | 'extended' | 'main'; - -export type ColumnType = - | 'bigint' - | 'boolean' - | 'bytea' - | 'character' - | 'character varying' - | 'date' - | 'double precision' - | 'integer' - | 'jsonb' - | 'polygon' - | 'text' - | 'time' - | 'time with time zone' - | 'time without time zone' - | 'timestamp' - | 'timestamp with time zone' - | 'timestamp without time zone' - | 'uuid' - | 'vector' - | 'enum' - | 'serial' - | 'real'; - -export type DatabaseSchema = { - databaseName: string; - schemaName: string; - functions: DatabaseFunction[]; - enums: DatabaseEnum[]; - tables: DatabaseTable[]; - extensions: DatabaseExtension[]; - parameters: DatabaseParameter[]; - overrides: DatabaseOverride[]; - warnings: string[]; -}; - -export type DatabaseParameter = { - name: string; - databaseName: string; - value: string | number | null | undefined; - scope: ParameterScope; - synchronize: boolean; -}; - -export type ParameterScope = 'database' | 'user'; - -export type DatabaseOverride = { - name: string; - value: { name: string; type: OverrideType; sql: string }; - synchronize: boolean; -}; - -export type OverrideType = 'function' | 'index' | 'trigger'; - -export type DatabaseEnum = { - name: string; - values: string[]; - synchronize: boolean; -}; - -export type DatabaseFunction = { - name: string; - expression: string; - synchronize: boolean; - override?: DatabaseOverride; -}; - -export type DatabaseExtension = { - name: string; - synchronize: boolean; -}; - -export type DatabaseTable = { - name: string; - columns: DatabaseColumn[]; - indexes: DatabaseIndex[]; - constraints: DatabaseConstraint[]; - triggers: DatabaseTrigger[]; - synchronize: boolean; -}; - -export type DatabaseConstraint = - | DatabasePrimaryKeyConstraint - | DatabaseForeignKeyConstraint - | DatabaseUniqueConstraint - | DatabaseCheckConstraint; - -export type DatabaseColumn = { - primary: boolean; - name: string; - tableName: string; - comment?: string; - - type: ColumnType; - nullable: boolean; - isArray: boolean; - synchronize: boolean; - - default?: string; - length?: number; - storage?: ColumnStorage; - identity?: boolean; - - // enum values - enumName?: string; - - // numeric types - numericPrecision?: number; - numericScale?: number; -}; - -export type ColumnChanges = { - nullable?: boolean; - default?: string; - comment?: string; - storage?: ColumnStorage; -}; - -type ColumBasedConstraint = { - name: string; - tableName: string; - columnNames: string[]; -}; - -export type DatabasePrimaryKeyConstraint = ColumBasedConstraint & { - type: ConstraintType.PRIMARY_KEY; - synchronize: boolean; -}; - -export type DatabaseUniqueConstraint = ColumBasedConstraint & { - type: ConstraintType.UNIQUE; - synchronize: boolean; -}; - -export type DatabaseForeignKeyConstraint = ColumBasedConstraint & { - type: ConstraintType.FOREIGN_KEY; - referenceTableName: string; - referenceColumnNames: string[]; - onUpdate?: ActionType; - onDelete?: ActionType; - synchronize: boolean; -}; - -export type DatabaseCheckConstraint = { - type: ConstraintType.CHECK; - name: string; - tableName: string; - expression: string; - synchronize: boolean; -}; - -export type DatabaseTrigger = { - name: string; - tableName: string; - timing: TriggerTiming; - actions: TriggerAction[]; - scope: TriggerScope; - referencingNewTableAs?: string; - referencingOldTableAs?: string; - when?: string; - functionName: string; - override?: DatabaseOverride; - synchronize: boolean; -}; -export type TriggerTiming = 'before' | 'after' | 'instead of'; -export type TriggerAction = 'insert' | 'update' | 'delete' | 'truncate'; -export type TriggerScope = 'row' | 'statement'; - -export type DatabaseIndex = { - name: string; - tableName: string; - columnNames?: string[]; - expression?: string; - unique: boolean; - using?: string; - with?: string; - where?: string; - override?: DatabaseOverride; - synchronize: boolean; -}; - -export type SchemaDiff = { reason: string } & ( - | { type: 'ExtensionCreate'; extension: DatabaseExtension } - | { type: 'ExtensionDrop'; extensionName: string } - | { type: 'FunctionCreate'; function: DatabaseFunction } - | { type: 'FunctionDrop'; functionName: string } - | { type: 'TableCreate'; table: DatabaseTable } - | { type: 'TableDrop'; tableName: string } - | { type: 'ColumnAdd'; column: DatabaseColumn } - | { type: 'ColumnRename'; tableName: string; oldName: string; newName: string } - | { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges } - | { type: 'ColumnDrop'; tableName: string; columnName: string } - | { type: 'ConstraintAdd'; constraint: DatabaseConstraint } - | { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string } - | { type: 'ConstraintDrop'; tableName: string; constraintName: string } - | { type: 'IndexCreate'; index: DatabaseIndex } - | { type: 'IndexRename'; tableName: string; oldName: string; newName: string } - | { type: 'IndexDrop'; indexName: string } - | { type: 'TriggerCreate'; trigger: DatabaseTrigger } - | { type: 'TriggerDrop'; tableName: string; triggerName: string } - | { type: 'ParameterSet'; parameter: DatabaseParameter } - | { type: 'ParameterReset'; databaseName: string; parameterName: string } - | { type: 'EnumCreate'; enum: DatabaseEnum } - | { type: 'EnumDrop'; enumName: string } - | { type: 'OverrideCreate'; override: DatabaseOverride } - | { type: 'OverrideUpdate'; override: DatabaseOverride } - | { type: 'OverrideDrop'; overrideName: string } -); - -export type CompareFunction = (source: T, target: T) => SchemaDiff[]; -export type Comparer = { - onMissing: (source: T) => SchemaDiff[]; - onExtra: (target: T) => SchemaDiff[]; - onCompare: CompareFunction; - /** if two items have the same key, they are considered identical and can be renamed via `onRename` */ - getRenameKey?: (item: T) => string; - onRename?: (source: T, target: T) => SchemaDiff[]; -}; - -export enum Reason { - MissingInSource = 'missing in source', - MissingInTarget = 'missing in target', - Rename = 'name has changed', -} - -export type Timestamp = KyselyColumnType; -export type Generated = - T extends KyselyColumnType - ? KyselyColumnType - : KyselyColumnType; -export type Int8 = KyselyColumnType; diff --git a/server/src/types.ts b/server/src/types.ts index e404332fac..8cf128f497 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -3,10 +3,11 @@ import { VECTOR_EXTENSIONS } from 'src/constants'; import { Asset, AssetFile } from 'src/database'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { AssetOrder, AssetType, - DatabaseSslMode, ExifOrientation, ImageFormat, JobName, @@ -21,48 +22,48 @@ import { VideoCodec, } from 'src/enum'; -export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T; +export type DeepPartial = + T extends Record + ? { [K in keyof T]?: DeepPartial } + : T extends Array + ? DeepPartial[] + : T; export type RepositoryInterface = Pick; -export interface CropOptions { - top: number; - left: number; - width: number; - height: number; -} - -export interface FullsizeImageOptions { +export type FullsizeImageOptions = { format: ImageFormat; quality: number; enabled: boolean; -} + progressive?: boolean; +}; -export interface ImageOptions { +export type ImageOptions = { format: ImageFormat; quality: number; size: number; -} + progressive?: boolean; +}; -export interface RawImageInfo { +export type RawImageInfo = { width: number; height: number; channels: 1 | 2 | 3 | 4; -} +}; -interface DecodeImageOptions { +type DecodeImageOptions = { colorspace: string; - crop?: CropOptions; processInvalidImages: boolean; raw?: RawImageInfo; -} + edits?: AssetEditActionItem[]; +}; export interface DecodeToBufferOptions extends DecodeImageOptions { size?: number; orientation?: ExifOrientation; } -export type GenerateThumbnailOptions = Pick & DecodeToBufferOptions; +export type GenerateThumbnailOptions = Pick & DecodeToBufferOptions; export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo }; @@ -72,7 +73,6 @@ export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { ra export interface GenerateThumbnailsOptions { colorspace: string; - crop?: CropOptions; preview?: ImageOptions; processInvalidImages: boolean; thumbhash?: boolean; @@ -186,7 +186,7 @@ export interface IDelayedJob extends IBaseJob { delay?: number; } -export type JobSource = 'upload' | 'sidecar-write' | 'copy'; +export type JobSource = 'upload' | 'sidecar-write' | 'copy' | 'edit'; export interface IEntityJob extends IBaseJob { id: string; source?: JobSource; @@ -324,7 +324,7 @@ export type JobItem = // Sidecar Scanning | { name: JobName.SidecarQueueAll; data: IBaseJob } | { name: JobName.SidecarCheck; data: IEntityJob } - | { name: JobName.SidecarWrite; data: ISidecarWriteJob } + | { name: JobName.SidecarWrite; data: IEntityJob } // Facial Recognition | { name: JobName.AssetDetectFacesQueueAll; data: IBaseJob } @@ -385,27 +385,13 @@ export type JobItem = | { name: JobName.Ocr; data: IEntityJob } // Workflow - | { name: JobName.WorkflowRun; data: IWorkflowJob }; + | { name: JobName.WorkflowRun; data: IWorkflowJob } + + // Editor + | { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob }; export type VectorExtension = (typeof VECTOR_EXTENSIONS)[number]; -export type DatabaseConnectionURL = { - connectionType: 'url'; - url: string; -}; - -export type DatabaseConnectionParts = { - connectionType: 'parts'; - host: string; - port: number; - username: string; - password: string; - database: string; - ssl?: DatabaseSslMode; -}; - -export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts; - export interface ExtensionVersion { name: VectorExtension; availableVersion: string | null; @@ -472,6 +458,9 @@ export type StorageAsset = { originalFileName: string; fileSizeInByte: number | null; files: AssetFile[]; + make: string | null; + model: string | null; + lensModel: string | null; }; export type OnThisDayData = { year: number }; @@ -482,7 +471,9 @@ export interface MemoryData { export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }; export type SystemFlags = { mountChecks: Record }; -export type MaintenanceModeState = { isMaintenanceMode: true; secret: string } | { isMaintenanceMode: false }; +export type MaintenanceModeState = + | { isMaintenanceMode: true; secret: string; action?: SetMaintenanceModeDto } + | { isMaintenanceMode: false }; export type MemoriesState = { /** memories have already been created through this date */ lastOnThisDayDate: string; @@ -502,7 +493,7 @@ export interface SystemMetadata extends Record = { key: T; diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index f8d5f0ca08..7431cb3293 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -157,6 +157,18 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); } + case Permission.AssetEditGet: { + return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); + } + + case Permission.AssetEditCreate: { + return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); + } + + case Permission.AssetEditDelete: { + return await access.asset.checkOwnerAccess(auth.user.id, ids, auth.session?.hasElevatedPermission); + } + case Permission.AlbumRead: { const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); const isShared = await access.album.checkSharedAlbumAccess( diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index eead81eb84..74a0e9487d 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { GeneratedImageType, StorageCore } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AssetFile } from 'src/database'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; @@ -13,16 +13,20 @@ import { PartnerRepository } from 'src/repositories/partner.repository'; import { IBulkAsset, ImmichFile, UploadFile, UploadRequest } from 'src/types'; import { checkAccess } from 'src/utils/access'; -export const getAssetFile = (files: AssetFile[], type: AssetFileType | GeneratedImageType) => { - return files.find((file) => file.type === type); +export const getAssetFile = (files: AssetFile[], type: AssetFileType, { isEdited }: { isEdited: boolean }) => { + return files.find((file) => file.type === type && file.isEdited === isEdited); }; export const getAssetFiles = (files: AssetFile[]) => ({ - fullsizeFile: getAssetFile(files, AssetFileType.FullSize), - previewFile: getAssetFile(files, AssetFileType.Preview), - thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail), - sidecarFile: getAssetFile(files, AssetFileType.Sidecar), - tilesPath: getAssetFile(files, AssetFileType.Tiles), + fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }), + previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }), + thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }), + tilesPath: getAssetFile(files, AssetFileType.Tiles, { isEdited: false }), + sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }), + + editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }), + editedPreviewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }), + editedThumbnailFile: getAssetFile(files, AssetFileType.Preview, { isEdited: true }), }); export const addAssets = async ( @@ -200,3 +204,32 @@ export const asUploadRequest = (request: AuthRequest, file: Express.Multer.File) file: mapToUploadFile(file as ImmichFile), }; }; + +const isFlipped = (orientation?: string | null) => { + const value = Number(orientation); + return value && [5, 6, 7, 8, -90, 90].includes(value); +}; + +export const getDimensions = ({ + exifImageHeight: height, + exifImageWidth: width, + orientation, +}: { + exifImageHeight: number | null; + exifImageWidth: number | null; + orientation: string | null; +}) => { + if (!width || !height) { + return { width: 0, height: 0 }; + } + + if (isFlipped(orientation)) { + return { width: height, height: width }; + } + + return { width, height }; +}; + +export const isPanorama = (asset: { projectionType: string | null; originalFileName: string }) => { + return asset.projectionType === 'EQUIRECTANGULAR' || asset.originalFileName.toLowerCase().endsWith('.insp'); +}; diff --git a/server/src/utils/bbox.ts b/server/src/utils/bbox.ts new file mode 100644 index 0000000000..ad02e8355e --- /dev/null +++ b/server/src/utils/bbox.ts @@ -0,0 +1,32 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiPropertyOptions } from '@nestjs/swagger'; +import { Transform, Type } from 'class-transformer'; +import { IsNotEmpty, ValidateNested } from 'class-validator'; +import { Property } from 'src/decorators'; +import { BBoxDto } from 'src/dtos/bbox.dto'; +import { Optional } from 'src/validation'; + +type BBoxOptions = { optional?: boolean }; +export const ValidateBBox = (options: BBoxOptions & ApiPropertyOptions = {}) => { + const { optional, ...apiPropertyOptions } = options; + + return applyDecorators( + Transform(({ value }) => { + if (typeof value !== 'string') { + return value; + } + + const [west, south, east, north] = value.split(',', 4).map(Number); + return Object.assign(new BBoxDto(), { west, south, east, north }); + }), + Type(() => BBoxDto), + ValidateNested(), + Property({ + type: 'string', + description: 'Bounding box coordinates as west,south,east,north (WGS84)', + example: '11.075683,49.416711,11.117589,49.454875', + ...apiPropertyOptions, + }), + optional ? Optional({}) : IsNotEmpty(), + ); +}; diff --git a/server/src/utils/database-backups.ts b/server/src/utils/database-backups.ts new file mode 100644 index 0000000000..70bedb32b1 --- /dev/null +++ b/server/src/utils/database-backups.ts @@ -0,0 +1,24 @@ +export function isValidDatabaseBackupName(filename: string) { + return filename.match(/^[\d\w-.]+\.sql(?:\.gz)?$/); +} + +export function isValidDatabaseRoutineBackupName(filename: string) { + const oldBackupStyle = filename.match(/^immich-db-backup-\d+\.sql\.gz$/); + //immich-db-backup-20250729T114018-v1.136.0-pg14.17.sql.gz + const newBackupStyle = filename.match(/^immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/); + return oldBackupStyle || newBackupStyle; +} + +export function isFailedDatabaseBackupName(filename: string) { + return filename.match(/^immich-db-backup-.*\.sql\.gz\.tmp$/); +} + +export function findDatabaseBackupVersion(filename: string) { + return /-v(.*)-/.exec(filename)?.[1]; +} + +export class UnsupportedPostgresError extends Error { + constructor(databaseVersion: string) { + super(`Unsupported PostgreSQL version: ${databaseVersion}`); + } +} diff --git a/server/src/utils/database.spec.ts b/server/src/utils/database.spec.ts deleted file mode 100644 index 4c6a82ad8f..0000000000 --- a/server/src/utils/database.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { asPostgresConnectionConfig } from 'src/utils/database'; - -describe('database utils', () => { - describe('asPostgresConnectionConfig', () => { - it('should handle sslmode=require', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=require', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=prefer', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=prefer', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-ca', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-ca', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=verify-full', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=verify-full', - }), - ).toMatchObject({ ssl: {} }); - }); - - it('should handle sslmode=no-verify', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?sslmode=no-verify', - }), - ).toMatchObject({ ssl: { rejectUnauthorized: false } }); - }); - - it('should handle ssl=true', () => { - expect( - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=true', - }), - ).toMatchObject({ ssl: true }); - }); - - it('should reject invalid ssl', () => { - expect(() => - asPostgresConnectionConfig({ - connectionType: 'url', - url: 'postgres://postgres1:postgres2@database1:54320/immich?ssl=invalid', - }), - ).toThrowError('Invalid ssl option'); - }); - - it('should handle socket: URLs', () => { - expect( - asPostgresConnectionConfig({ connectionType: 'url', url: 'socket:/run/postgresql?db=database1' }), - ).toMatchObject({ host: '/run/postgresql', database: 'database1' }); - }); - - it('should handle sockets in postgres: URLs', () => { - expect( - asPostgresConnectionConfig({ connectionType: 'url', url: 'postgres:///database2?host=/path/to/socket' }), - ).toMatchObject({ - host: '/path/to/socket', - database: 'database2', - }); - }); - }); -}); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 656e8e628a..4dd0c9b302 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,4 +1,6 @@ +import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; import { + AliasedRawBuilder, DeduplicateJoinsPlugin, Expression, ExpressionBuilder, @@ -13,93 +15,32 @@ import { } from 'kysely'; import { PostgresJSDialect } from 'kysely-postgres-js'; import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; -import { parse } from 'pg-connection-string'; -import postgres, { Notice, PostgresError } from 'postgres'; +import { Notice, PostgresError } from 'postgres'; import { columns, Exif, lockableProperties, LockableProperty, Person } from 'src/database'; -import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { AssetFileType, AssetVisibility, DatabaseExtension } from 'src/enum'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; import { DB } from 'src/schema'; -import { DatabaseConnectionParams, VectorExtension } from 'src/types'; - -type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; - -const isValidSsl = (ssl?: string | boolean | object): ssl is Ssl => - typeof ssl !== 'string' || ssl === 'require' || ssl === 'allow' || ssl === 'prefer' || ssl === 'verify-full'; - -export const asPostgresConnectionConfig = (params: DatabaseConnectionParams) => { - if (params.connectionType === 'parts') { - return { - host: params.host, - port: params.port, - username: params.username, - password: params.password, - database: params.database, - ssl: params.ssl === DatabaseSslMode.Disable ? false : params.ssl, - }; - } - - const { host, port, user, password, database, ...rest } = parse(params.url); - let ssl: Ssl | undefined; - if (rest.ssl) { - if (!isValidSsl(rest.ssl)) { - throw new Error(`Invalid ssl option: ${rest.ssl}`); - } - ssl = rest.ssl; - } - - return { - host: host ?? undefined, - port: port ? Number(port) : undefined, - username: user, - password, - database: database ?? undefined, - ssl, - }; -}; - -export const getKyselyConfig = ( - params: DatabaseConnectionParams, - options: Partial>> = {}, -): KyselyConfig => { - const config = asPostgresConnectionConfig(params); +import { VectorExtension } from 'src/types'; +export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => { return { dialect: new PostgresJSDialect({ - postgres: postgres({ - onnotice: (notice: Notice) => { + postgres: createPostgres({ + connection, + onNotice: (notice: Notice) => { if (notice['severity'] !== 'NOTICE') { console.warn('Postgres notice:', notice); } }, - max: 10, - types: { - date: { - to: 1184, - from: [1082, 1114, 1184], - serialize: (x: Date | string) => (x instanceof Date ? x.toISOString() : x), - parse: (x: string) => new Date(x), - }, - bigint: { - to: 20, - from: [20, 1700], - parse: (value: string) => Number.parseInt(value), - serialize: (value: number) => value.toString(), - }, - }, - connection: { - TimeZone: 'UTC', - }, - host: config.host, - port: config.port, - username: config.username, - password: config.password, - database: config.database, - ssl: config.ssl, - ...options, }), }), log(event) { if (event.level === 'error') { + if (isAssetChecksumConstraint(event.error)) { + return; + } + console.error('Query failed :', { durationMs: event.queryDurationMillis, error: event.error, @@ -180,13 +121,14 @@ export function withSmartSearch(qb: SelectQueryBuilder) { .select((eb) => toJson(eb, 'smart_search').as('smartSearch')); } -export function withFaces(eb: ExpressionBuilder, withDeletedFace?: boolean) { +export function withFaces(eb: ExpressionBuilder, withHidden?: boolean, withDeletedFace?: boolean) { return jsonArrayFrom( eb .selectFrom('asset_face') .selectAll('asset_face') .whereRef('asset_face.assetId', '=', 'asset.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)), + .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)) + .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', '=', true)), ).as('faces'); } @@ -208,7 +150,11 @@ export function withFilePath(eb: ExpressionBuilder, type: AssetFile .where('asset_file.type', '=', type); } -export function withFacesAndPeople(eb: ExpressionBuilder, withDeletedFace?: boolean) { +export function withFacesAndPeople( + eb: ExpressionBuilder, + withHidden?: boolean, + withDeletedFace?: boolean, +) { return jsonArrayFrom( eb .selectFrom('asset_face') @@ -220,7 +166,8 @@ export function withFacesAndPeople(eb: ExpressionBuilder, withDelet .selectAll('asset_face') .select((eb) => eb.table('person').$castTo().as('person')) .whereRef('asset_face.assetId', '=', 'asset.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)), + .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)) + .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)), ).as('faces'); } @@ -232,6 +179,7 @@ export function hasPeople(qb: SelectQueryBuilder, personIds: .select('assetId') .where('personId', '=', anyUuid(personIds!)) .where('deletedAt', 'is', null) + .where('isVisible', 'is', true) .groupBy('assetId') .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) .as('has_people'), @@ -346,6 +294,17 @@ export const tokenizeForSearch = (text: string): string[] => { return tokens; }; +// needed to properly type the return with the EditActionItem discriminated union type +type AliasedEditActions = AliasedRawBuilder; +export function withEdits(eb: ExpressionBuilder): AliasedEditActions { + return jsonArrayFrom( + eb + .selectFrom('asset_edit') + .select(['asset_edit.action', 'asset_edit.parameters']) + .whereRef('asset_edit.assetId', '=', 'asset.id'), + ).as('edits') as AliasedEditActions; +} + const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); /** TODO: This should only be used for search-related queries, not as a general purpose query builder */ @@ -446,7 +405,7 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('album_asset').whereRef('assetId', '=', 'asset.id')))), ) .$if(!!options.withExif, withExifInner) - .$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople)) + .$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople)) .$if(!options.withDeleted, (qb) => qb.where('asset.deletedAt', 'is', null)); } diff --git a/server/src/utils/date.ts b/server/src/utils/date.ts index 67ce549050..6cef48ecf8 100644 --- a/server/src/utils/date.ts +++ b/server/src/utils/date.ts @@ -1,3 +1,16 @@ +import { DateTime } from 'luxon'; + export const asDateString = (x: Date | string | null): string | null => { return x instanceof Date ? x.toISOString().split('T')[0] : x; }; + +export const extractTimeZone = (dateTimeOriginal?: string | null) => { + const extractedTimeZone = dateTimeOriginal ? DateTime.fromISO(dateTimeOriginal, { setZone: true }).zone : undefined; + return extractedTimeZone?.type === 'fixed' ? extractedTimeZone : undefined; +}; + +export const mergeTimeZone = (dateTimeOriginal?: string | null, timeZone?: string | null) => { + return dateTimeOriginal + ? DateTime.fromISO(dateTimeOriginal, { zone: 'UTC' }).setZone(timeZone ?? undefined) + : undefined; +}; diff --git a/server/src/utils/editor.spec.ts b/server/src/utils/editor.spec.ts new file mode 100644 index 0000000000..17db0d9da3 --- /dev/null +++ b/server/src/utils/editor.spec.ts @@ -0,0 +1,505 @@ +import { AssetFace } from 'src/database'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; +import { SourceType } from 'src/enum'; +import { boundingBoxOverlap, checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor'; +import { describe, expect, it } from 'vitest'; + +describe('boundingBoxOverlap', () => { + it('should return 1 for identical boxes', () => { + const box = { x1: 0, y1: 0, x2: 100, y2: 100 }; + expect(boundingBoxOverlap(box, box)).toBe(1); + }); + + it('should return 0 for non-overlapping boxes', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 200, y1: 200, x2: 300, y2: 300 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(0); + }); + + it('should return 0.5 for 50% overlap', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 50, y1: 0, x2: 150, y2: 100 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(0.5); + }); + + it('should return 0.25 for 25% overlap', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 50, y1: 50, x2: 150, y2: 150 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(0.25); + }); + + it('should return 1 when boxA is fully contained in boxB', () => { + const boxA = { x1: 25, y1: 25, x2: 75, y2: 75 }; + const boxB = { x1: 0, y1: 0, x2: 100, y2: 100 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(1); + }); + + it('should handle partial containment correctly', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 25, y1: 25, x2: 75, y2: 75 }; + // boxB is fully inside boxA, so overlap area is 50*50=2500, boxA area is 10000 + expect(boundingBoxOverlap(boxA, boxB)).toBe(0.25); + }); + + it('should handle boxes that touch at edges (no overlap)', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 100, y1: 0, x2: 200, y2: 100 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(0); + }); + + it('should handle vertical partial overlap', () => { + const boxA = { x1: 0, y1: 0, x2: 100, y2: 100 }; + const boxB = { x1: 0, y1: 50, x2: 100, y2: 150 }; + expect(boundingBoxOverlap(boxA, boxB)).toBe(0.5); + }); +}); + +const createFace = (params: Partial = {}): AssetFace => ({ + id: 'face-id', + deletedAt: null, + assetId: 'asset-id', + boundingBoxX1: 100, + boundingBoxX2: 200, + boundingBoxY1: 100, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 1000, + personId: null, + sourceType: SourceType.MachineLearning, + person: null, + updatedAt: new Date(), + updateId: 'update-id', + isVisible: true, + ...params, +}); + +describe('checkFaceVisibility', () => { + const assetDimensions = { width: 1000, height: 1000 }; + + it('should return only non-visible faces when no crop is provided', () => { + const faces = [ + createFace({ id: 'face-1', isVisible: true }), + createFace({ id: 'face-2', isVisible: false }), + createFace({ id: 'face-3', isVisible: false }), + ]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(0); + expect(result.visible.map((f) => f.id)).toEqual(['face-2', 'face-3']); + }); + + it('should return all faces as visible when all are marked not visible and no crop provided', () => { + const faces = [createFace({ id: 'face-1', isVisible: false }), createFace({ id: 'face-2', isVisible: false })]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(0); + }); + + it('should return empty visible array when all faces are already visible and no crop provided', () => { + const faces = [createFace({ id: 'face-1', isVisible: true }), createFace({ id: 'face-2', isVisible: true })]; + const result = checkFaceVisibility(faces, assetDimensions); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(0); + }); + + it('should return empty arrays when no faces provided', () => { + const result = checkFaceVisibility([], assetDimensions); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark face as visible when fully inside crop area', () => { + const faces = [createFace({ boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, boundingBoxY2: 200 })]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark face as hidden when fully outside crop area', () => { + const faces = [createFace({ boundingBoxX1: 600, boundingBoxY1: 600, boundingBoxX2: 700, boundingBoxY2: 700 })]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(1); + }); + + it('should mark face as visible when at least 50% overlaps with crop', () => { + // Face spans 100-200 (100px), crop starts at 150, so 50% overlap + const faces = [createFace({ boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, boundingBoxY2: 200 })]; + const crop = { x1: 150, y1: 100, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark face as hidden when less than 50% overlaps with crop', () => { + // Face spans 100-200 (100px), crop starts at 160, so 40% overlap + const faces = [createFace({ boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, boundingBoxY2: 200 })]; + const crop = { x1: 160, y1: 100, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(1); + }); + + it('should correctly categorize multiple faces', () => { + const faces = [ + createFace({ id: 'face-inside', boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, boundingBoxY2: 200 }), + createFace({ + id: 'face-outside', + boundingBoxX1: 800, + boundingBoxY1: 800, + boundingBoxX2: 900, + boundingBoxY2: 900, + }), + // face-partial: 400-500 overlaps with crop (100x100=10000 overlap, face is 200x200=40000, so 25% - hidden) + createFace({ + id: 'face-partial', + boundingBoxX1: 400, + boundingBoxY1: 400, + boundingBoxX2: 600, + boundingBoxY2: 600, + }), + ]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + // face-inside is fully visible, face-partial has 25% overlap (hidden), face-outside is fully hidden + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(2); + expect(result.visible.map((f) => f.id)).toContain('face-inside'); + expect(result.hidden.map((f) => f.id)).toContain('face-partial'); + expect(result.hidden.map((f) => f.id)).toContain('face-outside'); + }); + + it('should handle face coordinates scaled to different image dimensions', () => { + // Face stored at 50-100 in a 500x500 image, scaled to 1000x1000 becomes 100-200 + const faces = [ + createFace({ + boundingBoxX1: 50, + boundingBoxY1: 50, + boundingBoxX2: 100, + boundingBoxY2: 100, + imageWidth: 500, + imageHeight: 500, + }), + ]; + const crop = { x1: 0, y1: 0, x2: 200, y2: 200 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should categorize based on crop overlap when crop is provided, regardless of isVisible property', () => { + const faces = [ + createFace({ + id: 'face-inside-visible', + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + isVisible: true, + }), + createFace({ + id: 'face-inside-not-visible', + boundingBoxX1: 250, + boundingBoxY1: 250, + boundingBoxX2: 350, + boundingBoxY2: 350, + isVisible: false, + }), + createFace({ + id: 'face-outside-visible', + boundingBoxX1: 800, + boundingBoxY1: 800, + boundingBoxX2: 900, + boundingBoxY2: 900, + isVisible: true, + }), + createFace({ + id: 'face-outside-not-visible', + boundingBoxX1: 700, + boundingBoxY1: 700, + boundingBoxX2: 800, + boundingBoxY2: 800, + isVisible: false, + }), + ]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkFaceVisibility(faces, assetDimensions, crop); + + // When crop is provided, only overlap matters, not isVisible property + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(2); + expect(result.visible.map((f) => f.id)).toContain('face-inside-visible'); + expect(result.visible.map((f) => f.id)).toContain('face-inside-not-visible'); + expect(result.hidden.map((f) => f.id)).toContain('face-outside-visible'); + expect(result.hidden.map((f) => f.id)).toContain('face-outside-not-visible'); + }); + + it('should handle mixed visibility states with partial overlap and crop', () => { + const faces = [ + createFace({ + id: 'face-partial-50', + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + isVisible: true, + }), + createFace({ + id: 'face-partial-40', + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + isVisible: false, + }), + ]; + const crop1 = { x1: 150, y1: 100, x2: 500, y2: 500 }; // 50% overlap + const crop2 = { x1: 160, y1: 100, x2: 500, y2: 500 }; // 40% overlap + + const result1 = checkFaceVisibility([faces[0]], assetDimensions, crop1); + const result2 = checkFaceVisibility([faces[1]], assetDimensions, crop2); + + // 50% overlap should be visible + expect(result1.visible).toHaveLength(1); + expect(result1.hidden).toHaveLength(0); + + // 40% overlap should be hidden + expect(result2.visible).toHaveLength(0); + expect(result2.hidden).toHaveLength(1); + }); +}); + +const createOcr = ( + params: Partial = {}, +): AssetOcrResponseDto & { isVisible: boolean } => ({ + id: 'ocr-id', + assetId: 'asset-id', + x1: 0.1, + y1: 0.1, + x2: 0.2, + y2: 0.1, + x3: 0.2, + y3: 0.2, + x4: 0.1, + y4: 0.2, + boxScore: 0.9, + textScore: 0.9, + text: 'Sample Text', + isVisible: true, + ...params, +}); + +describe('checkOcrVisibility', () => { + const assetDimensions = { width: 1000, height: 1000 }; + + it('should return only non-visible OCR entries when no crop is provided', () => { + const ocrs = [ + createOcr({ id: 'ocr-1', isVisible: true }), + createOcr({ id: 'ocr-2', isVisible: false }), + createOcr({ id: 'ocr-3', isVisible: false }), + ]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(0); + expect(result.visible.map((o) => o.id)).toEqual(['ocr-2', 'ocr-3']); + }); + + it('should return all OCR entries as visible when all are marked not visible and no crop provided', () => { + const ocrs = [createOcr({ id: 'ocr-1', isVisible: false }), createOcr({ id: 'ocr-2', isVisible: false })]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(0); + }); + + it('should return empty visible array when all OCR entries are already visible and no crop provided', () => { + const ocrs = [createOcr({ id: 'ocr-1', isVisible: true }), createOcr({ id: 'ocr-2', isVisible: true })]; + const result = checkOcrVisibility(ocrs, assetDimensions); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(0); + }); + + it('should return empty arrays when no OCR entries provided', () => { + const result = checkOcrVisibility([], assetDimensions); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark OCR as visible when fully inside crop area', () => { + // OCR box at normalized coords 0.1-0.2 = 100-200px in 1000x1000 image + const ocrs = [createOcr()]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark OCR as hidden when fully outside crop area', () => { + // OCR box at normalized coords 0.8-0.9 = 800-900px + const ocrs = [createOcr({ x1: 0.8, y1: 0.8, x2: 0.9, y2: 0.8, x3: 0.9, y3: 0.9, x4: 0.8, y4: 0.9 })]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(1); + }); + + it('should mark OCR as visible when at least 50% overlaps with crop', () => { + // OCR at 100-200px (0.1-0.2 normalized), crop starts at 150 + const ocrs = [createOcr()]; + const crop = { x1: 150, y1: 100, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should mark OCR as hidden when less than 50% overlaps with crop', () => { + // OCR at 100-200px, crop starts at 160 = 40% overlap + const ocrs = [createOcr()]; + const crop = { x1: 160, y1: 100, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(0); + expect(result.hidden).toHaveLength(1); + }); + + it('should correctly categorize multiple OCR entries', () => { + const ocrs = [ + createOcr({ id: 'ocr-inside', x1: 0.1, y1: 0.1, x2: 0.2, y2: 0.1, x3: 0.2, y3: 0.2, x4: 0.1, y4: 0.2 }), + createOcr({ id: 'ocr-outside', x1: 0.8, y1: 0.8, x2: 0.9, y2: 0.8, x3: 0.9, y3: 0.9, x4: 0.8, y4: 0.9 }), + ]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(1); + expect(result.visible[0].id).toBe('ocr-inside'); + expect(result.hidden[0].id).toBe('ocr-outside'); + }); + + it('should handle rotated/skewed OCR polygons by using bounding box', () => { + // Rotated rectangle - the function should compute the bounding box correctly + const ocrs = [ + createOcr({ + id: 'ocr-rotated', + x1: 0.15, + y1: 0.1, // top + x2: 0.2, + y2: 0.15, // right + x3: 0.15, + y3: 0.2, // bottom + x4: 0.1, + y4: 0.15, // left + }), + ]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should handle different asset dimensions', () => { + const smallDimensions = { width: 500, height: 500 }; + // OCR at 0.1-0.2 normalized = 50-100px in 500x500 image + const ocrs = [createOcr()]; + const crop = { x1: 0, y1: 0, x2: 200, y2: 200 }; + + const result = checkOcrVisibility(ocrs, smallDimensions, crop); + + expect(result.visible).toHaveLength(1); + expect(result.hidden).toHaveLength(0); + }); + + it('should categorize based on crop overlap when crop is provided, regardless of isVisible property', () => { + const ocrs = [ + createOcr({ id: 'ocr-inside-visible', isVisible: true }), // Inside crop, already visible + createOcr({ id: 'ocr-inside-not-visible', isVisible: false }), // Inside crop, not visible + createOcr({ + id: 'ocr-outside-visible', + x1: 0.8, + y1: 0.8, + x2: 0.9, + y2: 0.8, + x3: 0.9, + y3: 0.9, + x4: 0.8, + y4: 0.9, + isVisible: true, + }), // Outside crop, already visible + createOcr({ + id: 'ocr-outside-not-visible', + x1: 0.8, + y1: 0.8, + x2: 0.9, + y2: 0.8, + x3: 0.9, + y3: 0.9, + x4: 0.8, + y4: 0.9, + isVisible: false, + }), // Outside crop, not visible + ]; + const crop = { x1: 0, y1: 0, x2: 500, y2: 500 }; + + const result = checkOcrVisibility(ocrs, assetDimensions, crop); + + // When crop is provided, only overlap matters, not isVisible property + expect(result.visible).toHaveLength(2); + expect(result.hidden).toHaveLength(2); + expect(result.visible.map((o) => o.id)).toContain('ocr-inside-visible'); + expect(result.visible.map((o) => o.id)).toContain('ocr-inside-not-visible'); + expect(result.hidden.map((o) => o.id)).toContain('ocr-outside-visible'); + expect(result.hidden.map((o) => o.id)).toContain('ocr-outside-not-visible'); + }); + + it('should handle mixed visibility states with partial overlap and crop', () => { + const ocrs = [ + createOcr({ id: 'ocr-partial-50', isVisible: true }), // 50% overlap + createOcr({ id: 'ocr-partial-40', isVisible: false }), // 40% overlap + ]; + const crop1 = { x1: 150, y1: 100, x2: 500, y2: 500 }; // 50% overlap + const crop2 = { x1: 160, y1: 100, x2: 500, y2: 500 }; // 40% overlap + + const result1 = checkOcrVisibility([ocrs[0]], assetDimensions, crop1); + const result2 = checkOcrVisibility([ocrs[1]], assetDimensions, crop2); + + // 50% overlap should be visible + expect(result1.visible).toHaveLength(1); + expect(result1.hidden).toHaveLength(0); + + // 40% overlap should be hidden + expect(result2.visible).toHaveLength(0); + expect(result2.hidden).toHaveLength(1); + }); +}); diff --git a/server/src/utils/editor.ts b/server/src/utils/editor.ts new file mode 100644 index 0000000000..21678f2a82 --- /dev/null +++ b/server/src/utils/editor.ts @@ -0,0 +1,107 @@ +import { AssetFace } from 'src/database'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; +import { ImageDimensions } from 'src/types'; + +type BoundingBox = { + x1: number; + y1: number; + x2: number; + y2: number; +}; + +export const boundingBoxOverlap = (boxA: BoundingBox, boxB: BoundingBox) => { + const overlapX1 = Math.max(boxA.x1, boxB.x1); + const overlapY1 = Math.max(boxA.y1, boxB.y1); + const overlapX2 = Math.min(boxA.x2, boxB.x2); + const overlapY2 = Math.min(boxA.y2, boxB.y2); + + const overlapArea = Math.max(0, overlapX2 - overlapX1) * Math.max(0, overlapY2 - overlapY1); + const faceArea = (boxA.x2 - boxA.x1) * (boxA.y2 - boxA.y1); + return overlapArea / faceArea; +}; + +const scale = (box: BoundingBox, target: ImageDimensions, source?: ImageDimensions) => { + const { width: sourceWidth = 1, height: sourceHeight = 1 } = source ?? {}; + + return { + x1: (box.x1 / sourceWidth) * target.width, + y1: (box.y1 / sourceHeight) * target.height, + x2: (box.x2 / sourceWidth) * target.width, + y2: (box.y2 / sourceHeight) * target.height, + }; +}; + +export const checkFaceVisibility = ( + faces: AssetFace[], + originalAssetDimensions: ImageDimensions, + crop?: BoundingBox, +): { visible: AssetFace[]; hidden: AssetFace[] } => { + if (!crop) { + return { + visible: faces.filter((face) => !face.isVisible), + hidden: [], + }; + } + + const status = faces.map((face) => { + const scaledFace = scale( + { + x1: face.boundingBoxX1, + y1: face.boundingBoxY1, + x2: face.boundingBoxX2, + y2: face.boundingBoxY2, + }, + originalAssetDimensions, + { width: face.imageWidth, height: face.imageHeight }, + ); + + const overlapPercentage = boundingBoxOverlap(scaledFace, crop); + + return { + face, + isVisible: overlapPercentage >= 0.5, + }; + }); + + return { + visible: status.filter((s) => s.isVisible).map((s) => s.face), + hidden: status.filter((s) => !s.isVisible).map((s) => s.face), + }; +}; + +export const checkOcrVisibility = ( + ocrs: (AssetOcrResponseDto & { isVisible: boolean })[], + originalAssetDimensions: ImageDimensions, + crop?: BoundingBox, +): { visible: AssetOcrResponseDto[]; hidden: AssetOcrResponseDto[] } => { + if (!crop) { + return { + visible: ocrs.filter((ocr) => !ocr.isVisible), + hidden: [], + }; + } + + const status = ocrs.map((ocr) => { + const ocrBox = scale( + { + x1: Math.min(ocr.x1, ocr.x2, ocr.x3, ocr.x4), + y1: Math.min(ocr.y1, ocr.y2, ocr.y3, ocr.y4), + x2: Math.max(ocr.x1, ocr.x2, ocr.x3, ocr.x4), + y2: Math.max(ocr.y1, ocr.y2, ocr.y3, ocr.y4), + }, + originalAssetDimensions, + ); + + const overlapPercentage = boundingBoxOverlap(ocrBox, crop); + + return { + ocr, + isVisible: overlapPercentage >= 0.5, + }; + }); + + return { + visible: status.filter((s) => s.isVisible).map((s) => s.ocr), + hidden: status.filter((s) => !s.isVisible).map((s) => s.ocr), + }; +}; diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 29c7f6f772..04f1ce48d9 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -34,7 +34,8 @@ type SendFile = Parameters; type SendFileOptions = SendFile[1]; const cacheControlHeaders: Record = { - [CacheControl.PrivateWithCache]: 'private, max-age=86400, no-transform', + [CacheControl.PrivateWithCache]: + 'private, max-age=86400, no-transform, stale-while-revalidate=2592000, stale-if-error=2592000', [CacheControl.PrivateWithoutCache]: 'private, no-cache, no-transform', [CacheControl.None]: null, // falsy value to prevent adding Cache-Control header }; @@ -42,7 +43,7 @@ const cacheControlHeaders: Record = { export const sendFile = async ( res: Response, next: NextFunction, - handler: () => Promise, + handler: () => Promise | ImmichFileResponse, logger: LoggingRepository, ): Promise => { // promisified version of 'res.sendFile' for cleaner async handling diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index 22de2e4083..47abb0ab89 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -2,10 +2,14 @@ import { createAdapter } from '@socket.io/redis-adapter'; import Redis from 'ioredis'; import { SignJWT } from 'jose'; import { randomBytes } from 'node:crypto'; +import { join } from 'node:path'; import { Server as SocketIO } from 'socket.io'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; +import { StorageCore } from 'src/cores/storage.core'; +import { MaintenanceAuthDto, MaintenanceDetectInstallResponseDto } from 'src/dtos/maintenance.dto'; +import { StorageFolder } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { AppRestartEvent } from 'src/repositories/event.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; export function sendOneShotAppRestart(state: AppRestartEvent): void { const server = new SocketIO(); @@ -72,3 +76,37 @@ export async function signMaintenanceJwt(secret: string, data: MaintenanceAuthDt export function generateMaintenanceSecret(): string { return randomBytes(64).toString('hex'); } + +export async function detectPriorInstall( + storageRepository: StorageRepository, +): Promise { + return { + storage: await Promise.all( + Object.values(StorageFolder).map(async (folder) => { + const path = StorageCore.getBaseFolder(folder); + const files = await storageRepository.readdir(path); + const filename = join(StorageCore.getBaseFolder(folder), '.immich'); + + let readable = false, + writable = false; + + try { + await storageRepository.readFile(filename); + readable = true; + + await storageRepository.overwriteFile(filename, Buffer.from(`${Date.now()}`)); + writable = true; + } catch { + // no-op + } + + return { + folder, + readable, + writable, + files: files.filter((fn) => fn !== '.immich').length, + }; + }), + ), + }; +} diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index c09f3a381b..862ed310bc 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -76,6 +76,7 @@ describe('mimeTypes', () => { { mimetype: 'image/x-sony-sr2', extension: '.sr2' }, { mimetype: 'image/x-sony-srf', extension: '.srf' }, { mimetype: 'image/x3f', extension: '.x3f' }, + { mimetype: 'application/mxf', extension: '.mxf' }, { mimetype: 'video/3gpp', extension: '.3gp' }, { mimetype: 'video/3gpp', extension: '.3gpp' }, { mimetype: 'video/avi', extension: '.avi' }, @@ -152,6 +153,33 @@ describe('mimeTypes', () => { } }); + describe('canBeTransparent', () => { + for (const img of [ + 'a.avif', + 'a.bmp', + 'a.gif', + 'a.heic', + 'a.heif', + 'a.hif', + 'a.jxl', + 'a.png', + 'a.svg', + 'a.tif', + 'a.tiff', + 'a.webp', + ]) { + it(`should return true for ${img}`, () => { + expect(mimeTypes.canBeTransparent(img)).toBe(true); + }); + } + + for (const img of ['a.jpg', 'a.jpeg', 'a.jpe', 'a.insp', 'a.jp2', 'a.cr3', 'a.dng', 'a.nef', 'a.arw']) { + it(`should return false for ${img}`, () => { + expect(mimeTypes.canBeTransparent(img)).toBe(false); + }); + } + }); + describe('animated image', () => { for (const img of ['a.avif', 'a.gif', 'a.webp']) { it('should identify animated image mime types as such', () => { @@ -188,7 +216,9 @@ describe('mimeTypes', () => { it('should contain only video mime types', () => { const values = Object.values(mimeTypes.video).flat(); - expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('video/'))); + expect(values).toEqual( + values.filter((mimeType) => mimeType.startsWith('video/') || mimeType === 'application/mxf'), + ); }); for (const [extension, v] of Object.entries(mimeTypes.video)) { diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index d15c1f078c..43421e7937 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,7 +1,7 @@ import { extname } from 'node:path'; import { AssetType } from 'src/enum'; -const raw: Record = { +const raw = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], '.ari': ['image/ari', 'image/x-arriflex-ari'], '.arw': ['image/arw', 'image/x-sony-arw'], @@ -41,6 +41,7 @@ const raw: Record = { **/ const webSupportedImage = { '.avif': ['image/avif'], + '.bmp': ['image/bmp'], '.gif': ['image/gif'], '.jpeg': ['image/jpeg'], '.jpg': ['image/jpeg'], @@ -48,10 +49,8 @@ const webSupportedImage = { '.webp': ['image/webp'], }; -const image: Record = { +const webUnsupportedImage = { ...raw, - ...webSupportedImage, - '.bmp': ['image/bmp'], '.heic': ['image/heic'], '.heif': ['image/heif'], '.hif': ['image/hif'], @@ -64,6 +63,11 @@ const image: Record = { '.tiff': ['image/tiff'], }; +const image: Record = { + ...webSupportedImage, + ...webUnsupportedImage, +}; + const possiblyAnimatedImageExtensions = new Set(['.avif', '.gif', '.heic', '.heif', '.jxl', '.png', '.webp']); const possiblyAnimatedImage: Record = Object.fromEntries( Object.entries(image).filter(([key]) => possiblyAnimatedImageExtensions.has(key)), @@ -73,6 +77,21 @@ const extensionOverrides: Record = { 'image/jpeg': '.jpg', }; +const transparentCapableExtensions = new Set([ + '.avif', + '.bmp', + '.gif', + '.heic', + '.heif', + '.hif', + '.jxl', + '.png', + '.svg', + '.tif', + '.tiff', + '.webp', +]); + const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']); const profile: Record = Object.fromEntries( Object.entries(image).filter(([key]) => profileExtensions.has(key)), @@ -94,6 +113,7 @@ const video: Record = { '.mpeg': ['video/mpeg'], '.mpg': ['video/mpeg'], '.mts': ['video/mp2t'], + '.mxf': ['application/mxf'], '.vob': ['video/mpeg'], '.webm': ['video/webm'], '.wmv': ['video/x-ms-wmv'], @@ -120,6 +140,7 @@ export const mimeTypes = { sidecar, video, raw, + webUnsupportedImage, isAsset: (filename: string) => isType(filename, image) || isType(filename, video), isImage: (filename: string) => isType(filename, image), @@ -128,6 +149,7 @@ export const mimeTypes = { isProfile: (filename: string) => isType(filename, profile), isSidecar: (filename: string) => isType(filename, sidecar), isVideo: (filename: string) => isType(filename, video), + canBeTransparent: (filename: string) => transparentCapableExtensions.has(extname(filename).toLowerCase()), isRaw: (filename: string) => isType(filename, raw), lookup, /** return an extension (including a leading `.`) for a mime-type */ @@ -136,9 +158,12 @@ export const mimeTypes = { const contentType = lookup(filename); if (contentType.startsWith('image/')) { return AssetType.Image; - } else if (contentType.startsWith('video/')) { + } + + if (contentType.startsWith('video/') || contentType === 'application/mxf') { return AssetType.Video; } + return AssetType.Other; }, getSupportedFileExtensions: () => [...Object.keys(image), ...Object.keys(video)], diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 08f1401d50..7d2e99a215 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -261,6 +261,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) const options: SwaggerDocumentOptions = { operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, extraModels: extraSyncModels, + ignoreGlobalPrefix: true, }; const specification = SwaggerModule.createDocument(app, config, options); diff --git a/server/src/utils/tasks.ts b/server/src/utils/tasks.ts new file mode 100644 index 0000000000..4a8276fc46 --- /dev/null +++ b/server/src/utils/tasks.ts @@ -0,0 +1,13 @@ +export type Task = () => Promise | unknown; + +export class Tasks { + private tasks: Task[] = []; + + push(...tasks: Task[]) { + this.tasks.push(...tasks); + } + + async all() { + await Promise.all(this.tasks.map((item) => item())); + } +} diff --git a/server/src/utils/transform.spec.ts b/server/src/utils/transform.spec.ts new file mode 100644 index 0000000000..5efeac02a6 --- /dev/null +++ b/server/src/utils/transform.spec.ts @@ -0,0 +1,293 @@ +import { AssetEditAction, AssetEditActionItem, MirrorAxis } from 'src/dtos/editing.dto'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; +import { transformFaceBoundingBox, transformOcrBoundingBox } from 'src/utils/transform'; +import { describe, expect, it } from 'vitest'; + +describe('transformFaceBoundingBox', () => { + const baseFace = { + boundingBoxX1: 100, + boundingBoxY1: 100, + boundingBoxX2: 200, + boundingBoxY2: 200, + imageWidth: 1000, + imageHeight: 800, + }; + + const baseDimensions = { width: 1000, height: 800 }; + + describe('with no edits', () => { + it('should return unchanged bounding box', () => { + const result = transformFaceBoundingBox(baseFace, [], baseDimensions); + expect(result).toEqual(baseFace); + }); + }); + + describe('with crop edit', () => { + it('should adjust bounding box for crop offset', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 400, height: 300 } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.boundingBoxX1).toBe(50); + expect(result.boundingBoxY1).toBe(50); + expect(result.boundingBoxX2).toBe(150); + expect(result.boundingBoxY2).toBe(150); + expect(result.imageWidth).toBe(400); + expect(result.imageHeight).toBe(300); + }); + + it('should handle face partially outside crop area', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 150, y: 150, width: 400, height: 300 } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.boundingBoxX1).toBe(-50); + expect(result.boundingBoxY1).toBe(-50); + expect(result.boundingBoxX2).toBe(50); + expect(result.boundingBoxY2).toBe(50); + }); + }); + + describe('with rotate edit', () => { + it('should rotate 90 degrees clockwise', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 90 } }]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.imageWidth).toBe(800); + expect(result.imageHeight).toBe(1000); + + expect(result.boundingBoxX1).toBe(600); + expect(result.boundingBoxY1).toBe(100); + expect(result.boundingBoxX2).toBe(700); + expect(result.boundingBoxY2).toBe(200); + }); + + it('should rotate 180 degrees', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 180 } }]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.imageWidth).toBe(1000); + expect(result.imageHeight).toBe(800); + + expect(result.boundingBoxX1).toBe(800); + expect(result.boundingBoxY1).toBe(600); + expect(result.boundingBoxX2).toBe(900); + expect(result.boundingBoxY2).toBe(700); + }); + + it('should rotate 270 degrees', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 270 } }]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.imageWidth).toBe(800); + expect(result.imageHeight).toBe(1000); + }); + }); + + describe('with mirror edit', () => { + it('should mirror horizontally', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.boundingBoxX1).toBe(800); + expect(result.boundingBoxY1).toBe(100); + expect(result.boundingBoxX2).toBe(900); + expect(result.boundingBoxY2).toBe(200); + expect(result.imageWidth).toBe(1000); + expect(result.imageHeight).toBe(800); + }); + + it('should mirror vertically', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.boundingBoxX1).toBe(100); + expect(result.boundingBoxY1).toBe(600); + expect(result.boundingBoxX2).toBe(200); + expect(result.boundingBoxY2).toBe(700); + expect(result.imageWidth).toBe(1000); + expect(result.imageHeight).toBe(800); + }); + }); + + describe('with combined edits', () => { + it('should apply crop then rotate', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 400, height: 300 } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.imageWidth).toBe(300); + expect(result.imageHeight).toBe(400); + }); + + it('should apply crop then mirror', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 400 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, baseDimensions); + + expect(result.boundingBoxX1).toBe(100); + expect(result.boundingBoxX2).toBe(200); + expect(result.boundingBoxY1).toBe(200); + expect(result.boundingBoxY2).toBe(300); + }); + }); + + describe('with scaled dimensions', () => { + it('should scale face to match different image dimensions', () => { + const scaledDimensions = { width: 500, height: 400 }; // Half the original size + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 50, y: 50, width: 200, height: 150 } }, + ]; + const result = transformFaceBoundingBox(baseFace, edits, scaledDimensions); + + expect(result.boundingBoxX1).toBe(0); + expect(result.boundingBoxY1).toBe(0); + expect(result.boundingBoxX2).toBe(50); + expect(result.boundingBoxY2).toBe(50); + }); + }); +}); + +describe('transformOcrBoundingBox', () => { + const baseOcr: AssetOcrResponseDto = { + id: 'ocr-1', + assetId: 'asset-1', + x1: 0.1, + y1: 0.1, + x2: 0.2, + y2: 0.1, + x3: 0.2, + y3: 0.2, + x4: 0.1, + y4: 0.2, + boxScore: 0.9, + textScore: 0.85, + text: 'Test OCR', + }; + + const baseDimensions = { width: 1000, height: 800 }; + + describe('with no edits', () => { + it('should return unchanged bounding box', () => { + const result = transformOcrBoundingBox(baseOcr, [], baseDimensions); + expect(result).toEqual(baseOcr); + }); + }); + + describe('with crop edit', () => { + it('should adjust normalized coordinates for crop', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 100, y: 80, width: 400, height: 320 } }, + ]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + // Original OCR: (0.1,0.1)-(0.2,0.2) on 1000x800 = (100,80)-(200,160) + // After crop offset (100,80): (0,0)-(100,80) + // Normalized to 400x320: (0,0)-(0.25,0.25) + expect(result.x1).toBeCloseTo(0, 5); + expect(result.y1).toBeCloseTo(0, 5); + expect(result.x2).toBeCloseTo(0.25, 5); + expect(result.y2).toBeCloseTo(0, 5); + expect(result.x3).toBeCloseTo(0.25, 5); + expect(result.y3).toBeCloseTo(0.25, 5); + expect(result.x4).toBeCloseTo(0, 5); + expect(result.y4).toBeCloseTo(0.25, 5); + }); + }); + + describe('with rotate edit', () => { + it('should rotate normalized coordinates 90 degrees and reorder points', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 90 } }]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.id).toBe(baseOcr.id); + expect(result.text).toBe(baseOcr.text); + expect(result.x1).toBeCloseTo(0.8, 5); + expect(result.y1).toBeCloseTo(0.1, 5); + expect(result.x2).toBeCloseTo(0.9, 5); + expect(result.y2).toBeCloseTo(0.1, 5); + expect(result.x3).toBeCloseTo(0.9, 5); + expect(result.y3).toBeCloseTo(0.2, 5); + expect(result.x4).toBeCloseTo(0.8, 5); + expect(result.y4).toBeCloseTo(0.2, 5); + }); + + it('should rotate 180 degrees and reorder points', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 180 } }]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.x1).toBeCloseTo(0.8, 5); + expect(result.y1).toBeCloseTo(0.8, 5); + expect(result.x2).toBeCloseTo(0.9, 5); + expect(result.y2).toBeCloseTo(0.8, 5); + expect(result.x3).toBeCloseTo(0.9, 5); + expect(result.y3).toBeCloseTo(0.9, 5); + expect(result.x4).toBeCloseTo(0.8, 5); + expect(result.y4).toBeCloseTo(0.9, 5); + }); + + it('should rotate 270 degrees and reorder points', () => { + const edits: AssetEditActionItem[] = [{ action: AssetEditAction.Rotate, parameters: { angle: 270 } }]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.id).toBe(baseOcr.id); + expect(result.text).toBe(baseOcr.text); + expect(result.x1).toBeCloseTo(0.1, 5); + expect(result.y1).toBeCloseTo(0.8, 5); + expect(result.x2).toBeCloseTo(0.2, 5); + expect(result.y2).toBeCloseTo(0.8, 5); + expect(result.x3).toBeCloseTo(0.2, 5); + expect(result.y3).toBeCloseTo(0.9, 5); + expect(result.x4).toBeCloseTo(0.1, 5); + expect(result.y4).toBeCloseTo(0.9, 5); + }); + }); + + describe('with mirror edit', () => { + it('should mirror horizontally', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + ]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.x1).toBeCloseTo(0.9, 5); + expect(result.y1).toBeCloseTo(0.1, 5); + }); + + it('should mirror vertically', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }, + ]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.x1).toBeCloseTo(0.1, 5); + expect(result.y1).toBeCloseTo(0.9, 5); + }); + }); + + describe('with combined edits', () => { + it('should preserve OCR metadata through transforms', () => { + const edits: AssetEditActionItem[] = [ + { action: AssetEditAction.Crop, parameters: { x: 0, y: 0, width: 500, height: 400 } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]; + const result = transformOcrBoundingBox(baseOcr, edits, baseDimensions); + + expect(result.id).toBe(baseOcr.id); + expect(result.assetId).toBe(baseOcr.assetId); + expect(result.boxScore).toBe(baseOcr.boxScore); + expect(result.textScore).toBe(baseOcr.textScore); + expect(result.text).toBe(baseOcr.text); + }); + }); +}); diff --git a/server/src/utils/transform.ts b/server/src/utils/transform.ts new file mode 100644 index 0000000000..261595eb66 --- /dev/null +++ b/server/src/utils/transform.ts @@ -0,0 +1,243 @@ +import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; +import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; +import { ImageDimensions } from 'src/types'; +import { applyToPoint, compose, flipX, flipY, identity, Matrix, rotate, scale, translate } from 'transformation-matrix'; + +export const getOutputDimensions = ( + edits: AssetEditActionItem[], + startingDimensions: ImageDimensions, +): ImageDimensions => { + let { width, height } = startingDimensions; + + const crop = edits.find((edit) => edit.action === AssetEditAction.Crop); + if (crop) { + width = crop.parameters.width; + height = crop.parameters.height; + } + + for (const edit of edits) { + if (edit.action === AssetEditAction.Rotate) { + const angleDegrees = edit.parameters.angle; + if (angleDegrees === 90 || angleDegrees === 270) { + [width, height] = [height, width]; + } + } + } + + return { width, height }; +}; + +export const createAffineMatrix = ( + edits: AssetEditActionItem[], + scalingParameters?: { + pointSpace: ImageDimensions; + targetSpace: ImageDimensions; + }, +): Matrix => { + let scalingMatrix: Matrix = identity(); + + if (scalingParameters) { + const { pointSpace, targetSpace } = scalingParameters; + const scaleX = targetSpace.width / pointSpace.width; + scalingMatrix = scale(scaleX); + } + + return compose( + scalingMatrix, + ...edits.map((edit) => { + switch (edit.action) { + case 'rotate': { + const angleInRadians = (-edit.parameters.angle * Math.PI) / 180; + return rotate(angleInRadians); + } + case 'mirror': { + return edit.parameters.axis === 'horizontal' ? flipY() : flipX(); + } + default: { + return identity(); + } + } + }), + ); +}; + +export type Point = { x: number; y: number }; + +type TransformState = { + points: Point[]; + currentWidth: number; + currentHeight: number; +}; + +/** + * Transforms an array of points through a series of edit operations (crop, rotate, mirror). + * Points should be in absolute pixel coordinates relative to the starting dimensions. + */ +export const transformPoints = ( + points: Point[], + edits: AssetEditActionItem[], + startingDimensions: ImageDimensions, + { inverse = false } = {}, +): TransformState => { + let currentWidth = startingDimensions.width; + let currentHeight = startingDimensions.height; + let transformedPoints = [...points]; + + // Handle crop first if not inverting + if (!inverse) { + const crop = edits.find((edit) => edit.action === 'crop'); + if (crop) { + const { x: cropX, y: cropY, width: cropWidth, height: cropHeight } = crop.parameters; + transformedPoints = transformedPoints.map((p) => ({ + x: p.x - cropX, + y: p.y - cropY, + })); + currentWidth = cropWidth; + currentHeight = cropHeight; + } + } + + // Apply rotate and mirror transforms + const editSequence = inverse ? edits.toReversed() : edits; + for (const edit of editSequence) { + let matrix: Matrix = identity(); + if (edit.action === 'rotate') { + const angleDegrees = edit.parameters.angle; + const angleRadians = (angleDegrees * Math.PI) / 180; + const newWidth = angleDegrees === 90 || angleDegrees === 270 ? currentHeight : currentWidth; + const newHeight = angleDegrees === 90 || angleDegrees === 270 ? currentWidth : currentHeight; + + matrix = compose( + translate(newWidth / 2, newHeight / 2), + rotate(inverse ? -angleRadians : angleRadians), + translate(-currentWidth / 2, -currentHeight / 2), + ); + + currentWidth = newWidth; + currentHeight = newHeight; + } else if (edit.action === 'mirror') { + matrix = compose( + translate(currentWidth / 2, currentHeight / 2), + edit.parameters.axis === 'horizontal' ? flipY() : flipX(), + translate(-currentWidth / 2, -currentHeight / 2), + ); + } else { + // Skip non-affine transformations + continue; + } + + transformedPoints = transformedPoints.map((p) => applyToPoint(matrix, p)); + } + + // Handle crop last if inverting + if (inverse) { + const crop = edits.find((edit) => edit.action === 'crop'); + if (crop) { + const { x: cropX, y: cropY } = crop.parameters; + transformedPoints = transformedPoints.map((p) => ({ + x: p.x + cropX, + y: p.y + cropY, + })); + } + } + + return { + points: transformedPoints, + currentWidth, + currentHeight, + }; +}; + +type FaceBoundingBox = { + boundingBoxX1: number; + boundingBoxX2: number; + boundingBoxY1: number; + boundingBoxY2: number; + imageWidth: number; + imageHeight: number; +}; + +export const transformFaceBoundingBox = ( + box: FaceBoundingBox, + edits: AssetEditActionItem[], + imageDimensions: ImageDimensions, +): FaceBoundingBox => { + if (edits.length === 0) { + return box; + } + + const scaleX = imageDimensions.width / box.imageWidth; + const scaleY = imageDimensions.height / box.imageHeight; + + const points: Point[] = [ + { x: box.boundingBoxX1 * scaleX, y: box.boundingBoxY1 * scaleY }, + { x: box.boundingBoxX2 * scaleX, y: box.boundingBoxY2 * scaleY }, + ]; + + const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); + + // Ensure x1,y1 is top-left and x2,y2 is bottom-right + const [p1, p2] = transformedPoints; + return { + boundingBoxX1: Math.min(p1.x, p2.x), + boundingBoxY1: Math.min(p1.y, p2.y), + boundingBoxX2: Math.max(p1.x, p2.x), + boundingBoxY2: Math.max(p1.y, p2.y), + imageWidth: currentWidth, + imageHeight: currentHeight, + }; +}; + +const reorderQuadPointsForRotation = (points: Point[], rotationDegrees: number): Point[] => { + const [p1, p2, p3, p4] = points; + switch (rotationDegrees) { + case 90: { + return [p4, p1, p2, p3]; + } + case 180: { + return [p3, p4, p1, p2]; + } + case 270: { + return [p2, p3, p4, p1]; + } + default: { + return points; + } + } +}; + +export const transformOcrBoundingBox = ( + box: AssetOcrResponseDto, + edits: AssetEditActionItem[], + imageDimensions: ImageDimensions, +): AssetOcrResponseDto => { + if (edits.length === 0) { + return box; + } + + const points: Point[] = [ + { x: box.x1 * imageDimensions.width, y: box.y1 * imageDimensions.height }, + { x: box.x2 * imageDimensions.width, y: box.y2 * imageDimensions.height }, + { x: box.x3 * imageDimensions.width, y: box.y3 * imageDimensions.height }, + { x: box.x4 * imageDimensions.width, y: box.y4 * imageDimensions.height }, + ]; + + const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); + + // Reorder points to maintain semantic ordering (topLeft, topRight, bottomRight, bottomLeft) + const netRotation = edits.find((e) => e.action == AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; + const reorderedPoints = reorderQuadPointsForRotation(transformedPoints, netRotation); + + const [p1, p2, p3, p4] = reorderedPoints; + return { + ...box, + x1: p1.x / currentWidth, + y1: p1.y / currentHeight, + x2: p2.x / currentWidth, + y2: p2.y / currentHeight, + x3: p3.x / currentWidth, + y3: p3.y / currentHeight, + x4: p4.x / currentWidth, + y4: p4.y / currentHeight, + }; +}; diff --git a/server/src/validation.ts b/server/src/validation.ts index 6d4bbfbe36..b959de94b1 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -33,6 +33,7 @@ import { import { CronJob } from 'cron'; import { DateTime } from 'luxon'; import sanitize from 'sanitize-filename'; +import { Property, PropertyOptions } from 'src/decorators'; import { isIP, isIPRange } from 'validator'; @Injectable() @@ -66,7 +67,7 @@ export class FileNotEmptyValidator extends FileValidator { } type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; -export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => { +export const ValidateUUID = (options?: UUIDOptions & PropertyOptions) => { const { optional, each, nullable, ...apiPropertyOptions } = { optional: false, each: false, @@ -75,12 +76,55 @@ export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => { }; return applyDecorators( IsUUID('4', { each }), - ApiProperty({ format: 'uuid', ...apiPropertyOptions }), + Property({ format: 'uuid', ...apiPropertyOptions }), optional ? Optional({ nullable }) : IsNotEmpty(), each ? IsArray() : IsString(), ); }; +export function IsAxisAlignedRotation() { + return ValidateBy( + { + name: 'isAxisAlignedRotation', + validator: { + validate(value: any) { + return [0, 90, 180, 270].includes(value); + }, + defaultMessage: buildMessage( + (eachPrefix) => eachPrefix + '$property must be one of the following values: 0, 90, 180, 270', + {}, + ), + }, + }, + {}, + ); +} + +@ValidatorConstraint({ name: 'uniqueEditActions' }) +class UniqueEditActionsValidator implements ValidatorConstraintInterface { + validate(edits: { action: string; parameters?: unknown }[]): boolean { + if (!Array.isArray(edits)) { + return true; + } + + const actionSet = new Set(); + for (const edit of edits) { + const key = edit.action === 'mirror' ? `${edit.action}-${JSON.stringify(edit.parameters)}` : edit.action; + if (actionSet.has(key)) { + return false; + } + actionSet.add(key); + } + return true; + } + + defaultMessage(): string { + return 'Duplicate edit actions are not allowed'; + } +} + +export const IsUniqueEditActions = () => Validate(UniqueEditActionsValidator); + export class UUIDParamDto { @IsNotEmpty() @IsUUID('4') @@ -96,6 +140,16 @@ export class UUIDAssetIDParamDto { assetId!: string; } +export class FilenameParamDto { + @IsNotEmpty() + @IsString() + @ApiProperty({ format: 'string' }) + @Matches(/^[a-zA-Z0-9_\-.]+$/, { + message: 'Filename contains invalid characters', + }) + filename!: string; +} + type PinCodeOptions = { optional?: boolean } & OptionalOptions; export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => { const { optional, nullable, emptyToNull, ...apiPropertyOptions } = { @@ -178,19 +232,20 @@ export const ValidateHexColor = () => { return applyDecorators(...decorators); }; -type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { - const { optional, nullable, format, ...apiPropertyOptions } = { - optional: false, - nullable: false, - format: 'date-time', - ...options, - }; +type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; +export const ValidateDate = (options?: DateOptions & PropertyOptions) => { + const { + optional, + nullable = false, + emptyToNull = false, + format = 'date-time', + ...apiPropertyOptions + } = options || {}; - const decorators = [ - ApiProperty({ format, ...apiPropertyOptions }), + return applyDecorators( + Property({ format, ...apiPropertyOptions }), IsDate(), - optional ? Optional({ nullable: true }) : IsNotEmpty(), + optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), Transform(({ key, value }) => { if (value === null || value === undefined) { return value; @@ -202,19 +257,17 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => { return new Date(value as string); }), - ]; - - if (optional) { - decorators.push(Optional({ nullable })); - } - - return applyDecorators(...decorators); + ); }; -type StringOptions = { optional?: boolean; nullable?: boolean; trim?: boolean }; +type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean }; export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => { - const { optional, nullable, trim, ...apiPropertyOptions } = options || {}; - const decorators = [ApiProperty(apiPropertyOptions), IsString(), optional ? Optional({ nullable }) : IsNotEmpty()]; + const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {}; + const decorators = [ + ApiProperty(apiPropertyOptions), + IsString(), + optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), + ]; if (trim) { decorators.push(Transform(({ value }: { value: string }) => value?.trim())); @@ -223,11 +276,11 @@ export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => return applyDecorators(...decorators); }; -type BooleanOptions = { optional?: boolean; nullable?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => { - const { optional, nullable, ...apiPropertyOptions } = options || {}; +type BooleanOptions = OptionalOptions & { optional?: boolean }; +export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => { + const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {}; const decorators = [ - ApiProperty(apiPropertyOptions), + Property(apiPropertyOptions), IsBoolean(), Transform(({ value }) => { if (value == 'true') { @@ -237,7 +290,7 @@ export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) = } return value; }), - optional ? Optional({ nullable }) : IsNotEmpty(), + optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), ]; return applyDecorators(...decorators); @@ -374,3 +427,25 @@ export function IsIPRange(options: IsIPRangeOptions, validationOptions?: Validat validationOptions, ); } + +@ValidatorConstraint({ name: 'isGreaterThanOrEqualTo' }) +export class IsGreaterThanOrEqualToConstraint implements ValidatorConstraintInterface { + validate(value: unknown, args: ValidationArguments) { + const relatedPropertyName = args.constraints?.[0] as string; + const relatedValue = (args.object as Record)[relatedPropertyName]; + if (!Number.isFinite(value) || !Number.isFinite(relatedValue)) { + return true; + } + + return Number(value) >= Number(relatedValue); + } + + defaultMessage(args: ValidationArguments) { + const relatedPropertyName = args.constraints?.[0] as string; + return `${args.property} must be greater than or equal to ${relatedPropertyName}`; + } +} + +export const IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => { + return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions); +}; diff --git a/server/src/workers/maintenance.ts b/server/src/workers/maintenance.ts index fcfe990121..035ec600af 100644 --- a/server/src/workers/maintenance.ts +++ b/server/src/workers/maintenance.ts @@ -12,12 +12,11 @@ async function bootstrap() { const app = await NestFactory.create(MaintenanceModule, { bufferLogs: true }); app.get(AppRepository).setCloseFn(() => app.close()); + void configureExpress(app, { permitSwaggerWrite: false, ssr: MaintenanceWorkerService, }); - - void app.get(MaintenanceWorkerService).logSecret(); } bootstrap().catch((error) => { diff --git a/server/test/factories/album-user.factory.ts b/server/test/factories/album-user.factory.ts new file mode 100644 index 0000000000..6e2f8cb832 --- /dev/null +++ b/server/test/factories/album-user.factory.ts @@ -0,0 +1,54 @@ +import { Selectable } from 'kysely'; +import { AlbumUserRole } from 'src/enum'; +import { AlbumUserTable } from 'src/schema/tables/album-user.table'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumUserLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AlbumUserFactory { + #user!: UserFactory; + + private constructor(private readonly value: Selectable) { + value.userId ??= newUuid(); + this.#user = UserFactory.from({ id: value.userId }); + } + + static create(dto: AlbumUserLike = {}) { + return AlbumUserFactory.from(dto).build(); + } + + static from(dto: AlbumUserLike = {}) { + return new AlbumUserFactory({ + albumId: newUuid(), + userId: newUuid(), + role: AlbumUserRole.Editor, + createId: newUuidV7(), + createdAt: newDate(), + updateId: newUuidV7(), + updatedAt: newDate(), + ...dto, + }); + } + + album(dto: AlbumUserLike = {}, builder?: FactoryBuilder) { + const album = build(AlbumFactory.from(dto), builder); + this.value.albumId = album.build().id; + return this; + } + + user(dto: UserLike = {}, builder?: FactoryBuilder) { + const user = build(UserFactory.from(dto), builder); + this.value.userId = user.build().id; + this.#user = user; + return this; + } + + build() { + return { + ...this.value, + user: this.#user.build(), + }; + } +} diff --git a/server/test/factories/album.factory.ts b/server/test/factories/album.factory.ts new file mode 100644 index 0000000000..f401cd343d --- /dev/null +++ b/server/test/factories/album.factory.ts @@ -0,0 +1,87 @@ +import { Selectable } from 'kysely'; +import { AssetOrder } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { AlbumUserFactory } from 'test/factories/album-user.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumLike, AlbumUserLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AlbumFactory { + #owner: UserFactory; + #sharedLinks: Selectable[] = []; + #albumUsers: AlbumUserFactory[] = []; + #assets: AssetFactory[] = []; + + private constructor(private readonly value: Selectable) { + value.ownerId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.ownerId }); + } + + static create(dto: AlbumLike = {}) { + return AlbumFactory.from(dto).build(); + } + + static from(dto: AlbumLike = {}) { + return new AlbumFactory({ + id: newUuid(), + ownerId: newUuid(), + albumName: 'My Album', + albumThumbnailAssetId: null, + createdAt: newDate(), + deletedAt: null, + description: 'Album description', + isActivityEnabled: false, + order: AssetOrder.Desc, + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }).owner(); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder) { + this.#owner = build(UserFactory.from(dto), builder); + this.value.ownerId = this.#owner.build().id; + return this; + } + + sharedLinks() { + this.#sharedLinks = []; + return this; + } + + albumUser(dto: AlbumUserLike = {}, builder?: FactoryBuilder) { + const albumUser = build(AlbumUserFactory.from(dto).album(this.value), builder); + this.#albumUsers.push(albumUser); + return this; + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + + // use album owner by default + if (!dto.ownerId) { + asset.owner(this.#owner.build()); + } + + if (!this.#assets) { + this.#assets = []; + } + + this.#assets.push(asset); + + return this; + } + + build() { + return { + ...this.value, + owner: this.#owner.build(), + assets: this.#assets.map((asset) => asset.build()), + albumUsers: this.#albumUsers.map((albumUser) => albumUser.build()), + sharedLinks: this.#sharedLinks, + }; + } +} diff --git a/server/test/factories/asset-edit.factory.ts b/server/test/factories/asset-edit.factory.ts new file mode 100644 index 0000000000..897ed26d61 --- /dev/null +++ b/server/test/factories/asset-edit.factory.ts @@ -0,0 +1,41 @@ +import { Selectable } from 'kysely'; +import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.dto'; +import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AssetEditLike, AssetLike, FactoryBuilder } from 'test/factories/types'; +import { newDate, newUuid } from 'test/small.factory'; + +export class AssetEditFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetEditLike = {}) { + return AssetEditFactory.from(dto).build(); + } + + static from(dto: AssetEditLike = {}) { + const id = dto.id ?? newUuid(); + const updateId = dto.updateId ?? newUuid(); + + return new AssetEditFactory({ + id, + assetId: newUuid(), + action: AssetEditAction.Crop, + parameters: { x: 5, y: 6, width: 200, height: 100 }, + sequence: 1, + updateId, + updatedAt: newDate(), + ...dto, + }); + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + this.value.assetId = asset.build().id; + return this; + } + + build() { + return { ...this.value } as Omit, 'action' | 'parameters'> & AssetEditActionItem; + } +} diff --git a/server/test/factories/asset-exif.factory.ts b/server/test/factories/asset-exif.factory.ts new file mode 100644 index 0000000000..da4d689ebf --- /dev/null +++ b/server/test/factories/asset-exif.factory.ts @@ -0,0 +1,55 @@ +import { Selectable } from 'kysely'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetExifLike } from 'test/factories/types'; +import { factory } from 'test/small.factory'; + +export class AssetExifFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetExifLike = {}) { + return AssetExifFactory.from(dto).build(); + } + + static from(dto: AssetExifLike = {}) { + return new AssetExifFactory({ + updatedAt: factory.date(), + updateId: factory.uuid(), + assetId: factory.uuid(), + autoStackId: null, + bitsPerSample: null, + city: 'Austin', + colorspace: null, + country: 'United States of America', + dateTimeOriginal: factory.date(), + description: '', + exifImageHeight: 420, + exifImageWidth: 42, + exposureTime: null, + fileSizeInByte: 69, + fNumber: 1.7, + focalLength: 4.38, + fps: null, + iso: 947, + latitude: 30.267_334_570_570_195, + longitude: -97.789_833_534_282_07, + lensModel: null, + livePhotoCID: null, + make: 'Google', + model: 'Pixel 7', + modifyDate: factory.date(), + orientation: '1', + profileDescription: null, + projectionType: null, + rating: 4, + lockedProperties: [], + state: 'Texas', + tags: ['parent/child'], + timeZone: 'UTC-6', + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/asset-face.factory.ts b/server/test/factories/asset-face.factory.ts new file mode 100644 index 0000000000..b2286cad54 --- /dev/null +++ b/server/test/factories/asset-face.factory.ts @@ -0,0 +1,47 @@ +import { Selectable } from 'kysely'; +import { SourceType } from 'src/enum'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { build } from 'test/factories/builder.factory'; +import { PersonFactory } from 'test/factories/person.factory'; +import { AssetFaceLike, FactoryBuilder, PersonLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AssetFaceFactory { + #person: PersonFactory | null = null; + + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetFaceLike = {}) { + return AssetFaceFactory.from(dto).build(); + } + + static from(dto: AssetFaceLike = {}) { + return new AssetFaceFactory({ + assetId: newUuid(), + boundingBoxX1: 100, + boundingBoxX2: 200, + boundingBoxY1: 100, + boundingBoxY2: 200, + deletedAt: null, + id: newUuid(), + imageHeight: 500, + imageWidth: 400, + isVisible: true, + personId: null, + sourceType: SourceType.MachineLearning, + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + person(dto: PersonLike = {}, builder?: FactoryBuilder) { + this.#person = build(PersonFactory.from(dto), builder); + this.value.personId = this.#person.build().id; + return this; + } + + build() { + return { ...this.value, person: this.#person?.build() ?? null }; + } +} diff --git a/server/test/factories/asset-file.factory.ts b/server/test/factories/asset-file.factory.ts new file mode 100644 index 0000000000..511ab45bb7 --- /dev/null +++ b/server/test/factories/asset-file.factory.ts @@ -0,0 +1,44 @@ +import { Selectable } from 'kysely'; +import { AssetFileType } from 'src/enum'; +import { AssetFileTable } from 'src/schema/tables/asset-file.table'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AssetFileLike, AssetLike, FactoryBuilder } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AssetFileFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetFileLike = {}) { + return AssetFileFactory.from(dto).build(); + } + + static from(dto: AssetFileLike = {}) { + const id = dto.id ?? newUuid(); + const isEdited = dto.isEdited ?? false; + + return new AssetFileFactory({ + id, + assetId: newUuid(), + createdAt: newDate(), + updatedAt: newDate(), + type: AssetFileType.Thumbnail, + path: `/data/12/34/thumbs/${id.slice(0, 2)}/${id.slice(2, 4)}/${id}${isEdited ? '_edited' : ''}.jpg`, + updateId: newUuidV7(), + isProgressive: false, + isTransparent: false, + isEdited, + ...dto, + }); + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + this.value.assetId = asset.build().id; + return this; + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts new file mode 100644 index 0000000000..4d54ba820b --- /dev/null +++ b/server/test/factories/asset.factory.ts @@ -0,0 +1,152 @@ +import { Selectable } from 'kysely'; +import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { AssetEditFactory } from 'test/factories/asset-edit.factory'; +import { AssetExifFactory } from 'test/factories/asset-exif.factory'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { build } from 'test/factories/builder.factory'; +import { StackFactory } from 'test/factories/stack.factory'; +import { + AssetEditLike, + AssetExifLike, + AssetFaceLike, + AssetFileLike, + AssetLike, + FactoryBuilder, + StackLike, + UserLike, +} from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AssetFactory { + #owner!: UserFactory; + #assetExif?: AssetExifFactory; + #files: AssetFileFactory[] = []; + #edits: AssetEditFactory[] = []; + #faces: AssetFaceFactory[] = []; + #stack?: Selectable & { assets: Selectable[]; primaryAsset: Selectable }; + + private constructor(private readonly value: Selectable) { + value.ownerId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.ownerId }); + } + + static create(dto: AssetLike = {}) { + return AssetFactory.from(dto).build(); + } + + static from(dto: AssetLike = {}) { + const id = dto.id ?? newUuid(); + + const originalFileName = dto.originalFileName ?? (dto.type === AssetType.Video ? `MOV_${id}.mp4` : `IMG_${id}.jpg`); + + return new AssetFactory({ + id, + createdAt: newDate(), + updatedAt: newDate(), + deletedAt: null, + updateId: newUuidV7(), + status: AssetStatus.Active, + checksum: newSha1(), + deviceAssetId: '', + deviceId: '', + duplicateId: null, + duration: null, + encodedVideoPath: null, + fileCreatedAt: newDate(), + fileModifiedAt: newDate(), + isExternal: false, + isFavorite: false, + isOffline: false, + libraryId: null, + livePhotoVideoId: null, + localDateTime: newDate(), + originalFileName, + originalPath: `/data/library/${originalFileName}`, + ownerId: newUuid(), + stackId: null, + thumbhash: null, + type: AssetType.Image, + visibility: AssetVisibility.Timeline, + width: null, + height: null, + isEdited: false, + ...dto, + }); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder) { + this.#owner = build(UserFactory.from(dto), builder); + this.value.ownerId = this.#owner.build().id; + return this; + } + + exif(dto: AssetExifLike = {}, builder?: FactoryBuilder) { + this.#assetExif = build(AssetExifFactory.from(dto), builder); + return this; + } + + edit(dto: AssetEditLike = {}, builder?: FactoryBuilder) { + this.#edits.push(build(AssetEditFactory.from(dto).asset(this.value), builder)); + this.value.isEdited = true; + return this; + } + + face(dto: AssetFaceLike = {}, builder?: FactoryBuilder) { + this.#faces.push(build(AssetFaceFactory.from({ assetId: this.value?.id, ...dto }), builder)); + return this; + } + + file(dto: AssetFileLike = {}, builder?: FactoryBuilder) { + this.#files.push(build(AssetFileFactory.from(dto).asset(this.value), builder)); + return this; + } + + files(dto?: 'edits'): AssetFactory; + files(items: AssetFileLike[], builder?: FactoryBuilder): AssetFactory; + files(items: AssetFileType[], builder?: FactoryBuilder): AssetFactory; + files(dto?: 'edits' | AssetFileLike[] | AssetFileType[], builder?: FactoryBuilder): AssetFactory { + const items: AssetFileLike[] = []; + + if (dto === undefined || dto === 'edits') { + items.push(...Object.values(AssetFileType).map((type) => ({ type }))); + + if (dto === 'edits') { + items.push(...Object.values(AssetFileType).map((type) => ({ type, isEdited: true }))); + } + } else { + for (const item of dto) { + items.push(typeof item === 'string' ? { type: item as AssetFileType } : item); + } + } + for (const item of items) { + this.file(item, builder); + } + + return this; + } + + stack(dto: StackLike = {}, builder?: FactoryBuilder) { + this.#stack = build(StackFactory.from(dto).primaryAsset(this.value), builder).build(); + this.value.stackId = this.#stack.id; + return this; + } + + build() { + const exif = this.#assetExif?.build(); + + return { + ...this.value, + owner: this.#owner.build(), + exifInfo: exif as NonNullable, + files: this.#files.map((file) => file.build()), + edits: this.#edits.map((edit) => edit.build()), + faces: this.#faces.map((face) => face.build()), + stack: this.#stack ?? null, + tags: [], + }; + } +} diff --git a/server/test/factories/auth.factory.ts b/server/test/factories/auth.factory.ts new file mode 100644 index 0000000000..9c738aabac --- /dev/null +++ b/server/test/factories/auth.factory.ts @@ -0,0 +1,48 @@ +import { AuthDto } from 'src/dtos/auth.dto'; +import { build } from 'test/factories/builder.factory'; +import { SharedLinkFactory } from 'test/factories/shared-link.factory'; +import { FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; + +export class AuthFactory { + #user: UserFactory; + #sharedLink?: SharedLinkFactory; + + private constructor(user: UserFactory) { + this.#user = user; + } + + static create(dto: UserLike = {}) { + return AuthFactory.from(dto).build(); + } + + static from(dto: UserLike = {}) { + return new AuthFactory(UserFactory.from(dto)); + } + + apiKey() { + // TODO + return this; + } + + sharedLink(dto: SharedLinkLike = {}, builder?: FactoryBuilder) { + this.#sharedLink = build(SharedLinkFactory.from(dto), builder); + return this; + } + + build(): AuthDto { + const { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes } = this.#user.build(); + + return { + user: { + id, + isAdmin, + name, + email, + quotaUsageInBytes, + quotaSizeInBytes, + }, + sharedLink: this.#sharedLink?.build(), + }; + } +} diff --git a/server/test/factories/builder.factory.ts b/server/test/factories/builder.factory.ts new file mode 100644 index 0000000000..4efa7a498f --- /dev/null +++ b/server/test/factories/builder.factory.ts @@ -0,0 +1,5 @@ +import { FactoryBuilder } from 'test/factories/types'; + +export const build = (factory: T, builder?: FactoryBuilder) => { + return builder ? builder(factory) : factory; +}; diff --git a/server/test/factories/person.factory.ts b/server/test/factories/person.factory.ts new file mode 100644 index 0000000000..8e016e5398 --- /dev/null +++ b/server/test/factories/person.factory.ts @@ -0,0 +1,34 @@ +import { Selectable } from 'kysely'; +import { PersonTable } from 'src/schema/tables/person.table'; +import { PersonLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class PersonFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: PersonLike = {}) { + return PersonFactory.from(dto).build(); + } + + static from(dto: PersonLike = {}) { + return new PersonFactory({ + birthDate: null, + color: null, + createdAt: newDate(), + faceAssetId: null, + id: newUuid(), + isFavorite: false, + isHidden: false, + name: 'person', + ownerId: newUuid(), + thumbnailPath: '/data/thumbs/person-thumbnail.jpg', + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/shared-link.factory.ts b/server/test/factories/shared-link.factory.ts new file mode 100644 index 0000000000..5ac5f1756b --- /dev/null +++ b/server/test/factories/shared-link.factory.ts @@ -0,0 +1,71 @@ +import { Selectable } from 'kysely'; +import { SharedLinkType } from 'src/enum'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumLike, AssetLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { factory, newDate, newUuid } from 'test/small.factory'; + +export class SharedLinkFactory { + #owner: UserFactory; + #album?: AlbumFactory; + #assets: AssetFactory[] = []; + + private constructor(private readonly value: Selectable) { + value.userId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.userId }); + } + + static create(dto: SharedLinkLike = {}) { + return SharedLinkFactory.from(dto).build(); + } + + static from(dto: SharedLinkLike = {}) { + const type = dto.type ?? SharedLinkType.Individual; + const albumId = (dto.albumId ?? type === SharedLinkType.Album) ? newUuid() : null; + + return new SharedLinkFactory({ + id: factory.uuid(), + description: 'Shared link description', + userId: newUuid(), + key: factory.buffer(), + type, + albumId, + createdAt: newDate(), + expiresAt: null, + allowUpload: true, + allowDownload: true, + showExif: true, + password: null, + slug: null, + ...dto, + }); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder): SharedLinkFactory { + this.#owner = build(UserFactory.from(dto), builder); + return this; + } + + album(dto: AlbumLike = {}, builder?: FactoryBuilder) { + this.#album = build(AlbumFactory.from(dto), builder); + return this; + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + this.#assets.push(asset); + return this; + } + + build() { + return { + ...this.value, + owner: this.#owner.build(), + album: this.#album?.build() ?? null, + assets: this.#assets.map((asset) => asset.build()), + }; + } +} diff --git a/server/test/factories/stack.factory.ts b/server/test/factories/stack.factory.ts new file mode 100644 index 0000000000..69775973c4 --- /dev/null +++ b/server/test/factories/stack.factory.ts @@ -0,0 +1,52 @@ +import { Selectable } from 'kysely'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AssetLike, FactoryBuilder, StackLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class StackFactory { + #assets: AssetFactory[] = []; + #primaryAsset: AssetFactory; + + private constructor(private readonly value: Selectable) { + this.#primaryAsset = AssetFactory.from(); + this.value.primaryAssetId = this.#primaryAsset.build().id; + } + + static create(dto: StackLike = {}) { + return StackFactory.from(dto).build(); + } + + static from(dto: StackLike = {}) { + return new StackFactory({ + createdAt: newDate(), + id: newUuid(), + ownerId: newUuid(), + primaryAssetId: newUuid(), + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + this.#assets.push(build(AssetFactory.from(dto), builder)); + return this; + } + + primaryAsset(dto: AssetLike = {}, builder?: FactoryBuilder) { + this.#primaryAsset = build(AssetFactory.from(dto), builder); + this.value.primaryAssetId = this.#primaryAsset.build().id; + this.#assets.push(this.#primaryAsset); + return this; + } + + build() { + return { + ...this.value, + assets: this.#assets.map((asset) => asset.build()), + primaryAsset: this.#primaryAsset.build(), + }; + } +} diff --git a/server/test/factories/types.ts b/server/test/factories/types.ts new file mode 100644 index 0000000000..c5a327a624 --- /dev/null +++ b/server/test/factories/types.ts @@ -0,0 +1,26 @@ +import { Selectable } from 'kysely'; +import { AlbumUserTable } from 'src/schema/tables/album-user.table'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetEditTable } from 'src/schema/tables/asset-edit.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { AssetFileTable } from 'src/schema/tables/asset-file.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { PersonTable } from 'src/schema/tables/person.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { StackTable } from 'src/schema/tables/stack.table'; +import { UserTable } from 'src/schema/tables/user.table'; + +export type FactoryBuilder = (builder: T) => R; + +export type AssetLike = Partial>; +export type AssetExifLike = Partial>; +export type AssetEditLike = Partial>; +export type AssetFileLike = Partial>; +export type AlbumLike = Partial>; +export type AlbumUserLike = Partial>; +export type SharedLinkLike = Partial>; +export type UserLike = Partial>; +export type AssetFaceLike = Partial>; +export type PersonLike = Partial>; +export type StackLike = Partial>; diff --git a/server/test/factories/user.factory.ts b/server/test/factories/user.factory.ts new file mode 100644 index 0000000000..125ce91e86 --- /dev/null +++ b/server/test/factories/user.factory.ts @@ -0,0 +1,59 @@ +import { Selectable } from 'kysely'; +import { UserStatus } from 'src/enum'; +import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { UserLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class UserFactory { + #metadata: Selectable[] = []; + + private constructor(private value: Selectable) {} + + static create(dto: UserLike = {}) { + return UserFactory.from(dto).build(); + } + + static from(dto: UserLike = {}) { + return new UserFactory({ + id: newUuid(), + email: 'test@immich.cloud', + password: '', + pinCode: null, + createdAt: newDate(), + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, + avatarColor: null, + deletedAt: null, + oauthId: '', + updatedAt: newDate(), + storageLabel: null, + name: 'Test User', + quotaSizeInBytes: null, + quotaUsageInBytes: 0, + status: UserStatus.Active, + profileChangedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + metadata(dto: Partial> & Pick, 'key' | 'value'>) { + this.#metadata.push({ + updatedAt: newDate(), + updateId: newUuid(), + userId: newUuid(), + ...dto, + }); + + return this; + } + + build() { + return { + ...this.value, + metadata: this.#metadata, + }; + } +} diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts deleted file mode 100644 index d36989bbcf..0000000000 --- a/server/test/fixtures/album.stub.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { AlbumUserRole, AssetOrder } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; - -export const albumStub = { - empty: Object.freeze({ - id: 'album-1', - albumName: 'Empty album', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - sharedWithUser: Object.freeze({ - id: 'album-2', - albumName: 'Empty album shared with user', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [ - { - user: userStub.user1, - role: AlbumUserRole.Editor, - }, - ], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - sharedWithMultiple: Object.freeze({ - id: 'album-3', - albumName: 'Empty album shared with users', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [ - { - user: userStub.user1, - role: AlbumUserRole.Editor, - }, - { - user: userStub.user2, - role: AlbumUserRole.Editor, - }, - ], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - sharedWithAdmin: Object.freeze({ - id: 'album-3', - albumName: 'Empty album shared with admin', - description: '', - ownerId: authStub.user1.user.id, - owner: userStub.user1, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [ - { - user: userStub.admin, - role: AlbumUserRole.Editor, - }, - ], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - oneAsset: Object.freeze({ - id: 'album-4', - albumName: 'Album with one asset', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [assetStub.image], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - twoAssets: Object.freeze({ - id: 'album-4a', - albumName: 'Album with two assets', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [assetStub.image, assetStub.withLocation], - albumThumbnailAsset: assetStub.image, - albumThumbnailAssetId: assetStub.image.id, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - emptyWithValidThumbnail: Object.freeze({ - id: 'album-5', - albumName: 'Empty album with valid thumbnail', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: assetStub.image, - albumThumbnailAssetId: assetStub.image.id, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), -}; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts deleted file mode 100644 index f5935d5d0e..0000000000 --- a/server/test/fixtures/asset.stub.ts +++ /dev/null @@ -1,903 +0,0 @@ -import { AssetFace, AssetFile, Exif } from 'src/database'; -import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; -import { StorageAsset } from 'src/types'; -import { authStub } from 'test/fixtures/auth.stub'; -import { fileStub } from 'test/fixtures/file.stub'; -import { userStub } from 'test/fixtures/user.stub'; - -export const previewFile: AssetFile = { - id: 'file-1', - type: AssetFileType.Preview, - path: '/uploads/user-id/thumbs/path.jpg', -}; - -const thumbnailFile: AssetFile = { - id: 'file-2', - type: AssetFileType.Thumbnail, - path: '/uploads/user-id/webp/path.ext', -}; - -const fullsizeFile: AssetFile = { - id: 'file-3', - type: AssetFileType.FullSize, - path: '/uploads/user-id/fullsize/path.webp', -}; - -const sidecarFileWithExt: AssetFile = { - id: 'sidecar-with-ext', - type: AssetFileType.Sidecar, - path: '/original/path.ext.xmp', -}; - -const sidecarFileWithoutExt: AssetFile = { - id: 'sidecar-without-ext', - type: AssetFileType.Sidecar, - path: '/original/path.xmp', -}; - -const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile]; - -export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => { - return { - id: stackId, - assets, - ownerId: assets[0].ownerId, - primaryAsset: assets[0], - primaryAssetId: assets[0].id, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - updateId: expect.any(String), - }; -}; - -export const assetStub = { - storageAsset: (asset: Partial = {}) => ({ - id: 'asset-id', - ownerId: 'user-id', - livePhotoVideoId: null, - type: AssetType.Image, - isExternal: false, - checksum: Buffer.from('file hash'), - timeZone: null, - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - originalPath: '/original/path.jpg', - originalFileName: 'IMG_123.jpg', - fileSizeInByte: 12_345, - files: [], - ...asset, - }), - noResizePath: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'IMG_123.jpg', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/library/IMG_123.jpg', - files: [thumbnailFile], - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: {} as Exif, - deletedAt: null, - isExternal: false, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - - noWebpPath: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/library/IMG_456.jpg', - files: [previewFile], - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'IMG_456.jpg', - faces: [], - isExternal: false, - exifInfo: { - fileSizeInByte: 123_000, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - - noThumbhash: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - files, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - deletedAt: null, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - - primaryImage: Object.freeze({ - id: 'primary-asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.admin, - ownerId: 'admin-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - files, - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 1000, - exifImageWidth: 1000, - } as Exif, - stackId: 'stack-1', - stack: stackStub('stack-1', [ - { id: 'primary-asset-id' } as MapAsset & { exifInfo: Exif }, - { id: 'stack-child-asset-1' } as MapAsset & { exifInfo: Exif }, - { id: 'stack-child-asset-2' } as MapAsset & { exifInfo: Exif }, - ]), - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - visibility: AssetVisibility.Timeline, - }), - - image: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - files, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2025-01-01T01:02:03.456Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - stack: null, - orientation: '', - projectionType: null, - height: 3840, - width: 2160, - visibility: AssetVisibility.Timeline, - }), - - trashed: Object.freeze({ - id: 'asset-id', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - status: AssetStatus.Trashed, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - - trashedOffline: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: new Date('2023-02-24T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: false, - duration: null, - libraryId: 'library-id', - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: true, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - archived: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - exifImageHeight: 3840, - exifImageWidth: 2160, - } as Exif, - duplicateId: null, - isOffline: false, - libraryId: null, - stackId: null, - updateId: '42', - visibility: AssetVisibility.Timeline, - }), - - external: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/user1/photo.jpg', - checksum: Buffer.from('path hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - libraryId: 'library-id', - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - stackId: null, - stack: null, - visibility: AssetVisibility.Timeline, - }), - - image1: Object.freeze({ - id: 'asset-id-1', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - deletedAt: null, - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - isExternal: false, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - stackId: null, - libraryId: null, - stack: null, - visibility: AssetVisibility.Timeline, - }), - - imageFrom2015: Object.freeze({ - id: 'asset-id-2015', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - files, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2015-02-23T05:06:29.716Z'), - updatedAt: new Date('2015-02-23T05:06:29.716Z'), - localDateTime: new Date('2015-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - visibility: AssetVisibility.Timeline, - }), - - video: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'asset-id.ext', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Video, - files: [previewFile], - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: { - fileSizeInByte: 100_000, - exifImageHeight: 2160, - exifImageWidth: 3840, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - }), - - livePhotoMotionAsset: Object.freeze({ - status: AssetStatus.Active, - id: fileStub.livePhotoMotion.uuid, - originalPath: fileStub.livePhotoMotion.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Video, - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 100_000, - timeZone: `America/New_York`, - }, - files: [] as AssetFile[], - libraryId: null, - visibility: AssetVisibility.Hidden, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), - - livePhotoStillAsset: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), - - livePhotoWithOriginalFileName: Object.freeze({ - id: 'live-photo-still-asset', - status: AssetStatus.Active, - originalPath: fileStub.livePhotoStill.originalPath, - originalFileName: fileStub.livePhotoStill.originalName, - ownerId: authStub.user1.user.id, - type: AssetType.Image, - livePhotoVideoId: 'live-photo-motion-asset', - fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), - fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - exifInfo: { - fileSizeInByte: 25_000, - timeZone: `America/New_York`, - }, - files: [] as AssetFile[], - libraryId: null, - faces: [] as AssetFace[], - visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), - - withLocation: Object.freeze({ - id: 'asset-with-favorite-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - checksum: Buffer.from('file hash', 'utf8'), - originalPath: '/original/path.ext', - type: AssetType.Image, - files: [previewFile], - thumbhash: null, - encodedVideoPath: null, - createdAt: new Date('2023-02-22T05:06:29.716Z'), - updatedAt: new Date('2023-02-22T05:06:29.716Z'), - localDateTime: new Date('2020-12-31T23:59:00.000Z'), - isFavorite: false, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - updateId: 'foo', - libraryId: null, - stackId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - exifInfo: { - latitude: 100, - longitude: 100, - fileSizeInByte: 23_456, - city: 'test-city', - state: 'test-state', - country: 'test-country', - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - tags: [], - visibility: AssetVisibility.Timeline, - }), - - sidecar: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - thumbhash: null, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files: [previewFile, sidecarFileWithExt], - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: 'foo', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - }), - - sidecarWithoutExt: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - thumbhash: null, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files: [previewFile, sidecarFileWithoutExt], - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - deletedAt: null, - duplicateId: null, - isOffline: false, - visibility: AssetVisibility.Timeline, - }), - - hasEncodedVideo: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - originalFileName: 'asset-id.ext', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Video, - files: [previewFile], - thumbhash: null, - encodedVideoPath: '/encoded/video/path.mp4', - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: false, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - faces: [], - exifInfo: { - fileSizeInByte: 100_000, - } as Exif, - deletedAt: null, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - stack: null, - visibility: AssetVisibility.Timeline, - }), - - hasFileExtension: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/user1/photo.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isExternal: true, - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - libraryId: 'library-id', - sharedLinks: [], - originalFileName: 'photo.jpg', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - } as Exif, - duplicateId: null, - isOffline: false, - visibility: AssetVisibility.Timeline, - }), - - imageDng: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.dng', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.dng', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - profileDescription: 'Adobe RGB', - bitsPerSample: 14, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - }), - - imageHif: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.hif', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.hif', - faces: [], - deletedAt: null, - exifInfo: { - fileSizeInByte: 5000, - profileDescription: 'Adobe RGB', - bitsPerSample: 14, - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - }), - panoramaTif: Object.freeze({ - id: 'asset-id', - status: AssetStatus.Active, - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.tif', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.Image, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - duration: null, - isExternal: false, - livePhotoVideo: null, - livePhotoVideoId: null, - sharedLinks: [], - originalFileName: 'asset-id.tif', - faces: [], - deletedAt: null, - sidecarPath: null, - exifInfo: { - fileSizeInByte: 5000, - projectionType: 'EQUIRECTANGULAR', - } as Exif, - duplicateId: null, - isOffline: false, - updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, - }), -}; diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts deleted file mode 100644 index f655a3944e..0000000000 --- a/server/test/fixtures/face.stub.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { SourceType } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { personStub } from 'test/fixtures/person.stub'; - -export const faceStub = { - face1: Object.freeze({ - id: 'assetFaceId1', - assetId: assetStub.image.id, - asset: { - ...assetStub.image, - libraryId: null, - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - stackId: null, - }, - personId: personStub.withName.id, - person: personStub.withName, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - faceSearch: { faceId: 'assetFaceId1', embedding: '[1, 2, 3, 4]' }, - deletedAt: new Date(), - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - primaryFace1: Object.freeze({ - id: 'assetFaceId2', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: personStub.primaryPerson.id, - person: personStub.primaryPerson, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - faceSearch: { faceId: 'assetFaceId2', embedding: '[1, 2, 3, 4]' }, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - mergeFace1: Object.freeze({ - id: 'assetFaceId3', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: personStub.mergePerson.id, - person: personStub.mergePerson, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - faceSearch: { faceId: 'assetFaceId3', embedding: '[1, 2, 3, 4]' }, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - noPerson1: Object.freeze({ - id: 'assetFaceId8', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: null, - person: null, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - faceSearch: { faceId: 'assetFaceId8', embedding: '[1, 2, 3, 4]' }, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - noPerson2: Object.freeze({ - id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: null, - person: null, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - faceSearch: { faceId: 'assetFaceId9', embedding: '[1, 2, 3, 4]' }, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - fromExif1: Object.freeze({ - id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: personStub.randomPerson.id, - person: personStub.randomPerson, - boundingBoxX1: 100, - boundingBoxY1: 100, - boundingBoxX2: 200, - boundingBoxY2: 200, - imageHeight: 500, - imageWidth: 400, - sourceType: SourceType.Exif, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - fromExif2: Object.freeze({ - id: 'assetFaceId9', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: personStub.randomPerson.id, - person: personStub.randomPerson, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.Exif, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), - withBirthDate: Object.freeze({ - id: 'assetFaceId10', - assetId: assetStub.image.id, - asset: assetStub.image, - personId: personStub.withBirthDate.id, - person: personStub.withBirthDate, - boundingBoxX1: 0, - boundingBoxY1: 0, - boundingBoxX2: 1, - boundingBoxY2: 1, - imageHeight: 1024, - imageWidth: 1024, - sourceType: SourceType.MachineLearning, - deletedAt: null, - updatedAt: new Date('2023-01-01T00:00:00Z'), - updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', - }), -}; diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index 35a7a8ed7d..6ab32e1f02 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -1,172 +1,7 @@ -import { AssetType } from 'src/enum'; -import { previewFile } from 'test/fixtures/asset.stub'; +import { AssetFileType, AssetType } from 'src/enum'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; import { userStub } from 'test/fixtures/user.stub'; -const updateId = '0d1173e3-4d80-4d76-b41e-57d56de21125'; - -export const personStub = { - noName: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - hidden: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: true, - isFavorite: false, - color: 'red', - }), - withName: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'assetFaceId', - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - withBirthDate: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: new Date('1976-06-30'), - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - noThumbnail: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - newThumbnail: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/new/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'asset-id', - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - primaryPerson: Object.freeze({ - id: 'person-1', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - mergePerson: Object.freeze({ - id: 'person-2', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 2', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - randomPerson: Object.freeze({ - id: 'person-3', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: '', - birthDate: null, - thumbnailPath: '/path/to/thumbnail', - faces: [], - faceAssetId: null, - faceAsset: null, - isHidden: false, - isFavorite: false, - color: 'red', - }), - isFavorite: Object.freeze({ - id: 'person-4', - createdAt: new Date('2021-01-01'), - updatedAt: new Date('2021-01-01'), - updateId, - ownerId: userStub.admin.id, - name: 'Person 1', - birthDate: null, - thumbnailPath: '/path/to/thumbnail.jpg', - faces: [], - faceAssetId: 'assetFaceId', - faceAsset: null, - isHidden: false, - isFavorite: true, - color: 'red', - }), -}; - export const personThumbnailStub = { newThumbnailStart: Object.freeze({ ownerId: userStub.admin.id, @@ -179,7 +14,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), newThumbnailMiddle: Object.freeze({ ownerId: userStub.admin.id, @@ -192,7 +27,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), newThumbnailEnd: Object.freeze({ ownerId: userStub.admin.id, @@ -205,7 +40,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), rawEmbeddedThumbnail: Object.freeze({ ownerId: userStub.admin.id, @@ -218,7 +53,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.dng', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), negativeCoordinate: Object.freeze({ ownerId: userStub.admin.id, @@ -231,7 +66,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), overflowingCoordinate: Object.freeze({ ownerId: userStub.admin.id, @@ -244,7 +79,7 @@ export const personThumbnailStub = { type: AssetType.Image, originalPath: '/original/path.jpg', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), videoThumbnail: Object.freeze({ ownerId: userStub.admin.id, @@ -257,6 +92,6 @@ export const personThumbnailStub = { type: AssetType.Video, originalPath: '/original/path.mp4', exifOrientation: '1', - previewPath: previewFile.path, + previewPath: AssetFileFactory.create({ type: AssetFileType.Preview }).path, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 19a62ad193..a42ff743bc 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,11 +1,8 @@ import { UserAdmin } from 'src/database'; -import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto'; -import { ExifResponseDto } from 'src/dtos/exif.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; -import { mapUser } from 'src/dtos/user.dto'; -import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; +import { AssetFactory } from 'test/factories/asset.factory'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -20,89 +17,6 @@ const sharedLinkBytes = Buffer.from( 'hex', ); -const assetInfo: ExifResponseDto = { - make: 'camera-make', - model: 'camera-model', - exifImageWidth: 500, - exifImageHeight: 500, - fileSizeInByte: 100, - orientation: 'orientation', - dateTimeOriginal: today, - modifyDate: today, - timeZone: 'America/Los_Angeles', - lensModel: 'fancy', - fNumber: 100, - focalLength: 100, - iso: 100, - exposureTime: '1/16', - latitude: 100, - longitude: 100, - city: 'city', - state: 'state', - country: 'country', - description: 'description', - projectionType: null, -}; - -const assetResponse: AssetResponseDto = { - id: 'id_1', - createdAt: today, - deviceAssetId: 'device_asset_id_1', - ownerId: 'user_id_1', - deviceId: 'device_id_1', - type: AssetType.Video, - originalMimeType: 'image/jpeg', - originalPath: 'fake_path/jpeg', - originalFileName: 'asset_1.jpeg', - thumbhash: null, - fileModifiedAt: today, - isOffline: false, - fileCreatedAt: today, - localDateTime: today, - updatedAt: today, - isFavorite: false, - isArchived: false, - duration: '0:00:00.00000', - exifInfo: assetInfo, - livePhotoVideoId: null, - tags: [], - people: [], - checksum: 'ZmlsZSBoYXNo', - isTrashed: false, - libraryId: 'library-id', - hasMetadata: true, - visibility: AssetVisibility.Timeline, -}; - -const assetResponseWithoutMetadata = { - id: 'id_1', - type: AssetType.Video, - originalMimeType: 'image/jpeg', - thumbhash: null, - localDateTime: today, - duration: '0:00:00.00000', - livePhotoVideoId: null, - hasMetadata: false, -} as AssetResponseDto; - -const albumResponse: AlbumResponseDto = { - albumName: 'Test Album', - description: '', - albumThumbnailAssetId: null, - createdAt: today, - updatedAt: today, - id: 'album-123', - ownerId: 'admin_id', - owner: mapUser(userStub.admin), - albumUsers: [], - shared: false, - hasSharedLink: false, - assets: [], - assetCount: 1, - isActivityEnabled: true, - order: AssetOrder.Desc, -}; - export const sharedLinkStub = { individual: Object.freeze({ id: '123', @@ -117,7 +31,7 @@ export const sharedLinkStub = { albumId: null, album: null, description: null, - assets: [assetStub.image], + assets: [AssetFactory.create()], password: 'password', slug: null, }), @@ -161,7 +75,7 @@ export const sharedLinkStub = { id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, - type: SharedLinkType.Album, + type: SharedLinkType.Individual, createdAt: today, expiresAt: tomorrow, allowUpload: false, @@ -169,97 +83,89 @@ export const sharedLinkStub = { showExif: false, description: null, password: null, - assets: [], - slug: null, - albumId: 'album-123', - album: { - id: 'album-123', - updateId: '42', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - albumName: 'Test Album', - description: '', - createdAt: today, - updatedAt: today, - deletedAt: null, - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - albumUsers: [], - sharedLinks: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - assets: [ - { - id: 'id_1', - status: AssetStatus.Active, - owner: undefined as unknown as UserAdmin, - ownerId: 'user_id_1', - deviceAssetId: 'device_asset_id_1', - deviceId: 'device_id_1', - type: AssetType.Video, - originalPath: 'fake_path/jpeg', - checksum: Buffer.from('file hash', 'utf8'), - fileModifiedAt: today, - fileCreatedAt: today, - localDateTime: today, - createdAt: today, + assets: [ + { + id: 'id_1', + status: AssetStatus.Active, + owner: undefined as unknown as UserAdmin, + ownerId: 'user_id_1', + deviceAssetId: 'device_asset_id_1', + deviceId: 'device_id_1', + type: AssetType.Video, + originalPath: 'fake_path/jpeg', + checksum: Buffer.from('file hash', 'utf8'), + fileModifiedAt: today, + fileCreatedAt: today, + localDateTime: today, + createdAt: today, + updatedAt: today, + isFavorite: false, + isArchived: false, + isExternal: false, + isOffline: false, + files: [], + thumbhash: null, + encodedVideoPath: '', + duration: null, + livePhotoVideo: null, + livePhotoVideoId: null, + originalFileName: 'asset_1.jpeg', + exifInfo: { + projectionType: null, + livePhotoCID: null, + assetId: 'id_1', + description: 'description', + exifImageWidth: 500, + exifImageHeight: 500, + fileSizeInByte: 100, + orientation: 'orientation', + dateTimeOriginal: today, + modifyDate: today, + timeZone: 'America/Los_Angeles', + latitude: 100, + longitude: 100, + city: 'city', + state: 'state', + country: 'country', + make: 'camera-make', + model: 'camera-model', + lensModel: 'fancy', + fNumber: 100, + focalLength: 100, + iso: 100, + exposureTime: '1/16', + fps: 100, + profileDescription: 'sRGB', + bitsPerSample: 8, + colorspace: 'sRGB', + autoStackId: null, + rating: 3, updatedAt: today, - isFavorite: false, - isArchived: false, - isExternal: false, - isOffline: false, - files: [], - thumbhash: null, - encodedVideoPath: '', - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - originalFileName: 'asset_1.jpeg', - exifInfo: { - projectionType: null, - livePhotoCID: null, - assetId: 'id_1', - description: 'description', - exifImageWidth: 500, - exifImageHeight: 500, - fileSizeInByte: 100, - orientation: 'orientation', - dateTimeOriginal: today, - modifyDate: today, - timeZone: 'America/Los_Angeles', - latitude: 100, - longitude: 100, - city: 'city', - state: 'state', - country: 'country', - make: 'camera-make', - model: 'camera-model', - lensModel: 'fancy', - fNumber: 100, - focalLength: 100, - iso: 100, - exposureTime: '1/16', - fps: 100, - profileDescription: 'sRGB', - bitsPerSample: 8, - colorspace: 'sRGB', - autoStackId: null, - rating: 3, - updatedAt: today, - updateId: '42', - }, - sharedLinks: [], - faces: [], - sidecarPath: null, - deletedAt: null, - duplicateId: null, updateId: '42', libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: 500, + height: 500, + tags: [], }, - ], - }, + sharedLinks: [], + faces: [], + sidecarPath: null, + deletedAt: null, + duplicateId: null, + updateId: '42', + libraryId: null, + stackId: null, + visibility: AssetVisibility.Timeline, + width: 500, + height: 500, + isEdited: false, + }, + ], + albumId: null, + album: null, + slug: null, }), passwordRequired: Object.freeze({ id: '123', @@ -312,20 +218,4 @@ export const sharedLinkResponseStub = { userId: 'admin_id', slug: null, }), - readonlyNoMetadata: Object.freeze({ - id: '123', - userId: 'admin_id', - key: sharedLinkBytes.toString('base64url'), - type: SharedLinkType.Album, - createdAt: today, - expiresAt: tomorrow, - description: null, - password: null, - allowUpload: false, - allowDownload: false, - showMetadata: false, - slug: null, - album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime }, - assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }], - }), }; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 807da5197f..21b49ab899 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -38,21 +38,4 @@ export const userStub = { quotaSizeInBytes: null, quotaUsageInBytes: 0, }, - user2: { - ...authStub.user2.user, - status: UserStatus.Active, - profileChangedAt: new Date('2021-01-01'), - metadata: [], - name: 'immich_name', - storageLabel: null, - oauthId: '', - shouldChangePassword: false, - avatarColor: null, - profileImagePath: '', - createdAt: new Date('2021-01-01'), - deletedAt: null, - updatedAt: new Date('2021-01-01'), - quotaSizeInBytes: null, - quotaUsageInBytes: 0, - }, }; diff --git a/server/test/mappers.ts b/server/test/mappers.ts new file mode 100644 index 0000000000..7ccd61a48c --- /dev/null +++ b/server/test/mappers.ts @@ -0,0 +1,52 @@ +import { Selectable } from 'kysely'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { AssetFaceFactory } from 'test/factories/asset-face.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; + +export const getForStorageTemplate = (asset: ReturnType) => { + return { + id: asset.id, + ownerId: asset.ownerId, + livePhotoVideoId: asset.livePhotoVideoId, + type: asset.type, + isExternal: asset.isExternal, + checksum: asset.checksum, + timeZone: asset.exifInfo.timeZone, + visibility: asset.visibility, + fileCreatedAt: asset.fileCreatedAt, + originalPath: asset.originalPath, + originalFileName: asset.originalFileName, + fileSizeInByte: asset.exifInfo.fileSizeInByte, + files: asset.files, + make: asset.exifInfo.make, + model: asset.exifInfo.model, + lensModel: asset.exifInfo.lensModel, + isEdited: asset.isEdited, + }; +}; + +export const getAsDetectedFace = (face: ReturnType) => ({ + faces: [ + { + boundingBox: { + x1: face.boundingBoxX1, + y1: face.boundingBoxY1, + x2: face.boundingBoxX2, + y2: face.boundingBoxY2, + }, + embedding: '[1, 2, 3, 4]', + score: 0.2, + }, + ], + imageHeight: face.imageHeight, + imageWidth: face.imageWidth, +}); + +export const getForFacialRecognitionJob = ( + face: ReturnType, + asset: Pick, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null, +) => ({ + ...face, + asset, + faceSearch: { faceId: face.id, embedding: '[1, 2, 3, 4]' }, +}); diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 82ea2cd1fc..cf863db2f0 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -6,6 +6,7 @@ import { Stats } from 'node:fs'; import { Writable } from 'node:stream'; import { AssetFace } from 'src/database'; import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto'; +import { AssetEditActionItem, AssetEditsCreateDto } from 'src/dtos/editing.dto'; import { AlbumUserRole, AssetType, @@ -19,6 +20,7 @@ import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; @@ -56,6 +58,7 @@ import { AlbumTable } from 'src/schema/tables/album.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; +import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetTable } from 'src/schema/tables/asset.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { MemoryTable } from 'src/schema/tables/memory.table'; @@ -68,6 +71,7 @@ import { UserTable } from 'src/schema/tables/user.table'; import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service'; import { MetadataService } from 'src/services/metadata.service'; import { SyncService } from 'src/services/sync.service'; +import { UploadFile } from 'src/types'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory'; @@ -179,6 +183,12 @@ export class MediumTestContext { return { asset, result }; } + async newMetadata(dto: Insertable) { + const { assetId, ...item } = dto; + const result = await this.get(AssetRepository).upsertMetadata(assetId, [item]); + return { metadata: dto, result }; + } + async newAssetFile(dto: Insertable) { const result = await this.get(AssetRepository).upsertFile(dto); return { result }; @@ -223,6 +233,14 @@ export class MediumTestContext { return { albumUser: { albumId, userId, role }, result }; } + async softDeleteAsset(assetId: string) { + await this.database.updateTable('asset').set({ deletedAt: new Date() }).where('id', '=', assetId).execute(); + } + + async softDeleteAlbum(albumId: string) { + await this.database.updateTable('album').set({ deletedAt: new Date() }).where('id', '=', albumId).execute(); + } + async newJobStatus(dto: Partial> & { assetId: string }) { const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId }); const result = await this.get(AssetRepository).upsertJobStatus(jobStatus); @@ -271,6 +289,11 @@ export class MediumTestContext { const result = await this.get(TagRepository).upsertAssetIds(tagsAssets); return { tagsAssets, result }; } + + async newEdits(assetId: string, dto: AssetEditsCreateDto) { + const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits as AssetEditActionItem[]); + return { edits }; + } } export class SyncTestContext extends MediumTestContext { @@ -376,6 +399,7 @@ const newRealRepository = (key: ClassConstructor, db: Kysely): T => { case AlbumUserRepository: case ActivityRepository: case AssetRepository: + case AssetEditRepository: case AssetJobRepository: case MemoryRepository: case NotificationRepository: @@ -527,6 +551,7 @@ const assetInsert = (asset: Partial> = {}) => { fileModifiedAt: now, localDateTime: now, visibility: AssetVisibility.Timeline, + isEdited: false, }; return { @@ -573,6 +598,7 @@ const assetFaceInsert = (assetFace: Partial & { assetId: string }) => imageWidth: assetFace.imageWidth ?? 10, personId: assetFace.personId ?? null, sourceType: assetFace.sourceType ?? SourceType.MachineLearning, + isVisible: assetFace.isVisible ?? true, }; return { @@ -589,8 +615,6 @@ const assetJobStatusInsert = ( duplicatesDetectedAt: date, facesRecognizedAt: date, metadataExtractedAt: date, - previewAt: date, - thumbnailAt: date, }; return { @@ -618,7 +642,7 @@ const personInsert = (person: Partial> & { ownerId: stri }; }; -const sha256 = (value: string) => createHash('sha256').update(value).digest('base64'); +const sha256 = (value: string) => createHash('sha256').update(value).digest(); const sessionInsert = ({ id = newUuid(), @@ -739,6 +763,17 @@ const loginResponse = (): LoginResponseDto => { }; }; +const uploadFile = (file: Partial = {}) => { + return { + uuid: newUuid(), + checksum: randomBytes(32), + originalPath: '/path/to/file.jpg', + originalName: 'file.jpg', + size: 123_456, + ...file, + }; +}; + export const mediumFactory = { assetInsert, assetFaceInsert, @@ -753,4 +788,5 @@ export const mediumFactory = { loginDetails, loginResponse, tagInsert, + uploadFile, }; diff --git a/server/test/medium/specs/repositories/asset-edit.repository.spec.ts b/server/test/medium/specs/repositories/asset-edit.repository.spec.ts new file mode 100644 index 0000000000..512c6c73f4 --- /dev/null +++ b/server/test/medium/specs/repositories/asset-edit.repository.spec.ts @@ -0,0 +1,115 @@ +import { Kysely } from 'kysely'; +import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(AssetEditRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetEditRepository.name, () => { + describe('replaceAll', () => { + it('should set isEdited on insert', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should set isEdited when inserting multiple edits', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should keep isEdited when removing some edits', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + ]); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: true }); + }); + + it('should set isEdited to false if all edits are deleted', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + + await sut.replaceAll(asset.id, [ + { action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } }, + { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } }, + { action: AssetEditAction.Rotate, parameters: { angle: 90 } }, + ]); + + await sut.replaceAll(asset.id, []); + + await expect( + ctx.database.selectFrom('asset').select('isEdited').where('id', '=', asset.id).executeTakeFirstOrThrow(), + ).resolves.toEqual({ isEdited: false }); + }); + }); +}); diff --git a/server/test/medium/specs/repositories/asset-job.repository.spec.ts b/server/test/medium/specs/repositories/asset-job.repository.spec.ts new file mode 100644 index 0000000000..6af3aa778f --- /dev/null +++ b/server/test/medium/specs/repositories/asset-job.repository.spec.ts @@ -0,0 +1,118 @@ +import { Kysely } from 'kysely'; +import { AssetFileType } from 'src/enum'; +import { AssetJobRepository } from 'src/repositories/asset-job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +const consume = async (generator: AsyncIterableIterator) => { + const values: T[] = []; + + for await (const value of generator) { + values.push(value); + } + + return values; +}; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(AssetJobRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetJobRepository.name, () => { + describe('streamForThumbnailJob', () => { + it('should work', async () => { + const { sut } = setup(); + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(stream.next()).resolves.toEqual({ done: true, value: undefined }); + }); + + it('should queue an asset with missing thumbnails', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(consume(stream)).resolves.toEqual([expect.objectContaining({ id: asset.id })]); + }); + + it('should skip assets without missing thumbnails', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, thumbhash: Buffer.from('fake-thumbhash-buffer') }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: false }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should queue assets with a missing full size', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ + ownerId: user.id, + thumbhash: Buffer.from('fake-thumbhash-buffer'), + originalFileName: 'photo.cr2', + }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should skip assets with after they have full size previews', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id, thumbhash: Buffer.from('fake-thumbhash-buffer') }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.FullSize, path: 'fullsize.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + + it('should skip assets with web-compatible originals', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ + ownerId: user.id, + thumbhash: Buffer.from('fake-thumbhash-buffer'), + originalFileName: 'photo.jpg', + }); + await ctx.newJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: 'thumbnail.jpg' }); + await ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: 'preview.jpg' }); + + const stream = sut.streamForThumbnailJob({ force: false, fullsizeEnabled: true }); + await expect(consume(stream)).resolves.not.toEqual( + expect.arrayContaining([expect.objectContaining({ id: asset.id })]), + ); + }); + }); +}); diff --git a/server/test/medium/specs/repositories/asset.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts new file mode 100644 index 0000000000..97f503e9ed --- /dev/null +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -0,0 +1,150 @@ +import { Kysely } from 'kysely'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(AssetRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetRepository.name, () => { + describe('upsertExif', () => { + it('should append to locked columns', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] }); + + await sut.upsertExif( + { assetId: asset.id, lockedProperties: ['description'] }, + { lockedPropertiesBehavior: 'append' }, + ); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] }); + }); + + it('should deduplicate locked columns', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal', 'description'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] }); + + await sut.upsertExif( + { assetId: asset.id, lockedProperties: ['description'] }, + { lockedPropertiesBehavior: 'append' }, + ); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] }); + }); + }); + + describe('unlockProperties', () => { + it('should unlock one property', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal', 'description'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] }); + + await sut.unlockProperties(asset.id, ['dateTimeOriginal']); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['description'] }); + }); + + it('should unlock all properties', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ + assetId: asset.id, + dateTimeOriginal: '2023-11-19T18:11:00', + lockedProperties: ['dateTimeOriginal', 'description'], + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] }); + + await sut.unlockProperties(asset.id, ['description', 'dateTimeOriginal']); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + }); + }); +}); diff --git a/server/test/medium/specs/repositories/person.repository.spec.ts b/server/test/medium/specs/repositories/person.repository.spec.ts new file mode 100644 index 0000000000..30f0fd33c0 --- /dev/null +++ b/server/test/medium/specs/repositories/person.repository.spec.ts @@ -0,0 +1,68 @@ +import { Kysely } from 'kysely'; +import { AssetFileType } from 'src/enum'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { DB } from 'src/schema'; +import { BaseService } from 'src/services/base.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { ctx } = newMediumService(BaseService, { + database: db || defaultDatabase, + real: [], + mock: [LoggingRepository], + }); + return { ctx, sut: ctx.get(PersonRepository) }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(PersonRepository.name, () => { + describe('getDataForThumbnailGenerationJob', () => { + it('should not return the edited preview path', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { person } = await ctx.newPerson({ ownerId: user.id }); + + const { assetFace } = await ctx.newAssetFace({ + assetId: asset.id, + personId: person.id, + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }); + + // theres a circular dependency between assetFace and person, so we need to update the person after creating the assetFace + await ctx.database.updateTable('person').set({ faceAssetId: assetFace.id }).where('id', '=', person.id).execute(); + + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: 'preview_edited.jpg', + isEdited: true, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: 'preview_unedited.jpg', + isEdited: false, + }); + + const result = await sut.getDataForThumbnailGenerationJob(person.id); + + expect(result).toEqual( + expect.objectContaining({ + previewPath: 'preview_unedited.jpg', + }), + ); + }); + }); +}); diff --git a/server/test/medium/specs/services/asset-media.service.spec.ts b/server/test/medium/specs/services/asset-media.service.spec.ts new file mode 100644 index 0000000000..cdd47e3dc4 --- /dev/null +++ b/server/test/medium/specs/services/asset-media.service.spec.ts @@ -0,0 +1,261 @@ +import { Kysely } from 'kysely'; +import { AssetMediaStatus } from 'src/dtos/asset-media-response.dto'; +import { AssetMediaSize } from 'src/dtos/asset-media.dto'; +import { AssetFileType } from 'src/enum'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { EventRepository } from 'src/repositories/event.repository'; +import { JobRepository } from 'src/repositories/job.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { AssetMediaService } from 'src/services/asset-media.service'; +import { AssetService } from 'src/services/asset.service'; +import { ImmichFileResponse } from 'src/utils/file'; +import { mediumFactory, newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(AssetMediaService, { + database: db || defaultDatabase, + real: [AccessRepository, AssetRepository, UserRepository], + mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AssetService.name, () => { + describe('uploadAsset', () => { + it('should work', async () => { + const { sut, ctx } = setup(); + + ctx.getMock(StorageRepository).utimes.mockResolvedValue(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 }); + const auth = factory.auth({ user: { id: user.id } }); + const file = mediumFactory.uploadFile(); + + await expect( + sut.uploadAsset( + auth, + { + deviceId: 'some-id', + deviceAssetId: 'some-id', + fileModifiedAt: new Date(), + fileCreatedAt: new Date(), + assetData: Buffer.from('some data'), + }, + file, + ), + ).resolves.toEqual({ + id: expect.any(String), + status: AssetMediaStatus.CREATED, + }); + + expect(ctx.getMock(EventRepository).emit).toHaveBeenCalledWith('AssetCreate', { + asset: expect.objectContaining({ deviceAssetId: 'some-id' }), + }); + }); + + it('should work with an empty metadata list', async () => { + const { sut, ctx } = setup(); + + ctx.getMock(StorageRepository).utimes.mockResolvedValue(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, fileSizeInByte: 12_345 }); + const auth = factory.auth({ user: { id: user.id } }); + const file = mediumFactory.uploadFile(); + + await expect( + sut.uploadAsset( + auth, + { + deviceId: 'some-id', + deviceAssetId: 'some-id', + fileModifiedAt: new Date(), + fileCreatedAt: new Date(), + assetData: Buffer.from('some data'), + metadata: [], + }, + file, + ), + ).resolves.toEqual({ + id: expect.any(String), + status: AssetMediaStatus.CREATED, + }); + }); + }); + + describe('viewThumbnail', () => { + it('should return original thumbnail by default when both exist', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create both original and edited thumbnails + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/original/preview.jpg', + isEdited: false, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/edited/preview.jpg', + isEdited: true, + }); + + const auth = factory.auth({ user: { id: user.id } }); + const result = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW }); + + expect(result).toBeInstanceOf(ImmichFileResponse); + expect((result as ImmichFileResponse).path).toBe('/original/preview.jpg'); + }); + + it('should return edited thumbnail when edited=true', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create both original and edited thumbnails + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/original/preview.jpg', + isEdited: false, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/edited/preview.jpg', + isEdited: true, + }); + + const auth = factory.auth({ user: { id: user.id } }); + const result = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW, edited: true }); + + expect(result).toBeInstanceOf(ImmichFileResponse); + expect((result as ImmichFileResponse).path).toBe('/edited/preview.jpg'); + }); + + it('should return original thumbnail when edited=false', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create both original and edited thumbnails + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/original/preview.jpg', + isEdited: false, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/edited/preview.jpg', + isEdited: true, + }); + + const auth = factory.auth({ user: { id: user.id } }); + const result = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW, edited: false }); + + expect(result).toBeInstanceOf(ImmichFileResponse); + expect((result as ImmichFileResponse).path).toBe('/original/preview.jpg'); + }); + + it('should return original thumbnail when only original exists and edited=false', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create only original thumbnail + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/original/preview.jpg', + isEdited: false, + }); + + const auth = factory.auth({ user: { id: user.id } }); + const result = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW, edited: false }); + + expect(result).toBeInstanceOf(ImmichFileResponse); + expect((result as ImmichFileResponse).path).toBe('/original/preview.jpg'); + }); + + it('should return original thumbnail when only original exists and edited=true', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create only original thumbnail + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Preview, + path: '/original/preview.jpg', + isEdited: false, + }); + + const auth = factory.auth({ user: { id: user.id } }); + const result = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.PREVIEW, edited: true }); + + expect(result).toBeInstanceOf(ImmichFileResponse); + expect((result as ImmichFileResponse).path).toBe('/original/preview.jpg'); + }); + + it('should work with thumbnail size', async () => { + const { sut, ctx } = setup(); + + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + // Create both original and edited thumbnails + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/original/thumbnail.jpg', + isEdited: false, + }); + await ctx.newAssetFile({ + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/edited/thumbnail.jpg', + isEdited: true, + }); + + const auth = factory.auth({ user: { id: user.id } }); + + // Test default (should get original) + const resultDefault = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.THUMBNAIL }); + expect(resultDefault).toBeInstanceOf(ImmichFileResponse); + expect((resultDefault as ImmichFileResponse).path).toBe('/original/thumbnail.jpg'); + + // Test edited=true (should get edited) + const resultEdited = await sut.viewThumbnail(auth, asset.id, { size: AssetMediaSize.THUMBNAIL, edited: true }); + expect(resultEdited).toBeInstanceOf(ImmichFileResponse); + expect((resultEdited as ImmichFileResponse).path).toBe('/edited/thumbnail.jpg'); + }); + }); +}); diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index 8b54019fcf..2569b29353 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -1,12 +1,15 @@ import { Kysely } from 'kysely'; -import { AssetFileType, JobName, SharedLinkType } from 'src/enum'; +import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetFileType, AssetMetadataKey, AssetStatus, JobName, SharedLinkType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { OcrRepository } from 'src/repositories/ocr.repository'; import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository'; import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; import { StackRepository } from 'src/repositories/stack.repository'; @@ -25,6 +28,7 @@ const setup = (db?: Kysely) => { database: db || defaultDatabase, real: [ AssetRepository, + AssetEditRepository, AssetJobRepository, AlbumRepository, AccessRepository, @@ -32,7 +36,7 @@ const setup = (db?: Kysely) => { StackRepository, UserRepository, ], - mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository], + mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository, OcrRepository], }); }; @@ -246,6 +250,66 @@ describe(AssetService.name, () => { }); }); + it('should delete a stacked primary asset (2 assets)', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]); + + const stackRepo = ctx.get(StackRepository); + + expect(result).toMatchObject({ primaryAssetId: asset1.id }); + + await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true }); + + // stack is deleted as well + await expect(stackRepo.getById(stack.id)).resolves.toBe(undefined); + }); + + it('should delete a stacked primary asset (3 assets)', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset3 } = await ctx.newAsset({ ownerId: user.id }); + const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id, asset3.id]); + + expect(result).toMatchObject({ primaryAssetId: asset1.id }); + + await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true }); + + // new primary asset is picked + await expect(ctx.get(StackRepository).getById(stack.id)).resolves.toMatchObject({ primaryAssetId: asset2.id }); + }); + + it('should delete a stacked primary asset (3 trashed assets)', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset3 } = await ctx.newAsset({ ownerId: user.id }); + const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id, asset3.id]); + + await ctx.get(AssetRepository).updateAll([asset1.id, asset2.id, asset3.id], { + deletedAt: new Date(), + status: AssetStatus.Deleted, + }); + + expect(result).toMatchObject({ primaryAssetId: asset1.id }); + + await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true }); + + // stack is deleted as well + await expect(ctx.get(StackRepository).getById(stack.id)).resolves.toBe(undefined); + }); + it('should not delete offline assets', async () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); @@ -270,13 +334,13 @@ describe(AssetService.name, () => { }); describe('update', () => { - it('should update dateTimeOriginal', async () => { + it('should automatically lock lockable columns', async () => { const { sut, ctx } = setup(); ctx.getMock(JobRepository).queue.mockResolvedValue(); const { user } = await ctx.newUser(); const auth = factory.auth({ user }); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await ctx.newExif({ assetId: asset.id, description: 'test' }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' }); await expect( ctx.database @@ -285,7 +349,14 @@ describe(AssetService.name, () => { .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(), ).resolves.toEqual({ lockedProperties: null }); - await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + await sut.update(auth, asset.id, { + latitude: 42, + longitude: 42, + rating: 3, + description: 'foo', + dateTimeOriginal: '2023-11-19T18:11:00+01:00', + }); await expect( ctx.database @@ -293,16 +364,100 @@ describe(AssetService.name, () => { .select('lockedProperties') .where('assetId', '=', asset.id) .executeTakeFirstOrThrow(), - ).resolves.toEqual({ lockedProperties: ['dateTimeOriginal'] }); + ).resolves.toEqual({ + lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'], + }); + }); + + it('should update dateTimeOriginal', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00' }); + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( expect.objectContaining({ - exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00' }), + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }), + }), + ); + }); + + it('should update dateTimeOriginal with time zone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }), + }), + ); + }); + + it('should update dateTimeOriginal with time zone UTC+0', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test', timeZone: 'UTC-7' }); + + await sut.update(auth, asset.id, { dateTimeOriginal: '2023-11-19T18:11:00.000Z' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: 'UTC' }), }), ); }); }); describe('updateAll', () => { + it('should automatically lock lockable columns', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lockedProperties: null }); + + await sut.updateAll(auth, { + ids: [asset.id], + latitude: 42, + description: 'foo', + longitude: 42, + rating: 3, + dateTimeOriginal: '2023-11-19T18:11:00+01:00', + }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .select('lockedProperties') + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ + lockedProperties: ['timeZone', 'rating', 'description', 'latitude', 'longitude', 'dateTimeOriginal'], + }); + }); + it('should relatively update assets', async () => { const { sut, ctx } = setup(); ctx.getMock(JobRepository).queueAll.mockResolvedValue(); @@ -313,13 +468,6 @@ describe(AssetService.name, () => { await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11 }); - await expect( - ctx.database - .selectFrom('asset_exif') - .select('lockedProperties') - .where('assetId', '=', asset.id) - .executeTakeFirstOrThrow(), - ).resolves.toEqual({ lockedProperties: ['timeZone', 'dateTimeOriginal'] }); await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( expect.objectContaining({ exifInfo: expect.objectContaining({ @@ -328,5 +476,427 @@ describe(AssetService.name, () => { }), ); }); + + it('should relatively update assets with timezone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00', timeZone: 'UTC+5' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -1441 }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-18T18:10:00+00:00', + timeZone: 'UTC+5', + lockedProperties: ['timeZone', 'dateTimeOriginal'], + }), + }), + ); + }); + + it('should relatively update assets and set a timezone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11, timeZone: 'UTC+5' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-19T18:00:00+00:00', + timeZone: 'UTC+5', + }), + }), + ); + }); + + it('should set asset time zones to UTC', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00', timeZone: 'UTC-7' }); + + await sut.updateAll(auth, { ids: [asset.id], timeZone: 'UTC' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-19T18:11:00+00:00', + timeZone: 'UTC', + }), + }), + ); + }); + + it('should update dateTimeOriginal', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: null }), + }), + ); + }); + + it('should update dateTimeOriginal with time zone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00+00:00', timeZone: 'UTC-7' }), + }), + ); + }); + + it('should update dateTimeOriginal with UTC time zone', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queueAll.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: 'test', timeZone: 'UTC-7' }); + + await sut.updateAll(auth, { ids: [asset.id], dateTimeOriginal: '2023-11-19T18:11:00.000Z' }); + + await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual( + expect.objectContaining({ + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-19T18:11:00+00:00', timeZone: 'UTC' }), + }), + ); + }); + }); + + describe('getOcr', () => { + it('should require access', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + + await expect(sut.getOcr(auth, asset.id)).rejects.toThrow('Not found or no asset.read access'); + }); + + it('should work', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); + ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]); + + await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([ + expect.objectContaining({ x1: 0.1, x2: 0.3, x3: 0.3, x4: 0.1, y1: 0.2, y2: 0.2, y3: 0.4, y4: 0.4 }), + ]); + }); + + it('should apply rotation', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); + await ctx.database + .insertInto('asset_edit') + .values({ assetId: asset.id, action: AssetEditAction.Rotate, parameters: { angle: 90 }, sequence: 1 }) + .execute(); + ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]); + + await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([ + expect.objectContaining({ + x1: 0.6, + x2: 0.8, + x3: 0.8, + x4: 0.6, + y1: expect.any(Number), + y2: expect.any(Number), + y3: 0.3, + y4: 0.3, + }), + ]); + }); + }); + + describe('getOcr', () => { + it('should require access', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + + await expect(sut.getOcr(auth, asset.id)).rejects.toThrow('Not found or no asset.read access'); + }); + + it('should work', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); + ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]); + + await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([ + expect.objectContaining({ x1: 0.1, x2: 0.3, x3: 0.3, x4: 0.1, y1: 0.2, y2: 0.2, y3: 0.4, y4: 0.4 }), + ]); + }); + + it('should apply rotation', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); + await ctx.database + .insertInto('asset_edit') + .values({ assetId: asset.id, action: AssetEditAction.Rotate, parameters: { angle: 90 }, sequence: 1 }) + .execute(); + ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]); + + await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([ + expect.objectContaining({ + x1: 0.6, + x2: 0.8, + x3: 0.8, + x4: 0.6, + y1: expect.any(Number), + y2: expect.any(Number), + y3: 0.3, + y4: 0.3, + }), + ]); + }); + }); + + describe('upsertBulkMetadata', () => { + it('should work', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const items = [{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } }]; + + await sut.upsertBulkMetadata(auth, { items }); + + const metadata = await ctx.get(AssetRepository).getMetadata(asset.id); + expect(metadata.length).toEqual(1); + expect(metadata[0]).toEqual( + expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } }), + ); + }); + + it('should work on conflict', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'old-id' } }); + + // verify existing metadata + await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([ + expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'old-id' } }), + ]); + + const items = [{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'new-id' } }]; + await sut.upsertBulkMetadata(auth, { items }); + + // verify updated metadata + await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([ + expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'new-id' } }), + ]); + }); + + it('should work with multiple assets', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + + const items = [ + { assetId: asset1.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + { assetId: asset2.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } }, + ]; + + await sut.upsertBulkMetadata(auth, { items }); + + const metadata1 = await ctx.get(AssetRepository).getMetadata(asset1.id); + expect(metadata1).toEqual([ + expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }), + ]); + + const metadata2 = await ctx.get(AssetRepository).getMetadata(asset2.id); + expect(metadata2).toEqual([ + expect.objectContaining({ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } }), + ]); + }); + + it('should work with multiple metadata for the same asset', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + const items = [ + { assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }, + { assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } }, + ]; + + await sut.upsertBulkMetadata(auth, { items }); + + const metadata = await ctx.get(AssetRepository).getMetadata(asset.id); + expect(metadata).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + key: AssetMetadataKey.MobileApp, + value: { iCloudId: 'id1' }, + }), + expect.objectContaining({ + key: 'some-other-key', + value: { foo: 'bar' }, + }), + ]), + ); + }); + }); + + describe('deleteBulkMetadata', () => { + it('should work', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'foo' } }); + + await sut.deleteBulkMetadata(auth, { items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }] }); + + const metadata = await ctx.get(AssetRepository).getMetadata(asset.id); + expect(metadata.length).toEqual(0); + }); + + it('should work even if the item does not exist', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await sut.deleteBulkMetadata(auth, { items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }] }); + + const metadata = await ctx.get(AssetRepository).getMetadata(asset.id); + expect(metadata.length).toEqual(0); + }); + + it('should work with multiple assets', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset1.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset2.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id2' } }); + + await sut.deleteBulkMetadata(auth, { + items: [ + { assetId: asset1.id, key: AssetMetadataKey.MobileApp }, + { assetId: asset2.id, key: AssetMetadataKey.MobileApp }, + ], + }); + + await expect(ctx.get(AssetRepository).getMetadata(asset1.id)).resolves.toEqual([]); + await expect(ctx.get(AssetRepository).getMetadata(asset2.id)).resolves.toEqual([]); + }); + + it('should work with multiple metadata for the same asset', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }); + await ctx.newMetadata({ assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } }); + + await sut.deleteBulkMetadata(auth, { + items: [ + { assetId: asset.id, key: AssetMetadataKey.MobileApp }, + { assetId: asset.id, key: 'some-other-key' }, + ], + }); + + await expect(ctx.get(AssetRepository).getMetadata(asset.id)).resolves.toEqual([]); + }); + + it('should not delete unspecified keys', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newMetadata({ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } }); + await ctx.newMetadata({ assetId: asset.id, key: 'some-other-key', value: { foo: 'bar' } }); + + await sut.deleteBulkMetadata(auth, { + items: [{ assetId: asset.id, key: AssetMetadataKey.MobileApp }], + }); + + const metadata = await ctx.get(AssetRepository).getMetadata(asset.id); + expect(metadata).toEqual([expect.objectContaining({ key: 'some-other-key', value: { foo: 'bar' } })]); + }); + }); + + describe('editAsset', () => { + it('should require access', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + + await expect( + sut.editAsset(auth, asset.id, { edits: [{ action: AssetEditAction.Rotate, parameters: { angle: 90 } }] }), + ).rejects.toThrow('Not found or no asset.edit.create access'); + }); + + it('should work', async () => { + const { sut, ctx } = setup(); + ctx.getMock(JobRepository).queue.mockResolvedValue(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' }); + + const editAction = { action: AssetEditAction.Rotate, parameters: { angle: 90 } } as const; + const editResponse = { ...editAction, id: expect.any(String) }; + await expect(sut.editAsset(auth, asset.id, { edits: [editAction] })).resolves.toEqual({ + assetId: asset.id, + edits: [editResponse], + }); + + await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual( + expect.objectContaining({ isEdited: true }), + ); + await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editResponse]); + }); }); }); diff --git a/server/test/medium/specs/services/audit.database.spec.ts b/server/test/medium/specs/services/audit.database.spec.ts index 7506fcf2c3..b4ddf78a4f 100644 --- a/server/test/medium/specs/services/audit.database.spec.ts +++ b/server/test/medium/specs/services/audit.database.spec.ts @@ -1,3 +1,5 @@ +import { AssetEditAction } from 'src/dtos/editing.dto'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; import { UserRepository } from 'src/repositories/user.repository'; @@ -45,6 +47,27 @@ describe('audit', () => { }); }); + describe('asset_edit_audit', () => { + it('should not cascade asset deletes to asset_edit_audit', async () => { + const assetEditRepo = ctx.get(AssetEditRepository); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + await ctx.database.deleteFrom('asset').where('id', '=', asset.id).execute(); + + await expect( + ctx.database.selectFrom('asset_edit_audit').select(['id']).where('assetId', '=', asset.id).execute(), + ).resolves.toHaveLength(0); + }); + }); + describe('assets_audit', () => { it('should not cascade user deletes to assets_audit', async () => { const userRepo = ctx.get(UserRepository); diff --git a/server/test/medium/specs/services/metadata.service.spec.ts b/server/test/medium/specs/services/metadata.service.spec.ts index 4c3499857d..6dc66e3ed5 100644 --- a/server/test/medium/specs/services/metadata.service.spec.ts +++ b/server/test/medium/specs/services/metadata.service.spec.ts @@ -1,24 +1,21 @@ +import { Kysely } from 'kysely'; import { Stats } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { AssetJobRepository } from 'src/repositories/asset-job.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; +import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; +import { TagRepository } from 'src/repositories/tag.repository'; +import { DB } from 'src/schema'; import { MetadataService } from 'src/services/metadata.service'; -import { automock, newRandomImage, newTestService, ServiceMocks } from 'test/utils'; - -const metadataRepository = new MetadataRepository( - // eslint-disable-next-line no-sparse-arrays - automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }), -); - -const createTestFile = async (exifData: Record) => { - const data = newRandomImage(); - const filePath = join(tmpdir(), 'test.png'); - await writeFile(filePath, data); - await metadataRepository.writeTags(filePath, exifData); - return { filePath }; -}; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB, newRandomImage } from 'test/utils'; type TimeZoneTest = { description: string; @@ -31,24 +28,52 @@ type TimeZoneTest = { }; }; +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const { sut, ctx } = newMediumService(MetadataService, { + database: db || defaultDatabase, + real: [ + AssetRepository, + AssetJobRepository, + ConfigRepository, + MetadataRepository, + SystemMetadataRepository, + TagRepository, + ], + mock: [EventRepository, StorageRepository, LoggingRepository], + }); + + ctx.getMock(StorageRepository).stat.mockResolvedValue({ + size: 123_456, + mtime: new Date(654_321), + mtimeMs: 654_321, + birthtimeMs: 654_322, + } as Stats); + + return { sut, ctx }; +}; + +const createTestFile = async (exifData: Record) => { + const { ctx } = setup(); + const data = newRandomImage(); + const filePath = join(tmpdir(), 'test.png'); + await writeFile(filePath, data); + await ctx.get(MetadataRepository).writeTags(filePath, exifData); + return { filePath }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + describe(MetadataService.name, () => { - let sut: MetadataService; - let mocks: ServiceMocks; - - beforeEach(() => { - ({ sut, mocks } = newTestService(MetadataService, { metadata: metadataRepository })); - - mocks.storage.stat.mockResolvedValue({ - size: 123_456, - mtime: new Date(654_321), - mtimeMs: 654_321, - birthtimeMs: 654_322, - } as Stats); - - delete process.env.TZ; + afterEach(() => { + vi.unstubAllEnvs(); }); it('should be defined', () => { + const { sut } = setup(); expect(sut).toBeDefined(); }); @@ -79,30 +104,52 @@ describe(MetadataService.name, () => { ]; it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => { - process.env.TZ = serverTimeZone ?? undefined; + vi.stubEnv('TZ', serverTimeZone); + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); const { filePath } = await createTestFile(exifData); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ - id: 'asset-1', - originalPath: filePath, - files: [], - } as any); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: '' }); - await sut.handleMetadataExtraction({ id: 'asset-1' }); + await sut.handleMetadataExtraction({ id: asset.id }); - expect(mocks.asset.upsertExif).toHaveBeenCalledWith( - expect.objectContaining({ - dateTimeOriginal: new Date(expected.dateTimeOriginal), - timeZone: expected.timeZone, - }), - { lockedPropertiesBehavior: 'skip' }, + await expect( + ctx.database + .selectFrom('asset_exif') + .select(['dateTimeOriginal', 'timeZone', 'lockedProperties']) + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ + dateTimeOriginal: new Date(expected.dateTimeOriginal), + timeZone: expected.timeZone, + lockedProperties: null, + }); + + await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual( + expect.objectContaining({ localDateTime: new Date(expected.localDateTime) }), ); + }); - expect(mocks.asset.update).toHaveBeenCalledWith( - expect.objectContaining({ - localDateTime: new Date(expected.localDateTime), - }), - ); + it('should handle dates far in the future', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + const { filePath } = await createTestFile({ CreateDate: '42603:05:04 04:12:48' }); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: '' }); + + await sut.handleMetadataExtraction({ id: asset.id }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .where('assetId', '=', asset.id) + .select('dateTimeOriginal') + .executeTakeFirstOrThrow(), + // note that this date is technically wrong. it does not throw though and should get the user's attention either way. + ).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') }); }); }); }); diff --git a/server/test/medium/specs/services/ocr.service.spec.ts b/server/test/medium/specs/services/ocr.service.spec.ts index 45c34dd09e..d9d3a9f9b9 100644 --- a/server/test/medium/specs/services/ocr.service.spec.ts +++ b/server/test/medium/specs/services/ocr.service.spec.ts @@ -57,6 +57,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'Test OCR', textScore: 0.95, + isVisible: true, x1: 10, y1: 10, x2: 50, @@ -106,6 +107,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'One', textScore: 0.9, + isVisible: true, x1: 0, y1: 1, x2: 2, @@ -121,6 +123,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'Two', textScore: 0.89, + isVisible: true, x1: 8, y1: 9, x2: 10, @@ -136,6 +139,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'Three', textScore: 0.88, + isVisible: true, x1: 16, y1: 17, x2: 18, @@ -151,6 +155,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'Four', textScore: 0.87, + isVisible: true, x1: 24, y1: 25, x2: 26, @@ -166,6 +171,7 @@ describe(OcrService.name, () => { id: expect.any(String), text: 'Five', textScore: 0.86, + isVisible: true, x1: 32, y1: 33, x2: 34, diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts index f26834c5e2..c86bd96a10 100644 --- a/server/test/medium/specs/services/person.service.spec.ts +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -1,5 +1,9 @@ import { Kysely } from 'kysely'; +import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; +import { AssetFaceCreateDto } from 'src/dtos/person.dto'; import { AccessRepository } from 'src/repositories/access.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PersonRepository } from 'src/repositories/person.repository'; @@ -15,7 +19,7 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(PersonService, { database: db || defaultDatabase, - real: [AccessRepository, DatabaseRepository, PersonRepository], + real: [AccessRepository, DatabaseRepository, PersonRepository, AssetRepository, AssetEditRepository], mock: [LoggingRepository, StorageRepository], }); }; @@ -77,4 +81,679 @@ describe(PersonService.name, () => { expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath); }); }); + + describe('createFace', () => { + it('should store and retrieve the face as-is when there are no edits', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 200 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 200, + imageHeight: 200, + x: 50, + y: 50, + width: 150, + height: 150, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + // retrieve an asset's faces + const faces = sut.getFacesById(auth, { id: asset.id }); + + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 50, + boundingBoxX2: 200, + boundingBoxY2: 200, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Crop)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 200 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Crop, + parameters: { + x: 50, + y: 50, + width: 150, + height: 200, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 150, + imageHeight: 200, + x: 0, + y: 0, + width: 100, + height: 100, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + // retrieve an asset's faces + const faces = sut.getFacesById(auth, { id: asset.id }); + + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 0, + boundingBoxY1: 0, + boundingBoxX2: 100, + boundingBoxY2: 100, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toHaveLength(1); + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 50, + boundingBoxX2: 150, + boundingBoxY2: 150, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Rotate 90)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 200 }); + await ctx.newExif({ assetId: asset.id, exifImageWidth: 200, exifImageHeight: 100 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Rotate, + parameters: { + angle: 90, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 100, + imageHeight: 200, + x: 25, + y: 50, + width: 10, + height: 10, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: expect.closeTo(25, 1), + boundingBoxY1: expect.closeTo(50, 1), + boundingBoxX2: expect.closeTo(35, 1), + boundingBoxY2: expect.closeTo(60, 1), + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 65, + boundingBoxX2: 60, + boundingBoxY2: 75, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Mirror Horizontal)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 200, + imageHeight: 100, + x: 50, + y: 25, + width: 100, + height: 50, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 25, + boundingBoxX2: 150, + boundingBoxY2: 75, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 25, + boundingBoxX2: 150, + boundingBoxY2: 75, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Crop + Rotate)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Crop, + parameters: { + x: 50, + y: 0, + width: 150, + height: 200, + }, + }, + { + action: AssetEditAction.Rotate, + parameters: { + angle: 90, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 200, + imageHeight: 150, + x: 50, + y: 25, + width: 10, + height: 20, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: expect.closeTo(50, 1), + boundingBoxY1: expect.closeTo(25, 1), + boundingBoxX2: expect.closeTo(60, 1), + boundingBoxY2: expect.closeTo(45, 1), + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 75, + boundingBoxY1: 140, + boundingBoxX2: 95, + boundingBoxY2: 150, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Crop + Mirror)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Crop, + parameters: { + x: 50, + y: 0, + width: 150, + height: 100, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 150, + imageHeight: 100, + x: 25, + y: 25, + width: 75, + height: 50, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 25, + boundingBoxY1: 25, + boundingBoxX2: 100, + boundingBoxY2: 75, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 100, + boundingBoxY1: 25, + boundingBoxX2: 175, + boundingBoxY2: 75, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Rotate + Mirror)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 150 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Rotate, + parameters: { + angle: 90, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 200, + imageHeight: 150, + x: 50, + y: 25, + width: 15, + height: 20, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: expect.closeTo(50, 1), + boundingBoxY1: expect.closeTo(25, 1), + boundingBoxX2: expect.closeTo(65, 1), + boundingBoxY2: expect.closeTo(45, 1), + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 25, + boundingBoxY1: 50, + boundingBoxX2: 45, + boundingBoxY2: 65, + }), + ]), + ); + }); + + it('should properly transform the coordinates when the asset is edited (Crop + Rotate + Mirror)', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Crop, + parameters: { + x: 50, + y: 25, + width: 100, + height: 150, + }, + }, + { + action: AssetEditAction.Rotate, + parameters: { + angle: 270, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 150, + imageHeight: 150, + x: 25, + y: 50, + width: 75, + height: 50, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: expect.closeTo(25, 1), + boundingBoxY1: expect.closeTo(50, 1), + boundingBoxX2: expect.closeTo(100, 1), + boundingBoxY2: expect.closeTo(100, 1), + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 50, + boundingBoxY1: 75, + boundingBoxX2: 100, + boundingBoxY2: 150, + }), + ]), + ); + }); + + it('should properly transform the coordinates with multiple mirrors in sequence', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 100 }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Vertical, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 100, + imageHeight: 100, + x: 10, + y: 10, + width: 80, + height: 80, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }), + ]), + ); + }); + + it('should properly handle exif orientation when creating a face on an edited asset', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 }); + await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 100, orientation: '6' }); + + await ctx.newEdits(asset.id, { + edits: [ + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Horizontal, + }, + }, + { + action: AssetEditAction.Mirror, + parameters: { + axis: MirrorAxis.Vertical, + }, + }, + ], + }); + + const auth = factory.auth({ user }); + + const dto: AssetFaceCreateDto = { + imageWidth: 100, + imageHeight: 100, + x: 10, + y: 10, + width: 80, + height: 80, + personId: person.id, + assetId: asset.id, + }; + + await sut.createFace(auth, dto); + + const faces = sut.getFacesById(auth, { id: asset.id }); + await expect(faces).resolves.toHaveLength(1); + await expect(faces).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 110, + boundingBoxY1: 10, + boundingBoxX2: 190, + boundingBoxY2: 90, + }), + ]), + ); + + // remove edits and verify the stored coordinates map to the original image + await ctx.newEdits(asset.id, { edits: [] }); + const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id }); + + await expect(facesAfterRemovingEdits).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + person: expect.objectContaining({ id: person.id }), + boundingBoxX1: 10, + boundingBoxY1: 10, + boundingBoxX2: 90, + boundingBoxY2: 90, + }), + ]), + ); + }); + }); }); diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index 517e6cc277..f58ffb6a25 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -1,5 +1,6 @@ import { Kysely } from 'kysely'; import { AccessRepository } from 'src/repositories/access.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerRepository } from 'src/repositories/partner.repository'; @@ -16,7 +17,14 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(SearchService, { database: db || defaultDatabase, - real: [AccessRepository, DatabaseRepository, SearchRepository, PartnerRepository, PersonRepository], + real: [ + AccessRepository, + AssetRepository, + DatabaseRepository, + SearchRepository, + PartnerRepository, + PersonRepository, + ], mock: [LoggingRepository], }); }; @@ -52,4 +60,32 @@ describe(SearchService.name, () => { expect.objectContaining({ id: assets[1].id }), ]); }); + + describe('searchStatistics', () => { + it('should return statistics when filtering by personIds', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { person } = await ctx.newPerson({ ownerId: user.id }); + await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + const auth = factory.auth({ user: { id: user.id } }); + + const result = await sut.searchStatistics(auth, { personIds: [person.id] }); + + expect(result).toEqual({ total: 1 }); + }); + + it('should return zero when no assets match the personIds filter', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { person } = await ctx.newPerson({ ownerId: user.id }); + + const auth = factory.auth({ user: { id: user.id } }); + + const result = await sut.searchStatistics(auth, { personIds: [person.id] }); + + expect(result).toEqual({ total: 0 }); + }); + }); }); diff --git a/server/test/medium/specs/services/shared-link.service.spec.ts b/server/test/medium/specs/services/shared-link.service.spec.ts index acc51374d1..5873d469a5 100644 --- a/server/test/medium/specs/services/shared-link.service.spec.ts +++ b/server/test/medium/specs/services/shared-link.service.spec.ts @@ -90,11 +90,474 @@ describe(SharedLinkService.name, () => { assetIds: assets.map(({ asset }) => asset.id), }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({ + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({ assets: assets.map(({ asset }) => expect.objectContaining({ id: asset.id })), }); }); + describe('getAll', () => { + it('should return all shared links even when they share the same createdAt', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sameTimestamp = '2024-01-01T00:00:00.000Z'; + + const link1 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + createdAt: sameTimestamp, + }); + + const link2 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + createdAt: sameTimestamp, + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(2); + const ids = result.map((r) => r.id); + expect(ids).toContain(link1.id); + expect(ids).toContain(link2.id); + }); + + it('should return shared links sorted by createdAt in descending order', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + const link1 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + createdAt: '2021-01-01T00:00:00.000Z', + }); + + const link2 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + createdAt: '2023-01-01T00:00:00.000Z', + }); + + const link3 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + createdAt: '2022-01-01T00:00:00.000Z', + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(3); + expect(result.map((r) => r.id)).toEqual([link2.id, link3.id, link1.id]); + }); + + it('should not return shared links belonging to other users', async () => { + const { sut, ctx } = setup(); + + const { user: userA } = await ctx.newUser(); + const { user: userB } = await ctx.newUser(); + const authA = factory.auth({ user: userA }); + const authB = factory.auth({ user: userB }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + const linkA = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: userA.id, + allowUpload: false, + type: SharedLinkType.Individual, + }); + + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: userB.id, + allowUpload: false, + type: SharedLinkType.Individual, + }); + + const resultA = await sut.getAll(authA, {}); + expect(resultA).toHaveLength(1); + expect(resultA[0].id).toBe(linkA.id); + + const resultB = await sut.getAll(authB, {}); + expect(resultB).toHaveLength(1); + expect(resultB[0].id).not.toBe(linkA.id); + }); + + it('should filter by albumId', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { album: album1 } = await ctx.newAlbum({ ownerId: user.id }); + const { album: album2 } = await ctx.newAlbum({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + const link1 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album1.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album2.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const result = await sut.getAll(auth, { albumId: album1.id }); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(link1.id); + }); + + it('should return album shared links with album data', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(1); + expect(result[0].album).toBeDefined(); + expect(result[0].album!.id).toBe(album.id); + }); + + it('should return multiple album shared links without sql error from json group by', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { album: album1 } = await ctx.newAlbum({ ownerId: user.id }); + const { album: album2 } = await ctx.newAlbum({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + const link1 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album1.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const link2 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album2.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(2); + const ids = result.map((r) => r.id); + expect(ids).toContain(link1.id); + expect(ids).toContain(link2.id); + expect(result[0].album).toBeDefined(); + expect(result[1].album).toBeDefined(); + }); + + it('should return mixed album and individual shared links together', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { album } = await ctx.newAlbum({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + const albumLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const albumLink2 = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + const individualLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + assetIds: [asset.id], + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(3); + const ids = result.map((r) => r.id); + expect(ids).toContain(albumLink.id); + expect(ids).toContain(albumLink2.id); + expect(ids).toContain(individualLink.id); + }); + + it('should return only the first asset as cover for an individual shared link', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const assets = await Promise.all([ + ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2021-01-01T00:00:00.000Z' }), + ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2023-01-01T00:00:00.000Z' }), + ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2022-01-01T00:00:00.000Z' }), + ]); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + assetIds: assets.map(({ asset }) => asset.id), + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(1); + expect(result[0].assets).toHaveLength(1); + expect(result[0].assets[0].id).toBe(assets[0].asset.id); + }); + }); + + describe('get', () => { + it('should not return trashed assets for an individual shared link', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { asset: visibleAsset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: visibleAsset.id, make: 'Canon' }); + + const { asset: trashedAsset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: trashedAsset.id, make: 'Canon' }); + await ctx.softDeleteAsset(trashedAsset.id); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sharedLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + assetIds: [visibleAsset.id, trashedAsset.id], + }); + + const result = await sut.get(auth, sharedLink.id); + expect(result).toBeDefined(); + expect(result!.assets).toHaveLength(1); + expect(result!.assets[0].id).toBe(visibleAsset.id); + }); + + it('should return empty assets when all individually shared assets are trashed', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + await ctx.softDeleteAsset(asset.id); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sharedLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + assetIds: [asset.id], + }); + + await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({ + assets: [], + }); + }); + + it('should not return trashed assets in a shared album', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const { asset: visibleAsset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: visibleAsset.id, make: 'Canon' }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: visibleAsset.id }); + + const { asset: trashedAsset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: trashedAsset.id, make: 'Canon' }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: trashedAsset.id }); + await ctx.softDeleteAsset(trashedAsset.id); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sharedLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: true, + type: SharedLinkType.Album, + }); + + await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({ + album: expect.objectContaining({ assetCount: 1 }), + }); + }); + + it('should return an empty asset count when all album assets are trashed', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const { asset } = await ctx.newAsset({ ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, make: 'Canon' }); + await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); + await ctx.softDeleteAsset(asset.id); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sharedLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({ + album: expect.objectContaining({ assetCount: 0 }), + }); + }); + + it('should not return an album shared link when the album is trashed', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + const sharedLink = await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + await ctx.softDeleteAlbum(album.id); + + await expect(sut.get(auth, sharedLink.id)).rejects.toThrow('Shared link not found'); + }); + }); + + describe('getAll', () => { + it('should not return trashed assets as cover for an individual shared link', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const { asset: trashedAsset } = await ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: '2020-01-01T00:00:00.000Z', + }); + await ctx.softDeleteAsset(trashedAsset.id); + + const { asset: visibleAsset } = await ctx.newAsset({ + ownerId: user.id, + fileCreatedAt: '2021-01-01T00:00:00.000Z', + }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + allowUpload: false, + type: SharedLinkType.Individual, + assetIds: [trashedAsset.id, visibleAsset.id], + }); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(1); + expect(result[0].assets).toHaveLength(1); + expect(result[0].assets[0].id).toBe(visibleAsset.id); + }); + + it('should not return an album shared link when the album is trashed', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const { album } = await ctx.newAlbum({ ownerId: user.id }); + + const sharedLinkRepo = ctx.get(SharedLinkRepository); + await sharedLinkRepo.create({ + key: randomBytes(16), + id: factory.uuid(), + userId: user.id, + albumId: album.id, + allowUpload: false, + type: SharedLinkType.Album, + }); + + await ctx.softDeleteAlbum(album.id); + + const result = await sut.getAll(auth, {}); + expect(result).toHaveLength(0); + }); + }); + it('should remove individually shared asset', async () => { const { sut, ctx } = setup(); @@ -114,7 +577,7 @@ describe(SharedLinkService.name, () => { assetIds: [asset.id], }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({ + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({ assets: [expect.objectContaining({ id: asset.id })], }); @@ -122,6 +585,6 @@ describe(SharedLinkService.name, () => { assetIds: [asset.id], }); - await expect(sut.getMine({ user, sharedLink }, {})).resolves.toHaveProperty('assets', []); + await expect(sut.getMine({ user, sharedLink }, [])).resolves.toHaveProperty('assets', []); }); }); diff --git a/server/test/medium/specs/services/sync.service.spec.ts b/server/test/medium/specs/services/sync.service.spec.ts index b5443d7e62..c040d584b8 100644 --- a/server/test/medium/specs/services/sync.service.spec.ts +++ b/server/test/medium/specs/services/sync.service.spec.ts @@ -1,9 +1,10 @@ +import { schemaFromCode } from '@immich/sql-tools'; import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { AssetMetadataKey, UserMetadataKey } from 'src/enum'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { SyncRepository } from 'src/repositories/sync.repository'; +import { BaseSync, SyncRepository } from 'src/repositories/sync.repository'; import { DB } from 'src/schema'; import { SyncService } from 'src/services/sync.service'; import { newMediumService } from 'test/medium.factory'; @@ -222,5 +223,21 @@ describe(SyncService.name, () => { expect(after).toHaveLength(1); expect(after[0].id).toBe(keep.id); }); + + it('should cleanup every table', async () => { + const { sut } = setup(); + + const auditTables = schemaFromCode() + .tables.filter((table) => table.name.endsWith('_audit')) + .map(({ name }) => name); + + const auditCleanupSpy = vi.spyOn(BaseSync.prototype as any, 'auditCleanup'); + await expect(sut.onAuditTableCleanup()).resolves.toBeUndefined(); + + expect(auditCleanupSpy).toHaveBeenCalledTimes(auditTables.length); + for (const table of auditTables) { + expect(auditCleanupSpy, `Audit table ${table} was not cleaned up`).toHaveBeenCalledWith(table, 31); + } + }); }); }); diff --git a/server/test/medium/specs/services/tag.service.spec.ts b/server/test/medium/specs/services/tag.service.spec.ts index 2ec498e56d..989e4f535f 100644 --- a/server/test/medium/specs/services/tag.service.spec.ts +++ b/server/test/medium/specs/services/tag.service.spec.ts @@ -1,12 +1,15 @@ import { Kysely } from 'kysely'; import { JobStatus } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; +import { AssetRepository } from 'src/repositories/asset.repository'; +import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { TagRepository } from 'src/repositories/tag.repository'; import { DB } from 'src/schema'; import { TagService } from 'src/services/tag.service'; import { upsertTags } from 'src/utils/tag'; import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; let defaultDatabase: Kysely; @@ -14,8 +17,8 @@ let defaultDatabase: Kysely; const setup = (db?: Kysely) => { return newMediumService(TagService, { database: db || defaultDatabase, - real: [TagRepository, AccessRepository], - mock: [LoggingRepository], + real: [AssetRepository, TagRepository, AccessRepository], + mock: [EventRepository, LoggingRepository], }); }; @@ -24,6 +27,32 @@ beforeAll(async () => { }); describe(TagService.name, () => { + describe('addAssets', () => { + it('should lock exif column', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const [tag] = await upsertTags(ctx.get(TagRepository), { userId: user.id, tags: ['tag-1'] }); + const authDto = factory.auth({ user }); + + await sut.addAssets(authDto, tag.id, { ids: [asset.id] }); + await expect( + ctx.database + .selectFrom('asset_exif') + .select(['lockedProperties', 'tags']) + .where('assetId', '=', asset.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ + lockedProperties: ['tags'], + tags: ['tag-1'], + }); + await expect(ctx.get(TagRepository).getByValue(user.id, 'tag-1')).resolves.toEqual( + expect.objectContaining({ id: tag.id }), + ); + await expect(ctx.get(TagRepository).getAssetIds(tag.id, [asset.id])).resolves.toContain(asset.id); + }); + }); describe('deleteEmptyTags', () => { it('single tag exists, not connected to any assets, and is deleted', async () => { const { sut, ctx } = setup(); diff --git a/server/test/medium/specs/services/workflow.service.spec.ts b/server/test/medium/specs/services/workflow.service.spec.ts index 1fddc2a7cf..229737c531 100644 --- a/server/test/medium/specs/services/workflow.service.spec.ts +++ b/server/test/medium/specs/services/workflow.service.spec.ts @@ -611,6 +611,100 @@ describe(WorkflowService.name, () => { sut.update(auth, created.id, { actions: [{ pluginActionId: factory.uuid(), actionConfig: {} }] }), ).rejects.toThrow(); }); + + it('should update trigger type', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.PersonRecognized, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [], + actions: [], + }); + + await sut.update(auth, created.id, { + triggerType: PluginTriggerType.AssetCreate, + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.triggerType).toBe(PluginTriggerType.AssetCreate); + }); + + it('should add filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [ + { pluginFilterId: testFilterId, filterConfig: { first: true } }, + { pluginFilterId: testFilterId, filterConfig: { second: true } }, + ], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(2); + expect(fetched.filters[0].filterConfig).toEqual({ first: true }); + expect(fetched.filters[1].filterConfig).toEqual({ second: true }); + }); + + it('should replace existing filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [{ pluginFilterId: testFilterId, filterConfig: { original: true } }], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [{ pluginFilterId: testFilterId, filterConfig: { replaced: true } }], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(1); + expect(fetched.filters[0].filterConfig).toEqual({ replaced: true }); + }); + + it('should remove existing filters', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + const created = await sut.create(auth, { + triggerType: PluginTriggerType.AssetCreate, + name: 'test-workflow', + description: 'Test', + enabled: true, + filters: [{ pluginFilterId: testFilterId, filterConfig: { toRemove: true } }], + actions: [], + }); + + await sut.update(auth, created.id, { + filters: [], + }); + + const fetched = await sut.get(auth, created.id); + expect(fetched.filters).toHaveLength(0); + }); }); describe('delete', () => { diff --git a/server/test/medium/specs/sync/sync-album-asset.spec.ts b/server/test/medium/specs/sync/sync-album-asset.spec.ts index 4f053937b8..123b6f9484 100644 --- a/server/test/medium/specs/sync/sync-album-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-asset.spec.ts @@ -52,6 +52,8 @@ describe(SyncRequestType.AlbumAssetsV1, () => { livePhotoVideoId: null, stackId: null, libraryId: null, + width: 1920, + height: 1080, }); const { album } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id }); @@ -79,6 +81,9 @@ describe(SyncRequestType.AlbumAssetsV1, () => { livePhotoVideoId: asset.livePhotoVideoId, stackId: asset.stackId, libraryId: asset.libraryId, + width: asset.width, + height: asset.height, + isEdited: asset.isEdited, }, type: SyncEntityType.AlbumAssetCreateV1, }, diff --git a/server/test/medium/specs/sync/sync-asset-edit.spec.ts b/server/test/medium/specs/sync/sync-asset-edit.spec.ts new file mode 100644 index 0000000000..43b2450b49 --- /dev/null +++ b/server/test/medium/specs/sync/sync-asset-edit.spec.ts @@ -0,0 +1,300 @@ +import { Kysely } from 'kysely'; +import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; +import { SyncEntityType, SyncRequestType } from 'src/enum'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; +import { DB } from 'src/schema'; +import { SyncTestContext } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = async (db?: Kysely) => { + const ctx = new SyncTestContext(db || defaultDatabase); + const { auth, user, session } = await ctx.newSyncAuthUser(); + return { auth, user, session, ctx }; +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(SyncRequestType.AssetEditsV1, () => { + it('should detect and sync the first asset edit', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset.id, + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + sequence: 0, + }, + type: SyncEntityType.AssetEditV1, + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should detect and sync multiple asset edits for the same asset', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + { + action: AssetEditAction.Rotate, + parameters: { angle: 90 }, + }, + { + action: AssetEditAction.Mirror, + parameters: { axis: MirrorAxis.Horizontal }, + }, + ]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset.id, + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + sequence: 0, + }, + type: SyncEntityType.AssetEditV1, + }, + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset.id, + action: AssetEditAction.Rotate, + parameters: { angle: 90 }, + sequence: 1, + }, + type: SyncEntityType.AssetEditV1, + }, + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset.id, + action: AssetEditAction.Mirror, + parameters: { axis: MirrorAxis.Horizontal }, + sequence: 2, + }, + type: SyncEntityType.AssetEditV1, + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]), + ); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should detect and sync updated edits', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + // Create initial edit + const edits = await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + const response1 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + await ctx.syncAckAll(auth, response1); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + + // update the existing edit + await ctx.database + .updateTable('asset_edit') + .set({ + parameters: { x: 50, y: 60, width: 150, height: 250 }, + }) + .where('id', '=', edits[0].id) + .execute(); + + const response2 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + expect(response2).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset.id, + action: AssetEditAction.Crop, + parameters: { x: 50, y: 60, width: 150, height: 250 }, + sequence: 0, + }, + type: SyncEntityType.AssetEditV1, + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]), + ); + + await ctx.syncAckAll(auth, response2); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should detect and sync deleted asset edits', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + // Create initial edit + const edits = await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + const response1 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + await ctx.syncAckAll(auth, response1); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + + // Delete all edits + await assetEditRepo.replaceAll(asset.id, []); + + const response2 = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + expect(response2).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + editId: edits[0].id, + }, + type: SyncEntityType.AssetEditDeleteV1, + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]), + ); + + await ctx.syncAckAll(auth, response2); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should only sync asset edits for own user', async () => { + const { auth, ctx } = await setup(); + const { user: user2 } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + const { session } = await ctx.newSession({ userId: user2.id }); + const auth2 = factory.auth({ session, user: user2 }); + + await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + // User 2 should see their own edit + await expect(ctx.syncStream(auth2, [SyncRequestType.AssetEditsV1])).resolves.toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetEditV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + // User 1 should not see user 2's edit + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should sync edits for multiple assets', async () => { + const { auth, ctx } = await setup(); + const { asset: asset1 } = await ctx.newAsset({ ownerId: auth.user.id }); + const { asset: asset2 } = await ctx.newAsset({ ownerId: auth.user.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + await assetEditRepo.replaceAll(asset1.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + await assetEditRepo.replaceAll(asset2.id, [ + { + action: AssetEditAction.Rotate, + parameters: { angle: 270 }, + }, + ]); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetEditsV1]); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset1.id, + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + sequence: 0, + }, + type: SyncEntityType.AssetEditV1, + }, + { + ack: expect.any(String), + data: { + id: expect.any(String), + assetId: asset2.id, + action: AssetEditAction.Rotate, + parameters: { angle: 270 }, + sequence: 0, + }, + type: SyncEntityType.AssetEditV1, + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]), + ); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); + + it('should not sync edits for partner assets', async () => { + const { auth, ctx } = await setup(); + const { user: partner } = await ctx.newUser(); + await ctx.newPartner({ sharedById: partner.id, sharedWithId: auth.user.id }); + const { asset } = await ctx.newAsset({ ownerId: partner.id }); + const assetEditRepo = ctx.get(AssetEditRepository); + + await assetEditRepo.replaceAll(asset.id, [ + { + action: AssetEditAction.Crop, + parameters: { x: 10, y: 20, width: 100, height: 200 }, + }, + ]); + + // Should not see partner's asset edits in own sync + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetEditsV1]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts index 8b4310e600..34a1e8e73c 100644 --- a/server/test/medium/specs/sync/sync-asset-face.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -97,3 +97,134 @@ describe(SyncEntityType.AssetFaceV1, () => { await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]); }); }); + +describe(SyncEntityType.AssetFaceV2, () => { + it('should detect and sync the first asset face', async () => { + const { auth, ctx } = await setup(); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetFace.id, + assetId: asset.id, + personId: person.id, + imageWidth: assetFace.imageWidth, + imageHeight: assetFace.imageHeight, + boundingBoxX1: assetFace.boundingBoxX1, + boundingBoxY1: assetFace.boundingBoxY1, + boundingBoxX2: assetFace.boundingBoxX2, + boundingBoxY2: assetFace.boundingBoxY2, + sourceType: assetFace.sourceType, + }), + type: 'AssetFaceV2', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should detect and sync a deleted asset face', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + await personRepo.deleteAssetFace(assetFace.id); + + const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetFaceId: assetFace.id, + }, + type: 'AssetFaceDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should not sync an asset face or asset face delete for an unrelated user', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { user: user2 } = await ctx.newUser(); + const { session } = await ctx.newSession({ userId: user2.id }); + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id }); + const auth2 = factory.auth({ session, user: user2 }); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceV2 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + + await personRepo.deleteAssetFace(assetFace.id); + + expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([ + expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }), + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); + + it('should contain the deletedAt and isVisible fields in AssetFaceV2', async () => { + const { auth, ctx } = await setup(); + const personRepo = ctx.get(PersonRepository); + const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); + const { person } = await ctx.newPerson({ ownerId: auth.user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + + let response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: expect.objectContaining({ + id: assetFace.id, + assetId: asset.id, + personId: person.id, + imageWidth: assetFace.imageWidth, + imageHeight: assetFace.imageHeight, + boundingBoxX1: assetFace.boundingBoxX1, + boundingBoxY1: assetFace.boundingBoxY1, + boundingBoxX2: assetFace.boundingBoxX2, + boundingBoxY2: assetFace.boundingBoxY2, + sourceType: assetFace.sourceType, + deletedAt: null, + isVisible: true, + }), + type: 'AssetFaceV2', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + + await personRepo.deleteAssetFace(assetFace.id); + + response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); + expect(response).toEqual([ + { + ack: expect.any(String), + data: { + assetFaceId: assetFace.id, + }, + type: 'AssetFaceDeleteV1', + }, + expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }), + ]); + + await ctx.syncAckAll(auth, response); + await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]); + }); +}); diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index 066cb2de4d..a1a898d9b3 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -37,6 +37,8 @@ describe(SyncEntityType.AssetV1, () => { deletedAt: null, duration: '0:10:00.00000', libraryId: null, + width: 1920, + height: 1080, }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]); @@ -60,6 +62,9 @@ describe(SyncEntityType.AssetV1, () => { stackId: null, livePhotoVideoId: null, libraryId: asset.libraryId, + width: asset.width, + height: asset.height, + isEdited: asset.isEdited, }, type: 'AssetV1', }, diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index c30cfcf6bd..345d4a1e29 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -63,9 +63,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => { type: asset.type, visibility: asset.visibility, duration: asset.duration, + isEdited: asset.isEdited, stackId: null, livePhotoVideoId: null, libraryId: asset.libraryId, + width: null, + height: null, }, type: SyncEntityType.PartnerAssetV1, }, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 5ba77ddc2f..68667fa109 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -9,6 +9,7 @@ export const newAssetRepositoryMock = (): Mocked Promise.resolve(`${input} (hashed)`)), - hashSha256: vitest.fn().mockImplementation((input) => `${input} (hashed)`), + hashSha256: vitest.fn().mockImplementation((input) => Buffer.from(`${input} (hashed)`)), verifySha256: vitest.fn().mockImplementation(() => true), hashSha1: vitest.fn().mockImplementation((input) => Buffer.from(`${input.toString()} (hashed)`)), hashFile: vitest.fn().mockImplementation((input) => `${input} (file-hashed)`), diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts deleted file mode 100644 index 0ff869ca28..0000000000 --- a/server/test/repositories/database.repository.mock.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DatabaseRepository } from 'src/repositories/database.repository'; -import { RepositoryInterface } from 'src/types'; -import { Mocked, vitest } from 'vitest'; - -export const newDatabaseRepositoryMock = (): Mocked> => { - return { - shutdown: vitest.fn(), - getExtensionVersions: vitest.fn(), - getVectorExtension: vitest.fn(), - getExtensionVersionRange: vitest.fn(), - getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), - getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'), - createExtension: vitest.fn().mockResolvedValue(void 0), - dropExtension: vitest.fn(), - updateVectorExtension: vitest.fn(), - reindexVectorsIfNeeded: vitest.fn(), - getDimensionSize: vitest.fn(), - setDimensionSize: vitest.fn(), - deleteAllSearchEmbeddings: vitest.fn(), - prewarm: vitest.fn(), - runMigrations: vitest.fn(), - revertLastMigration: vitest.fn(), - withLock: vitest.fn().mockImplementation((_, function_: () => Promise) => function_()), - tryLock: vitest.fn(), - isBusy: vitest.fn(), - wait: vitest.fn(), - migrateFilePaths: vitest.fn(), - }; -}; diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 33687921cf..b46a5fe349 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -13,6 +13,6 @@ export const newMediaRepositoryMock = (): Mocked v4(); @@ -139,7 +148,7 @@ const sessionFactory = (session: Partial = {}) => ({ updateId: newUuidV7(), deviceOS: 'android', deviceType: 'mobile', - token: 'abc123', + token: Buffer.from('abc123'), parentId: null, expiresAt: null, userId: newUuid(), @@ -159,11 +168,18 @@ const queueStatisticsFactory = (dto?: Partial) => ({ ...dto, }); -const stackFactory = () => ({ - id: newUuid(), - ownerId: newUuid(), - primaryAssetId: newUuid(), -}); +const stackFactory = ({ owner, assets, ...stack }: DeepPartial = {}): Stack => { + const ownerId = newUuid(); + + return { + id: newUuid(), + primaryAssetId: assets?.[0].id ?? newUuid(), + ownerId, + owner: userFactory(owner ?? { id: ownerId }), + assets: assets?.map((asset) => assetFactory(asset)) ?? [], + ...stack, + }; +}; const userFactory = (user: Partial = {}) => ({ id: newUuid(), @@ -222,36 +238,43 @@ const userAdminFactory = (user: Partial = {}) => { }; }; -const assetFactory = (asset: Partial = {}) => ({ - id: newUuid(), - createdAt: newDate(), - updatedAt: newDate(), - deletedAt: null, - updateId: newUuidV7(), - status: AssetStatus.Active, - checksum: newSha1(), - deviceAssetId: '', - deviceId: '', - duplicateId: null, - duration: null, - encodedVideoPath: null, - fileCreatedAt: newDate(), - fileModifiedAt: newDate(), - isExternal: false, - isFavorite: false, - isOffline: false, - libraryId: null, - livePhotoVideoId: null, - localDateTime: newDate(), - originalFileName: 'IMG_123.jpg', - originalPath: `/data/12/34/IMG_123.jpg`, - ownerId: newUuid(), - stackId: null, - thumbhash: null, - type: AssetType.Image, - visibility: AssetVisibility.Timeline, - ...asset, -}); +const assetFactory = ( + asset: Omit, 'exifInfo' | 'owner' | 'stack' | 'tags' | 'faces' | 'files' | 'edits'> = {}, +) => { + return { + id: newUuid(), + createdAt: newDate(), + updatedAt: newDate(), + deletedAt: null, + updateId: newUuidV7(), + status: AssetStatus.Active, + checksum: newSha1(), + deviceAssetId: '', + deviceId: '', + duplicateId: null, + duration: null, + encodedVideoPath: null, + fileCreatedAt: newDate(), + fileModifiedAt: newDate(), + isExternal: false, + isFavorite: false, + isOffline: false, + libraryId: null, + livePhotoVideoId: null, + localDateTime: newDate(), + originalFileName: 'IMG_123.jpg', + originalPath: `/data/12/34/IMG_123.jpg`, + ownerId: newUuid(), + stackId: null, + thumbhash: null, + type: AssetType.Image, + visibility: AssetVisibility.Timeline, + width: null, + height: null, + isEdited: false, + ...asset, + }; +}; const activityFactory = (activity: Partial = {}) => { const userId = activity.userId || newUuid(); @@ -331,6 +354,7 @@ const assetSidecarWriteFactory = () => { id: newUuid(), path: '/path/to/original-path.jpg.xmp', type: AssetFileType.Sidecar, + isEdited: false, }, ], exifInfo: { @@ -339,6 +363,7 @@ const assetSidecarWriteFactory = () => { latitude: 12, longitude: 12, dateTimeOriginal: '2023-11-22T04:56:12.196Z', + timeZone: 'UTC-6', } as unknown as Exif, }; }; @@ -358,6 +383,7 @@ const assetOcrFactory = ( boxScore?: number; textScore?: number; text?: string; + isVisible?: boolean; } = {}, ) => ({ id: newUuid(), @@ -373,13 +399,138 @@ const assetOcrFactory = ( boxScore: 0.95, textScore: 0.92, text: 'Sample Text', + isVisible: true, ...ocr, }); +const assetFileFactory = (file: Partial = {}) => ({ + id: newUuid(), + type: AssetFileType.Preview, + path: '/uploads/user-id/thumbs/path.jpg', + isEdited: false, + isProgressive: false, + ...file, +}); + +const exifFactory = (exif: Partial = {}) => ({ + assetId: newUuid(), + autoStackId: null, + bitsPerSample: null, + city: 'Austin', + colorspace: null, + country: 'United States of America', + dateTimeOriginal: newDate(), + description: '', + exifImageHeight: 420, + exifImageWidth: 42, + exposureTime: null, + fileSizeInByte: 69, + fNumber: 1.7, + focalLength: 4.38, + fps: null, + iso: 947, + latitude: 30.267_334_570_570_195, + longitude: -97.789_833_534_282_07, + lensModel: null, + livePhotoCID: null, + make: 'Google', + model: 'Pixel 7', + modifyDate: newDate(), + orientation: '1', + profileDescription: null, + projectionType: null, + rating: 4, + state: 'Texas', + tags: ['parent/child'], + timeZone: 'UTC-6', + ...exif, +}); + +const tagFactory = (tag: Partial): Tag => ({ + id: newUuid(), + color: null, + createdAt: newDate(), + parentId: null, + updatedAt: newDate(), + value: `tag-${newUuid()}`, + ...tag, +}); + +const faceFactory = ({ person, ...face }: DeepPartial = {}): AssetFace => ({ + assetId: newUuid(), + boundingBoxX1: 1, + boundingBoxX2: 2, + boundingBoxY1: 1, + boundingBoxY2: 2, + deletedAt: null, + id: newUuid(), + imageHeight: 420, + imageWidth: 42, + isVisible: true, + personId: null, + sourceType: SourceType.MachineLearning, + updatedAt: newDate(), + updateId: newUuidV7(), + person: person === null ? null : personFactory(person), + ...face, +}); + +const assetEditFactory = (edit?: Partial): AssetEditActionItem => { + switch (edit?.action) { + case AssetEditAction.Crop: { + return { action: AssetEditAction.Crop, parameters: { height: 42, width: 42, x: 0, y: 10 }, ...edit }; + } + case AssetEditAction.Mirror: { + return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal }, ...edit }; + } + case AssetEditAction.Rotate: { + return { action: AssetEditAction.Rotate, parameters: { angle: 90 }, ...edit }; + } + default: { + return { action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Vertical } }; + } + } +}; + +const personFactory = (person?: Partial): Person => ({ + birthDate: newDate(), + color: null, + createdAt: newDate(), + faceAssetId: null, + id: newUuid(), + isFavorite: false, + isHidden: false, + name: 'person', + ownerId: newUuid(), + thumbnailPath: '/path/to/person/thumbnail.jpg', + updatedAt: newDate(), + updateId: newUuidV7(), + ...person, +}); + +const albumFactory = (album?: Partial>) => ({ + albumName: 'My Album', + albumThumbnailAssetId: null, + albumUsers: [], + assets: [], + createdAt: newDate(), + deletedAt: null, + description: 'Album description', + id: newUuid(), + isActivityEnabled: false, + order: AssetOrder.Desc, + ownerId: newUuid(), + sharedLinks: [], + updatedAt: newDate(), + updateId: newUuidV7(), + ...album, +}); + export const factory = { activity: activityFactory, apiKey: apiKeyFactory, asset: assetFactory, + assetFile: assetFileFactory, assetOcr: assetOcrFactory, auth: authFactory, authApiKey: authApiKeyFactory, @@ -396,7 +547,14 @@ export const factory = { jobAssets: { sidecarWrite: assetSidecarWriteFactory, }, + exif: exifFactory, + face: faceFactory, + person: personFactory, + assetEdit: assetEditFactory, + tag: tagFactory, + album: albumFactory, uuid: newUuid, + buffer: () => Buffer.from('this is a fake buffer'), date: newDate, responses: { badRequest: (message: any = null) => ({ diff --git a/server/test/sql-tools/check-constraint-default-name.stub.ts b/server/test/sql-tools/check-constraint-default-name.stub.ts deleted file mode 100644 index 1cb7c0644a..0000000000 --- a/server/test/sql-tools/check-constraint-default-name.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -@Check({ expression: '1=1' }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create a check constraint with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.CHECK, - name: 'CHK_8d2ecfd49b984941f6b2589799', - tableName: 'table1', - expression: '1=1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/check-constraint-override-name.stub.ts b/server/test/sql-tools/check-constraint-override-name.stub.ts deleted file mode 100644 index 3752dcfb22..0000000000 --- a/server/test/sql-tools/check-constraint-override-name.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Check, Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -@Check({ name: 'CHK_test', expression: '1=1' }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create a check constraint with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.CHECK, - name: 'CHK_test', - tableName: 'table1', - expression: '1=1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-create-date.stub.ts b/server/test/sql-tools/column-create-date.stub.ts deleted file mode 100644 index db5add2a12..0000000000 --- a/server/test/sql-tools/column-create-date.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CreateDateColumn, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @CreateDateColumn() - createdAt!: string; -} - -export const description = 'should register a table with an created at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'createdAt', - tableName: 'table1', - type: 'timestamp with time zone', - default: 'now()', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-array.stub.ts b/server/test/sql-tools/column-default-array.stub.ts deleted file mode 100644 index b5e9b7d04a..0000000000 --- a/server/test/sql-tools/column-default-array.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', array: true, default: [] }) - column1!: string[]; -} - -export const description = 'should register a table with a column with a default value (array)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: true, - primary: false, - synchronize: true, - default: "'{}'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-boolean.stub.ts b/server/test/sql-tools/column-default-boolean.stub.ts deleted file mode 100644 index 6454333599..0000000000 --- a/server/test/sql-tools/column-default-boolean.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'boolean', default: true }) - column1!: boolean; -} - -export const description = 'should register a table with a column with a default value (boolean)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'boolean', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: 'true', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-date.stub.ts b/server/test/sql-tools/column-default-date.stub.ts deleted file mode 100644 index 70f4d520f9..0000000000 --- a/server/test/sql-tools/column-default-date.stub.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -const date = new Date(2023, 0, 1); - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: date }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (date)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: "'2023-01-01T00:00:00.000Z'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-function.stub.ts b/server/test/sql-tools/column-default-function.stub.ts deleted file mode 100644 index 1066a9af21..0000000000 --- a/server/test/sql-tools/column-default-function.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: () => 'now()' }) - column1!: string; -} - -export const description = 'should register a table with a column with a default function'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: 'now()', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-null.stub.ts b/server/test/sql-tools/column-default-null.stub.ts deleted file mode 100644 index b517ca5a96..0000000000 --- a/server/test/sql-tools/column-default-null.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: null }) - column1!: string; -} - -export const description = 'should register a nullable column from a default of null'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-number.stub.ts b/server/test/sql-tools/column-default-number.stub.ts deleted file mode 100644 index 7954f2498b..0000000000 --- a/server/test/sql-tools/column-default-number.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'integer', default: 0 }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (number)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'integer', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: '0', - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-default-string.stub.ts b/server/test/sql-tools/column-default-string.stub.ts deleted file mode 100644 index 0d0a18a0eb..0000000000 --- a/server/test/sql-tools/column-default-string.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'character varying', default: 'foo' }) - column1!: string; -} - -export const description = 'should register a table with a column with a default value (string)'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - default: "'foo'", - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-delete-date.stub.ts b/server/test/sql-tools/column-delete-date.stub.ts deleted file mode 100644 index de494ad16e..0000000000 --- a/server/test/sql-tools/column-delete-date.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DatabaseSchema, DeleteDateColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @DeleteDateColumn() - deletedAt!: string; -} - -export const description = 'should register a table with a deleted at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'deletedAt', - tableName: 'table1', - type: 'timestamp with time zone', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-enum-type.stub.ts b/server/test/sql-tools/column-enum-type.stub.ts deleted file mode 100644 index 563835d720..0000000000 --- a/server/test/sql-tools/column-enum-type.stub.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Column, DatabaseSchema, registerEnum, Table } from 'src/sql-tools'; - -enum Test { - Foo = 'foo', - Bar = 'bar', -} - -const test_enum = registerEnum({ name: 'test_enum', values: Object.values(Test) }); - -@Table() -export class Table1 { - @Column({ enum: test_enum }) - column1!: string; -} - -export const description = 'should accept an enum type'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [ - { - name: 'test_enum', - values: ['foo', 'bar'], - synchronize: true, - }, - ], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'enum', - enumName: 'test_enum', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-generated-identity.ts b/server/test/sql-tools/column-generated-identity.ts deleted file mode 100644 index 29f7ba969a..0000000000 --- a/server/test/sql-tools/column-generated-identity.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryGeneratedColumn({ strategy: 'identity' }) - column1!: string; -} - -export const description = 'should register a table with a generated identity column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'integer', - identity: true, - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_50c4f9905061b1e506d38a2a380', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-generated-uuid.stub.ts b/server/test/sql-tools/column-generated-uuid.stub.ts deleted file mode 100644 index 0d4d78a84f..0000000000 --- a/server/test/sql-tools/column-generated-uuid.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryGeneratedColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryGeneratedColumn({ strategy: 'uuid' }) - column1!: string; -} - -export const description = 'should register a table with a primary generated uuid column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'uuid', - default: 'uuid_generate_v4()', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_50c4f9905061b1e506d38a2a380', - tableName: 'table1', - columnNames: ['column1'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-index-name-default.ts b/server/test/sql-tools/column-index-name-default.ts deleted file mode 100644 index ea1fb17fb4..0000000000 --- a/server/test/sql-tools/column-index-name-default.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ index: true }) - column1!: string; -} - -export const description = 'should create a column with an index'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_50c4f9905061b1e506d38a2a38', - columnNames: ['column1'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-index-name.ts b/server/test/sql-tools/column-index-name.ts deleted file mode 100644 index 2a37469600..0000000000 --- a/server/test/sql-tools/column-index-name.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ indexName: 'IDX_test' }) - column1!: string; -} - -export const description = 'should create a column with an index if a name is provided'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_test', - columnNames: ['column1'], - tableName: 'table1', - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-inferred-nullable.stub.ts b/server/test/sql-tools/column-inferred-nullable.stub.ts deleted file mode 100644 index 50810291d3..0000000000 --- a/server/test/sql-tools/column-inferred-nullable.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ default: null }) - column1!: string; -} - -export const description = 'should infer nullable from the default value'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-default.stub.ts b/server/test/sql-tools/column-name-default.stub.ts deleted file mode 100644 index 57e15fc8b6..0000000000 --- a/server/test/sql-tools/column-name-default.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column() - column1!: string; -} - -export const description = 'should register a table with a column with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-override.stub.ts b/server/test/sql-tools/column-name-override.stub.ts deleted file mode 100644 index 8741162735..0000000000 --- a/server/test/sql-tools/column-name-override.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ name: 'column-1' }) - column1!: string; -} - -export const description = 'should register a table with a column with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column-1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-name-string.stub.ts b/server/test/sql-tools/column-name-string.stub.ts deleted file mode 100644 index e4a60f51b9..0000000000 --- a/server/test/sql-tools/column-name-string.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column('column-1') - column1!: string; -} - -export const description = 'should register a table with a column with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column-1', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-nullable.stub.ts b/server/test/sql-tools/column-nullable.stub.ts deleted file mode 100644 index 31c72fe97c..0000000000 --- a/server/test/sql-tools/column-nullable.stub.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should set nullable correctly'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-string-length.stub.ts b/server/test/sql-tools/column-string-length.stub.ts deleted file mode 100644 index a04cfbd117..0000000000 --- a/server/test/sql-tools/column-string-length.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Column, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ length: 2 }) - column1!: string; -} - -export const description = 'should use create a string column with a fixed length'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - length: 2, - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts b/server/test/sql-tools/column-unique-constraint-name-default.stub.ts deleted file mode 100644 index 076a93bf57..0000000000 --- a/server/test/sql-tools/column-unique-constraint-name-default.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'uuid', unique: true }) - id!: string; -} - -export const description = 'should create a unique key constraint with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts b/server/test/sql-tools/column-unique-constraint-name-override.stub.ts deleted file mode 100644 index d4c3d5bb6a..0000000000 --- a/server/test/sql-tools/column-unique-constraint-name-override.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column({ type: 'uuid', unique: true, uniqueConstraintName: 'UQ_test' }) - id!: string; -} - -export const description = 'should create a unique key constraint with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/column-update-date.stub.ts b/server/test/sql-tools/column-update-date.stub.ts deleted file mode 100644 index dfa09888c0..0000000000 --- a/server/test/sql-tools/column-update-date.stub.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { DatabaseSchema, Table, UpdateDateColumn } from 'src/sql-tools'; - -@Table() -export class Table1 { - @UpdateDateColumn() - updatedAt!: string; -} - -export const description = 'should register a table with an updated at date column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'updatedAt', - tableName: 'table1', - type: 'timestamp with time zone', - default: 'now()', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts b/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts deleted file mode 100644 index 3b7a8781b9..0000000000 --- a/server/test/sql-tools/errors/table-duplicate-decorator.stub.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Table } from 'src/sql-tools'; - -@Table({ name: 'table-1' }) -@Table({ name: 'table-2' }) -export class Table1 {} - -export const message = 'Table table-2 has already been registered'; diff --git a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts b/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts deleted file mode 100644 index 2523701e49..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-column-order.stub.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id1!: string; - - @PrimaryColumn({ type: 'uuid' }) - id2!: string; -} - -@Table() -@ForeignKeyConstraint({ - columns: ['parentId1', 'parentId2'], - referenceTable: () => Table1, - referenceColumns: ['id2', 'id1'], -}) -export class Table2 { - @Column({ type: 'uuid' }) - parentId1!: string; - - @Column({ type: 'uuid' }) - parentId2!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id1', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - { - name: 'id2', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_e457e8b1301b7bc06ef78188ee4', - tableName: 'table1', - columnNames: ['id1', 'id2'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId1', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - { - name: 'parentId2', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_aed36d04470eba20161aa8b1dc', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_aed36d04470eba20161aa8b1dc6', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - referenceColumnNames: ['id2', 'id1'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts deleted file mode 100644 index dcd957676a..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-column.stub.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId2'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should warn against missing column in foreign key constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.columns] Unable to find column (Table2.parentId2)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts deleted file mode 100644 index 238f4174f3..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-column.stub.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, referenceColumns: ['foo'] }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should warn against missing reference column in foreign key constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.referenceColumns] Unable to find column (Table1.foo)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts b/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts deleted file mode 100644 index c6d6fd5b09..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-missing-reference-table.stub.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Column, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; - -class Foo {} - -@Table() -@ForeignKeyConstraint({ - columns: ['parentId'], - referenceTable: () => Foo, -}) -export class Table1 { - @Column() - parentId!: string; -} - -export const description = 'should warn against missing reference table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'parentId', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: ['[@ForeignKeyConstraint.referenceTable] Unable to find table (Foo)'], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts b/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts deleted file mode 100644 index a86611bb50..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-multiple-columns.stub.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id1!: string; - - @PrimaryColumn({ type: 'uuid' }) - id2!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId1', 'parentId2'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId1!: string; - - @Column({ type: 'uuid' }) - parentId2!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id1', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - { - name: 'id2', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_e457e8b1301b7bc06ef78188ee4', - tableName: 'table1', - columnNames: ['id1', 'id2'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId1', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - { - name: 'parentId2', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_aed36d04470eba20161aa8b1dc', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_aed36d04470eba20161aa8b1dc6', - tableName: 'table2', - columnNames: ['parentId1', 'parentId2'], - referenceColumnNames: ['id1', 'id2'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts deleted file mode 100644 index 8bb436c9ac..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-no-index.stub.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1, index: false }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint to the target table without an index'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts b/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts deleted file mode 100644 index 6680b13b91..0000000000 --- a/server/test/sql-tools/foreign-key-constraint-no-primary.stub.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @Column() - foo!: string; -} - -@Table() -@ForeignKeyConstraint({ - columns: ['bar'], - referenceTable: () => Table1, - referenceColumns: ['foo'], -}) -export class Table2 { - @Column() - bar!: string; -} - -export const description = 'should create a foreign key constraint to the target table without a primary key'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'foo', - tableName: 'table1', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'bar', - tableName: 'table2', - type: 'character varying', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_7d9c784c98d12365d198d52e4e', - tableName: 'table2', - columnNames: ['bar'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_7d9c784c98d12365d198d52e4e6', - tableName: 'table2', - columnNames: ['bar'], - referenceTableName: 'table1', - referenceColumnNames: ['foo'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-constraint.stub.ts b/server/test/sql-tools/foreign-key-constraint.stub.ts deleted file mode 100644 index 518c5aa6bb..0000000000 --- a/server/test/sql-tools/foreign-key-constraint.stub.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, ForeignKeyConstraint, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -@ForeignKeyConstraint({ columns: ['parentId'], referenceTable: () => Table1 }) -export class Table2 { - @Column({ type: 'uuid' }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint to the target table'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-inferred-type.stub.ts b/server/test/sql-tools/foreign-key-inferred-type.stub.ts deleted file mode 100644 index 33f1c2dfde..0000000000 --- a/server/test/sql-tools/foreign-key-inferred-type.stub.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -export class Table2 { - @ForeignKeyColumn(() => Table1, {}) - parentId!: string; -} - -export const description = 'should infer the column type from the reference column'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts b/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts deleted file mode 100644 index 288f7c6698..0000000000 --- a/server/test/sql-tools/foreign-key-with-unique-constraint.stub.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ConstraintType, DatabaseSchema, ForeignKeyColumn, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -@Table() -export class Table2 { - @ForeignKeyColumn(() => Table1, { unique: true }) - parentId!: string; -} - -export const description = 'should create a foreign key constraint with a unique constraint'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - { - name: 'table2', - columns: [ - { - name: 'parentId', - tableName: 'table2', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_3fcca5cc563abf256fc346e3ff', - tableName: 'table2', - columnNames: ['parentId'], - unique: false, - synchronize: true, - }, - ], - triggers: [], - constraints: [ - { - type: ConstraintType.FOREIGN_KEY, - name: 'FK_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - referenceColumnNames: ['id'], - referenceTableName: 'table1', - synchronize: true, - }, - { - type: ConstraintType.UNIQUE, - name: 'UQ_3fcca5cc563abf256fc346e3ff4', - tableName: 'table2', - columnNames: ['parentId'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-name-default.stub.ts b/server/test/sql-tools/index-name-default.stub.ts deleted file mode 100644 index 1918106eaa..0000000000 --- a/server/test/sql-tools/index-name-default.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create an index with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_b249cc64cf63b8a22557cdc853', - tableName: 'table1', - unique: false, - columnNames: ['id'], - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-name-override.stub.ts b/server/test/sql-tools/index-name-override.stub.ts deleted file mode 100644 index a48dc6e6d6..0000000000 --- a/server/test/sql-tools/index-name-override.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ name: 'IDX_test', columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should create an index with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_test', - tableName: 'table1', - unique: false, - columnNames: ['id'], - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-with-expression.ts b/server/test/sql-tools/index-with-expression.ts deleted file mode 100644 index 07755b7f96..0000000000 --- a/server/test/sql-tools/index-with-expression.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ expression: '"id" IS NOT NULL' }) -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should create an index based off of an expression'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_376788d186160c4faa5aaaef63', - tableName: 'table1', - unique: false, - expression: '"id" IS NOT NULL', - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/index-with-where.stub.ts b/server/test/sql-tools/index-with-where.stub.ts deleted file mode 100644 index 86a4a3089d..0000000000 --- a/server/test/sql-tools/index-with-where.stub.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Column, DatabaseSchema, Index, Table } from 'src/sql-tools'; - -@Table() -@Index({ columns: ['id'], where: '"id" IS NOT NULL' }) -export class Table1 { - @Column({ nullable: true }) - column1!: string; -} - -export const description = 'should create an index with a where clause'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'column1', - tableName: 'table1', - type: 'character varying', - nullable: true, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [ - { - name: 'IDX_9f4e073964c0395f51f9b39900', - tableName: 'table1', - unique: false, - columnNames: ['id'], - where: '"id" IS NOT NULL', - synchronize: true, - }, - ], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts b/server/test/sql-tools/primary-key-constraint-name-default.stub.ts deleted file mode 100644 index 7edfd6ff36..0000000000 --- a/server/test/sql-tools/primary-key-constraint-name-default.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table() -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a primary key constraint to the table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts b/server/test/sql-tools/primary-key-constraint-name-override.stub.ts deleted file mode 100644 index ce1f2a096c..0000000000 --- a/server/test/sql-tools/primary-key-constraint-name-override.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConstraintType, DatabaseSchema, PrimaryColumn, Table } from 'src/sql-tools'; - -@Table({ primaryConstraintName: 'PK_test' }) -export class Table1 { - @PrimaryColumn({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a primary key constraint to the table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: true, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.PRIMARY_KEY, - name: 'PK_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-default.stub.ts b/server/test/sql-tools/table-name-default.stub.ts deleted file mode 100644 index 4384944364..0000000000 --- a/server/test/sql-tools/table-name-default.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table() -export class Table1 {} - -export const description = 'should register a table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-override.stub.ts b/server/test/sql-tools/table-name-override.stub.ts deleted file mode 100644 index 5bccc429d0..0000000000 --- a/server/test/sql-tools/table-name-override.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table({ name: 'table-1' }) -export class Table1 {} - -export const description = 'should register a table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table-1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/table-name-string-option.stub.ts b/server/test/sql-tools/table-name-string-option.stub.ts deleted file mode 100644 index f394699172..0000000000 --- a/server/test/sql-tools/table-name-string-option.stub.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DatabaseSchema, Table } from 'src/sql-tools'; - -@Table('table-1') -export class Table1 {} - -export const description = 'should register a table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table-1', - columns: [], - indexes: [], - triggers: [], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-after-delete.stub.ts b/server/test/sql-tools/trigger-after-delete.stub.ts deleted file mode 100644 index dcceaf25ce..0000000000 --- a/server/test/sql-tools/trigger-after-delete.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AfterDeleteTrigger, DatabaseSchema, registerFunction, Table } from 'src/sql-tools'; - -const test_fn = registerFunction({ - name: 'test_fn', - body: 'SELECT 1;', - returnType: 'character varying', -}); - -@Table() -@AfterDeleteTrigger({ - name: 'my_trigger', - function: test_fn, - scope: 'row', -}) -export class Table1 {} - -export const description = 'should create a trigger'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [expect.any(Object)], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'my_trigger', - functionName: 'test_fn', - tableName: 'table1', - timing: 'after', - scope: 'row', - actions: ['delete'], - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-before-update.stub.ts b/server/test/sql-tools/trigger-before-update.stub.ts deleted file mode 100644 index 6bf6afc721..0000000000 --- a/server/test/sql-tools/trigger-before-update.stub.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { BeforeUpdateTrigger, DatabaseSchema, registerFunction, Table } from 'src/sql-tools'; - -const test_fn = registerFunction({ - name: 'test_fn', - body: 'SELECT 1;', - returnType: 'character varying', -}); - -@Table() -@BeforeUpdateTrigger({ - name: 'my_trigger', - function: test_fn, - scope: 'row', -}) -export class Table1 {} - -export const description = 'should create a trigger '; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [expect.any(Object)], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'my_trigger', - functionName: 'test_fn', - tableName: 'table1', - timing: 'before', - scope: 'row', - actions: ['update'], - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-name-default.stub.ts b/server/test/sql-tools/trigger-name-default.stub.ts deleted file mode 100644 index 382389bcf7..0000000000 --- a/server/test/sql-tools/trigger-name-default.stub.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { DatabaseSchema, Table, Trigger } from 'src/sql-tools'; - -@Table() -@Trigger({ - timing: 'before', - actions: ['insert'], - scope: 'row', - functionName: 'function1', -}) -export class Table1 {} - -export const description = 'should register a trigger with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'TR_ca71832b10b77ed600ef05df631', - tableName: 'table1', - functionName: 'function1', - actions: ['insert'], - scope: 'row', - timing: 'before', - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/trigger-name-override.stub.ts b/server/test/sql-tools/trigger-name-override.stub.ts deleted file mode 100644 index 33c4da6b67..0000000000 --- a/server/test/sql-tools/trigger-name-override.stub.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DatabaseSchema, Table, Trigger } from 'src/sql-tools'; - -@Table() -@Trigger({ - name: 'trigger1', - timing: 'before', - actions: ['insert'], - scope: 'row', - functionName: 'function1', -}) -export class Table1 {} - -export const description = 'should a trigger with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [], - indexes: [], - triggers: [ - { - name: 'trigger1', - tableName: 'table1', - functionName: 'function1', - actions: ['insert'], - scope: 'row', - timing: 'before', - synchronize: true, - }, - ], - constraints: [], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/unique-constraint-name-default.stub.ts b/server/test/sql-tools/unique-constraint-name-default.stub.ts deleted file mode 100644 index 90fbe09224..0000000000 --- a/server/test/sql-tools/unique-constraint-name-default.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; - -@Table() -@Unique({ columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a unique constraint to the table with a default name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_b249cc64cf63b8a22557cdc8537', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/sql-tools/unique-constraint-name-override.stub.ts b/server/test/sql-tools/unique-constraint-name-override.stub.ts deleted file mode 100644 index 3da7584c0c..0000000000 --- a/server/test/sql-tools/unique-constraint-name-override.stub.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Column, ConstraintType, DatabaseSchema, Table, Unique } from 'src/sql-tools'; - -@Table() -@Unique({ name: 'UQ_test', columns: ['id'] }) -export class Table1 { - @Column({ type: 'uuid' }) - id!: string; -} - -export const description = 'should add a unique constraint to the table with a specific name'; -export const schema: DatabaseSchema = { - databaseName: 'postgres', - schemaName: 'public', - functions: [], - enums: [], - extensions: [], - parameters: [], - overrides: [], - tables: [ - { - name: 'table1', - columns: [ - { - name: 'id', - tableName: 'table1', - type: 'uuid', - nullable: false, - isArray: false, - primary: false, - synchronize: true, - }, - ], - indexes: [], - triggers: [], - constraints: [ - { - type: ConstraintType.UNIQUE, - name: 'UQ_test', - tableName: 'table1', - columnNames: ['id'], - synchronize: true, - }, - ], - synchronize: true, - }, - ], - warnings: [], -}; diff --git a/server/test/utils.ts b/server/test/utils.ts index 77853f897a..b3e47b2b7e 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,4 @@ +import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; import { CallHandler, ExecutionContext, Provider, ValidationPipe } from '@nestjs/common'; import { APP_GUARD, APP_PIPE } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; @@ -7,9 +8,8 @@ import { NextFunction } from 'express'; import { Kysely } from 'kysely'; import multer from 'multer'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; -import { Readable, Writable } from 'node:stream'; +import { Duplex, Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; -import postgres from 'postgres'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AuthGuard } from 'src/middleware/auth.guard'; @@ -20,6 +20,7 @@ import { AlbumUserRepository } from 'src/repositories/album-user.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AppRepository } from 'src/repositories/app.repository'; +import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; @@ -69,12 +70,11 @@ import { DB } from 'src/schema'; import { AuthService } from 'src/services/auth.service'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; -import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; +import { getKyselyConfig } from 'src/utils/database'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; -import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock'; import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; @@ -216,6 +216,7 @@ export type ServiceOverrides = { app: AppRepository; audit: AuditRepository; asset: AssetRepository; + assetEdit: AssetEditRepository; assetJob: AssetJobRepository; config: ConfigRepository; cron: CronRepository; @@ -277,6 +278,14 @@ export const getMocks = () => { const loggerMock = { setContext: () => {} }; const configMock = { getEnv: () => ({}) }; + // eslint-disable-next-line no-sparse-arrays + const databaseMock = automock(DatabaseRepository, { args: [, loggerMock], strict: false }); + + databaseMock.withLock.mockImplementation((_type, fn) => fn()); + databaseMock.getPostgresVersion = vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'); + databaseMock.getPostgresVersionRange = vitest.fn().mockReturnValue('>=14.0.0'); + databaseMock.createExtension = vitest.fn().mockResolvedValue(void 0); + const mocks: ServiceMocks = { access: newAccessRepositoryMock(), // eslint-disable-next-line no-sparse-arrays @@ -289,10 +298,11 @@ export const getMocks = () => { album: automock(AlbumRepository, { strict: false }), albumUser: automock(AlbumUserRepository), asset: newAssetRepositoryMock(), + assetEdit: automock(AssetEditRepository), assetJob: automock(AssetJobRepository), app: automock(AppRepository, { strict: false }), config: newConfigRepositoryMock(), - database: newDatabaseRepositoryMock(), + database: databaseMock, downloadRepository: automock(DownloadRepository, { strict: false }), duplicateRepository: automock(DuplicateRepository), email: automock(EmailRepository, { args: [loggerMock] }), @@ -356,6 +366,7 @@ export const newTestService = ( overrides.apiKey || (mocks.apiKey as As), overrides.app || (mocks.app as As), overrides.asset || (mocks.asset as As), + overrides.assetEdit || (mocks.assetEdit as As), overrides.assetJob || (mocks.assetJob as As), overrides.audit || (mocks.audit as As), overrides.config || (mocks.config as As as ConfigRepository), @@ -434,13 +445,8 @@ const withDatabase = (url: string, name: string) => url.replace(`/${templateName export const getKyselyDB = async (suffix?: string): Promise> => { const testUrl = process.env.IMMICH_TEST_POSTGRES_URL!; - const sql = postgres({ - ...asPostgresConnectionConfig({ - connectionType: 'url', - url: withDatabase(testUrl, 'postgres'), - }), - max: 1, - }); + const connection = { connectionType: 'url', url: withDatabase(testUrl, 'postgres') } as DatabaseConnectionParams; + const sql = createPostgres({ maxConnections: 1, connection }); const randomSuffix = Math.random().toString(36).slice(2, 7); const dbName = `immich_${suffix ?? randomSuffix}`; @@ -492,6 +498,75 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st } as unknown as ChildProcessWithoutNullStreams; }); +export const mockDuplex = + (chunkCb?: (chunk: Buffer) => void) => + (command: string, exitCode: number, stdout: string, stderr: string, error?: unknown) => { + const duplex = new Duplex({ + write(chunk, _encoding, callback) { + chunkCb?.(chunk); + callback(); + }, + + read() {}, + + final(callback) { + callback(); + }, + }); + + setImmediate(() => { + if (error) { + duplex.destroy(error as Error); + } else if (exitCode === 0) { + /* eslint-disable unicorn/prefer-single-call */ + duplex.push(stdout); + duplex.push(null); + /* eslint-enable unicorn/prefer-single-call */ + } else { + duplex.destroy(new Error(`${command} non-zero exit code (${exitCode})\n${stderr}`)); + } + }); + + return duplex; + }; + +export const mockFork = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => { + const stdoutStream = new Readable({ + read() { + this.push(stdout); // write mock data to stdout + this.push(null); // end stream + }, + }); + + return { + stdout: stdoutStream, + stderr: new Readable({ + read() { + this.push(stderr); // write mock data to stderr + this.push(null); // end stream + }, + }), + stdin: new Writable({ + write(chunk, encoding, callback) { + callback(); + }, + }), + exitCode, + on: vitest.fn((event, callback: any) => { + if (event === 'close') { + stdoutStream.once('end', () => callback(0)); + } + if (event === 'error' && error) { + stdoutStream.once('end', () => callback(error)); + } + if (event === 'exit') { + stdoutStream.once('end', () => callback(exitCode)); + } + }), + kill: vitest.fn(), + } as unknown as ChildProcessWithoutNullStreams; +}); + export async function* makeStream(items: T[] = []): AsyncIterableIterator { for (const item of items) { await Promise.resolve(); diff --git a/server/test/vitest.config.mjs b/server/test/vitest.config.mjs index 6d9ee3a564..79d053d176 100644 --- a/server/test/vitest.config.mjs +++ b/server/test/vitest.config.mjs @@ -2,9 +2,6 @@ import swc from 'unplugin-swc'; import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; -// Set the timezone to UTC to avoid timezone issues during testing -process.env.TZ = 'UTC'; - export default defineConfig({ test: { root: './', @@ -25,6 +22,9 @@ export default defineConfig({ fallbackCJS: true, }, }, + env: { + TZ: 'UTC', + }, }, plugins: [swc.vite(), tsconfigPaths()], }); diff --git a/server/tsconfig.json b/server/tsconfig.json index e12b614f0d..fcb0ea2a97 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "node16", + "module": "node20", "strict": true, "declaration": true, "removeComments": true, diff --git a/web/.nvmrc b/web/.nvmrc index 9e2934aa34..32f8c50de0 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -24.11.1 +24.13.1 diff --git a/web/mise.toml b/web/mise.toml index 5aca2d737d..00b2b30c6b 100644 --- a/web/mise.toml +++ b/web/mise.toml @@ -1,56 +1,46 @@ [tasks.install] run = "pnpm install --filter immich-web --frozen-lockfile" -[tasks."svelte-kit-sync"] -env._.path = "./node_modules/.bin" -run = "svelte-kit sync" - [tasks.build] -env._.path = "./node_modules/.bin" -run = "vite build" +run = "pnpm run build" [tasks."build-stats"] -env.BUILD_STATS = "true" -env._.path = "./node_modules/.bin" -run = "vite build" +run = "pnpm run build:stats" [tasks.preview] -env._.path = "./node_modules/.bin" -run = "vite preview" +run = "pnpm run preview" [tasks.start] -env._.path = "./node_modules/.bin" -run = "vite dev --host 0.0.0.0 --port 3000" +depends = [":install", "//:sdk:install", "//:sdk:build"] +run = "pnpm run dev" + +[tasks."start-demo"] +env.IMMICH_SERVER_URL = "https://demo.immich.app" +run = { task = ":start" } [tasks.test] -depends = ["svelte-kit-sync"] -env._.path = "./node_modules/.bin" -run = "vitest" +run = "pnpm run test" [tasks.format] -env._.path = "./node_modules/.bin" -run = "prettier --check ." +run = "pnpm run format" [tasks."format-fix"] -env._.path = "./node_modules/.bin" -run = "prettier --write ." +run = "pnpm run format:fix" [tasks.lint] -env._.path = "./node_modules/.bin" -run = "eslint . --max-warnings 0 --concurrency 4" +run = "pnpm run lint" [tasks."lint-fix"] -run = { task = "lint --fix" } +run = "pnpm run lint:fix" -[tasks.check] -depends = ["svelte-kit-sync"] -env._.path = "./node_modules/.bin" -run = "tsc --noEmit" +[tasks.check-typescript] +run = "pnpm run check:typescript" [tasks."check-svelte"] -depends = ["svelte-kit-sync"] -env._.path = "./node_modules/.bin" -run = "svelte-check --no-tsconfig --fail-on-warnings" +run = "pnpm run check:svelte" + +[tasks.check] +run = { tasks = [":check-typescript", ":check-svelte"] } [tasks.checklist] run = [ diff --git a/web/package.json b/web/package.json index 97a0de7d97..925f8b98a1 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "2.4.0", + "version": "2.5.6", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { @@ -17,38 +17,36 @@ "lint": "eslint . --max-warnings 0 --concurrency 4", "lint:fix": "pnpm run lint --fix", "format": "prettier --check .", - "format:fix": "prettier --write . && pnpm run format:i18n", - "format:i18n": "pnpm dlx sort-json ../i18n/*.json", - "test": "vitest --run", + "format:fix": "prettier --write .", + "test": "vitest", "test:cov": "vitest --coverage", "test:watch": "vitest dev", "prepare": "svelte-kit sync" }, "dependencies": { - "@formatjs/icu-messageformat-parser": "^2.9.8", + "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", - "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.50.1", - "@mapbox/mapbox-gl-rtl-text": "0.2.3", + "@immich/sdk": "workspace:*", + "@immich/ui": "^0.64.0", + "@mapbox/mapbox-gl-rtl-text": "0.3.0", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", - "@photo-sphere-viewer/equirectangular-tiles-adapter": "^5.14.0", + "@photo-sphere-viewer/equirectangular-tiles-adapter": "^5.14.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0", "@photo-sphere-viewer/markers-plugin": "^5.14.0", "@photo-sphere-viewer/resolution-plugin": "^5.14.0", "@photo-sphere-viewer/settings-plugin": "^5.14.0", "@photo-sphere-viewer/video-plugin": "^5.14.0", "@types/geojson": "^7946.0.16", - "@zoom-image/core": "^0.41.0", + "@zoom-image/core": "^0.42.0", "@zoom-image/svelte": "^0.3.0", - "async-mutex": "^0.5.0", "dom-to-image": "^2.6.0", - "fabric": "^6.5.4", + "fabric": "^7.0.0", "geo-coordinates-parser": "^1.7.4", "geojson": "^0.5.0", "handlebars": "^4.7.8", "happy-dom": "^20.0.0", - "intl-messageformat": "^10.7.11", + "intl-messageformat": "^11.0.0", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", @@ -59,21 +57,23 @@ "socket.io-client": "~4.8.0", "svelte-gestures": "^5.2.2", "svelte-i18n": "^4.0.1", + "svelte-jsoneditor": "^3.10.0", "svelte-maplibre": "^1.2.5", "svelte-persisted-store": "^0.12.0", "tabbable": "^6.2.0", "thumbhash": "^0.1.1", + "transformation-matrix": "^3.1.0", "uplot": "^1.6.32" }, "devDependencies": { - "@eslint/js": "^9.36.0", + "@eslint/js": "^10.0.0", "@faker-js/faker": "^10.0.0", "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.9.0", + "@sveltejs/enhanced-img": "^0.10.0", "@sveltejs/kit": "^2.27.1", - "@sveltejs/vite-plugin-svelte": "6.2.1", + "@sveltejs/vite-plugin-svelte": "6.2.4", "@tailwindcss/vite": "^4.1.7", "@testing-library/jest-dom": "^6.4.2", "@testing-library/svelte": "^5.2.8", @@ -86,20 +86,20 @@ "@types/qrcode": "^1.5.5", "@vitest/coverage-v8": "^3.0.0", "dotenv": "^17.0.0", - "eslint": "^9.36.0", + "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-compat": "^6.0.2", "eslint-plugin-svelte": "^3.12.4", - "eslint-plugin-unicorn": "^62.0.0", + "eslint-plugin-unicorn": "^63.0.0", "factory.ts": "^1.4.1", - "globals": "^16.0.0", + "globals": "^17.0.0", "happy-dom": "^20.0.0", "prettier": "^3.7.4", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", "rollup-plugin-visualizer": "^6.0.0", - "svelte": "5.43.3", + "svelte": "5.53.5", "svelte-check": "^4.1.5", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.1.7", @@ -109,6 +109,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.11.1" + "node": "24.13.1" } } diff --git a/web/src/app.css b/web/src/app.css index bf7601f63b..3a4d29b466 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -4,7 +4,7 @@ /* @import '/usr/ui/dist/theme/default.css'; */ @utility immich-form-input { - @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; + @apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4; } @utility immich-form-label { @@ -49,7 +49,7 @@ } @theme { - --font-immich-mono: Overpass Mono, monospace; + --font-mono: 'GoogleSansCode', monospace; --spacing-18: 4.5rem; @@ -84,25 +84,25 @@ @layer utilities { @font-face { - font-family: 'Overpass'; - src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); - font-weight: 1 999; + font-family: 'GoogleSans'; + src: url('$lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations'); + font-weight: 410 900; font-style: normal; ascent-override: 106.25%; size-adjust: 106.25%; } @font-face { - font-family: 'Overpass Mono'; - src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); - font-weight: 1 999; + font-family: 'GoogleSansCode'; + src: url('$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations'); + font-weight: 1 900; font-style: monospace; - ascent-override: 106.25%; - size-adjust: 106.25%; } :root { - font-family: 'Overpass', sans-serif; + font-family: 'GoogleSans', sans-serif; + letter-spacing: 0.1px; + /* Used by layouts to ensure proper spacing between navbar and content */ --navbar-height: calc(4.5rem + 4px); --navbar-height-md: calc(4.5rem + 4px - 14px); @@ -148,6 +148,10 @@ color: #3a3a3a; } + body.asset-viewer-open { + background-color: black; + } + input:focus-visible { outline-offset: 0px !important; outline: none !important; diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts index 1606f92796..4a08e7bf61 100644 --- a/web/src/hooks.server.ts +++ b/web/src/hooks.server.ts @@ -1,12 +1,12 @@ -import overpass from '$lib/assets/fonts/overpass/Overpass.ttf?url'; -import overpassMono from '$lib/assets/fonts/overpass/OverpassMono.ttf?url'; +import GoogleSans from '$lib/assets/fonts/GoogleSans/GoogleSans.ttf?url'; +import GoogleSansCode from '$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf?url'; import type { Handle } from '@sveltejs/kit'; // only used during the build to replace the variables from app.html export const handle = (async ({ event, resolve }) => { return resolve(event, { transformPageChunk: ({ html }) => { - return html.replace('%app.font%', overpass).replace('%app.monofont%', overpassMono); + return html.replace('%app.font%', GoogleSans).replace('%app.monofont%', GoogleSansCode); }, }); }) satisfies Handle; diff --git a/web/src/lib/__mocks__/resize-observer.mock.ts b/web/src/lib/__mocks__/resize-observer.mock.ts new file mode 100644 index 0000000000..ffd1dad2fd --- /dev/null +++ b/web/src/lib/__mocks__/resize-observer.mock.ts @@ -0,0 +1,8 @@ +import { vi } from 'vitest'; + +export const getResizeObserverMock = () => + vi.fn(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + unobserve: vi.fn(), + })); diff --git a/web/src/lib/actions/autogrow.ts b/web/src/lib/actions/autogrow.ts deleted file mode 100644 index 0e6dec8e81..0000000000 --- a/web/src/lib/actions/autogrow.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { tick } from 'svelte'; -import type { Action } from 'svelte/action'; - -type Parameters = { - height?: string; - value: string; // added to enable reactivity -}; - -export const autoGrowHeight: Action = (textarea, { height = 'auto' }) => { - const update = () => { - void tick().then(() => { - textarea.style.height = height; - textarea.style.height = `${textarea.scrollHeight}px`; - }); - }; - - update(); - return { update }; -}; diff --git a/web/src/lib/actions/context-menu-navigation.ts b/web/src/lib/actions/context-menu-navigation.ts index 89b7b76d24..9d318d35b0 100644 --- a/web/src/lib/actions/context-menu-navigation.ts +++ b/web/src/lib/actions/context-menu-navigation.ts @@ -98,7 +98,7 @@ export const contextMenuNavigation: Action = (node, option const { destroy } = shortcuts(node, [ { shortcut: { key: 'ArrowUp' }, onShortcut: (event) => moveSelection('up', event) }, { shortcut: { key: 'ArrowDown' }, onShortcut: (event) => moveSelection('down', event) }, - { shortcut: { key: 'Escape' }, onShortcut: (event) => onEscape(event) }, + { shortcut: { key: 'Escape' }, onShortcut: (event) => onEscape(event), preventDefault: false }, { shortcut: { key: ' ' }, onShortcut: (event) => handleClick(event) }, { shortcut: { key: 'Enter' }, onShortcut: (event) => handleClick(event) }, ]); diff --git a/web/src/lib/actions/drag-and-drop.ts b/web/src/lib/actions/drag-and-drop.ts new file mode 100644 index 0000000000..04de6d9744 --- /dev/null +++ b/web/src/lib/actions/drag-and-drop.ts @@ -0,0 +1,118 @@ +export interface DragAndDropOptions { + index: number; + onDragStart?: (index: number) => void; + onDragEnter?: (index: number) => void; + onDrop?: (e: DragEvent, index: number) => void; + onDragEnd?: () => void; + isDragging?: boolean; + isDragOver?: boolean; +} + +export function dragAndDrop(node: HTMLElement, options: DragAndDropOptions) { + let { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; + + const isFormElement = (element: HTMLElement) => { + return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT'; + }; + + const handleDragStart = (e: DragEvent) => { + // Prevent drag if it originated from an input, textarea, or select element + const target = e.target as HTMLElement; + if (isFormElement(target)) { + e.preventDefault(); + return; + } + onDragStart?.(index); + }; + + const handleDragEnter = () => { + onDragEnter?.(index); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: DragEvent) => { + onDrop?.(e, index); + }; + + const handleDragEnd = () => { + onDragEnd?.(); + }; + + // Disable draggable when focusing on form elements (fixes Firefox input interaction) + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + node.setAttribute('draggable', 'false'); + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + node.setAttribute('draggable', 'true'); + } + }; + + node.setAttribute('draggable', 'true'); + node.setAttribute('role', 'button'); + node.setAttribute('tabindex', '0'); + + node.addEventListener('dragstart', handleDragStart); + node.addEventListener('dragenter', handleDragEnter); + node.addEventListener('dragover', handleDragOver); + node.addEventListener('drop', handleDrop); + node.addEventListener('dragend', handleDragEnd); + node.addEventListener('focusin', handleFocusIn); + node.addEventListener('focusout', handleFocusOut); + + // Update classes based on drag state + const updateClasses = (dragging: boolean, dragOver: boolean) => { + // Remove all drag-related classes first + node.classList.remove('opacity-50', 'border-gray-400', 'dark:border-gray-500', 'border-solid'); + + // Add back only the active ones + if (dragging) { + node.classList.add('opacity-50'); + } + + if (dragOver) { + node.classList.add('border-gray-400', 'dark:border-gray-500', 'border-solid'); + node.classList.remove('border-transparent'); + } else { + node.classList.add('border-transparent'); + } + }; + + updateClasses(isDragging || false, isDragOver || false); + + return { + update(newOptions: DragAndDropOptions) { + index = newOptions.index; + onDragStart = newOptions.onDragStart; + onDragEnter = newOptions.onDragEnter; + onDrop = newOptions.onDrop; + onDragEnd = newOptions.onDragEnd; + + const newIsDragging = newOptions.isDragging || false; + const newIsDragOver = newOptions.isDragOver || false; + + if (newIsDragging !== isDragging || newIsDragOver !== isDragOver) { + isDragging = newIsDragging; + isDragOver = newIsDragOver; + updateClasses(isDragging, isDragOver); + } + }, + destroy() { + node.removeEventListener('dragstart', handleDragStart); + node.removeEventListener('dragenter', handleDragEnter); + node.removeEventListener('dragover', handleDragOver); + node.removeEventListener('drop', handleDrop); + node.removeEventListener('dragend', handleDragEnd); + node.removeEventListener('focusin', handleFocusIn); + node.removeEventListener('focusout', handleFocusOut); + }, + }; +} diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts index c302e33d4c..829497ccdb 100644 --- a/web/src/lib/actions/focus-outside.ts +++ b/web/src/lib/actions/focus-outside.ts @@ -1,3 +1,5 @@ +import { on } from 'svelte/events'; + interface Options { onFocusOut?: (event: FocusEvent) => void; } @@ -19,11 +21,11 @@ export function focusOutside(node: HTMLElement, options: Options = {}) { } }; - node.addEventListener('focusout', handleFocusOut); + const off = on(node, 'focusout', handleFocusOut); return { destroy() { - node.removeEventListener('focusout', handleFocusOut); + off(); }, }; } diff --git a/web/src/lib/actions/scroll-memory.ts b/web/src/lib/actions/scroll-memory.ts index 1c19fdd8ab..9953bf00fb 100644 --- a/web/src/lib/actions/scroll-memory.ts +++ b/web/src/lib/actions/scroll-memory.ts @@ -1,14 +1,12 @@ import { navigating } from '$app/stores'; -import { AppRoute, SessionStorageKey } from '$lib/constants'; +import { SessionStorageKey } from '$lib/constants'; import { handlePromiseError } from '$lib/utils'; interface Options { /** - * {@link AppRoute} for subpages that scroll state should be kept while visiting. - * * This must be kept the same in all subpages of this route for the scroll memory clearer to work. */ - routeStartsWith: AppRoute; + routeStartsWith: string; /** * Function to clear additional data/state before scrolling (ex infinite scroll). */ diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index 8f01ce8924..b047dfc391 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -1,112 +1,9 @@ -import type { ActionReturn } from 'svelte/action'; - -export type Shortcut = { - key: string; - alt?: boolean; - ctrl?: boolean; - shift?: boolean; - meta?: boolean; -}; - -export type ShortcutOptions = { - shortcut: Shortcut; - /** If true, the event handler will not execute if the event comes from an input field */ - ignoreInputFields?: boolean; - onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown; - preventDefault?: boolean; -}; - -export const shortcutLabel = (shortcut: Shortcut) => { - let label = ''; - - if (shortcut.ctrl) { - label += 'Ctrl '; - } - if (shortcut.alt) { - label += 'Alt '; - } - if (shortcut.meta) { - label += 'Cmd '; - } - if (shortcut.shift) { - label += '⇧'; - } - label += shortcut.key.toUpperCase(); - - return label; -}; - -/** Determines whether an event should be ignored. The event will be ignored if: - * - The element dispatching the event is not the same as the element which the event listener is attached to - * - The element dispatching the event is an input field - * - The element dispatching the event is a map canvas - */ -export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { - if (event.target === event.currentTarget) { - return false; - } - const type = (event.target as HTMLInputElement).type; - return ( - ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type) || - (event.target instanceof HTMLCanvasElement && event.target.classList.contains('maplibregl-canvas')) - ); -}; - -export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { - return ( - shortcut.key.toLowerCase() === event.key.toLowerCase() && - Boolean(shortcut.alt) === event.altKey && - Boolean(shortcut.ctrl) === event.ctrlKey && - Boolean(shortcut.shift) === event.shiftKey && - Boolean(shortcut.meta) === event.metaKey - ); -}; - -/** Bind a single keyboard shortcut to node. */ -export const shortcut = ( - node: T, - option: ShortcutOptions, -): ActionReturn> => { - const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]); - - return { - update(newOption) { - shortcutsUpdate?.([newOption]); - }, - destroy, - }; -}; - -/** Binds multiple keyboard shortcuts to node */ -export const shortcuts = ( - node: T, - options: ShortcutOptions[], -): ActionReturn[]> => { - function onKeydown(event: KeyboardEvent) { - const ignoreShortcut = shouldIgnoreEvent(event); - for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) { - if (ignoreInputFields && ignoreShortcut) { - continue; - } - - if (matchesShortcut(event, shortcut)) { - if (preventDefault) { - event.preventDefault(); - } - onShortcut(event as KeyboardEvent & { currentTarget: T }); - return; - } - } - } - - node.addEventListener('keydown', onKeydown); - - return { - update(newOptions) { - options = newOptions; - }, - destroy() { - node.removeEventListener('keydown', onKeydown); - }, - }; -}; +export { + matchesShortcut, + shortcut, + shortcutLabel, + shortcuts, + shouldIgnoreEvent, + type Shortcut, + type ShortcutOptions, +} from '@immich/ui'; diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts index e49f04dbee..872d3d03bf 100644 --- a/web/src/lib/actions/thumbhash.ts +++ b/web/src/lib/actions/thumbhash.ts @@ -3,17 +3,27 @@ import { thumbHashToRGBA } from 'thumbhash'; /** * Renders a thumbnail onto a canvas from a base64 encoded hash. - * @param canvas - * @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString) */ -export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) { - const ctx = canvas.getContext('2d'); - if (ctx) { - const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash)); - const pixels = ctx.createImageData(w, h); - canvas.width = w; - canvas.height = h; - pixels.data.set(rgba); - ctx.putImageData(pixels, 0, 0); - } +export function thumbhash(canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) { + render(canvas, options); + + return { + update(newOptions: { base64ThumbHash: string }) { + render(canvas, newOptions); + }, + }; } + +const render = (canvas: HTMLCanvasElement, options: { base64ThumbHash: string }) => { + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + + const { w, h, rgba } = thumbHashToRGBA(decodeBase64(options.base64ThumbHash)); + const pixels = ctx.createImageData(w, h); + canvas.width = w; + canvas.height = h; + pixels.data.set(rgba); + ctx.putImageData(pixels, 0, 0); +}; diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index e67d3e1928..6288daa380 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -1,48 +1,35 @@ -import { photoZoomState } from '$lib/stores/zoom-image.store'; -import { useZoomImageWheel } from '@zoom-image/svelte'; -import { get } from 'svelte/store'; +import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; +import { createZoomImageWheel } from '@zoom-image/core'; export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => { - const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel(); + const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState }); - createZoomImage(node, { - maxZoom: 10, - }); + const unsubscribes = [ + assetViewerManager.on({ ZoomChange: (state) => zoomInstance.setState(state) }), + zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)), + ]; - const state = get(photoZoomState); - if (state) { - setZoomImageState(state); - } - - // Store original event handlers so we can prevent them when disabled - const wheelHandler = (event: WheelEvent) => { + const stopIfDisabled = (event: Event) => { if (options?.disabled) { event.stopImmediatePropagation(); } }; - const pointerDownHandler = (event: PointerEvent) => { - if (options?.disabled) { - event.stopImmediatePropagation(); - } - }; - - // Add handlers at capture phase with higher priority - node.addEventListener('wheel', wheelHandler, { capture: true }); - node.addEventListener('pointerdown', pointerDownHandler, { capture: true }); - - const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)]; + node.addEventListener('wheel', stopIfDisabled, { capture: true }); + node.addEventListener('pointerdown', stopIfDisabled, { capture: true }); + node.style.overflow = 'visible'; return { update(newOptions?: { disabled?: boolean }) { options = newOptions; }, destroy() { - node.removeEventListener('wheel', wheelHandler, { capture: true }); - node.removeEventListener('pointerdown', pointerDownHandler, { capture: true }); for (const unsubscribe of unsubscribes) { unsubscribe(); } + node.removeEventListener('wheel', stopIfDisabled, { capture: true }); + node.removeEventListener('pointerdown', stopIfDisabled, { capture: true }); + zoomInstance.cleanup(); }, }; }; diff --git a/web/src/lib/assets/apple/apple-splash-1125-2436.png b/web/src/lib/assets/apple/apple-splash-1125-2436.png deleted file mode 100644 index 0b48eb9259..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1125-2436.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1136-640.png b/web/src/lib/assets/apple/apple-splash-1136-640.png deleted file mode 100644 index 5fa6b3f63b..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1136-640.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1170-2532.png b/web/src/lib/assets/apple/apple-splash-1170-2532.png deleted file mode 100644 index f2fa5ffb55..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1170-2532.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1179-2556.png b/web/src/lib/assets/apple/apple-splash-1179-2556.png deleted file mode 100644 index 633b63a792..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1179-2556.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1242-2208.png b/web/src/lib/assets/apple/apple-splash-1242-2208.png deleted file mode 100644 index f57719892e..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1242-2208.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1242-2688.png b/web/src/lib/assets/apple/apple-splash-1242-2688.png deleted file mode 100644 index 308393c571..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1242-2688.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1284-2778.png b/web/src/lib/assets/apple/apple-splash-1284-2778.png deleted file mode 100644 index 7471ab1594..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1284-2778.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1290-2796.png b/web/src/lib/assets/apple/apple-splash-1290-2796.png deleted file mode 100644 index 74041cefdb..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1290-2796.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1334-750.png b/web/src/lib/assets/apple/apple-splash-1334-750.png deleted file mode 100644 index b7d23946f2..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1334-750.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1536-2048.png b/web/src/lib/assets/apple/apple-splash-1536-2048.png deleted file mode 100644 index 96572dbc98..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1536-2048.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1620-2160.png b/web/src/lib/assets/apple/apple-splash-1620-2160.png deleted file mode 100644 index 23b4f0b185..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1620-2160.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1668-2224.png b/web/src/lib/assets/apple/apple-splash-1668-2224.png deleted file mode 100644 index 4ece3a1c39..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1668-2224.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1668-2388.png b/web/src/lib/assets/apple/apple-splash-1668-2388.png deleted file mode 100644 index 7486415097..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1668-2388.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-1792-828.png b/web/src/lib/assets/apple/apple-splash-1792-828.png deleted file mode 100644 index aaa9064a06..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-1792-828.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2048-1536.png b/web/src/lib/assets/apple/apple-splash-2048-1536.png deleted file mode 100644 index a0e0a35179..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2048-1536.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2048-2732.png b/web/src/lib/assets/apple/apple-splash-2048-2732.png deleted file mode 100644 index 7f807caf0e..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2048-2732.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2160-1620.png b/web/src/lib/assets/apple/apple-splash-2160-1620.png deleted file mode 100644 index 498668ae5e..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2160-1620.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2208-1242.png b/web/src/lib/assets/apple/apple-splash-2208-1242.png deleted file mode 100644 index 4e37708249..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2208-1242.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2224-1668.png b/web/src/lib/assets/apple/apple-splash-2224-1668.png deleted file mode 100644 index 9cd0b7e970..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2224-1668.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2388-1668.png b/web/src/lib/assets/apple/apple-splash-2388-1668.png deleted file mode 100644 index 458f9a2f1f..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2388-1668.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2436-1125.png b/web/src/lib/assets/apple/apple-splash-2436-1125.png deleted file mode 100644 index b0533892bc..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2436-1125.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2532-1170.png b/web/src/lib/assets/apple/apple-splash-2532-1170.png deleted file mode 100644 index 96007d8413..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2532-1170.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2556-1179.png b/web/src/lib/assets/apple/apple-splash-2556-1179.png deleted file mode 100644 index eb99264527..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2556-1179.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2688-1242.png b/web/src/lib/assets/apple/apple-splash-2688-1242.png deleted file mode 100644 index 9631f79452..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2688-1242.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2732-2048.png b/web/src/lib/assets/apple/apple-splash-2732-2048.png deleted file mode 100644 index 61ef4284a1..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2732-2048.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2778-1284.png b/web/src/lib/assets/apple/apple-splash-2778-1284.png deleted file mode 100644 index f8e363ab75..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2778-1284.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-2796-1290.png b/web/src/lib/assets/apple/apple-splash-2796-1290.png deleted file mode 100644 index b229e21bd6..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-2796-1290.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-640-1136.png b/web/src/lib/assets/apple/apple-splash-640-1136.png deleted file mode 100644 index c2cb5083fb..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-640-1136.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-750-1334.png b/web/src/lib/assets/apple/apple-splash-750-1334.png deleted file mode 100644 index ae41d4aa01..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-750-1334.png and /dev/null differ diff --git a/web/src/lib/assets/apple/apple-splash-828-1792.png b/web/src/lib/assets/apple/apple-splash-828-1792.png deleted file mode 100644 index efa06a230c..0000000000 Binary files a/web/src/lib/assets/apple/apple-splash-828-1792.png and /dev/null differ diff --git a/web/src/lib/assets/empty-workflows.svg b/web/src/lib/assets/empty-workflows.svg new file mode 100644 index 0000000000..f601ca984a --- /dev/null +++ b/web/src/lib/assets/empty-workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/lib/assets/fonts/GoogleSans/GoogleSans.ttf b/web/src/lib/assets/fonts/GoogleSans/GoogleSans.ttf new file mode 100644 index 0000000000..5d9102f856 Binary files /dev/null and b/web/src/lib/assets/fonts/GoogleSans/GoogleSans.ttf differ diff --git a/web/src/lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf b/web/src/lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf new file mode 100644 index 0000000000..b68d037edf Binary files /dev/null and b/web/src/lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf differ diff --git a/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf b/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf deleted file mode 100644 index 281dd742bb..0000000000 Binary files a/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf and /dev/null differ diff --git a/web/src/lib/assets/fonts/overpass/Overpass.ttf b/web/src/lib/assets/fonts/overpass/Overpass.ttf deleted file mode 100644 index 1cf730a5ad..0000000000 Binary files a/web/src/lib/assets/fonts/overpass/Overpass.ttf and /dev/null differ diff --git a/web/src/lib/assets/fonts/overpass/OverpassMono.ttf b/web/src/lib/assets/fonts/overpass/OverpassMono.ttf deleted file mode 100644 index 71ef818b33..0000000000 Binary files a/web/src/lib/assets/fonts/overpass/OverpassMono.ttf and /dev/null differ diff --git a/web/src/lib/attachments/drag-and-drop.svelte.ts b/web/src/lib/attachments/drag-and-drop.svelte.ts new file mode 100644 index 0000000000..950e8e5b80 --- /dev/null +++ b/web/src/lib/attachments/drag-and-drop.svelte.ts @@ -0,0 +1,105 @@ +import type { Attachment } from 'svelte/attachments'; + +export interface DragAndDropOptions { + index: number; + onDragStart?: (index: number) => void; + onDragEnter?: (index: number) => void; + onDrop?: (e: DragEvent, index: number) => void; + onDragEnd?: () => void; + isDragging?: boolean; + isDragOver?: boolean; +} + +export function dragAndDrop(options: DragAndDropOptions): Attachment { + return (node: Element) => { + const element = node as HTMLElement; + const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; + + const isFormElement = (el: HTMLElement) => { + return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'; + }; + + const handleDragStart = (e: DragEvent) => { + // Prevent drag if it originated from an input, textarea, or select element + const target = e.target as HTMLElement; + if (isFormElement(target)) { + e.preventDefault(); + return; + } + onDragStart?.(index); + }; + + const handleDragEnter = () => { + onDragEnter?.(index); + }; + + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + }; + + const handleDrop = (e: DragEvent) => { + onDrop?.(e, index); + }; + + const handleDragEnd = () => { + onDragEnd?.(); + }; + + // Disable draggable when focusing on form elements (fixes Firefox input interaction) + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + element.setAttribute('draggable', 'false'); + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (isFormElement(target)) { + element.setAttribute('draggable', 'true'); + } + }; + + // Update classes based on drag state + const updateClasses = (dragging: boolean, dragOver: boolean) => { + // Remove all drag-related classes first + element.classList.remove('opacity-50', 'border-light-500', 'border-solid'); + + // Add back only the active ones + if (dragging) { + element.classList.add('opacity-50'); + } + + if (dragOver) { + element.classList.add('border-light-500', 'border-solid'); + element.classList.remove('border-transparent'); + } else { + element.classList.add('border-transparent'); + } + }; + + element.setAttribute('draggable', 'true'); + element.setAttribute('role', 'button'); + element.setAttribute('tabindex', '0'); + + element.addEventListener('dragstart', handleDragStart); + element.addEventListener('dragenter', handleDragEnter); + element.addEventListener('dragover', handleDragOver); + element.addEventListener('drop', handleDrop); + element.addEventListener('dragend', handleDragEnd); + element.addEventListener('focusin', handleFocusIn); + element.addEventListener('focusout', handleFocusOut); + + updateClasses(isDragging || false, isDragOver || false); + + return () => { + element.removeEventListener('dragstart', handleDragStart); + element.removeEventListener('dragenter', handleDragEnter); + element.removeEventListener('dragover', handleDragOver); + element.removeEventListener('drop', handleDrop); + element.removeEventListener('dragend', handleDragEnd); + element.removeEventListener('focusin', handleFocusIn); + element.removeEventListener('focusout', handleFocusOut); + }; + }; +} diff --git a/web/src/lib/cast/cast-button.svelte b/web/src/lib/cast/cast-button.svelte deleted file mode 100644 index 392418daa5..0000000000 --- a/web/src/lib/cast/cast-button.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if castManager.availableDestinations.length > 0 && castManager.availableDestinations[0].type === CastDestinationType.GCAST} - void GCastDestination.showCastDialog()} - aria-label={$t('cast')} - /> -{/if} diff --git a/web/src/lib/components/ActionButton.svelte b/web/src/lib/components/ActionButton.svelte deleted file mode 100644 index e0e7e1eff7..0000000000 --- a/web/src/lib/components/ActionButton.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -{#if action.$if?.() ?? true} - onAction(action)} /> -{/if} diff --git a/web/src/lib/components/ActionMenuItem.svelte b/web/src/lib/components/ActionMenuItem.svelte new file mode 100644 index 0000000000..d50d50bf0b --- /dev/null +++ b/web/src/lib/components/ActionMenuItem.svelte @@ -0,0 +1,16 @@ + + +{#if icon && isEnabled(action)} + onAction(action)} /> +{/if} diff --git a/web/src/lib/components/AdminCard.svelte b/web/src/lib/components/AdminCard.svelte new file mode 100644 index 0000000000..4aaf890ca4 --- /dev/null +++ b/web/src/lib/components/AdminCard.svelte @@ -0,0 +1,33 @@ + + + + +
+
+ + {title} +
+ {#if headerAction} + + {/if} +
+
+ +
+ {@render children?.()} +
+
+
diff --git a/web/src/lib/components/AssetViewerEvents.svelte b/web/src/lib/components/AssetViewerEvents.svelte new file mode 100644 index 0000000000..b636908b76 --- /dev/null +++ b/web/src/lib/components/AssetViewerEvents.svelte @@ -0,0 +1,24 @@ + diff --git a/web/src/lib/components/BreadcrumbActionPage.svelte b/web/src/lib/components/BreadcrumbActionPage.svelte new file mode 100644 index 0000000000..cdde67b725 --- /dev/null +++ b/web/src/lib/components/BreadcrumbActionPage.svelte @@ -0,0 +1,61 @@ + + +
+
+ + + {#if enabledActions.length > 0} + + + + {/if} +
+ + + +
diff --git a/web/src/lib/components/Image.spec.ts b/web/src/lib/components/Image.spec.ts new file mode 100644 index 0000000000..8435e1bb25 --- /dev/null +++ b/web/src/lib/components/Image.spec.ts @@ -0,0 +1,87 @@ +import Image from '$lib/components/Image.svelte'; +import { cancelImageUrl } from '$lib/utils/sw-messaging'; +import { fireEvent, render } from '@testing-library/svelte'; + +vi.mock('$lib/utils/sw-messaging', () => ({ + cancelImageUrl: vi.fn(), +})); + +describe('Image component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders an img element when src is provided', () => { + const { baseElement } = render(Image, { src: '/test.jpg', alt: 'test' }); + const img = baseElement.querySelector('img'); + expect(img).not.toBeNull(); + expect(img!.getAttribute('src')).toBe('/test.jpg'); + }); + + it('does not render an img element when src is undefined', () => { + const { baseElement } = render(Image, { src: undefined }); + const img = baseElement.querySelector('img'); + expect(img).toBeNull(); + }); + + it('calls onStart when src is set', () => { + const onStart = vi.fn(); + render(Image, { src: '/test.jpg', onStart }); + expect(onStart).toHaveBeenCalledOnce(); + }); + + it('calls onLoad when image loads', async () => { + const onLoad = vi.fn(); + const { baseElement } = render(Image, { src: '/test.jpg', onLoad }); + const img = baseElement.querySelector('img')!; + await fireEvent.load(img); + expect(onLoad).toHaveBeenCalledOnce(); + }); + + it('calls onError when image fails to load', async () => { + const onError = vi.fn(); + const { baseElement } = render(Image, { src: '/test.jpg', onError }); + const img = baseElement.querySelector('img')!; + await fireEvent.error(img); + expect(onError).toHaveBeenCalledOnce(); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + expect(onError.mock.calls[0][0].message).toBe('Failed to load image: /test.jpg'); + }); + + it('calls cancelImageUrl on unmount', () => { + const { unmount } = render(Image, { src: '/test.jpg' }); + expect(cancelImageUrl).not.toHaveBeenCalled(); + unmount(); + expect(cancelImageUrl).toHaveBeenCalledWith('/test.jpg'); + }); + + it('does not call onLoad after unmount', async () => { + const onLoad = vi.fn(); + const { baseElement, unmount } = render(Image, { src: '/test.jpg', onLoad }); + const img = baseElement.querySelector('img')!; + unmount(); + await fireEvent.load(img); + expect(onLoad).not.toHaveBeenCalled(); + }); + + it('does not call onError after unmount', async () => { + const onError = vi.fn(); + const { baseElement, unmount } = render(Image, { src: '/test.jpg', onError }); + const img = baseElement.querySelector('img')!; + unmount(); + await fireEvent.error(img); + expect(onError).not.toHaveBeenCalled(); + }); + + it('passes through additional HTML attributes', () => { + const { baseElement } = render(Image, { + src: '/test.jpg', + alt: 'test alt', + class: 'my-class', + draggable: false, + }); + const img = baseElement.querySelector('img')!; + expect(img.getAttribute('alt')).toBe('test alt'); + expect(img.getAttribute('draggable')).toBe('false'); + }); +}); diff --git a/web/src/lib/components/Image.svelte b/web/src/lib/components/Image.svelte new file mode 100644 index 0000000000..801a466ca8 --- /dev/null +++ b/web/src/lib/components/Image.svelte @@ -0,0 +1,54 @@ + + +{#if capturedSource} + {#key capturedSource} + + {/key} +{/if} diff --git a/web/src/lib/components/OnEvents.svelte b/web/src/lib/components/OnEvents.svelte index 3933f4df7b..fe8039cf38 100644 --- a/web/src/lib/components/OnEvents.svelte +++ b/web/src/lib/components/OnEvents.svelte @@ -1,33 +1,24 @@ diff --git a/web/src/lib/components/QueueCard.svelte b/web/src/lib/components/QueueCard.svelte index f57fb984a2..b7cde7b8f1 100644 --- a/web/src/lib/components/QueueCard.svelte +++ b/web/src/lib/components/QueueCard.svelte @@ -2,8 +2,10 @@ import QueueCardBadge from '$lib/components/QueueCardBadge.svelte'; import QueueCardButton from '$lib/components/QueueCardButton.svelte'; import Badge from '$lib/elements/Badge.svelte'; - import { asQueueItem, getQueueDetailUrl } from '$lib/services/queue.service'; + import { Route } from '$lib/route'; + import { asQueueItem } from '$lib/services/queue.service'; import { locale } from '$lib/stores/preferences.store'; + import { transformToTitleCase } from '$lib/utils'; import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk'; import { Icon, IconButton, Link } from '@immich/ui'; import { @@ -50,9 +52,9 @@ {/if}
- +